use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use spin_serde::{DependencyName, FixedVersionBackwardCompatible};
use std::collections::BTreeMap;
use crate::{
metadata::MetadataExt,
values::{ValuesMap, ValuesMapBuilder},
};
pub type LockedMap<T> = std::collections::BTreeMap<String, T>;
pub const SERVICE_CHAINING_KEY: &str = "local_service_chaining";
pub const HOST_REQ_OPTIONAL: &str = "optional";
pub const HOST_REQ_REQUIRED: &str = "required";
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MustUnderstand {
HostRequirements,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HostRequirement {
LocalServiceChaining,
}
#[derive(Clone, Debug, Deserialize)]
pub struct LockedApp {
pub spin_lock_version: FixedVersionBackwardCompatible<1>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub must_understand: Vec<MustUnderstand>,
#[serde(default, skip_serializing_if = "ValuesMap::is_empty")]
pub metadata: ValuesMap,
#[serde(
default,
skip_serializing_if = "ValuesMap::is_empty",
deserialize_with = "deserialize_host_requirements"
)]
pub host_requirements: ValuesMap,
#[serde(default, skip_serializing_if = "LockedMap::is_empty")]
pub variables: LockedMap<Variable>,
pub triggers: Vec<LockedTrigger>,
pub components: Vec<LockedComponent>,
}
fn deserialize_host_requirements<'de, D>(deserializer: D) -> Result<ValuesMap, D::Error>
where
D: serde::Deserializer<'de>,
{
struct HostRequirementsVisitor;
impl<'de> serde::de::Visitor<'de> for HostRequirementsVisitor {
type Value = ValuesMap;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("struct ValuesMap")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
use serde::de::Error;
let mut hr = ValuesMapBuilder::new();
while let Some(key) = map.next_key::<String>()? {
let value: serde_json::Value = map.next_value()?;
if value.as_str() == Some(HOST_REQ_OPTIONAL) {
continue;
}
hr.serializable(key, value).map_err(A::Error::custom)?;
}
Ok(hr.build())
}
}
let m = deserializer.deserialize_map(HostRequirementsVisitor)?;
let unsupported: Vec<_> = m
.keys()
.filter(|k| !SUPPORTED_HOST_REQS.contains(&k.as_str()))
.map(|k| k.to_string())
.collect();
if unsupported.is_empty() {
Ok(m)
} else {
let msg = format!("This version of Spin does not support the following features required by this application: {}", unsupported.join(", "));
Err(serde::de::Error::custom(msg))
}
}
const SUPPORTED_HOST_REQS: &[&str] = &[SERVICE_CHAINING_KEY];
impl Serialize for LockedApp {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let version = if self.must_understand.is_empty() && self.host_requirements.is_empty() {
0
} else {
1
};
let mut la = serializer.serialize_struct("LockedApp", 7)?;
la.serialize_field("spin_lock_version", &version)?;
if !self.must_understand.is_empty() {
la.serialize_field("must_understand", &self.must_understand)?;
}
if !self.metadata.is_empty() {
la.serialize_field("metadata", &self.metadata)?;
}
if !self.host_requirements.is_empty() {
la.serialize_field("host_requirements", &self.host_requirements)?;
}
if !self.variables.is_empty() {
la.serialize_field("variables", &self.variables)?;
}
la.serialize_field("triggers", &self.triggers)?;
la.serialize_field("components", &self.components)?;
la.end()
}
}
impl LockedApp {
pub fn from_json(contents: &[u8]) -> serde_json::Result<Self> {
serde_json::from_slice(contents)
}
pub fn to_json(&self) -> serde_json::Result<Vec<u8>> {
serde_json::to_vec_pretty(&self)
}
pub fn get_metadata<'this, T: Deserialize<'this>>(
&'this self,
key: crate::MetadataKey<T>,
) -> crate::Result<Option<T>> {
self.metadata.get_typed(key)
}
pub fn require_metadata<'this, T: Deserialize<'this>>(
&'this self,
key: crate::MetadataKey<T>,
) -> crate::Result<T> {
self.metadata.require_typed(key)
}
pub fn ensure_needs_only(&self, supported: &[&str]) -> Result<(), String> {
let unmet_requirements = self
.host_requirements
.keys()
.filter(|hr| !supported.contains(&hr.as_str()))
.map(|s| s.to_string())
.collect::<Vec<_>>();
if unmet_requirements.is_empty() {
Ok(())
} else {
let message = unmet_requirements.join(", ");
Err(message)
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LockedComponent {
pub id: String,
#[serde(default, skip_serializing_if = "ValuesMap::is_empty")]
pub metadata: ValuesMap,
pub source: LockedComponentSource,
#[serde(default, skip_serializing_if = "LockedMap::is_empty")]
pub env: LockedMap<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub files: Vec<ContentPath>,
#[serde(default, skip_serializing_if = "LockedMap::is_empty")]
pub config: LockedMap<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub dependencies: BTreeMap<DependencyName, LockedComponentDependency>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LockedComponentDependency {
pub source: LockedComponentSource,
pub export: Option<String>,
#[serde(default, skip_serializing_if = "InheritConfiguration::is_none")]
pub inherit: InheritConfiguration,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum InheritConfiguration {
All,
Some(Vec<String>),
}
impl Default for InheritConfiguration {
fn default() -> Self {
InheritConfiguration::Some(vec![])
}
}
impl InheritConfiguration {
fn is_none(&self) -> bool {
matches!(self, InheritConfiguration::Some(configs) if configs.is_empty())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LockedComponentSource {
pub content_type: String,
#[serde(flatten)]
pub content: ContentRef,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ContentPath {
#[serde(flatten)]
pub content: ContentRef,
pub path: PathBuf,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ContentRef {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "spin_serde::base64"
)]
pub inline: Option<Vec<u8>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub digest: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LockedTrigger {
pub id: String,
pub trigger_type: String,
pub trigger_config: Value,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Variable {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub secret: bool,
}
#[cfg(test)]
mod test {
use super::*;
use crate::values::ValuesMapBuilder;
#[test]
fn locked_app_with_no_host_reqs_serialises_as_v0_and_v0_deserialises_as_v1() {
let locked_app = LockedApp {
spin_lock_version: Default::default(),
must_understand: Default::default(),
metadata: Default::default(),
host_requirements: Default::default(),
variables: Default::default(),
triggers: Default::default(),
components: Default::default(),
};
let json = locked_app.to_json().unwrap();
assert!(String::from_utf8_lossy(&json).contains(r#""spin_lock_version": 0"#));
let reloaded = LockedApp::from_json(&json).unwrap();
assert_eq!(1, Into::<usize>::into(reloaded.spin_lock_version));
}
#[test]
fn locked_app_with_host_reqs_serialises_as_v1() {
let mut host_requirements = ValuesMapBuilder::new();
host_requirements.string(SERVICE_CHAINING_KEY, "bar");
let host_requirements = host_requirements.build();
let locked_app = LockedApp {
spin_lock_version: Default::default(),
must_understand: vec![MustUnderstand::HostRequirements],
metadata: Default::default(),
host_requirements,
variables: Default::default(),
triggers: Default::default(),
components: Default::default(),
};
let json = locked_app.to_json().unwrap();
assert!(String::from_utf8_lossy(&json).contains(r#""spin_lock_version": 1"#));
let reloaded = LockedApp::from_json(&json).unwrap();
assert_eq!(1, Into::<usize>::into(reloaded.spin_lock_version));
assert_eq!(1, reloaded.must_understand.len());
assert_eq!(1, reloaded.host_requirements.len());
}
#[test]
fn deserialising_ignores_unknown_fields() {
use serde_json::json;
let j = serde_json::to_vec_pretty(&json!({
"spin_lock_version": 1,
"triggers": [],
"components": [],
"never_create_field_with_this_name": 123
}))
.unwrap();
let locked = LockedApp::from_json(&j).unwrap();
assert_eq!(0, locked.triggers.len());
}
#[test]
fn deserialising_does_not_ignore_must_understand_unknown_fields() {
use serde_json::json;
let j = serde_json::to_vec_pretty(&json!({
"spin_lock_version": 1,
"must_understand": vec!["never_create_field_with_this_name"],
"triggers": [],
"components": [],
"never_create_field_with_this_name": 123
}))
.unwrap();
let err = LockedApp::from_json(&j).expect_err(
"Should have refused to deserialise due to non-understood must-understand field",
);
assert!(err
.to_string()
.contains("never_create_field_with_this_name"));
}
#[test]
fn deserialising_accepts_must_understands_that_it_does_understand() {
use serde_json::json;
let j = serde_json::to_vec_pretty(&json!({
"spin_lock_version": 1,
"must_understand": vec!["host_requirements"],
"host_requirements": {
SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
},
"triggers": [],
"components": [],
"never_create_field_with_this_name": 123
}))
.unwrap();
let locked = LockedApp::from_json(&j).unwrap();
assert_eq!(1, locked.must_understand.len());
assert_eq!(1, locked.host_requirements.len());
}
#[test]
fn deserialising_rejects_host_requirements_that_are_not_supported() {
use serde_json::json;
let j = serde_json::to_vec_pretty(&json!({
"spin_lock_version": 1,
"must_understand": vec!["host_requirements"],
"host_requirements": {
SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
"accelerated_spline_reticulation": HOST_REQ_REQUIRED
},
"triggers": [],
"components": []
}))
.unwrap();
let err = LockedApp::from_json(&j).expect_err(
"Should have refused to deserialise due to non-understood host requirement",
);
assert!(err.to_string().contains("accelerated_spline_reticulation"));
}
#[test]
fn deserialising_skips_optional_host_requirements() {
use serde_json::json;
let j = serde_json::to_vec_pretty(&json!({
"spin_lock_version": 1,
"must_understand": vec!["host_requirements"],
"host_requirements": {
SERVICE_CHAINING_KEY: HOST_REQ_REQUIRED,
"accelerated_spline_reticulation": HOST_REQ_OPTIONAL
},
"triggers": [],
"components": []
}))
.unwrap();
let locked = LockedApp::from_json(&j).unwrap();
assert_eq!(1, locked.must_understand.len());
assert_eq!(1, locked.host_requirements.len());
}
}