use std::collections::{BTreeMap, HashMap};
use schemars::JsonSchema;
use serde::{de, Deserialize, Serialize};
use utoipa::ToSchema;
pub mod api;
#[cfg(feature = "wit")]
pub mod bindings;
#[cfg(feature = "wit")]
pub use bindings::*;
pub mod validation;
pub const DEFAULT_SPREAD_WEIGHT: usize = 100;
pub const OAM_VERSION: &str = "core.oam.dev/v1beta1";
pub const APPLICATION_KIND: &str = "Application";
pub const VERSION_ANNOTATION_KEY: &str = "version";
pub const DESCRIPTION_ANNOTATION_KEY: &str = "description";
pub const SHARED_ANNOTATION_KEY: &str = "experimental.wasmcloud.dev/shared";
pub const SPREADSCALER_TRAIT: &str = "spreadscaler";
pub const DAEMONSCALER_TRAIT: &str = "daemonscaler";
pub const LINK_TRAIT: &str = "link";
pub const LATEST_VERSION: &str = "latest";
pub const DEFAULT_LINK_NAME: &str = "default";
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Manifest {
#[serde(rename = "apiVersion")]
pub api_version: String,
pub kind: String,
pub metadata: Metadata,
pub spec: Specification,
}
impl Manifest {
pub fn version(&self) -> &str {
self.metadata
.annotations
.get(VERSION_ANNOTATION_KEY)
.map(|v| v.as_str())
.unwrap_or_default()
}
pub fn description(&self) -> Option<&str> {
self.metadata
.annotations
.get(DESCRIPTION_ANNOTATION_KEY)
.map(|v| v.as_str())
}
pub fn shared(&self) -> bool {
self.metadata
.annotations
.get(SHARED_ANNOTATION_KEY)
.is_some_and(|v| v.parse::<bool>().unwrap_or(false))
}
pub fn components(&self) -> impl Iterator<Item = &Component> {
self.spec.components.iter()
}
pub fn missing_shared_components(&self, deployed_apps: &[&Manifest]) -> Vec<&Component> {
self.spec
.components
.iter()
.filter(|shared_component| {
match &shared_component.properties {
Properties::Capability {
properties:
CapabilityProperties {
image: None,
application: Some(shared_app),
..
},
}
| Properties::Component {
properties:
ComponentProperties {
image: None,
application: Some(shared_app),
..
},
} => {
if deployed_apps.iter().filter(|a| a.shared()).any(|m| {
m.metadata.name == shared_app.name
&& m.components().any(|c| {
c.name == shared_app.component
&& std::mem::discriminant(&c.properties)
== std::mem::discriminant(&shared_component.properties)
})
}) {
false
} else {
true
}
}
_ => false,
}
})
.collect()
}
pub fn wasm_components(&self) -> impl Iterator<Item = &Component> {
self.components()
.filter(|c| matches!(c.properties, Properties::Component { .. }))
}
pub fn capability_providers(&self) -> impl Iterator<Item = &Component> {
self.components()
.filter(|c| matches!(c.properties, Properties::Capability { .. }))
}
pub fn component_lookup(&self) -> HashMap<&String, &Component> {
self.components()
.map(|c| (&c.name, c))
.collect::<HashMap<&String, &Component>>()
}
pub fn links(&self) -> impl Iterator<Item = &Trait> {
self.components()
.flat_map(|c| c.traits.as_ref())
.flatten()
.filter(|t| t.is_link())
}
pub fn policies(&self) -> impl Iterator<Item = &Policy> {
self.spec.policies.iter()
}
pub fn policy_lookup(&self) -> HashMap<&String, &Policy> {
self.spec
.policies
.iter()
.map(|p| (&p.name, p))
.collect::<HashMap<&String, &Policy>>()
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
pub struct Metadata {
pub name: String,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub annotations: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub labels: BTreeMap<String, String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
pub struct Specification {
pub components: Vec<Component>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub policies: Vec<Policy>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
pub struct Policy {
pub name: String,
pub properties: BTreeMap<String, String>,
#[serde(rename = "type")]
pub policy_type: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
pub struct Component {
pub name: String,
#[serde(flatten)]
pub properties: Properties,
#[serde(skip_serializing_if = "Option::is_none")]
pub traits: Option<Vec<Trait>>,
}
impl Component {
fn secrets(&self) -> Vec<SecretProperty> {
let mut secrets = Vec::new();
if let Some(traits) = self.traits.as_ref() {
let l: Vec<SecretProperty> = traits
.iter()
.filter_map(|t| {
if let TraitProperty::Link(link) = &t.properties {
let mut tgt_iter = link.target.secrets.clone();
if let Some(src) = &link.source {
tgt_iter.extend(src.secrets.clone());
}
Some(tgt_iter)
} else {
None
}
})
.flatten()
.collect();
secrets.extend(l);
};
match &self.properties {
Properties::Component { properties } => {
secrets.extend(properties.secrets.clone());
}
Properties::Capability { properties } => secrets.extend(properties.secrets.clone()),
};
secrets
}
fn links(&self) -> impl Iterator<Item = &Trait> {
self.traits.iter().flatten().filter(|t| t.is_link())
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
#[serde(tag = "type")]
pub enum Properties {
#[serde(rename = "component", alias = "actor")]
Component { properties: ComponentProperties },
#[serde(rename = "capability")]
Capability { properties: CapabilityProperties },
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ComponentProperties {
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub application: Option<SharedApplicationComponentProperties>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub config: Vec<ConfigProperty>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub secrets: Vec<SecretProperty>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default, ToSchema, JsonSchema)]
pub struct ConfigDefinition {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub config: Vec<ConfigProperty>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub secrets: Vec<SecretProperty>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, ToSchema, JsonSchema)]
pub struct SecretProperty {
pub name: String,
pub properties: SecretSourceProperty,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, ToSchema, JsonSchema)]
pub struct SecretSourceProperty {
pub policy: String,
pub key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub field: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CapabilityProperties {
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub application: Option<SharedApplicationComponentProperties>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub config: Vec<ConfigProperty>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub secrets: Vec<SecretProperty>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
pub struct SharedApplicationComponentProperties {
pub name: String,
pub component: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Trait {
#[serde(rename = "type")]
pub trait_type: String,
pub properties: TraitProperty,
}
impl Trait {
pub fn new_link(props: LinkProperty) -> Trait {
Trait {
trait_type: LINK_TRAIT.to_owned(),
properties: TraitProperty::Link(props),
}
}
pub fn is_link(&self) -> bool {
self.trait_type == LINK_TRAIT
}
pub fn is_scaler(&self) -> bool {
self.trait_type == SPREADSCALER_TRAIT || self.trait_type == DAEMONSCALER_TRAIT
}
pub fn new_spreadscaler(props: SpreadScalerProperty) -> Trait {
Trait {
trait_type: SPREADSCALER_TRAIT.to_owned(),
properties: TraitProperty::SpreadScaler(props),
}
}
pub fn new_daemonscaler(props: SpreadScalerProperty) -> Trait {
Trait {
trait_type: DAEMONSCALER_TRAIT.to_owned(),
properties: TraitProperty::SpreadScaler(props),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum TraitProperty {
Link(LinkProperty),
SpreadScaler(SpreadScalerProperty),
Custom(serde_json::Value),
}
impl From<LinkProperty> for TraitProperty {
fn from(value: LinkProperty) -> Self {
Self::Link(value)
}
}
impl From<SpreadScalerProperty> for TraitProperty {
fn from(value: SpreadScalerProperty) -> Self {
Self::SpreadScaler(value)
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ConfigProperty {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<HashMap<String, String>>,
}
impl PartialEq<ConfigProperty> for String {
fn eq(&self, other: &ConfigProperty) -> bool {
self == &other.name
}
}
#[derive(Debug, Serialize, Clone, PartialEq, Eq, ToSchema, JsonSchema, Default)]
#[serde(deny_unknown_fields)]
pub struct LinkProperty {
pub namespace: String,
pub package: String,
pub interfaces: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<ConfigDefinition>,
pub target: TargetConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing)]
#[deprecated(since = "0.13.0")]
pub source_config: Option<Vec<ConfigProperty>>,
#[serde(default, skip_serializing)]
#[deprecated(since = "0.13.0")]
pub target_config: Option<Vec<ConfigProperty>>,
}
impl<'de> Deserialize<'de> for LinkProperty {
fn deserialize<D>(d: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let json = serde_json::value::Value::deserialize(d)?;
let mut target = TargetConfig::default();
let mut source = None;
if let Some(t) = json.get("target") {
if t.is_string() {
let name = t.as_str().unwrap();
let mut tgt = vec![];
if let Some(tgt_config) = json.get("target_config") {
tgt = serde_json::from_value(tgt_config.clone()).map_err(de::Error::custom)?;
}
target = TargetConfig {
name: name.to_string(),
config: tgt,
secrets: vec![],
};
} else {
target =
serde_json::from_value(json["target"].clone()).map_err(de::Error::custom)?;
}
}
if let Some(s) = json.get("source_config") {
let src: Vec<ConfigProperty> =
serde_json::from_value(s.clone()).map_err(de::Error::custom)?;
source = Some(ConfigDefinition {
config: src,
secrets: vec![],
});
}
if let Some(s) = json.get("source") {
source = Some(serde_json::from_value(s.clone()).map_err(de::Error::custom)?);
}
if json.get("namespace").is_none() {
return Err(de::Error::custom("namespace is required"));
}
if json.get("package").is_none() {
return Err(de::Error::custom("package is required"));
}
if json.get("interfaces").is_none() {
return Err(de::Error::custom("interfaces is required"));
}
Ok(LinkProperty {
namespace: json["namespace"].as_str().unwrap().to_string(),
package: json["package"].as_str().unwrap().to_string(),
interfaces: json["interfaces"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect(),
source,
target,
name: json.get("name").map(|v| v.as_str().unwrap().to_string()),
..Default::default()
})
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default, ToSchema, JsonSchema)]
pub struct TargetConfig {
pub name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub config: Vec<ConfigProperty>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub secrets: Vec<SecretProperty>,
}
impl PartialEq<TargetConfig> for String {
fn eq(&self, other: &TargetConfig) -> bool {
self == &other.name
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SpreadScalerProperty {
#[serde(alias = "replicas")]
pub instances: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub spread: Vec<Spread>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Spread {
pub name: String,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub requirements: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub weight: Option<usize>,
}
impl Default for Spread {
fn default() -> Self {
Spread {
name: "default".to_string(),
requirements: BTreeMap::default(),
weight: None,
}
}
}
#[cfg(test)]
mod test {
use std::io::BufReader;
use std::path::Path;
use anyhow::Result;
use super::*;
pub(crate) fn deserialize_yaml(filepath: impl AsRef<Path>) -> Result<Manifest> {
let file = std::fs::File::open(filepath)?;
let reader = BufReader::new(file);
let yaml_string: Manifest = serde_yaml::from_reader(reader)?;
Ok(yaml_string)
}
pub(crate) fn deserialize_json(filepath: impl AsRef<Path>) -> Result<Manifest> {
let file = std::fs::File::open(filepath)?;
let reader = BufReader::new(file);
let json_string: Manifest = serde_json::from_reader(reader)?;
Ok(json_string)
}
#[test]
fn test_oam_deserializer() {
let res = deserialize_json("../../oam/simple1.json");
match res {
Ok(parse_results) => parse_results,
Err(error) => panic!("Error {:?}", error),
};
let res = deserialize_yaml("../../oam/simple1.yaml");
match res {
Ok(parse_results) => parse_results,
Err(error) => panic!("Error {:?}", error),
};
}
#[test]
#[ignore] fn test_custom_traits() {
let manifest = deserialize_yaml("../../oam/custom.yaml").expect("Should be able to parse");
let component = manifest
.spec
.components
.into_iter()
.find(|comp| matches!(comp.properties, Properties::Component { .. }))
.expect("Should be able to find component");
let traits = component.traits.expect("Should have Vec of traits");
assert!(
traits
.iter()
.any(|t| matches!(t.properties, TraitProperty::Custom(_))),
"Should have found custom property trait: {traits:?}"
);
}
#[test]
fn test_config() {
let manifest = deserialize_yaml("../../oam/config.yaml").expect("Should be able to parse");
let props = match &manifest.spec.components[0].properties {
Properties::Component { properties } => properties,
_ => panic!("Should have found capability component"),
};
assert_eq!(props.config.len(), 1, "Should have found a config property");
let config_property = props.config.first().expect("Should have a config property");
assert!(config_property.name == "component_config");
assert!(config_property
.properties
.as_ref()
.is_some_and(|p| p.get("lang").is_some_and(|v| v == "EN-US")));
let props = match &manifest.spec.components[1].properties {
Properties::Capability { properties } => properties,
_ => panic!("Should have found capability component"),
};
assert_eq!(props.config.len(), 1, "Should have found a config property");
let config_property = props.config.first().expect("Should have a config property");
assert!(config_property.name == "provider_config");
assert!(config_property
.properties
.as_ref()
.is_some_and(|p| p.get("default-port").is_some_and(|v| v == "8080")));
assert!(config_property.properties.as_ref().is_some_and(|p| p
.get("cache_file")
.is_some_and(|v| v == "/tmp/mycache.json")));
}
#[test]
fn test_component_matching() {
let manifest = deserialize_yaml("../../oam/simple2.yaml").expect("Should be able to parse");
assert_eq!(
manifest
.spec
.components
.iter()
.filter(|component| matches!(component.properties, Properties::Component { .. }))
.count(),
1,
"Should have found 1 component property"
);
assert_eq!(
manifest
.spec
.components
.iter()
.filter(|component| matches!(component.properties, Properties::Capability { .. }))
.count(),
2,
"Should have found 2 capability properties"
);
}
#[test]
fn test_trait_matching() {
let manifest = deserialize_yaml("../../oam/simple2.yaml").expect("Should be able to parse");
let traits = manifest
.spec
.components
.clone()
.into_iter()
.find(|component| matches!(component.properties, Properties::Component { .. }))
.expect("Should find component component")
.traits
.expect("Should have traits object");
assert_eq!(traits.len(), 1, "Should have 1 trait");
assert!(
matches!(traits[0].properties, TraitProperty::SpreadScaler(_)),
"Should have spreadscaler properties"
);
let traits = manifest
.spec
.components
.into_iter()
.find(|component| {
matches!(
&component.properties,
Properties::Capability {
properties: CapabilityProperties { image, .. }
} if image.clone().expect("image to be present") == "wasmcloud.azurecr.io/httpserver:0.13.1"
)
})
.expect("Should find capability component")
.traits
.expect("Should have traits object");
assert_eq!(traits.len(), 1, "Should have 1 trait");
assert!(
matches!(traits[0].properties, TraitProperty::Link(_)),
"Should have link property"
);
if let TraitProperty::Link(ld) = &traits[0].properties {
assert_eq!(ld.source.as_ref().unwrap().config, vec![]);
assert_eq!(ld.target.name, "userinfo".to_string());
} else {
panic!("trait property was not a link definition");
}
}
#[test]
fn test_oam_serializer() {
let mut spread_vec: Vec<Spread> = Vec::new();
let spread_item = Spread {
name: "eastcoast".to_string(),
requirements: BTreeMap::from([("zone".to_string(), "us-east-1".to_string())]),
weight: Some(80),
};
spread_vec.push(spread_item);
let spread_item = Spread {
name: "westcoast".to_string(),
requirements: BTreeMap::from([("zone".to_string(), "us-west-1".to_string())]),
weight: Some(20),
};
spread_vec.push(spread_item);
let mut trait_vec: Vec<Trait> = Vec::new();
let spreadscalerprop = SpreadScalerProperty {
instances: 4,
spread: spread_vec,
};
let trait_item = Trait::new_spreadscaler(spreadscalerprop);
trait_vec.push(trait_item);
let linkdefprop = LinkProperty {
target: TargetConfig {
name: "webcap".to_string(),
..Default::default()
},
namespace: "wasi".to_string(),
package: "http".to_string(),
interfaces: vec!["incoming-handler".to_string()],
source: Some(ConfigDefinition {
config: {
vec![ConfigProperty {
name: "http".to_string(),
properties: Some(HashMap::from([("port".to_string(), "8080".to_string())])),
}]
},
..Default::default()
}),
name: Some("default".to_string()),
..Default::default()
};
let trait_item = Trait::new_link(linkdefprop);
trait_vec.push(trait_item);
let mut component_vec: Vec<Component> = Vec::new();
let component_item = Component {
name: "userinfo".to_string(),
properties: Properties::Component {
properties: ComponentProperties {
image: Some("wasmcloud.azurecr.io/fake:1".to_string()),
application: None,
id: None,
config: vec![],
secrets: vec![],
},
},
traits: Some(trait_vec),
};
component_vec.push(component_item);
let component_item = Component {
name: "webcap".to_string(),
properties: Properties::Capability {
properties: CapabilityProperties {
image: Some("wasmcloud.azurecr.io/httpserver:0.13.1".to_string()),
application: None,
id: None,
config: vec![],
secrets: vec![],
},
},
traits: None,
};
component_vec.push(component_item);
let mut spread_vec: Vec<Spread> = Vec::new();
let spread_item = Spread {
name: "haslights".to_string(),
requirements: BTreeMap::from([("zone".to_string(), "enabled".to_string())]),
weight: Some(DEFAULT_SPREAD_WEIGHT),
};
spread_vec.push(spread_item);
let spreadscalerprop = SpreadScalerProperty {
instances: 1,
spread: spread_vec,
};
let mut trait_vec: Vec<Trait> = Vec::new();
let trait_item = Trait::new_spreadscaler(spreadscalerprop);
trait_vec.push(trait_item);
let component_item = Component {
name: "ledblinky".to_string(),
properties: Properties::Capability {
properties: CapabilityProperties {
image: Some("wasmcloud.azurecr.io/ledblinky:0.0.1".to_string()),
application: None,
id: None,
config: vec![],
secrets: vec![],
},
},
traits: Some(trait_vec),
};
component_vec.push(component_item);
let spec = Specification {
components: component_vec,
policies: vec![],
};
let metadata = Metadata {
name: "my-example-app".to_string(),
annotations: BTreeMap::from([
(VERSION_ANNOTATION_KEY.to_string(), "v0.0.1".to_string()),
(
DESCRIPTION_ANNOTATION_KEY.to_string(),
"This is my app".to_string(),
),
]),
labels: BTreeMap::from([(
"prefix.dns.prefix/name-for_a.123".to_string(),
"this is a valid label".to_string(),
)]),
};
let manifest = Manifest {
api_version: OAM_VERSION.to_owned(),
kind: APPLICATION_KIND.to_owned(),
metadata,
spec,
};
let serialized_json =
serde_json::to_vec(&manifest).expect("Should be able to serialize JSON");
let serialized_yaml = serde_yaml::to_string(&manifest)
.expect("Should be able to serialize YAML")
.into_bytes();
let json_manifest: Manifest = serde_json::from_slice(&serialized_json)
.expect("Should be able to deserialize JSON roundtrip");
let yaml_manifest: Manifest = serde_yaml::from_slice(&serialized_yaml)
.expect("Should be able to deserialize YAML roundtrip");
assert!(
!json_manifest
.spec
.components
.into_iter()
.any(|component| component
.traits
.unwrap_or_default()
.into_iter()
.any(|t| matches!(t.properties, TraitProperty::Custom(_)))),
"Should have found custom properties"
);
assert!(
!yaml_manifest
.spec
.components
.into_iter()
.any(|component| component
.traits
.unwrap_or_default()
.into_iter()
.any(|t| matches!(t.properties, TraitProperty::Custom(_)))),
"Should have found custom properties"
);
}
#[test]
fn test_deprecated_fields_not_set() {
let manifest = deserialize_yaml("../../oam/simple2.yaml").expect("Should be able to parse");
let traits = manifest
.spec
.components
.clone()
.into_iter()
.filter(|component| matches!(component.name.as_str(), "webcap"))
.find(|component| matches!(component.properties, Properties::Capability { .. }))
.expect("Should find component component")
.traits
.expect("Should have traits object");
assert_eq!(traits.len(), 1, "Should have 1 trait");
if let TraitProperty::Link(ld) = &traits[0].properties {
assert_eq!(ld.source.as_ref().unwrap().config, vec![]);
#[allow(deprecated)]
let source_config = &ld.source_config;
assert_eq!(source_config, &None);
} else {
panic!("trait property was not a link definition");
};
}
}