use std::collections::{BTreeMap, HashMap, HashSet};
use std::iter;
use openusd::{sdf, tf, usd};
use crate::error::Error;
use crate::load::{self, CustomData, Declaration, PropertyDeclaration, Source};
use crate::model::{
API_SCHEMA_OVERRIDE, Base, Class, ForeignClass, Library, Metadata, NON_APPLIED, Property, PropertyApi, SCHEMA_BASE,
Shape, Site, TYPED, is_root,
};
use crate::names;
use crate::validate::Violation;
const API_SCHEMA_TYPE: &str = "apiSchemaType";
pub fn library(source: &Source) -> Result<Library, Error> {
let flattened = source.stage.flatten()?;
let mut index: HashMap<&tf::Token, &Declaration> = HashMap::new();
for declaration in &source.declarations {
index.entry(&declaration.name).or_insert(declaration);
}
let mut classes = Vec::new();
for declaration in source.declarations.iter().filter(|d| d.generated) {
classes.push(class(&flattened, &index, declaration)?);
}
let mut reflected: Vec<Class> = Vec::new();
for owner in &classes {
for schema in &owner.metadata.reflected_api_schemas {
if classes
.iter()
.chain(&reflected)
.any(|class| &class.identifier == schema)
{
continue;
}
let Some(declaration) = index.get(schema) else {
return Err(owner.violation(Violation::UnknownReflectedSchema { schema: schema.clone() }));
};
reflected.push(class(&flattened, &index, declaration)?);
}
}
let mut inherited: Vec<Class> = Vec::new();
let held = |name: &tf::Token, reflected: &[Class], inherited: &[Class]| {
classes
.iter()
.chain(reflected)
.chain(inherited)
.any(|class| &class.identifier == name)
};
for owner in &classes {
let foreign = owner
.bases
.iter()
.filter(|base| base.library != source.library && !is_root(&base.identifier));
for base in foreign {
if !held(&base.identifier, &reflected, &inherited) {
inherited.push(class(&flattened, &index, index[&base.identifier])?);
}
}
}
let mut next = 0;
while let Some(ancestor) = inherited.get(next) {
let schemas = ancestor.metadata.reflected_api_schemas.clone();
for schema in &schemas {
if let Some(declaration) = index.get(schema)
&& !held(schema, &reflected, &inherited)
{
inherited.push(class(&flattened, &index, declaration)?);
}
}
next += 1;
}
let foreign = source
.declarations
.iter()
.filter(|declaration| declaration.library != source.library)
.map(|declaration| ForeignClass {
library: declaration.library.clone(),
class_name: class_name(declaration),
upstream_name: upstream_name(declaration),
})
.collect();
Ok(Library {
name: source.library.clone(),
use_literal_identifiers: source.use_literal_identifiers,
skip_code_generation: source.skip_code_generation,
classes,
reflected,
inherited,
foreign,
source_layers: load::watched_layers(&source.stage),
declared_tokens: source.tokens.clone(),
metadata: Vec::new(),
kinds: Vec::new(),
})
}
fn class(
flattened: &sdf::Layer,
index: &HashMap<&tf::Token, &Declaration>,
declaration: &Declaration,
) -> Result<Class, Error> {
let bases = chain(index, declaration)?;
let direct_base = direct_base(&bases, &declaration.name);
let is_typed = bases.iter().any(|base| base.identifier.as_str() == TYPED);
let (family, version) = usd::SchemaRegistry::parse_schema_family_and_version(&declaration.name);
let kind = kind(declaration, is_typed)?;
let metadata = metadata(declaration);
let properties = properties(flattened, index, declaration, &bases, &metadata, kind)?;
Ok(Class {
identifier: declaration.name.clone(),
family,
version,
library: declaration.library.clone(),
upstream_name: upstream_name(declaration),
kind,
is_typed,
authored_base_count: declaration.bases.len(),
bases,
properties,
applied_api_schemas: applied_api_schemas(flattened, declaration, kind),
documentation: declaration.documentation.clone(),
direct_base,
authored_fields: declaration.fields.clone(),
fields: composed_fields(flattened, &declaration.origin.path)?,
authored_type_name: declaration.type_name.clone(),
api_schemas_op: declaration.api_schemas.clone(),
metadata,
origin: declaration.origin.clone(),
})
}
fn chain(index: &HashMap<&tf::Token, &Declaration>, declaration: &Declaration) -> Result<Vec<Base>, Error> {
let mut walked: Vec<&Declaration> = Vec::new();
let mut seen = HashSet::new();
seen.insert(declaration.name.clone());
let mut current = declaration;
while let Some(name) = current.bases.first() {
let Some(parent) = index.get(name) else {
return Err(Error::Definition {
origin: current.origin.describe(),
violation: Violation::MissingBase { name: name.clone() },
});
};
if !seen.insert(parent.name.clone()) {
let mut chain: Vec<&str> = walked.iter().map(|base| base.name.as_str()).collect();
chain.push(parent.name.as_str());
return Err(Error::Definition {
origin: declaration.origin.describe(),
violation: Violation::CyclicInheritance {
chain: chain.join(" -> "),
},
});
}
walked.push(parent);
current = parent;
}
let mut reaches_typed = false;
let mut bases: Vec<Base> = walked
.iter()
.rev()
.map(|parent| {
let kind = kind(parent, reaches_typed).unwrap_or(usd::SchemaKind::AbstractBase);
reaches_typed |= parent.name.as_str() == TYPED;
Base {
identifier: parent.name.clone(),
library: parent.library.clone(),
generated: parent.generated,
reflected_api_schemas: parent.custom_data.tokens("reflectedAPISchemas"),
class_name: class_name(parent),
kind,
}
})
.collect();
bases.reverse();
Ok(bases)
}
fn class_name(declaration: &Declaration) -> String {
declaration
.custom_data
.string("className")
.unwrap_or_else(|| names::proper_case(declaration.name.as_str()))
}
fn upstream_name(declaration: &Declaration) -> String {
format!("{}{}", declaration.prefix, class_name(declaration))
}
fn kind(declaration: &Declaration, is_typed: bool) -> Result<usd::SchemaKind, Error> {
let is_concrete = declaration.type_name.is_some();
let is_api = !is_typed && !is_concrete && !is_root(&declaration.name);
let spelling = declaration.custom_data.string(API_SCHEMA_TYPE);
if let Some(spelling) = spelling.as_deref() {
return match spelling {
"singleApply" => Ok(usd::SchemaKind::SingleApplyApi),
"multipleApply" => Ok(usd::SchemaKind::MultipleApplyApi),
NON_APPLIED => Ok(usd::SchemaKind::NonAppliedApi),
other => Err(Error::Definition {
origin: declaration.origin.describe(),
violation: Violation::UnknownApiSchemaType {
spelling: other.to_owned(),
},
}),
};
}
Ok(if is_api {
usd::SchemaKind::SingleApplyApi
} else if is_concrete {
usd::SchemaKind::ConcreteTyped
} else if is_typed {
usd::SchemaKind::AbstractTyped
} else {
usd::SchemaKind::AbstractBase
})
}
fn metadata(declaration: &Declaration) -> Metadata {
Metadata {
property_namespace_prefix: declaration
.custom_data
.string("propertyNamespacePrefix")
.map(|prefix| tf::Token::from(prefix.as_str())),
auto_apply_to: declaration.custom_data.tokens("apiSchemaAutoApplyTo"),
can_only_apply_to: declaration.custom_data.tokens("apiSchemaCanOnlyApplyTo"),
allowed_instance_names: declaration.custom_data.tokens("apiSchemaAllowedInstanceNames"),
instance_restrictions: declaration.custom_data.instance_restrictions(),
fallback_types: declaration.custom_data.tokens("fallbackTypes"),
class_name: class_name(declaration),
reflected_api_schemas: declaration.custom_data.tokens("reflectedAPISchemas"),
schema_tokens: declaration.custom_data.token_declarations("schemaTokens"),
}
}
fn direct_base(bases: &[Base], name: &tf::Token) -> Option<tf::Token> {
bases
.first()
.map(|base| base.identifier.clone())
.or_else(|| (name.as_str() != SCHEMA_BASE).then(|| tf::Token::new(SCHEMA_BASE)))
}
fn applied_api_schemas(flattened: &sdf::Layer, declaration: &Declaration, kind: usd::SchemaKind) -> Vec<tf::Token> {
let applied = flattened
.prim(&declaration.origin.path)
.ok()
.flatten()
.and_then(|prim| prim.api_schemas())
.map(|list_op| list_op.flatten())
.unwrap_or_default();
if kind != usd::SchemaKind::MultipleApplyApi {
return applied;
}
applied
.iter()
.map(|name| {
let (identifier, instance) = usd::SchemaRegistry::type_name_and_instance(name);
let instance = instance.as_ref().map_or("", tf::Token::as_str);
usd::SchemaRegistry::make_multiple_apply_name_template(identifier.as_str(), instance)
})
.collect()
}
fn properties(
flattened: &sdf::Layer,
index: &HashMap<&tf::Token, &Declaration>,
declaration: &Declaration,
bases: &[Base],
metadata: &Metadata,
kind: usd::SchemaKind,
) -> Result<Vec<Property>, Error> {
let Some(prim) = flattened.prim(&declaration.origin.path)? else {
return Ok(Vec::new());
};
let mut properties = Vec::new();
for name in prim.property_children().unwrap_or_default() {
let path = declaration.origin.path.append_property(name.as_str())?;
let Some(shape) = shape_at(flattened, &path)? else {
continue;
};
let local = declaration.properties.iter().find(|p| p.name == name);
let sites = declaring_sites(flattened, index, declaration, bases, &name)?;
let origin = sites
.last()
.map_or_else(|| declaration.origin.clone(), |site| site.origin.clone());
properties.push(Property {
schematics_name: schematics_name(&name, metadata, kind),
api: property_api(declaration, local)?,
is_local: local.is_some(),
name,
shape,
sites,
fields: composed_fields(flattened, &path)?,
origin,
});
}
Ok(properties)
}
fn shape_at(flattened: &sdf::Layer, path: &sdf::Path) -> Result<Option<Shape>, Error> {
let data = flattened.data();
let spec_type = match data.spec_type(path) {
Some(spec_type @ (sdf::SpecType::Attribute | sdf::SpecType::Relationship)) => spec_type,
_ => return Ok(None),
};
let field = |key: sdf::FieldKey| data.try_field(path, key.as_str());
Ok(Some(Shape {
spec_type,
type_name: field(sdf::FieldKey::TypeName)?.and_then(|value| value.try_as_token_ref().cloned()),
variability: field(sdf::FieldKey::Variability)?
.and_then(|value| value.into_owned().get())
.unwrap_or_default(),
custom: field(sdf::FieldKey::Custom)?
.and_then(|value| value.into_owned().get())
.unwrap_or(false),
}))
}
fn composed_fields(flattened: &sdf::Layer, path: &sdf::Path) -> Result<BTreeMap<String, sdf::Value>, Error> {
let data = flattened.data();
let mut fields = BTreeMap::new();
for name in data.list_fields(path).unwrap_or_default() {
if let Some(value) = data.try_field(path, &name)? {
fields.insert(name, value.into_owned());
}
}
Ok(fields)
}
fn declaring_sites(
flattened: &sdf::Layer,
index: &HashMap<&tf::Token, &Declaration>,
declaration: &Declaration,
bases: &[Base],
name: &tf::Token,
) -> Result<Vec<Site>, Error> {
let ancestors = bases.iter().filter_map(|base| index.get(&base.identifier).copied());
let mut sites = Vec::new();
for class in iter::once(declaration).chain(ancestors) {
let Some(property) = class.properties.iter().find(|p| &p.name == name) else {
continue;
};
let path = class.origin.path.append_property(name.as_str())?;
let Some(shape) = shape_at(flattened, &path)? else {
continue;
};
sites.push(Site {
class: class.name.clone(),
is_override: property.custom_data.flag(API_SCHEMA_OVERRIDE).unwrap_or(false),
shape,
api_name: api_name(&property.custom_data, name),
origin: property.origin.clone(),
});
}
Ok(sites)
}
fn api_name(custom_data: &CustomData, name: &tf::Token) -> Option<String> {
if custom_data.flag(API_SCHEMA_OVERRIDE).unwrap_or(false) {
return None;
}
match custom_data.string("apiName").as_deref() {
Some("") => None,
Some(explicit) => Some(explicit.to_owned()),
None => Some(names::camel_case(name.as_str())),
}
}
fn property_api(declaration: &Declaration, property: Option<&PropertyDeclaration>) -> Result<PropertyApi, Error> {
let Some(property) = property else {
return Ok(PropertyApi::default());
};
let custom_get = match property.custom_data.string("apiGetImplementation").as_deref() {
None | Some("generated") => false,
Some("custom") => true,
Some(other) => {
return Err(Error::Definition {
origin: declaration.origin.describe(),
violation: Violation::UnknownApiGetImplementation {
spelling: other.to_owned(),
},
});
}
};
Ok(PropertyApi { custom_get })
}
fn schematics_name(name: &tf::Token, metadata: &Metadata, kind: usd::SchemaKind) -> tf::Token {
if kind != usd::SchemaKind::MultipleApplyApi {
return name.clone();
}
let Some(prefix) = &metadata.property_namespace_prefix else {
return name.clone();
};
let placeholder = usd::SchemaRegistry::make_multiple_apply_name_template("", "");
if name == &placeholder {
return usd::SchemaRegistry::make_multiple_apply_name_template(prefix.as_str(), "");
}
if usd::SchemaRegistry::is_multiple_apply_name_template(name.as_str()) {
return name.clone();
}
usd::SchemaRegistry::make_multiple_apply_name_template(prefix.as_str(), name.as_str())
}
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
use crate::tests::{read_fixture, read_source};
#[test]
fn contrived_library() {
let library = read_fixture("schema.usda").expect("resolves");
assert_eq!(library.name, "usdContrived");
let kind = |identifier: &str| {
library
.classes
.iter()
.find(|class| class.identifier.as_str() == identifier)
.unwrap_or_else(|| panic!("{identifier} is generated"))
.kind
};
assert_eq!(kind("Base"), usd::SchemaKind::AbstractTyped);
assert_eq!(kind("Derived"), usd::SchemaKind::ConcreteTyped);
assert_eq!(kind("SingleApplyAPI"), usd::SchemaKind::SingleApplyApi);
assert_eq!(kind("MultipleApplyAPI"), usd::SchemaKind::MultipleApplyApi);
assert_eq!(kind("NonAppliedAPI"), usd::SchemaKind::NonAppliedApi);
}
#[test]
fn multi_apply_templates() {
let library = read_fixture("schema.usda").expect("resolves");
let class = library
.classes
.iter()
.find(|class| class.identifier.as_str() == "MultipleApplyAPI")
.expect("the multiple-apply schema");
for property in class.local_properties() {
let name = property.schematics_name.as_str();
assert!(
name.starts_with("test:__INSTANCE_NAME__"),
"{name} is not under the declared prefix"
);
}
}
#[test]
fn property_names_own() {
let library = read_fixture("schema.usda").expect("resolves");
let hairman = library
.classes
.iter()
.find(|class| class.identifier.as_str() == "TestPxHairman")
.expect("the class carrying the api-name cases");
let names: Vec<&str> = hairman.local_properties().map(|p| p.name.as_str()).collect();
assert!(
names.iter().all(|name| !name.contains('.')),
"a property carries the prim path: {names:?}"
);
let accessor = |name: &str| {
hairman
.local_properties()
.find(|property| property.name.as_str() == name)
.unwrap_or_else(|| panic!("{name} is declared"))
.api_name()
};
assert_eq!(
accessor("riStatements:attributes:user:Gofur_GeomOnHairdensity"),
Some("Gofur_GeomOnHairdensity"),
"the declared apiName is what the accessor is called"
);
assert_eq!(
accessor("attrWithoutGeneratedAccessorAPI"),
None,
"an empty apiName asks for no accessor"
);
assert_eq!(accessor("temp"), Some("temp"), "the default is the name itself");
}
#[test]
fn redeclared_stays_local() {
let library = read_fixture("schema.usda").expect("resolves");
let derived = library
.classes
.iter()
.find(|class| class.identifier.as_str() == "Derived")
.expect("the derived class");
let property = derived
.local_properties()
.find(|property| property.name.as_str() == "overrideBaseTrueDerivedFalse")
.expect("a redeclared property is still declared here");
assert!(!property.is_override(), "Derived turns the override off");
assert_eq!(
property.sites.iter().map(|site| site.is_override).collect::<Vec<_>>(),
vec![false, true],
"Derived's own declaration is the strongest, and Base's stands behind it"
);
assert_eq!(
property.api_name(),
Some("overrideBaseTrueDerivedFalse"),
"so Derived asks for the accessor Base suppressed"
);
assert_eq!(
property.sites.last().map(|site| site.class.as_str()),
Some("Base"),
"while Base is still what introduced it"
);
}
#[test]
fn reflected_foreign_resolved() {
let library = read_fixture("schema.usda").expect("resolves");
let names: Vec<&str> = library
.reflected
.iter()
.map(|class| class.identifier.as_str())
.collect();
assert_eq!(names, vec!["TestReflectedExternalAPI"]);
let external = &library.reflected[0];
assert_eq!(external.library, "usd");
assert_eq!(external.kind, usd::SchemaKind::SingleApplyApi);
let mut properties: Vec<&str> = external.local_properties().map(|p| p.name.as_str()).collect();
properties.sort_unstable();
assert_eq!(
properties,
vec![
"testAttrDuplicate",
"testAttrExternal",
"testRelDuplicate",
"testRelExternal"
]
);
}
#[test]
fn custom_data_list_spellings() {
let dir = tempfile::tempdir().expect("tempdir");
let library = read_source(
dir.path(),
r#"#usda 1.0
over "GLOBAL" (
customData = {
string libraryName = "testLists"
}
)
{
}
class "APISchemaBase" {}
class "StringsAPI" (
inherits = </APISchemaBase>
customData = {
token apiSchemaType = "singleApply"
string[] apiSchemaCanOnlyApplyTo = ["Shape"]
}
) {}
class "TokensAPI" (
inherits = </APISchemaBase>
customData = {
token apiSchemaType = "singleApply"
token[] apiSchemaCanOnlyApplyTo = ["Shape"]
}
) {}
"#,
)
.expect("resolves");
for identifier in ["StringsAPI", "TokensAPI"] {
let class = library
.classes
.iter()
.find(|class| class.identifier.as_str() == identifier)
.unwrap_or_else(|| panic!("{identifier} is generated"));
assert_eq!(
class.metadata.can_only_apply_to.len(),
1,
"{identifier} lost its restriction"
);
}
}
#[test]
fn strongest_declaration_wins() {
let dir = tempfile::tempdir().expect("tempdir");
fs::write(
dir.path().join("sublayer.usda"),
r#"#usda 1.0
over "GLOBAL" (
customData = {
string libraryName = "testStrength"
}
)
{
}
class "Typed" {}
class "Base" {}
"#,
)
.expect("write");
let schema = dir.path().join("schema.usda");
fs::write(
&schema,
r#"#usda 1.0
(
subLayers = [
@sublayer.usda@
]
)
over "GLOBAL" (
customData = {
string libraryName = "testStrength"
}
)
{
}
class "Base" (
inherits = </Typed>
) {}
class Child "Child" (
inherits = </Base>
) {}
"#,
)
.expect("write");
let (library, _) = crate::configure()
.search_path(dir.path())
.read(&schema, &[])
.expect("the root's Base is the one that counts");
let child = library
.classes
.iter()
.find(|class| class.identifier.as_str() == "Child")
.expect("the child class");
assert!(child.is_typed, "Child reaches Typed through the root's Base");
assert_eq!(child.kind, usd::SchemaKind::ConcreteTyped);
}
#[test]
fn inherited_declared_by() {
let library = read_fixture("schema.usda").expect("resolves");
let derived = library
.classes
.iter()
.find(|class| class.identifier.as_str() == "Derived")
.expect("the derived class");
let inherited: Vec<&str> = derived
.properties
.iter()
.filter_map(|property| property.sites.last())
.map(|site| site.class.as_str())
.filter(|declared| *declared != "Derived")
.collect();
assert!(
inherited.contains(&"Base"),
"Derived carries no property declared by Base: {inherited:?}"
);
}
}