use std::collections::BTreeMap;
use openusd::{kind, sdf, tf, usd};
use crate::model::{Class, Library, Property};
pub fn with_family<R>(library: &Library, f: impl FnOnce(&usd::SchemaFamily<'_>) -> R) -> R {
let lists: Vec<Lists<'_>> = library.classes.iter().map(Lists::of).collect();
let nested: Vec<Nested<'_>> = library
.classes
.iter()
.zip(&lists)
.map(|(class, lists)| Nested::of(class, lists))
.collect();
let decls: Vec<usd::SchemaDecl<'_>> = library
.classes
.iter()
.zip(&lists)
.zip(&nested)
.map(|((class, lists), nested)| declare(class, lists, nested))
.collect();
let metadata: Vec<usd::MetadataDecl<'_>> = library
.metadata
.iter()
.map(|field| {
let decl =
usd::MetadataDecl::new(field.name.as_str(), field.type_name.as_str()).applies_to(field.applies_to);
match &field.fallback {
Some(fallback) => decl.fallback_borrowed(fallback),
None => decl,
}
})
.collect();
let kinds: Vec<kind::Decl<'_>> = library
.kinds
.iter()
.map(|declared| {
let decl = kind::Decl::new(&declared.name);
match &declared.base {
Some(base) => decl.base(base),
None => decl,
}
})
.collect();
f(&usd::SchemaFamily::new(&library.name, &decls)
.metadata(&metadata)
.kinds(&kinds))
}
struct Lists<'a> {
bases: Vec<&'a str>,
applied_api_schemas: Vec<&'a str>,
override_property_names: Vec<&'a str>,
auto_apply_to: Vec<&'a str>,
can_only_apply_to: Vec<&'a str>,
allowed_instance_names: Vec<&'a str>,
fallback_types: Vec<&'a str>,
instances: Vec<(&'a str, Vec<&'a str>)>,
fields: Vec<usd::Field<'a>>,
property_fields: Vec<Vec<usd::Field<'a>>>,
}
struct Nested<'a> {
instances: Vec<usd::InstanceRestriction<'a>>,
properties: Vec<usd::PropertyDecl<'a>>,
}
impl<'a> Lists<'a> {
fn of(class: &'a Class) -> Self {
let tokens = |names: &'a [tf::Token]| names.iter().map(tf::Token::as_str).collect();
Self {
bases: class.direct_base.iter().map(tf::Token::as_str).collect(),
applied_api_schemas: tokens(&class.applied_api_schemas),
override_property_names: override_names(class),
auto_apply_to: tokens(&class.metadata.auto_apply_to),
can_only_apply_to: tokens(&class.metadata.can_only_apply_to),
allowed_instance_names: tokens(&class.metadata.allowed_instance_names),
fallback_types: tokens(&class.metadata.fallback_types),
instances: class
.metadata
.instance_restrictions
.iter()
.map(|(instance, allowed)| (instance.as_str(), tokens(allowed)))
.collect(),
fields: fields(&class.fields, sdf::SpecType::Prim),
property_fields: class
.properties
.iter()
.map(|property| fields(&property.fields, property.shape.spec_type))
.collect(),
}
}
}
impl<'a> Nested<'a> {
fn of(class: &'a Class, lists: &'a Lists<'a>) -> Self {
Self {
instances: lists
.instances
.iter()
.map(|(instance, allowed)| (*instance, allowed.as_slice()))
.collect(),
properties: class
.properties
.iter()
.zip(&lists.property_fields)
.map(|(property, fields)| declare_property(property, fields))
.collect(),
}
}
}
fn declare<'a>(class: &'a Class, lists: &'a Lists<'a>, nested: &'a Nested<'a>) -> usd::SchemaDecl<'a> {
let mut decl = usd::SchemaDecl::new(class.identifier.as_str(), class.kind)
.bases(&lists.bases)
.properties(&nested.properties)
.fields(&lists.fields)
.applied_api_schemas(&lists.applied_api_schemas)
.override_property_names(&lists.override_property_names)
.auto_apply_to(&lists.auto_apply_to)
.can_only_apply_to(&lists.can_only_apply_to)
.instance_restrictions(&nested.instances)
.allowed_instance_names(&lists.allowed_instance_names)
.fallback_types(&lists.fallback_types);
if let Some(prefix) = &class.metadata.property_namespace_prefix {
decl = decl.property_namespace_prefix(prefix.as_str());
}
decl
}
fn declare_property<'a>(property: &'a Property, fields: &'a [usd::Field<'a>]) -> usd::PropertyDecl<'a> {
let name = property.schematics_name.as_str();
let decl = if property.shape.spec_type == sdf::SpecType::Relationship {
usd::PropertyDecl::relationship(name)
} else {
usd::PropertyDecl::attribute(name, property.shape.type_name.as_ref().map_or("", tf::Token::as_str))
};
let decl = decl.fields(fields);
let decl = match property.shape.variability {
sdf::Variability::Uniform => decl.uniform(),
sdf::Variability::Varying => decl.varying(),
};
if property.shape.custom { decl.custom() } else { decl }
}
fn override_names(class: &Class) -> Vec<&str> {
class
.override_properties()
.map(|property| property.schematics_name.as_str())
.collect()
}
fn fields<'a>(fields: &'a BTreeMap<String, sdf::Value>, spec: sdf::SpecType) -> Vec<usd::Field<'a>> {
fields
.iter()
.filter(|(key, _)| {
key.as_str() != sdf::FieldKey::Documentation.as_str() && !usd::SchemaRegistry::is_reserved_field(spec, key)
})
.map(|(key, value)| usd::Field::borrowed(key, value))
.collect()
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::tests::{read_fixture, read_source};
const METADATA: &str = r#"#usda 1.0
def "GLOBAL" (
customData = {
string libraryName = "testMetadata"
}
)
{
}
class "Typed"
{
}
class "Marked" (
inherits = </Typed>
hidden = true
assetInfo = {
string version = "1"
}
customData = {
string className = "Marked"
}
)
{
}
class "Refined" (
inherits = </Marked>
)
{
}
"#;
fn declared(library: &Library) -> sdf::Data {
with_family(library, |family| family.to_data()).expect("the declarations register")
}
fn field(data: &sdf::Data, path: &str, key: sdf::FieldKey) -> Option<sdf::Value> {
data.spec(&sdf::path(path).expect("a path"))?.get(key.as_str()).cloned()
}
fn registry(library: &Library) -> Arc<usd::SchemaRegistry> {
with_family(library, |family| {
usd::SchemaRegistryBuilder::empty().register(family).build()
})
.expect("the declarations register")
}
#[test]
fn concrete_carries_type_name() {
let library = read_fixture("schema.usda").expect("resolves");
let data = declared(&library);
let type_name = |path: &str| {
field(&data, path, sdf::FieldKey::TypeName)
.and_then(sdf::Value::try_as_token)
.map(|token| token.as_str().to_owned())
};
assert_eq!(type_name("/Derived").as_deref(), Some("Derived"));
assert_eq!(type_name("/Base"), None, "an abstract schema is no prim type");
assert_eq!(type_name("/SingleApplyAPI"), None, "nor is an API schema");
}
#[test]
fn applied_names_templated() {
let library = read_fixture("schema.usda").expect("resolves");
let data = declared(&library);
let applied = field(&data, "/PublicMultipleApplyAPI", sdf::FieldKey::ApiSchemas)
.and_then(sdf::Value::try_as_token_list_op)
.expect("the built-in list");
assert_eq!(
applied.explicit_items,
vec![
tf::Token::from("MultiApplyAPI:__INSTANCE_NAME__"),
tf::Token::from("BuiltinMultiApplyAPI:__INSTANCE_NAME__:builtin"),
]
);
}
#[test]
fn fallback_types_recorded() {
let library = read_fixture("schema.usda").expect("resolves");
let data = declared(&library);
let fallbacks = field(&data, "/", sdf::FieldKey::FallbackPrimTypes)
.and_then(sdf::Value::try_as_dictionary)
.expect("the fallback dictionary");
assert_eq!(
fallbacks.get("Derived").and_then(sdf::Value::try_as_token_vec_ref),
Some(&vec![tf::Token::from("OldDerived"), tf::Token::from("OlderDerived")])
);
}
#[test]
fn prim_metadata_survives() {
let dir = tempfile::tempdir().expect("tempdir");
let library = read_source(dir.path(), METADATA).expect("resolves");
let data = declared(&library);
for identifier in ["Marked", "Refined"] {
let class = library
.classes
.iter()
.find(|class| class.identifier.as_str() == identifier)
.expect("the class");
for key in [sdf::FieldKey::Hidden, sdf::FieldKey::AssetInfo] {
let carried = field(&data, &format!("/{identifier}"), key);
assert!(carried.is_some(), "{identifier} lost {}", key.as_str());
assert_eq!(
carried.as_ref(),
class.fields.get(key.as_str()),
"{identifier}.{} is not what the model carried",
key.as_str()
);
}
}
assert_eq!(
field(&data, "/Marked", sdf::FieldKey::CustomData),
None,
"the generator's own input is not schema data"
);
assert_eq!(
field(&data, "/Marked", sdf::FieldKey::InheritPaths),
None,
"a declaration is already flat"
);
}
#[test]
fn relationship_variability_kept() {
let dir = tempfile::tempdir().expect("tempdir");
let library = read_source(
dir.path(),
r#"#usda 1.0
def "GLOBAL" (
customData = {
string libraryName = "testVariability"
}
)
{
}
class "Typed" {}
class "Bound" (
inherits = </Typed>
)
{
rel plain
varying rel loose
uniform double fixed = 1
double free = 1
}
"#,
)
.expect("resolves");
let data = declared(&library);
let variability = |name: &str| {
field(&data, &format!("/Bound.{name}"), sdf::FieldKey::Variability).and_then(sdf::Value::try_as_variability)
};
assert_eq!(variability("plain"), Some(sdf::Variability::Uniform));
assert_eq!(variability("loose"), None, "varying is the unauthored spelling");
assert_eq!(variability("fixed"), Some(sdf::Variability::Uniform));
assert_eq!(variability("free"), None);
}
#[test]
fn registers_as_a_family() {
let library = read_fixture("schema.usda").expect("resolves");
let registry = registry(&library);
let info = |identifier: &str| {
registry
.schema_info(&tf::Token::from(identifier))
.unwrap_or_else(|| panic!("{identifier} is registered"))
};
assert_eq!(info("Derived").kind(), usd::SchemaKind::ConcreteTyped);
assert_eq!(info("Base").kind(), usd::SchemaKind::AbstractTyped);
assert_eq!(info("MultipleApplyAPI").kind(), usd::SchemaKind::MultipleApplyApi);
assert_eq!(
info("MultipleApplyAPI")
.property_namespace_prefix()
.map(tf::Token::as_str),
Some("test")
);
assert!(
registry.is_a(&tf::Token::from("Derived"), &tf::Token::from("Base")),
"the bases the declarations record are what is_a walks"
);
assert_eq!(
info("Base").bases(),
vec![tf::Token::from("Typed")],
"a schema inheriting nothing still derives from the root, and says so"
);
let tokens = |names: &[&str]| names.iter().map(|name| tf::Token::from(*name)).collect::<Vec<_>>();
let single = info("SingleApplyAPI");
assert_eq!(single.auto_apply_to(), tokens(&["Derived", "ExternalPluginType"]));
assert_eq!(single.can_only_apply_to(None), tokens(&["Base", "ExternalPluginType"]));
let multiple = info("MultipleApplyAPI");
assert_eq!(multiple.allowed_instance_names(), tokens(&["foo", "bar", "baz"]));
assert_eq!(
multiple.can_only_apply_to(None),
tokens(&["Base", "ExternalPluginType"])
);
assert_eq!(
multiple.can_only_apply_to(Some(&tf::Token::from("foo"))),
tokens(&["Derived"]),
"an instance with its own restriction replaces the schema-wide one"
);
assert_eq!(
multiple.can_only_apply_to(Some(&tf::Token::from("baz"))),
tokens(&["Base", "ExternalPluginType"]),
"an instance without one falls back to it"
);
}
#[test]
fn root_declares_itself() {
let dir = tempfile::tempdir().expect("tempdir");
let library = read_source(
dir.path(),
r#"#usda 1.0
def "GLOBAL" (
customData = {
string libraryName = "testRoot"
}
)
{
}
class "SchemaBase" {}
"#,
)
.expect("resolves");
let class = library.classes.first().expect("SchemaBase");
assert_eq!(class.kind, usd::SchemaKind::AbstractBase);
assert_eq!(class.direct_base, None);
let registry = registry(&library);
let info = registry
.schema_info(&tf::Token::from("SchemaBase"))
.expect("SchemaBase is registered");
assert_eq!(info.kind(), usd::SchemaKind::AbstractBase);
assert!(info.bases().is_empty(), "the root names no base to walk up to");
}
}