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
14pub const DEFAULT_SPREAD_WEIGHT: usize = 100;
16pub const OAM_VERSION: &str = "core.oam.dev/v1beta1";
18pub const APPLICATION_KIND: &str = "Application";
22pub const VERSION_ANNOTATION_KEY: &str = "version";
25pub const DESCRIPTION_ANNOTATION_KEY: &str = "description";
28pub const SHARED_ANNOTATION_KEY: &str = "experimental.wasmcloud.dev/shared";
30pub const SPREADSCALER_TRAIT: &str = "spreadscaler";
32pub const DAEMONSCALER_TRAIT: &str = "daemonscaler";
34pub const LINK_TRAIT: &str = "link";
36pub const LATEST_VERSION: &str = "latest";
39pub const DEFAULT_LINK_NAME: &str = "default";
41
42#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
44#[serde(deny_unknown_fields)]
45pub struct Manifest {
46 #[serde(rename = "apiVersion")]
48 pub api_version: String,
49 pub kind: String,
51 pub metadata: Metadata,
53 pub spec: Specification,
55}
56
57impl Manifest {
58 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 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 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 pub fn components(&self) -> impl Iterator<Item = &Component> {
85 self.spec.components.iter()
86 }
87
88 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 && 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 pub fn wasm_components(&self) -> impl Iterator<Item = &Component> {
136 self.components()
137 .filter(|c| matches!(c.properties, Properties::Component { .. }))
138 }
139
140 pub fn capability_providers(&self) -> impl Iterator<Item = &Component> {
142 self.components()
143 .filter(|c| matches!(c.properties, Properties::Capability { .. }))
144 }
145
146 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 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 pub fn policies(&self) -> impl Iterator<Item = &Policy> {
163 self.spec.policies.iter()
164 }
165
166 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#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
178pub struct Metadata {
179 pub name: String,
181 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
183 pub annotations: BTreeMap<String, String>,
184 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
186 pub labels: BTreeMap<String, String>,
187}
188
189#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
191pub struct Specification {
192 pub components: Vec<Component>,
194
195 #[serde(default, skip_serializing_if = "Vec::is_empty")]
199 pub policies: Vec<Policy>,
200}
201
202#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
204pub struct Policy {
205 pub name: String,
207 pub properties: BTreeMap<String, String>,
209 #[serde(rename = "type")]
211 pub policy_type: String,
212}
213
214#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
216pub struct Component {
219 pub name: String,
221 #[serde(flatten)]
225 pub properties: Properties,
226 #[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 fn links(&self) -> impl Iterator<Item = &Trait> {
264 self.traits.iter().flatten().filter(|t| t.is_link())
265 }
266}
267
268#[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 #[serde(skip_serializing_if = "Option::is_none")]
284 pub image: Option<String>,
285 #[serde(skip_serializing_if = "Option::is_none")]
288 pub application: Option<SharedApplicationComponentProperties>,
289 #[serde(skip_serializing_if = "Option::is_none")]
292 pub id: Option<String>,
293 #[serde(default, skip_serializing_if = "Vec::is_empty")]
296 pub config: Vec<ConfigProperty>,
297 #[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 pub name: String,
316 pub properties: SecretSourceProperty,
319}
320
321#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, ToSchema, JsonSchema)]
322pub struct SecretSourceProperty {
323 pub policy: String,
325 pub key: String,
327 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub field: Option<String>,
331 #[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 #[serde(skip_serializing_if = "Option::is_none")]
342 pub image: Option<String>,
343 #[serde(skip_serializing_if = "Option::is_none")]
346 pub application: Option<SharedApplicationComponentProperties>,
347 #[serde(skip_serializing_if = "Option::is_none")]
350 pub id: Option<String>,
351 #[serde(default, skip_serializing_if = "Vec::is_empty")]
354 pub config: Vec<ConfigProperty>,
355 #[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 pub name: String,
365 pub component: String,
367}
368
369#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
370#[serde(deny_unknown_fields)]
371pub struct Trait {
372 #[serde(rename = "type")]
375 pub trait_type: String,
376 pub properties: TraitProperty,
378}
379
380impl Trait {
381 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 pub fn is_link(&self) -> bool {
391 self.trait_type == LINK_TRAIT
392 }
393
394 pub fn is_scaler(&self) -> bool {
396 self.trait_type == SPREADSCALER_TRAIT || self.trait_type == DAEMONSCALER_TRAIT
397 }
398
399 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#[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 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#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
461#[serde(deny_unknown_fields)]
462pub struct ConfigProperty {
463 pub name: String,
465 #[serde(skip_serializing_if = "Option::is_none")]
469 pub properties: Option<HashMap<String, String>>,
470}
471
472impl PartialEq<ConfigProperty> for String {
474 fn eq(&self, other: &ConfigProperty) -> bool {
475 self == &other.name
476 }
477}
478
479#[derive(Debug, Serialize, Clone, PartialEq, Eq, ToSchema, JsonSchema, Default)]
481#[serde(deny_unknown_fields)]
482pub struct LinkProperty {
483 pub namespace: String,
485 pub package: String,
487 pub interfaces: Vec<String>,
489 #[serde(default, skip_serializing_if = "Option::is_none")]
491 pub source: Option<ConfigDefinition>,
492 pub target: TargetConfig,
494 #[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 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 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 let Some(s) = json.get("source") {
547 source = Some(serde_json::from_value(s.clone()).map_err(de::Error::custom)?);
548 }
549
550 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 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#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
598#[serde(deny_unknown_fields)]
599pub struct SpreadScalerProperty {
600 #[serde(alias = "replicas")]
602 pub instances: usize,
603 #[serde(default, skip_serializing_if = "Vec::is_empty")]
605 pub spread: Vec<Spread>,
606}
607
608#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
610#[serde(deny_unknown_fields)]
611pub struct Spread {
612 pub name: String,
614 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
616 pub requirements: BTreeMap<String, String>,
617 #[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] 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 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 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 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 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 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}