use std::collections::BTreeMap;
use openusd::{sdf, usd};
use crate::doc;
use crate::error::Error;
use crate::model::{Class, DeclaredToken, Library, Property};
use crate::names;
use crate::validate::Violation;
#[derive(Debug)]
pub struct Token {
pub id: String,
pub value: String,
pub documentation: Vec<String>,
}
impl Library {
pub fn tokens(&self) -> Result<Vec<Token>, Error> {
let mut properties = Gathered::default();
let mut identifiers = Gathered::default();
for class in self.emitted() {
identifiers.add_schema(class)?;
properties.add_class(class, self)?;
}
for declared in &self.declared_tokens {
properties.declared(declared, &format!("a token of the {} library", self.name))?;
}
for field in &self.metadata {
let declared = DeclaredToken {
id: field.name.to_string(),
value: field.name.to_string(),
documentation: field.documentation.clone(),
};
properties.declared(&declared, &format!("a metadata field of the {} library", self.name))?;
}
let shared: Vec<Token> = identifiers.0.keys().filter_map(|id| properties.0.remove(id)).collect();
for token in shared {
for said in token.documentation {
identifiers.insert(token.id.clone(), token.value.clone(), &said)?;
}
}
let mut tokens = properties.sorted();
tokens.extend(identifiers.sorted());
Ok(tokens)
}
}
#[derive(Default)]
struct Gathered(BTreeMap<String, Token>);
impl Gathered {
fn insert(&mut self, id: String, value: String, documentation: &str) -> Result<(), Error> {
match self.0.get_mut(&id) {
Some(token) if token.value != value => {
return Err(Error::Definition {
origin: format!("token `{id}`"),
violation: Violation::TokenValueCollision {
id,
first: token.value.clone(),
second: value,
},
});
}
Some(token) => {
if !token.documentation.iter().any(|said| said == documentation) {
token.documentation.push(documentation.to_owned());
}
}
None => {
self.0.insert(
id.clone(),
Token {
id,
value,
documentation: vec![documentation.to_owned()],
},
);
}
}
Ok(())
}
fn add_schema(&mut self, class: &Class) -> Result<(), Error> {
let name = class.identifier.as_str().to_owned();
let described = named(class);
if class.version == 0 {
let documentation = format!("the schema identifier and family of {described}");
return self.insert(names::token_id(&name, true), name, &documentation);
}
self.insert(
names::token_id(&name, true),
name,
&format!("the schema identifier of {described}"),
)?;
let family = class.family.as_str().to_owned();
self.insert(
names::token_id(&family, true),
family,
&format!("the schema family of {described}"),
)
}
fn add_class(&mut self, class: &Class, library: &Library) -> Result<(), Error> {
let literal = library.use_literal_identifiers;
let described = named(class);
let mut attributes: Vec<&Property> = class
.properties
.iter()
.filter(|property| property.shape.spec_type == sdf::SpecType::Attribute)
.collect();
attributes.sort_by_cached_key(|property| property.name.as_str().to_lowercase());
for property in attributes {
self.insert(
property_id(property),
property.schematics_name.as_str().to_owned(),
&described,
)?;
if property.is_local {
let fallback = token_fallback(property);
if let Some(value) = &fallback {
let documentation = format!("the fallback of {}", describe(class, property));
self.insert(names::token_id(value, literal), value.clone(), &documentation)?;
}
for allowed in property.allowed_tokens() {
let value = allowed.as_str();
if value.is_empty() || fallback.as_deref() == Some(value) {
continue;
}
let documentation = format!("a value {} admits", describe(class, property));
self.insert(names::token_id(value, literal), value.to_owned(), &documentation)?;
}
}
}
for property in &class.properties {
if property.shape.spec_type == sdf::SpecType::Relationship {
self.insert(
property_id(property),
property.schematics_name.as_str().to_owned(),
&described,
)?;
}
}
for declared in &class.metadata.schema_tokens {
self.declared(declared, &format!("a token of the {described} schema"))?;
}
if let Some(prefix) = &class.metadata.property_namespace_prefix {
let value = prefix.as_str().to_owned();
let documentation = format!("the property namespace prefix of {described}");
self.insert(names::token_id(&value, true), value, &documentation)?;
}
Ok(())
}
fn declared(&mut self, declared: &DeclaredToken, fallback: &str) -> Result<(), Error> {
let documentation = match &declared.documentation {
Some(text) => doc::to_markdown(text, &doc::Symbols::default()),
None => fallback.to_owned(),
};
self.insert(
names::token_id(&declared.id, true),
declared.value.clone(),
&documentation,
)
}
fn sorted(self) -> Vec<Token> {
let mut tokens: Vec<Token> = self.0.into_values().collect();
tokens.sort_by_cached_key(|token| (token.id.to_lowercase(), token.id.clone()));
tokens
}
}
fn property_id(property: &Property) -> String {
let spelled = usd::SchemaRegistry::make_multiple_apply_name_instance(
property.schematics_name.as_str(),
"_MultipleApplyTemplate_",
);
names::token_id(spelled.as_str(), false)
}
fn token_fallback(property: &Property) -> Option<String> {
if property.type_name()? != sdf::ValueTypeName::TOKEN {
return None;
}
let fallback = property.fallback()?.clone().try_as_token()?;
(!fallback.as_str().is_empty()).then(|| fallback.as_str().to_owned())
}
fn describe(class: &Class, property: &Property) -> String {
let described = &class.metadata.class_name;
match property.api_name() {
Some(api_name) => format!("`{described}.{api_name}`"),
None => format!("the {} property `{}`", named(class), property.schematics_name),
}
}
fn named(class: &Class) -> String {
format!("`{}`", class.metadata.class_name)
}
#[cfg(test)]
mod tests {
use std::sync::LazyLock;
use super::*;
use crate::tests::{read_fixture, read_source};
fn corpus() -> &'static [Token] {
static ONCE: LazyLock<Vec<Token>> = LazyLock::new(|| {
read_fixture("schema.usda")
.expect("resolves")
.tokens()
.expect("gathers")
});
&ONCE
}
#[test]
fn reflected_tokens_gathered() {
let tokens = corpus();
let has = |id: &str| tokens.iter().any(|token| token.id == id);
assert!(has("testAttrExternal"), "a property of the reflected schema");
assert!(has("testRelExternal"), "and its relationship");
assert!(has("TestReflectedExternalAPI"), "and the schema identifier itself");
}
#[test]
fn identifiers_last() {
let tokens = corpus();
let position = |id: &str| tokens.iter().position(|token| token.id == id);
let derived = position("Derived").expect("the Derived identifier");
let property = position("testAttrOne").expect("a property name");
assert!(derived > property, "identifiers sort after property names");
}
#[test]
fn fallback_skips_allowed() {
let tokens = corpus();
let token = tokens
.iter()
.find(|token| token.value == "VariableTokenDefault")
.expect("the fallback is a token of its own");
let said = token.documentation.join(", ");
assert!(said.contains("the fallback of"), "{said}");
assert!(
!said.contains("a value"),
"the fallback is not also listed as a possible value: {said}"
);
}
#[test]
fn one_id_many_sources() {
let dir = tempfile::tempdir().expect("tempdir");
let library = read_source(
dir.path(),
r#"#usda 1.0
def "GLOBAL" (
customData = {
string libraryName = "testShared"
}
)
{
}
class "Typed" {}
class "Held" (
inherits = </Typed>
) {
token mode = "mode" (
allowedTokens = ["mode", "other"]
)
}
"#,
)
.expect("resolves");
let tokens = library.tokens().expect("gathers");
let token = tokens.iter().find(|token| token.id == "mode").expect("one token");
assert_eq!(token.value, "mode");
let said = token.documentation.join(", ");
assert!(said.contains("Held.mode"), "{said}");
assert!(said.contains("fallback"), "{said}");
}
#[test]
fn one_id_two_values() {
let dir = tempfile::tempdir().expect("tempdir");
let library = read_source(
dir.path(),
r#"#usda 1.0
def "GLOBAL" (
customData = {
string libraryName = "testClash"
dictionary libraryTokens = {
dictionary mode = {
string value = "somethingElse"
}
}
}
)
{
}
class "Typed" {}
class "Held" (
inherits = </Typed>
) {
token mode = "x"
}
"#,
)
.expect("resolves");
match library.tokens() {
Err(Error::Definition {
violation: Violation::TokenValueCollision { id, .. },
..
}) => assert_eq!(id, "mode"),
other => panic!("{other:?}"),
}
}
}