use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use crate::{Result, sdf, tf};
use super::{SchemaInfo, SchemaRegistry, Schematics, schema_registry};
#[derive(Debug, Clone, Default)]
pub struct PrimDefinition {
prop_map: HashMap<tf::Token, LayerAndPath>,
properties: Vec<tf::Token>,
applied_api_schemas: Vec<tf::Token>,
composed: Option<sdf::Data>,
}
#[derive(Debug, Clone, Copy)]
pub struct DefProperty<'a> {
definition: &'a PrimDefinition,
entry: &'a LayerAndPath,
}
#[derive(Debug, Clone)]
struct LayerAndPath {
store: DefStore,
schematics: Arc<Schematics>,
path: sdf::Path,
}
#[derive(Debug, Clone)]
enum DefStore {
Schematics,
Composed,
}
#[derive(Debug, Clone, Copy)]
struct Contribution<'a> {
spec: &'a sdf::SpecData,
origin: &'a Arc<Schematics>,
}
pub(super) type FamilyVersions = HashMap<(tf::Token, Option<tf::Token>), u32>;
const PRIM_METADATA: tf::Token = tf::Token::new("");
const COMPOSED_PRIM: &str = "ComposedProperties";
impl PrimDefinition {
pub fn property_names(&self) -> &[tf::Token] {
&self.properties
}
pub fn has_property(&self, name: &tf::Token) -> bool {
self.prop_map.contains_key(name)
}
pub fn applied_api_schemas(&self) -> &[tf::Token] {
&self.applied_api_schemas
}
pub fn is_empty(&self) -> bool {
self.prop_map.is_empty() && self.applied_api_schemas.is_empty()
}
pub fn property(&self, name: &tf::Token) -> Option<DefProperty<'_>> {
if name.as_str().is_empty() {
return None;
}
self.entry(name)
}
pub fn metadata(&self, field: impl AsRef<str>) -> Option<&sdf::Value> {
self.entry(&PRIM_METADATA)?.field(field)
}
pub fn attribute_fallback(&self, name: &tf::Token) -> Option<sdf::Value> {
self.property(name)?.attribute_fallback()
}
fn entry(&self, name: &tf::Token) -> Option<DefProperty<'_>> {
self.prop_map.get(name).map(|entry| DefProperty {
definition: self,
entry,
})
}
pub(super) fn from_class_prim(
schematics: &Arc<Schematics>,
identifier: &tf::Token,
applied_name: Option<tf::Token>,
overrides: &[tf::Token],
) -> Result<PrimDefinition, schema_registry::SchemaRegistryError> {
let path = sdf::Path::abs_root().append_path(identifier.as_str())?;
if schematics.data().spec(&path).is_none() {
return Err(schema_registry::SchemaRegistryError::MissingClassPrim {
identifier: identifier.clone(),
family: schematics.family().clone(),
});
}
let contributes_metadata = applied_name
.as_ref()
.is_none_or(|name| schema_registry::split_instance_name(name).1.is_none());
let mut definition = PrimDefinition {
applied_api_schemas: applied_name.into_iter().collect(),
..PrimDefinition::default()
};
if contributes_metadata {
let entry = LayerAndPath {
store: DefStore::Schematics,
schematics: schematics.clone(),
path: path.clone(),
};
definition.prop_map.insert(PRIM_METADATA, entry);
}
let overrides: HashSet<&tf::Token> = overrides.iter().collect();
for name in child_names(schematics.data(), &path, sdf::ChildrenKey::PropertyChildren) {
if overrides.contains(&name) {
continue;
}
let entry = LayerAndPath {
store: DefStore::Schematics,
schematics: schematics.clone(),
path: path.append_property(name.as_str())?,
};
definition.prop_map.insert(name.clone(), entry);
definition.properties.push(name);
}
definition.apply_property_order();
Ok(definition)
}
pub(super) fn compose_weaker_api(
&mut self,
weaker: &PrimDefinition,
instance: Option<&tf::Token>,
infos: &HashMap<tf::Token, SchemaInfo>,
seen: &mut FamilyVersions,
) {
let names: Vec<tf::Token> = match instance {
Some(instance) => weaker
.applied_api_schemas
.iter()
.map(|name| schema_registry::make_instance_name(name, instance))
.collect(),
None => weaker.applied_api_schemas.clone(),
};
if self.append_api_schemas(names, infos, seen) {
self.compose_properties_from(weaker, instance);
}
}
fn append_api_schemas(
&mut self,
names: Vec<tf::Token>,
infos: &HashMap<tf::Token, SchemaInfo>,
seen: &mut FamilyVersions,
) -> bool {
let start = self.applied_api_schemas.len();
let mut added = Vec::with_capacity(names.len());
for name in names {
let (identifier, instance) = schema_registry::split_instance_name(&name);
let Some(info) = infos.get(&identifier) else {
continue;
};
let key = (info.family().clone(), instance);
match seen.get(&key) {
None => {
seen.insert(key.clone(), info.version());
self.applied_api_schemas.push(name);
added.push(key);
}
Some(&version) if version == info.version() => {}
Some(_) => {
self.applied_api_schemas.truncate(start);
for key in added {
seen.remove(&key);
}
return false;
}
}
}
true
}
fn compose_properties_from(&mut self, weaker: &PrimDefinition, instance: Option<&tf::Token>) {
let mut names: Vec<&tf::Token> = weaker.prop_map.keys().collect();
names.sort_by(|a, b| sdf::element_cmp(a, b));
for name in names {
let instanced = match instance {
Some(instance) => schema_registry::make_instance_name(name, instance),
None => name.clone(),
};
self.add_or_compose_property(instanced, weaker, &weaker.prop_map[name]);
}
}
fn add_or_compose_property(&mut self, name: tf::Token, weaker: &PrimDefinition, entry: &LayerAndPath) {
let is_metadata = name == PRIM_METADATA;
if is_metadata && !self.prop_map.contains_key(&PRIM_METADATA) {
let weak = weaker.snapshot(entry);
let weak = Contribution {
spec: &weak,
origin: &entry.schematics,
};
if let Some(composed) = self.materialize(&name, None, weak) {
self.prop_map.insert(PRIM_METADATA, composed);
}
return;
}
let Some(existing) = self.prop_map.get(&name) else {
let installed = match entry.store {
DefStore::Schematics => entry.clone(),
DefStore::Composed => {
match self.write_composed(&name, &weaker.snapshot(entry), entry.schematics.clone()) {
Some(installed) => installed,
None => return,
}
}
};
self.prop_map.insert(name.clone(), installed);
if !is_metadata {
self.properties.push(name);
}
return;
};
let strong_origin = existing.schematics.clone();
let strong_spec = self.snapshot(existing);
let weak_spec = weaker.snapshot(entry);
let strong = Contribution {
spec: &strong_spec,
origin: &strong_origin,
};
let weak = Contribution {
spec: &weak_spec,
origin: &entry.schematics,
};
if let Some(composed) = self.materialize(&name, Some(strong), weak) {
self.prop_map.insert(name, composed);
}
}
pub(super) fn compose_override(&mut self, name: &tf::Token, schematics: &Arc<Schematics>, class_prim: &sdf::Path) {
let Some(existing) = self.prop_map.get(name) else {
return;
};
let Ok(path) = class_prim.append_property(name.as_str()) else {
return;
};
let mut composed = read_spec(schematics.data(), &path);
let defined = self.snapshot(existing);
if !types_match(&composed, &defined) {
return;
}
let origin = value_origin(
Some(Contribution {
spec: &composed,
origin: schematics,
}),
Contribution {
spec: &defined,
origin: &existing.schematics,
},
);
let variability = defined
.get(sdf::FieldKey::Variability.as_str())
.cloned()
.unwrap_or(sdf::Value::Variability(sdf::Variability::default()));
for (field, value) in defined.fields {
if !composed.contains(&field) {
composed.add(field, value);
}
}
composed.add(sdf::FieldKey::Variability, variability);
if let Some(installed) = self.write_composed(name, &composed, origin) {
self.prop_map.insert(name.clone(), installed);
}
}
fn materialize(
&mut self,
name: &tf::Token,
strong: Option<Contribution<'_>>,
weak: Contribution<'_>,
) -> Option<LayerAndPath> {
let merged = compose_fields(strong.map(|strong| strong.spec), weak.spec)?;
let origin = value_origin(strong, weak);
let mut composed = match strong {
Some(strong) => strong.spec.clone(),
None => sdf::SpecData::new(weak.spec.ty),
};
for (field, value) in merged {
composed.add(field, value);
}
self.write_composed(name, &composed, origin)
}
fn write_composed(
&mut self,
name: &tf::Token,
spec: &sdf::SpecData,
origin: Arc<Schematics>,
) -> Option<LayerAndPath> {
let path = composed_path(name)?;
let composed = self.composed.get_or_insert_with(sdf::Data::default);
*composed.create_spec(path.clone(), spec.ty) = spec.clone();
Some(LayerAndPath {
store: DefStore::Composed,
schematics: origin,
path,
})
}
fn snapshot(&self, entry: &LayerAndPath) -> sdf::SpecData {
match self.store_of(entry) {
Some(store) => read_spec(store, &entry.path),
None => sdf::SpecData::new(sdf::SpecType::default()),
}
}
fn store_of<'a>(&'a self, entry: &'a LayerAndPath) -> Option<&'a sdf::Data> {
match entry.store {
DefStore::Schematics => Some(entry.schematics.data()),
DefStore::Composed => self.composed.as_ref(),
}
}
pub(super) fn finish_composition(&mut self) {
self.properties.sort_by(|a, b| sdf::element_cmp(a, b));
self.apply_property_order();
}
fn apply_property_order(&mut self) {
let Some(order) = self
.metadata(sdf::FieldKey::PropertyOrder)
.and_then(|value| value.clone().try_as_token_vec())
else {
return;
};
sdf::apply_ordering(&mut self.properties, &order);
}
}
impl<'a> DefProperty<'a> {
pub fn spec_type(&self) -> sdf::SpecType {
self.store()
.and_then(|store| store.spec(&self.entry.path))
.map_or_else(sdf::SpecType::default, |spec| spec.ty)
}
pub fn field(&self, name: impl AsRef<str>) -> Option<&'a sdf::Value> {
let name = name.as_ref();
if SchemaRegistry::is_disallowed_field(name) {
return None;
}
self.store()?.spec(&self.entry.path)?.get(name)
}
pub fn type_name(&self) -> Option<sdf::ValueTypeName> {
sdf::ValueTypeName::find(self.type_name_token()?.as_str())
}
pub fn type_name_token(&self) -> Option<tf::Token> {
self.field(sdf::FieldKey::TypeName)?.clone().try_as_token()
}
pub fn variability(&self) -> sdf::Variability {
if let Some(declared) = self
.field(sdf::FieldKey::Variability)
.and_then(|value| value.clone().try_as_variability())
{
return declared;
}
match self.spec_type() {
sdf::SpecType::Relationship => sdf::Variability::Uniform,
_ => sdf::Variability::default(),
}
}
pub fn attribute_fallback(&self) -> Option<sdf::Value> {
if self.spec_type() != sdf::SpecType::Attribute {
return None;
}
self.fallback()
}
pub fn fallback(&self) -> Option<sdf::Value> {
match self.field(sdf::FieldKey::Default)?.clone() {
sdf::Value::ValueBlock | sdf::Value::None => None,
value => Some(value),
}
}
pub fn fallback_source(&self) -> &'a Schematics {
&self.entry.schematics
}
fn store(&self) -> Option<&'a sdf::Data> {
self.definition.store_of(self.entry)
}
}
fn value_origin(strong: Option<Contribution<'_>>, weak: Contribution<'_>) -> Arc<Schematics> {
let authors_default = |spec: &sdf::SpecData| spec.contains(sdf::FieldKey::Default.as_str());
match strong {
Some(strong) if authors_default(strong.spec) => strong.origin.clone(),
_ if authors_default(weak.spec) => weak.origin.clone(),
Some(strong) => strong.origin.clone(),
None => weak.origin.clone(),
}
}
fn read_spec(data: &sdf::Data, path: &sdf::Path) -> sdf::SpecData {
let mut spec = data
.spec(path)
.cloned()
.unwrap_or_else(|| sdf::SpecData::new(sdf::SpecType::default()));
spec.fields
.retain(|(field, _)| !SchemaRegistry::is_disallowed_field(field));
spec
}
pub(super) fn child_names(data: &sdf::Data, path: &sdf::Path, key: sdf::ChildrenKey) -> Vec<tf::Token> {
data.spec(path)
.and_then(|spec| spec.get(key.as_str()))
.and_then(|value| value.clone().try_as_token_vec())
.unwrap_or_default()
}
fn compose_fields(strong: Option<&sdf::SpecData>, weak: &sdf::SpecData) -> Option<Vec<(String, sdf::Value)>> {
if strong.is_some_and(|strong| !types_match(strong, weak)) {
return None;
}
let composing_prim = weak.ty == sdf::SpecType::Prim;
let mut merged = Vec::new();
for (field, weak_value) in &weak.fields {
let mergeable = field == sdf::FieldKey::PropertyOrder.as_str() || weak_value.is_dictionary();
if !mergeable && strong.is_some_and(|strong| strong.contains(field)) {
continue;
}
if field == sdf::FieldKey::Documentation.as_str()
|| (!composing_prim && field == sdf::FieldKey::Custom.as_str())
{
continue;
}
let strong_value = strong.filter(|_| mergeable).and_then(|strong| strong.get(field));
merged.push((field.clone(), merge_over(strong_value, weak_value.clone())));
}
(!merged.is_empty()).then_some(merged)
}
fn merge_over(strong: Option<&sdf::Value>, weak: sdf::Value) -> sdf::Value {
match (strong, weak) {
(Some(sdf::Value::TokenVec(strong)), sdf::Value::TokenVec(weak)) => {
let mut merged = strong.clone();
for token in weak {
if !merged.contains(&token) {
merged.push(token);
}
}
sdf::Value::TokenVec(merged)
}
(Some(sdf::Value::Dictionary(strong)), sdf::Value::Dictionary(weak)) => {
let mut merged = strong.clone();
sdf::dictionary_over(&mut merged, weak);
sdf::Value::Dictionary(merged)
}
(_, weak) => weak,
}
}
fn types_match(strong: &sdf::SpecData, weak: &sdf::SpecData) -> bool {
match strong.ty {
sdf::SpecType::Prim => weak.ty == sdf::SpecType::Prim,
sdf::SpecType::Relationship => weak.ty == sdf::SpecType::Relationship,
_ => {
weak.ty == sdf::SpecType::Attribute
&& strong.get(sdf::FieldKey::TypeName.as_str()) == weak.get(sdf::FieldKey::TypeName.as_str())
}
}
}
fn composed_path(name: &tf::Token) -> Option<sdf::Path> {
let prim = sdf::Path::abs_root().append_path(COMPOSED_PRIM).ok()?;
match name.as_str().is_empty() {
true => Some(prim),
false => prim.append_property(name.as_str()).ok(),
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::usd::SchemaRegistry;
use crate::{sdf, tf};
const MERGE_MANIFEST: &str = r#"#usda 1.0
def "APISchemaBase"
{
uniform token schemaKind = "abstractBase"
}
def "WeakAPI"
{
uniform token schemaKind = "singleApplyAPI"
uniform token[] bases = ["APISchemaBase"]
}
def "Thing"
{
uniform token schemaKind = "concreteTyped"
}
"#;
const MERGE_SCHEMATICS: &str = r#"#usda 1.0
class "APISchemaBase"
{
}
class "WeakAPI" (
documentation = "weak prim docs"
assetInfo = {
string only_weak = "weak"
string both = "weak"
dictionary nested = {
string only_weak = "weak"
string both = "weak"
}
}
)
{
reorder properties = ["shared", "only_weak"]
float shared = 1 (
documentation = "weak property docs"
displayGroup = "weak group"
assetInfo = {
string only_weak = "weak"
string both = "weak"
}
)
token mismatched = "weak"
float only_weak = 3
}
class Thing "Thing" (
apiSchemas = ["WeakAPI"]
documentation = "strong prim docs"
assetInfo = {
string both = "strong"
dictionary nested = {
string both = "strong"
}
}
)
{
reorder properties = ["mismatched"]
float shared (
documentation = "strong property docs"
assetInfo = {
string both = "strong"
}
)
float mismatched = 2
}
"#;
fn merge_registry() -> Arc<SchemaRegistry> {
SchemaRegistry::test_family(MERGE_MANIFEST, MERGE_SCHEMATICS)
}
#[test]
fn typed_properties_and_fallbacks() {
let registry = SchemaRegistry::test_registry();
let distant = registry
.concrete_prim_definition(&tf::Token::new("DistantLight"))
.expect("DistantLight");
assert!(distant.has_property(&tf::Token::new("inputs:angle")));
assert_eq!(
distant.attribute_fallback(&tf::Token::new("inputs:angle")),
Some(sdf::Value::Float(0.53))
);
assert_eq!(distant.attribute_fallback(&tf::Token::new("nonexistent")), None);
}
#[test]
fn built_ins_expand_transitively() {
let registry = SchemaRegistry::test_registry();
let distant = registry
.concrete_prim_definition(&tf::Token::new("DistantLight"))
.expect("DistantLight");
assert_eq!(
distant.property_names(),
[
tf::Token::new("collection:lightLink:expansionRule"),
tf::Token::new("collection:lightLink:includeRoot"),
tf::Token::new("collection:lightLink:includes"),
tf::Token::new("inputs:angle"),
tf::Token::new("inputs:intensity"),
tf::Token::new("light:shaderId"),
]
);
assert_eq!(
distant.applied_api_schemas(),
[tf::Token::new("LightAPI"), tf::Token::new("CollectionAPI:lightLink")]
);
}
#[test]
fn template_instantiated_by_built_in() {
let registry = SchemaRegistry::test_registry();
let light = registry
.api_prim_definition(&tf::Token::new("LightAPI"))
.expect("LightAPI");
assert_eq!(
light.attribute_fallback(&tf::Token::new("collection:lightLink:expansionRule")),
Some(sdf::Value::token("expandPrims"))
);
assert!(!light.has_property(&tf::Token::new("collection:__INSTANCE_NAME__:expansionRule")));
assert_eq!(
light.applied_api_schemas(),
[tf::Token::new("LightAPI"), tf::Token::new("CollectionAPI:lightLink")]
);
}
#[test]
fn multi_apply_lists_its_template() {
let registry = SchemaRegistry::test_registry();
let collection = registry
.api_prim_definition(&tf::Token::new("CollectionAPI"))
.expect("CollectionAPI");
assert_eq!(
collection.applied_api_schemas(),
[tf::Token::new("CollectionAPI:__INSTANCE_NAME__")]
);
}
#[test]
fn override_beats_built_in_fallback() {
let registry = SchemaRegistry::test_registry();
let distant = registry
.concrete_prim_definition(&tf::Token::new("DistantLight"))
.expect("DistantLight");
let light = registry
.api_prim_definition(&tf::Token::new("LightAPI"))
.expect("LightAPI");
assert_eq!(
light.attribute_fallback(&tf::Token::new("inputs:intensity")),
Some(sdf::Value::Float(1.0))
);
assert_eq!(
distant.attribute_fallback(&tf::Token::new("inputs:intensity")),
Some(sdf::Value::Float(50000.0))
);
assert_eq!(
distant.attribute_fallback(&tf::Token::new("light:shaderId")),
Some(sdf::Value::token("DistantLight"))
);
}
#[test]
fn override_supplies_missing_fallback() {
let registry = SchemaRegistry::test_registry();
let collection = registry
.api_prim_definition(&tf::Token::new("CollectionAPI"))
.expect("CollectionAPI");
let distant = registry
.concrete_prim_definition(&tf::Token::new("DistantLight"))
.expect("DistantLight");
assert_eq!(
collection.attribute_fallback(&tf::Token::new("collection:__INSTANCE_NAME__:includeRoot")),
None
);
assert_eq!(
distant.attribute_fallback(&tf::Token::new("collection:lightLink:includeRoot")),
Some(sdf::Value::Bool(true))
);
}
#[test]
fn override_keeps_defined_variability() {
let registry = SchemaRegistry::test_registry();
let distant = registry
.concrete_prim_definition(&tf::Token::new("DistantLight"))
.expect("DistantLight");
let intensity = distant
.property(&tf::Token::new("inputs:intensity"))
.expect("intensity");
assert_eq!(intensity.variability(), sdf::Variability::Varying);
assert_eq!(intensity.type_name(), Some(sdf::ValueTypeName::from("float")));
let include_root = distant
.property(&tf::Token::new("collection:lightLink:includeRoot"))
.expect("includeRoot");
assert_eq!(include_root.variability(), sdf::Variability::Uniform);
}
#[test]
fn api_definition_lists_itself() {
let registry = SchemaRegistry::test_registry();
let light = registry
.api_prim_definition(&tf::Token::new("LightAPI"))
.expect("LightAPI");
assert_eq!(light.applied_api_schemas()[0], tf::Token::new("LightAPI"));
assert_eq!(
light.attribute_fallback(&tf::Token::new("inputs:intensity")),
Some(sdf::Value::Float(1.0))
);
}
#[test]
fn multi_apply_keeps_templates() {
let registry = SchemaRegistry::test_registry();
let collection = registry
.api_prim_definition(&tf::Token::new("CollectionAPI"))
.expect("CollectionAPI");
let expansion = tf::Token::new("collection:__INSTANCE_NAME__:expansionRule");
assert!(collection.has_property(&expansion));
assert_eq!(
collection.attribute_fallback(&expansion),
Some(sdf::Value::token("expandPrims"))
);
assert_eq!(
collection.attribute_fallback(&tf::Token::new("collection:__INSTANCE_NAME__:includeRoot")),
None
);
}
#[test]
fn relationship_has_no_fallback() {
let registry = SchemaRegistry::test_registry();
let collection = registry
.api_prim_definition(&tf::Token::new("CollectionAPI"))
.expect("CollectionAPI");
let includes = tf::Token::new("collection:__INSTANCE_NAME__:includes");
let property = collection.property(&includes).expect("includes");
assert_eq!(property.spec_type(), sdf::SpecType::Relationship);
assert_eq!(collection.attribute_fallback(&includes), None);
}
#[test]
fn property_type_and_variability() {
let registry = SchemaRegistry::test_registry();
let collection = registry
.api_prim_definition(&tf::Token::new("CollectionAPI"))
.expect("CollectionAPI");
let expansion = collection
.property(&tf::Token::new("collection:__INSTANCE_NAME__:expansionRule"))
.expect("expansionRule");
assert_eq!(expansion.type_name(), Some(sdf::ValueTypeName::from("token")));
assert_eq!(expansion.variability(), sdf::Variability::Uniform);
let light = registry
.api_prim_definition(&tf::Token::new("LightAPI"))
.expect("LightAPI");
let intensity = light.property(&tf::Token::new("inputs:intensity")).expect("intensity");
assert_eq!(intensity.type_name(), Some(sdf::ValueTypeName::from("float")));
assert_eq!(intensity.variability(), sdf::Variability::Varying);
}
#[test]
fn abstract_and_unknown_have_no_definition() {
let registry = SchemaRegistry::test_registry();
assert!(
registry
.concrete_prim_definition(&tf::Token::new("NonboundableLightBase"))
.is_none()
);
assert!(registry.concrete_prim_definition(&tf::Token::new("Bogus")).is_none());
assert!(registry.empty_prim_definition().is_empty());
}
#[test]
fn weaker_fills_in_missing_fields() {
let registry = merge_registry();
let thing = registry
.concrete_prim_definition(&tf::Token::new("Thing"))
.expect("Thing");
assert_eq!(
thing.attribute_fallback(&tf::Token::new("shared")),
Some(sdf::Value::Float(1.0))
);
let shared = thing.property(&tf::Token::new("shared")).expect("shared");
assert_eq!(
shared.field(sdf::FieldKey::DisplayGroup),
Some(&sdf::Value::String("weak group".into()))
);
}
#[test]
fn documentation_never_from_weaker() {
let registry = merge_registry();
let thing = registry
.concrete_prim_definition(&tf::Token::new("Thing"))
.expect("Thing");
let shared = thing.property(&tf::Token::new("shared")).expect("shared");
assert_eq!(
shared.field(sdf::FieldKey::Documentation),
Some(&sdf::Value::String("strong property docs".into()))
);
assert_eq!(
thing.metadata(sdf::FieldKey::Documentation),
Some(&sdf::Value::String("strong prim docs".into()))
);
}
#[test]
fn dictionaries_merge_recursively() {
let registry = merge_registry();
let thing = registry
.concrete_prim_definition(&tf::Token::new("Thing"))
.expect("Thing");
let shared = thing.property(&tf::Token::new("shared")).expect("shared");
let asset_info = shared
.field(sdf::FieldKey::AssetInfo)
.expect("assetInfo")
.clone()
.try_as_dictionary()
.expect("dictionary");
assert_eq!(asset_info["both"], sdf::Value::String("strong".into()));
assert_eq!(asset_info["only_weak"], sdf::Value::String("weak".into()));
let prim_data = thing
.metadata(sdf::FieldKey::AssetInfo)
.expect("assetInfo")
.clone()
.try_as_dictionary()
.expect("dictionary");
assert_eq!(prim_data["both"], sdf::Value::String("strong".into()));
let nested = prim_data["nested"].clone().try_as_dictionary().expect("nested");
assert_eq!(nested["both"], sdf::Value::String("strong".into()));
assert_eq!(nested["only_weak"], sdf::Value::String("weak".into()));
}
#[test]
fn mismatched_types_do_not_merge() {
let registry = merge_registry();
let thing = registry
.concrete_prim_definition(&tf::Token::new("Thing"))
.expect("Thing");
let mismatched = thing.property(&tf::Token::new("mismatched")).expect("mismatched");
assert_eq!(mismatched.type_name(), Some(sdf::ValueTypeName::from("float")));
assert_eq!(
thing.attribute_fallback(&tf::Token::new("mismatched")),
Some(sdf::Value::Float(2.0))
);
}
#[test]
fn property_order_appends_weaker() {
let registry = merge_registry();
let thing = registry
.concrete_prim_definition(&tf::Token::new("Thing"))
.expect("Thing");
assert_eq!(
thing.property_names(),
[
tf::Token::new("mismatched"),
tf::Token::new("shared"),
tf::Token::new("only_weak"),
]
);
}
#[test]
fn disallowed_fields_are_not_fallbacks() {
let registry = SchemaRegistry::test_registry();
let distant = registry
.concrete_prim_definition(&tf::Token::new("DistantLight"))
.expect("DistantLight");
assert!(distant.metadata(sdf::FieldKey::Specifier).is_none());
assert!(distant.metadata(sdf::FieldKey::CustomData).is_none());
assert!(distant.metadata(sdf::ChildrenKey::PropertyChildren).is_none());
assert!(distant.metadata(sdf::FieldKey::ApiSchemas).is_some());
}
#[test]
fn prim_metadata_is_not_a_property() {
let registry = SchemaRegistry::test_registry();
let distant = registry
.concrete_prim_definition(&tf::Token::new("DistantLight"))
.expect("DistantLight");
assert!(distant.property(&tf::Token::default()).is_none());
assert!(distant.metadata(sdf::FieldKey::ApiSchemas).is_some());
}
}