wadm_types/
lib.rs

1use std::collections::{BTreeMap, HashMap};
2
3use schemars::JsonSchema;
4use serde::{de, Deserialize, Serialize};
5use utoipa::ToSchema;
6
7pub mod api;
8#[cfg(feature = "wit")]
9pub mod bindings;
10#[cfg(feature = "wit")]
11pub use bindings::*;
12pub mod validation;
13
14/// The default weight for a spread
15pub const DEFAULT_SPREAD_WEIGHT: usize = 100;
16/// The expected OAM api version
17pub const OAM_VERSION: &str = "core.oam.dev/v1beta1";
18/// The currently supported kind for OAM manifests.
19// NOTE(thomastaylor312): If we ever end up supporting more than one kind, we should use an enum for
20// this
21pub const APPLICATION_KIND: &str = "Application";
22/// The version key, as predefined by the [OAM
23/// spec](https://github.com/oam-dev/spec/blob/master/metadata.md#annotations-format)
24pub const VERSION_ANNOTATION_KEY: &str = "version";
25/// The description key, as predefined by the [OAM
26/// spec](https://github.com/oam-dev/spec/blob/master/metadata.md#annotations-format)
27pub const DESCRIPTION_ANNOTATION_KEY: &str = "description";
28/// The annotation key for shared applications
29pub const SHARED_ANNOTATION_KEY: &str = "experimental.wasmcloud.dev/shared";
30/// The identifier for the builtin spreadscaler trait type
31pub const SPREADSCALER_TRAIT: &str = "spreadscaler";
32/// The identifier for the builtin daemonscaler trait type
33pub const DAEMONSCALER_TRAIT: &str = "daemonscaler";
34/// The identifier for the builtin linkdef trait type
35pub const LINK_TRAIT: &str = "link";
36/// The string used for indicating a latest version. It is explicitly forbidden to use as a version
37/// for a manifest
38pub const LATEST_VERSION: &str = "latest";
39/// The default link name
40pub const DEFAULT_LINK_NAME: &str = "default";
41
42/// Manifest file based on the Open Application Model (OAM) specification for declaratively managing wasmCloud applications
43#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
44#[serde(deny_unknown_fields)]
45pub struct Manifest {
46    /// The OAM version of the manifest
47    #[serde(rename = "apiVersion")]
48    pub api_version: String,
49    /// The kind or type of manifest described by the spec
50    pub kind: String,
51    /// Metadata describing the manifest
52    pub metadata: Metadata,
53    /// The specification for this manifest
54    pub spec: Specification,
55}
56
57impl Manifest {
58    /// Returns a reference to the current version
59    pub fn version(&self) -> &str {
60        self.metadata
61            .annotations
62            .get(VERSION_ANNOTATION_KEY)
63            .map(|v| v.as_str())
64            .unwrap_or_default()
65    }
66
67    /// Returns a reference to the current description if it exists
68    pub fn description(&self) -> Option<&str> {
69        self.metadata
70            .annotations
71            .get(DESCRIPTION_ANNOTATION_KEY)
72            .map(|v| v.as_str())
73    }
74
75    /// Indicates if the manifest is shared, meaning it can be used by multiple applications
76    pub fn shared(&self) -> bool {
77        self.metadata
78            .annotations
79            .get(SHARED_ANNOTATION_KEY)
80            .is_some_and(|v| v.parse::<bool>().unwrap_or(false))
81    }
82
83    /// Returns the components in the manifest
84    pub fn components(&self) -> impl Iterator<Item = &Component> {
85        self.spec.components.iter()
86    }
87
88    /// Helper function to find shared components that are missing from the given list of
89    /// deployed applications
90    pub fn missing_shared_components(&self, deployed_apps: &[&Manifest]) -> Vec<&Component> {
91        self.spec
92            .components
93            .iter()
94            .filter(|shared_component| {
95                match &shared_component.properties {
96                    Properties::Capability {
97                        properties:
98                            CapabilityProperties {
99                                image: None,
100                                application: Some(shared_app),
101                                ..
102                            },
103                    }
104                    | Properties::Component {
105                        properties:
106                            ComponentProperties {
107                                image: None,
108                                application: Some(shared_app),
109                                ..
110                            },
111                    } => {
112                        if deployed_apps.iter().filter(|a| a.shared()).any(|m| {
113                            m.metadata.name == shared_app.name
114                                && m.components().any(|c| {
115                                    c.name == shared_app.component
116                                    // This compares just the enum variant, not the actual properties
117                                    // For example, if we reference a shared component that's a capability,
118                                    // we want to make sure the deployed component is a capability.
119                                    && std::mem::discriminant(&c.properties)
120                                        == std::mem::discriminant(&shared_component.properties)
121                                })
122                        }) {
123                            false
124                        } else {
125                            true
126                        }
127                    }
128                    _ => false,
129                }
130            })
131            .collect()
132    }
133
134    /// Returns only the WebAssembly components in the manifest
135    pub fn wasm_components(&self) -> impl Iterator<Item = &Component> {
136        self.components()
137            .filter(|c| matches!(c.properties, Properties::Component { .. }))
138    }
139
140    /// Returns only the provider components in the manifest
141    pub fn capability_providers(&self) -> impl Iterator<Item = &Component> {
142        self.components()
143            .filter(|c| matches!(c.properties, Properties::Capability { .. }))
144    }
145
146    /// Returns a map of component names to components in the manifest
147    pub fn component_lookup(&self) -> HashMap<&String, &Component> {
148        self.components()
149            .map(|c| (&c.name, c))
150            .collect::<HashMap<&String, &Component>>()
151    }
152
153    /// Returns only links in the manifest
154    pub fn links(&self) -> impl Iterator<Item = &Trait> {
155        self.components()
156            .flat_map(|c| c.traits.as_ref())
157            .flatten()
158            .filter(|t| t.is_link())
159    }
160
161    /// Returns only policies in the manifest
162    pub fn policies(&self) -> impl Iterator<Item = &Policy> {
163        self.spec.policies.iter()
164    }
165
166    /// Returns a map of policy names to policies in the manifest
167    pub fn policy_lookup(&self) -> HashMap<&String, &Policy> {
168        self.spec
169            .policies
170            .iter()
171            .map(|p| (&p.name, p))
172            .collect::<HashMap<&String, &Policy>>()
173    }
174}
175
176/// The metadata describing the manifest
177#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
178pub struct Metadata {
179    /// The name of the manifest. This must be unique per lattice
180    pub name: String,
181    /// Optional data for annotating this manifest see <https://github.com/oam-dev/spec/blob/master/metadata.md#annotations-format>
182    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
183    pub annotations: BTreeMap<String, String>,
184    /// Optional data for labeling this manifest, see <https://github.com/oam-dev/spec/blob/master/metadata.md#label-format>
185    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
186    pub labels: BTreeMap<String, String>,
187}
188
189/// A representation of an OAM specification
190#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
191pub struct Specification {
192    /// The list of components for describing an application
193    pub components: Vec<Component>,
194
195    /// The list of policies describing an application. This is for providing application-wide
196    /// setting such as configuration for a secrets backend, how to render Kubernetes services,
197    /// etc. It can be omitted if no policies are needed for an application.
198    #[serde(default, skip_serializing_if = "Vec::is_empty")]
199    pub policies: Vec<Policy>,
200}
201
202/// A policy definition
203#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
204pub struct Policy {
205    /// The name of this policy
206    pub name: String,
207    /// The properties for this policy
208    pub properties: BTreeMap<String, String>,
209    /// The type of the policy
210    #[serde(rename = "type")]
211    pub policy_type: String,
212}
213
214/// A component definition
215#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
216// TODO: figure out why this can't be uncommented
217// #[serde(deny_unknown_fields)]
218pub struct Component {
219    /// The name of this component
220    pub name: String,
221    /// The properties for this component
222    // NOTE(thomastaylor312): It would probably be better for us to implement a custom deserialze
223    // and serialize that combines this and the component type. This is good enough for first draft
224    #[serde(flatten)]
225    pub properties: Properties,
226    /// A list of various traits assigned to this component
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub traits: Option<Vec<Trait>>,
229}
230
231impl Component {
232    fn secrets(&self) -> Vec<SecretProperty> {
233        let mut secrets = Vec::new();
234        if let Some(traits) = self.traits.as_ref() {
235            let l: Vec<SecretProperty> = traits
236                .iter()
237                .filter_map(|t| {
238                    if let TraitProperty::Link(link) = &t.properties {
239                        let mut tgt_iter = link.target.secrets.clone();
240                        if let Some(src) = &link.source {
241                            tgt_iter.extend(src.secrets.clone());
242                        }
243                        Some(tgt_iter)
244                    } else {
245                        None
246                    }
247                })
248                .flatten()
249                .collect();
250            secrets.extend(l);
251        };
252
253        match &self.properties {
254            Properties::Component { properties } => {
255                secrets.extend(properties.secrets.clone());
256            }
257            Properties::Capability { properties } => secrets.extend(properties.secrets.clone()),
258        };
259        secrets
260    }
261
262    /// Returns only links in the component
263    fn links(&self) -> impl Iterator<Item = &Trait> {
264        self.traits.iter().flatten().filter(|t| t.is_link())
265    }
266}
267
268/// Properties that can be defined for a component
269#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
270#[serde(tag = "type")]
271pub enum Properties {
272    #[serde(rename = "component", alias = "actor")]
273    Component { properties: ComponentProperties },
274    #[serde(rename = "capability")]
275    Capability { properties: CapabilityProperties },
276}
277
278#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
279#[serde(deny_unknown_fields)]
280pub struct ComponentProperties {
281    /// The image reference to use. Required unless the component is a shared component
282    /// that is defined in another shared application.
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub image: Option<String>,
285    /// Information to locate a component within a shared application. Cannot be specified
286    /// if the image is specified.
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub application: Option<SharedApplicationComponentProperties>,
289    /// The component ID to use for this component. If not supplied, it will be generated
290    /// as a combination of the [Metadata::name] and the image reference.
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub id: Option<String>,
293    /// Named configuration to pass to the component. The component will be able to retrieve
294    /// these values at runtime using `wasi:runtime/config.`
295    #[serde(default, skip_serializing_if = "Vec::is_empty")]
296    pub config: Vec<ConfigProperty>,
297    /// Named secret references to pass to the component. The component will be able to retrieve
298    /// these values at runtime using `wasmcloud:secrets/store`.
299    #[serde(default, skip_serializing_if = "Vec::is_empty")]
300    pub secrets: Vec<SecretProperty>,
301}
302
303#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default, ToSchema, JsonSchema)]
304pub struct ConfigDefinition {
305    #[serde(default, skip_serializing_if = "Vec::is_empty")]
306    pub config: Vec<ConfigProperty>,
307    #[serde(default, skip_serializing_if = "Vec::is_empty")]
308    pub secrets: Vec<SecretProperty>,
309}
310
311#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, ToSchema, JsonSchema)]
312pub struct SecretProperty {
313    /// The name of the secret. This is used by a reference by the component or capability to
314    /// get the secret value as a resource.
315    pub name: String,
316    /// The properties of the secret that indicate how to retrieve the secret value from a secrets
317    /// backend and which backend to actually query.
318    pub properties: SecretSourceProperty,
319}
320
321#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, ToSchema, JsonSchema)]
322pub struct SecretSourceProperty {
323    /// The policy to use for retrieving the secret.
324    pub policy: String,
325    /// The key to use for retrieving the secret from the backend.
326    pub key: String,
327    /// The field to use for retrieving the secret from the backend. This is optional and can be
328    /// used to retrieve a specific field from a secret.
329    #[serde(default, skip_serializing_if = "Option::is_none")]
330    pub field: Option<String>,
331    /// The version of the secret to retrieve. If not supplied, the latest version will be used.
332    #[serde(default, skip_serializing_if = "Option::is_none")]
333    pub version: Option<String>,
334}
335
336#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
337#[serde(deny_unknown_fields)]
338pub struct CapabilityProperties {
339    /// The image reference to use. Required unless the component is a shared component
340    /// that is defined in another shared application.
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub image: Option<String>,
343    /// Information to locate a component within a shared application. Cannot be specified
344    /// if the image is specified.
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub application: Option<SharedApplicationComponentProperties>,
347    /// The component ID to use for this provider. If not supplied, it will be generated
348    /// as a combination of the [Metadata::name] and the image reference.
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub id: Option<String>,
351    /// Named configuration to pass to the provider. The merged set of configuration will be passed
352    /// to the provider at runtime using the provider SDK's `init()` function.
353    #[serde(default, skip_serializing_if = "Vec::is_empty")]
354    pub config: Vec<ConfigProperty>,
355    /// Named secret references to pass to the t. The provider will be able to retrieve
356    /// these values at runtime using `wasmcloud:secrets/store`.
357    #[serde(default, skip_serializing_if = "Vec::is_empty")]
358    pub secrets: Vec<SecretProperty>,
359}
360
361#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
362pub struct SharedApplicationComponentProperties {
363    /// The name of the shared application
364    pub name: String,
365    /// The name of the component in the shared application
366    pub component: String,
367}
368
369#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
370#[serde(deny_unknown_fields)]
371pub struct Trait {
372    /// The type of trait specified. This should be a unique string for the type of scaler. As we
373    /// plan on supporting custom scalers, these traits are not enumerated
374    #[serde(rename = "type")]
375    pub trait_type: String,
376    /// The properties of this trait
377    pub properties: TraitProperty,
378}
379
380impl Trait {
381    /// Helper that creates a new linkdef type trait with the given properties
382    pub fn new_link(props: LinkProperty) -> Trait {
383        Trait {
384            trait_type: LINK_TRAIT.to_owned(),
385            properties: TraitProperty::Link(props),
386        }
387    }
388
389    /// Check if a trait is a link
390    pub fn is_link(&self) -> bool {
391        self.trait_type == LINK_TRAIT
392    }
393
394    /// Check if a trait is a scaler
395    pub fn is_scaler(&self) -> bool {
396        self.trait_type == SPREADSCALER_TRAIT || self.trait_type == DAEMONSCALER_TRAIT
397    }
398
399    /// Helper that creates a new spreadscaler type trait with the given properties
400    pub fn new_spreadscaler(props: SpreadScalerProperty) -> Trait {
401        Trait {
402            trait_type: SPREADSCALER_TRAIT.to_owned(),
403            properties: TraitProperty::SpreadScaler(props),
404        }
405    }
406
407    pub fn new_daemonscaler(props: SpreadScalerProperty) -> Trait {
408        Trait {
409            trait_type: DAEMONSCALER_TRAIT.to_owned(),
410            properties: TraitProperty::SpreadScaler(props),
411        }
412    }
413}
414
415/// Properties for defining traits
416#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
417#[serde(untagged)]
418#[allow(clippy::large_enum_variant)]
419pub enum TraitProperty {
420    Link(LinkProperty),
421    SpreadScaler(SpreadScalerProperty),
422    // TODO(thomastaylor312): This is still broken right now with deserializing. If the incoming
423    // type specifies instances, it matches with spreadscaler first. So we need to implement a custom
424    // parser here
425    Custom(serde_json::Value),
426}
427
428impl From<LinkProperty> for TraitProperty {
429    fn from(value: LinkProperty) -> Self {
430        Self::Link(value)
431    }
432}
433
434impl From<SpreadScalerProperty> for TraitProperty {
435    fn from(value: SpreadScalerProperty) -> Self {
436        Self::SpreadScaler(value)
437    }
438}
439
440// impl From<serde_json::Value> for TraitProperty {
441//     fn from(value: serde_json::Value) -> Self {
442//         Self::Custom(value)
443//     }
444// }
445
446/// Properties for the config list associated with components, providers, and links
447///
448/// ## Usage
449/// Defining a config block, like so:
450/// ```yaml
451/// source_config:
452/// - name: "external-secret-kv"
453/// - name: "default-port"
454///   properties:
455///      port: "8080"
456/// ```
457///
458/// Will result in two config scalers being created, one with the name `basic-kv` and one with the
459/// name `default-port`. Wadm will not resolve collisions with configuration names between manifests.
460#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
461#[serde(deny_unknown_fields)]
462pub struct ConfigProperty {
463    /// Name of the config to ensure exists
464    pub name: String,
465    /// Optional properties to put with the configuration. If the properties are
466    /// omitted in the manifest, wadm will assume that the configuration is externally managed
467    /// and will not attempt to create it, only reporting the status as failed if not found.
468    #[serde(skip_serializing_if = "Option::is_none")]
469    pub properties: Option<HashMap<String, String>>,
470}
471
472/// This impl is a helper to help compare a `Vec<String>` to a `Vec<ConfigProperty>`
473impl PartialEq<ConfigProperty> for String {
474    fn eq(&self, other: &ConfigProperty) -> bool {
475        self == &other.name
476    }
477}
478
479/// Properties for links
480#[derive(Debug, Serialize, Clone, PartialEq, Eq, ToSchema, JsonSchema, Default)]
481#[serde(deny_unknown_fields)]
482pub struct LinkProperty {
483    /// WIT namespace for the link
484    pub namespace: String,
485    /// WIT package for the link
486    pub package: String,
487    /// WIT interfaces for the link
488    pub interfaces: Vec<String>,
489    /// Configuration to apply to the source of the link
490    #[serde(default, skip_serializing_if = "Option::is_none")]
491    pub source: Option<ConfigDefinition>,
492    /// Configuration to apply to the target of the link
493    pub target: TargetConfig,
494    /// The name of this link
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub name: Option<String>,
497
498    #[serde(default, skip_serializing)]
499    #[deprecated(since = "0.13.0")]
500    pub source_config: Option<Vec<ConfigProperty>>,
501
502    #[serde(default, skip_serializing)]
503    #[deprecated(since = "0.13.0")]
504    pub target_config: Option<Vec<ConfigProperty>>,
505}
506
507impl<'de> Deserialize<'de> for LinkProperty {
508    fn deserialize<D>(d: D) -> Result<Self, D::Error>
509    where
510        D: serde::Deserializer<'de>,
511    {
512        let json = serde_json::value::Value::deserialize(d)?;
513        let mut target = TargetConfig::default();
514        let mut source = None;
515
516        // Handling the old configuration -- translate to a TargetConfig
517        if let Some(t) = json.get("target") {
518            if t.is_string() {
519                let name = t.as_str().unwrap();
520                let mut tgt = vec![];
521                if let Some(tgt_config) = json.get("target_config") {
522                    tgt = serde_json::from_value(tgt_config.clone()).map_err(de::Error::custom)?;
523                }
524                target = TargetConfig {
525                    name: name.to_string(),
526                    config: tgt,
527                    secrets: vec![],
528                };
529            } else {
530                // Otherwise handle normally
531                target =
532                    serde_json::from_value(json["target"].clone()).map_err(de::Error::custom)?;
533            }
534        }
535
536        if let Some(s) = json.get("source_config") {
537            let src: Vec<ConfigProperty> =
538                serde_json::from_value(s.clone()).map_err(de::Error::custom)?;
539            source = Some(ConfigDefinition {
540                config: src,
541                secrets: vec![],
542            });
543        }
544
545        // If the source block is present then it takes priority
546        if let Some(s) = json.get("source") {
547            source = Some(serde_json::from_value(s.clone()).map_err(de::Error::custom)?);
548        }
549
550        // Validate that the required keys are all present
551        if json.get("namespace").is_none() {
552            return Err(de::Error::custom("namespace is required"));
553        }
554
555        if json.get("package").is_none() {
556            return Err(de::Error::custom("package is required"));
557        }
558
559        if json.get("interfaces").is_none() {
560            return Err(de::Error::custom("interfaces is required"));
561        }
562
563        Ok(LinkProperty {
564            namespace: json["namespace"].as_str().unwrap().to_string(),
565            package: json["package"].as_str().unwrap().to_string(),
566            interfaces: json["interfaces"]
567                .as_array()
568                .unwrap()
569                .iter()
570                .map(|v| v.as_str().unwrap().to_string())
571                .collect(),
572            source,
573            target,
574            name: json.get("name").map(|v| v.as_str().unwrap().to_string()),
575            ..Default::default()
576        })
577    }
578}
579
580#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default, ToSchema, JsonSchema)]
581pub struct TargetConfig {
582    /// The target this link applies to. This should be the name of a component in the manifest
583    pub name: String,
584    #[serde(default, skip_serializing_if = "Vec::is_empty")]
585    pub config: Vec<ConfigProperty>,
586    #[serde(default, skip_serializing_if = "Vec::is_empty")]
587    pub secrets: Vec<SecretProperty>,
588}
589
590impl PartialEq<TargetConfig> for String {
591    fn eq(&self, other: &TargetConfig) -> bool {
592        self == &other.name
593    }
594}
595
596/// Properties for spread scalers
597#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
598#[serde(deny_unknown_fields)]
599pub struct SpreadScalerProperty {
600    /// Number of instances to spread across matching requirements
601    #[serde(alias = "replicas")]
602    pub instances: usize,
603    /// Requirements for spreading those instances
604    #[serde(default, skip_serializing_if = "Vec::is_empty")]
605    pub spread: Vec<Spread>,
606}
607
608/// Configuration for various spreading requirements
609#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
610#[serde(deny_unknown_fields)]
611pub struct Spread {
612    /// The name of this spread requirement
613    pub name: String,
614    /// An arbitrary map of labels to match on for scaling requirements
615    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
616    pub requirements: BTreeMap<String, String>,
617    /// An optional weight for this spread. Higher weights are given more precedence
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub weight: Option<usize>,
620}
621
622impl Default for Spread {
623    fn default() -> Self {
624        Spread {
625            name: "default".to_string(),
626            requirements: BTreeMap::default(),
627            weight: None,
628        }
629    }
630}
631
632#[cfg(test)]
633mod test {
634    use std::io::BufReader;
635    use std::path::Path;
636
637    use anyhow::Result;
638
639    use super::*;
640
641    pub(crate) fn deserialize_yaml(filepath: impl AsRef<Path>) -> Result<Manifest> {
642        let file = std::fs::File::open(filepath)?;
643        let reader = BufReader::new(file);
644        let yaml_string: Manifest = serde_yaml::from_reader(reader)?;
645        Ok(yaml_string)
646    }
647
648    pub(crate) fn deserialize_json(filepath: impl AsRef<Path>) -> Result<Manifest> {
649        let file = std::fs::File::open(filepath)?;
650        let reader = BufReader::new(file);
651        let json_string: Manifest = serde_json::from_reader(reader)?;
652        Ok(json_string)
653    }
654
655    #[test]
656    fn test_oam_deserializer() {
657        let res = deserialize_json("../../oam/simple1.json");
658        match res {
659            Ok(parse_results) => parse_results,
660            Err(error) => panic!("Error {:?}", error),
661        };
662
663        let res = deserialize_yaml("../../oam/simple1.yaml");
664        match res {
665            Ok(parse_results) => parse_results,
666            Err(error) => panic!("Error {:?}", error),
667        };
668    }
669
670    #[test]
671    #[ignore] // see TODO in TraitProperty enum
672    fn test_custom_traits() {
673        let manifest = deserialize_yaml("../../oam/custom.yaml").expect("Should be able to parse");
674        let component = manifest
675            .spec
676            .components
677            .into_iter()
678            .find(|comp| matches!(comp.properties, Properties::Component { .. }))
679            .expect("Should be able to find component");
680        let traits = component.traits.expect("Should have Vec of traits");
681        assert!(
682            traits
683                .iter()
684                .any(|t| matches!(t.properties, TraitProperty::Custom(_))),
685            "Should have found custom property trait: {traits:?}"
686        );
687    }
688
689    #[test]
690    fn test_config() {
691        let manifest = deserialize_yaml("../../oam/config.yaml").expect("Should be able to parse");
692        let props = match &manifest.spec.components[0].properties {
693            Properties::Component { properties } => properties,
694            _ => panic!("Should have found capability component"),
695        };
696
697        assert_eq!(props.config.len(), 1, "Should have found a config property");
698        let config_property = props.config.first().expect("Should have a config property");
699        assert!(config_property.name == "component_config");
700        assert!(config_property
701            .properties
702            .as_ref()
703            .is_some_and(|p| p.get("lang").is_some_and(|v| v == "EN-US")));
704
705        let props = match &manifest.spec.components[1].properties {
706            Properties::Capability { properties } => properties,
707            _ => panic!("Should have found capability component"),
708        };
709
710        assert_eq!(props.config.len(), 1, "Should have found a config property");
711        let config_property = props.config.first().expect("Should have a config property");
712        assert!(config_property.name == "provider_config");
713        assert!(config_property
714            .properties
715            .as_ref()
716            .is_some_and(|p| p.get("default-port").is_some_and(|v| v == "8080")));
717        assert!(config_property.properties.as_ref().is_some_and(|p| p
718            .get("cache_file")
719            .is_some_and(|v| v == "/tmp/mycache.json")));
720    }
721
722    #[test]
723    fn test_component_matching() {
724        let manifest = deserialize_yaml("../../oam/simple2.yaml").expect("Should be able to parse");
725        assert_eq!(
726            manifest
727                .spec
728                .components
729                .iter()
730                .filter(|component| matches!(component.properties, Properties::Component { .. }))
731                .count(),
732            1,
733            "Should have found 1 component property"
734        );
735        assert_eq!(
736            manifest
737                .spec
738                .components
739                .iter()
740                .filter(|component| matches!(component.properties, Properties::Capability { .. }))
741                .count(),
742            2,
743            "Should have found 2 capability properties"
744        );
745    }
746
747    #[test]
748    fn test_trait_matching() {
749        let manifest = deserialize_yaml("../../oam/simple2.yaml").expect("Should be able to parse");
750        // Validate component traits
751        let traits = manifest
752            .spec
753            .components
754            .clone()
755            .into_iter()
756            .find(|component| matches!(component.properties, Properties::Component { .. }))
757            .expect("Should find component component")
758            .traits
759            .expect("Should have traits object");
760        assert_eq!(traits.len(), 1, "Should have 1 trait");
761        assert!(
762            matches!(traits[0].properties, TraitProperty::SpreadScaler(_)),
763            "Should have spreadscaler properties"
764        );
765        // Validate capability component traits
766        let traits = manifest
767            .spec
768            .components
769            .into_iter()
770            .find(|component| {
771                matches!(
772                    &component.properties,
773                    Properties::Capability {
774                        properties: CapabilityProperties { image, .. }
775                    } if image.clone().expect("image to be present") == "wasmcloud.azurecr.io/httpserver:0.13.1"
776                )
777            })
778            .expect("Should find capability component")
779            .traits
780            .expect("Should have traits object");
781        assert_eq!(traits.len(), 1, "Should have 1 trait");
782        assert!(
783            matches!(traits[0].properties, TraitProperty::Link(_)),
784            "Should have link property"
785        );
786        if let TraitProperty::Link(ld) = &traits[0].properties {
787            assert_eq!(ld.source.as_ref().unwrap().config, vec![]);
788            assert_eq!(ld.target.name, "userinfo".to_string());
789        } else {
790            panic!("trait property was not a link definition");
791        }
792    }
793
794    #[test]
795    fn test_oam_serializer() {
796        let mut spread_vec: Vec<Spread> = Vec::new();
797        let spread_item = Spread {
798            name: "eastcoast".to_string(),
799            requirements: BTreeMap::from([("zone".to_string(), "us-east-1".to_string())]),
800            weight: Some(80),
801        };
802        spread_vec.push(spread_item);
803        let spread_item = Spread {
804            name: "westcoast".to_string(),
805            requirements: BTreeMap::from([("zone".to_string(), "us-west-1".to_string())]),
806            weight: Some(20),
807        };
808        spread_vec.push(spread_item);
809        let mut trait_vec: Vec<Trait> = Vec::new();
810        let spreadscalerprop = SpreadScalerProperty {
811            instances: 4,
812            spread: spread_vec,
813        };
814        let trait_item = Trait::new_spreadscaler(spreadscalerprop);
815        trait_vec.push(trait_item);
816        let linkdefprop = LinkProperty {
817            target: TargetConfig {
818                name: "webcap".to_string(),
819                ..Default::default()
820            },
821            namespace: "wasi".to_string(),
822            package: "http".to_string(),
823            interfaces: vec!["incoming-handler".to_string()],
824            source: Some(ConfigDefinition {
825                config: {
826                    vec![ConfigProperty {
827                        name: "http".to_string(),
828                        properties: Some(HashMap::from([("port".to_string(), "8080".to_string())])),
829                    }]
830                },
831                ..Default::default()
832            }),
833            name: Some("default".to_string()),
834            ..Default::default()
835        };
836        let trait_item = Trait::new_link(linkdefprop);
837        trait_vec.push(trait_item);
838        let mut component_vec: Vec<Component> = Vec::new();
839        let component_item = Component {
840            name: "userinfo".to_string(),
841            properties: Properties::Component {
842                properties: ComponentProperties {
843                    image: Some("wasmcloud.azurecr.io/fake:1".to_string()),
844                    application: None,
845                    id: None,
846                    config: vec![],
847                    secrets: vec![],
848                },
849            },
850            traits: Some(trait_vec),
851        };
852        component_vec.push(component_item);
853        let component_item = Component {
854            name: "webcap".to_string(),
855            properties: Properties::Capability {
856                properties: CapabilityProperties {
857                    image: Some("wasmcloud.azurecr.io/httpserver:0.13.1".to_string()),
858                    application: None,
859                    id: None,
860                    config: vec![],
861                    secrets: vec![],
862                },
863            },
864            traits: None,
865        };
866        component_vec.push(component_item);
867
868        let mut spread_vec: Vec<Spread> = Vec::new();
869        let spread_item = Spread {
870            name: "haslights".to_string(),
871            requirements: BTreeMap::from([("zone".to_string(), "enabled".to_string())]),
872            weight: Some(DEFAULT_SPREAD_WEIGHT),
873        };
874        spread_vec.push(spread_item);
875        let spreadscalerprop = SpreadScalerProperty {
876            instances: 1,
877            spread: spread_vec,
878        };
879        let mut trait_vec: Vec<Trait> = Vec::new();
880        let trait_item = Trait::new_spreadscaler(spreadscalerprop);
881        trait_vec.push(trait_item);
882        let component_item = Component {
883            name: "ledblinky".to_string(),
884            properties: Properties::Capability {
885                properties: CapabilityProperties {
886                    image: Some("wasmcloud.azurecr.io/ledblinky:0.0.1".to_string()),
887                    application: None,
888                    id: None,
889                    config: vec![],
890                    secrets: vec![],
891                },
892            },
893            traits: Some(trait_vec),
894        };
895        component_vec.push(component_item);
896
897        let spec = Specification {
898            components: component_vec,
899            policies: vec![],
900        };
901        let metadata = Metadata {
902            name: "my-example-app".to_string(),
903            annotations: BTreeMap::from([
904                (VERSION_ANNOTATION_KEY.to_string(), "v0.0.1".to_string()),
905                (
906                    DESCRIPTION_ANNOTATION_KEY.to_string(),
907                    "This is my app".to_string(),
908                ),
909            ]),
910            labels: BTreeMap::from([(
911                "prefix.dns.prefix/name-for_a.123".to_string(),
912                "this is a valid label".to_string(),
913            )]),
914        };
915        let manifest = Manifest {
916            api_version: OAM_VERSION.to_owned(),
917            kind: APPLICATION_KIND.to_owned(),
918            metadata,
919            spec,
920        };
921        let serialized_json =
922            serde_json::to_vec(&manifest).expect("Should be able to serialize JSON");
923
924        let serialized_yaml = serde_yaml::to_string(&manifest)
925            .expect("Should be able to serialize YAML")
926            .into_bytes();
927
928        // Test the round trip back in
929        let json_manifest: Manifest = serde_json::from_slice(&serialized_json)
930            .expect("Should be able to deserialize JSON roundtrip");
931        let yaml_manifest: Manifest = serde_yaml::from_slice(&serialized_yaml)
932            .expect("Should be able to deserialize YAML roundtrip");
933
934        // Make sure the manifests don't contain any custom traits (to test that we aren't parsing
935        // the tagged enum poorly)
936        assert!(
937            !json_manifest
938                .spec
939                .components
940                .into_iter()
941                .any(|component| component
942                    .traits
943                    .unwrap_or_default()
944                    .into_iter()
945                    .any(|t| matches!(t.properties, TraitProperty::Custom(_)))),
946            "Should have found custom properties"
947        );
948
949        assert!(
950            !yaml_manifest
951                .spec
952                .components
953                .into_iter()
954                .any(|component| component
955                    .traits
956                    .unwrap_or_default()
957                    .into_iter()
958                    .any(|t| matches!(t.properties, TraitProperty::Custom(_)))),
959            "Should have found custom properties"
960        );
961    }
962
963    #[test]
964    fn test_deprecated_fields_not_set() {
965        let manifest = deserialize_yaml("../../oam/simple2.yaml").expect("Should be able to parse");
966        // Validate component traits
967        let traits = manifest
968            .spec
969            .components
970            .clone()
971            .into_iter()
972            .filter(|component| matches!(component.name.as_str(), "webcap"))
973            .find(|component| matches!(component.properties, Properties::Capability { .. }))
974            .expect("Should find component component")
975            .traits
976            .expect("Should have traits object");
977        assert_eq!(traits.len(), 1, "Should have 1 trait");
978        if let TraitProperty::Link(ld) = &traits[0].properties {
979            assert_eq!(ld.source.as_ref().unwrap().config, vec![]);
980            #[allow(deprecated)]
981            let source_config = &ld.source_config;
982            assert_eq!(source_config, &None);
983        } else {
984            panic!("trait property was not a link definition");
985        };
986    }
987}