use std::collections::{BTreeMap, BTreeSet};
use std::iter;
use openusd::{sdf, tf, usd, usda};
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use crate::model::{API_SCHEMA_BASE, Base, Class, Library, Property, Reflection, Shape, TYPED, is_root};
use crate::validate::Violation;
use crate::{Externs, TokenEnum, doc, error::Error, error::TokenEnumError, names, types};
pub struct RustLibrary {
pub library: String,
pub tokens: Vec<Constant>,
pub enums: Vec<RustEnum>,
pub classes: Vec<RustClass>,
pub metadata: Vec<MetadataTrait>,
}
pub struct MetadataTrait {
pub name: Ident,
pub documentation: String,
pub handle: syn::Path,
pub stage: bool,
pub fields: Vec<MetadataAccessor>,
}
pub struct MetadataAccessor {
pub getter: Ident,
pub setter: Option<Ident>,
pub token: TokenStream,
pub rust_type: syn::Type,
pub documentation: String,
}
pub struct RustEnum {
pub name: Ident,
pub documentation: String,
pub variants: Vec<RustVariant>,
pub default: Option<Ident>,
pub source: (String, String),
}
pub struct RustVariant {
pub name: Ident,
pub constant: TokenStream,
pub value: String,
}
pub struct Constant {
pub name: Ident,
pub value: String,
pub documentation: String,
}
pub struct RustClass {
pub name: Ident,
pub kind: usd::SchemaKind,
pub accessor_trait: Option<AccessorTrait>,
pub inherited: Vec<syn::Path>,
pub constant: TokenStream,
pub documentation: Option<String>,
pub accessors: Vec<RustAccessor>,
pub reflected: Vec<Reflected>,
pub instancing: Option<Instancing>,
}
pub struct Instancing {
pub prefix: String,
pub base_names: Vec<String>,
}
pub struct AccessorTrait {
pub name: Ident,
pub parent: syn::Path,
}
pub struct Reflected {
pub accessor: Ident,
pub view: syn::Path,
}
pub struct RustAccessor {
pub getter: Ident,
pub creator: Ident,
pub builder: Ident,
pub typed: Option<TypedRead>,
pub custom_get: bool,
pub token: TokenStream,
pub instanced: bool,
pub kind: PropertyKind,
pub custom: bool,
pub uniform: bool,
pub documentation: String,
}
pub struct TypedRead {
pub value: Ident,
pub value_at: Option<Ident>,
pub ty: syn::Type,
}
pub enum PropertyKind {
Attribute {
type_constant: syn::Path,
},
Relationship,
}
type Inventory<'a> = BTreeMap<&'a tf::Token, ClassNames<'a>>;
type Sourced<'a> = BTreeMap<(&'a str, &'a str), &'a Ident>;
type Placed<'a> = BTreeMap<&'a str, Result<syn::Path, &'a str>>;
struct ClassNames<'a> {
class: &'a Class,
view: String,
qualified: String,
qualifier: String,
accessors: Vec<AccessorNames<'a>>,
}
struct AccessorNames<'a> {
property: &'a Property,
getter: String,
creator: String,
builder: String,
value: Option<String>,
value_at: Option<String>,
cpp: String,
}
fn inventory(model: &Library) -> Inventory<'_> {
model
.classes
.iter()
.filter(|class| !is_root(&class.identifier))
.map(|class| (&class.identifier, ClassNames::of(class, None)))
.collect()
}
impl<'a> ClassNames<'a> {
fn of(class: &'a Class, home: Option<&syn::Path>) -> Self {
let view = class.metadata.class_name.clone();
let qualifier = match class.kind.is_applied_api_schema() {
true => view.clone(),
false => trait_name(&view),
};
let qualifier = match home {
Some(home) => super::within(home, &qualifier),
None => qualifier,
};
let accessors = class
.local_properties()
.filter(|property| offers(class, property))
.filter_map(|property| {
let accessor = named(property.api_name(), &property.shape)?;
let suffix = match property.shape.spec_type {
sdf::SpecType::Relationship => "Rel",
_ => "Attr",
};
Some(AccessorNames {
property,
cpp: format!("{}{suffix}", names::proper_case(property.api_name()?)),
getter: accessor.getter,
creator: accessor.creator,
builder: accessor.builder,
value: accessor.value,
value_at: accessor.value_at,
})
})
.collect();
ClassNames {
class,
qualified: class.upstream_name.clone(),
view,
qualifier,
accessors,
}
}
fn links(&self) -> Vec<(String, String)> {
let mut links = Vec::new();
for accessor in &self.accessors {
let methods = iter::once(("Create", &accessor.creator))
.chain((!accessor.property.api.custom_get).then_some(("Get", &accessor.getter)));
for (verb, method) in methods {
let cpp = format!("{verb}{}", accessor.cpp);
let markdown = doc::link(method, Some(&format!("{}::{method}", self.qualifier)));
links.push((format!("{}::{cpp}", self.qualified), markdown.clone()));
links.push((format!("{}::{cpp}", self.view), markdown.clone()));
links.push((cpp, markdown));
}
}
links
}
}
pub fn library(model: &Library, externs: &Externs, enums: &[TokenEnum]) -> Result<RustLibrary, Error> {
let tokens = constants(model)?;
let by_value = by_value(&tokens);
let enums = lower_enums(model, enums, &by_value)?;
let sourced: Sourced<'_> = enums
.iter()
.map(|lowered| ((lowered.source.0.as_str(), lowered.source.1.as_str()), &lowered.name))
.collect();
let inventory = inventory(model);
check_class_names(&inventory, &enums)?;
let placed = placed(externs, model);
let library = library_symbols(&inventory, model, &placed);
let classes = model
.classes
.iter()
.filter(|class| !is_root(&class.identifier))
.map(|class| lower_class(class, model, &placed, &by_value, &sourced, &inventory, &library))
.collect::<Result<_, _>>()?;
let metadata = lower_metadata(model, &by_value)?;
Ok(RustLibrary {
library: model.name.clone(),
tokens,
enums,
classes,
metadata,
})
}
fn lower_metadata(model: &Library, by_value: &BTreeMap<&str, &Ident>) -> Result<Vec<MetadataTrait>, Error> {
let kinds = [
(
usd::MetadataTargets::PRIMS,
super::items::PRIM_METADATA,
"prims",
"Prim",
),
(
usd::MetadataTargets::ATTRIBUTES,
super::items::ATTRIBUTE_METADATA,
"attributes",
"Attribute",
),
(
usd::MetadataTargets::RELATIONSHIPS,
super::items::RELATIONSHIP_METADATA,
"relationships",
"Relationship",
),
(
usd::MetadataTargets::LAYERS,
super::items::STAGE_METADATA,
"a stage",
"Stage",
),
];
let mut traits = Vec::new();
for (target, name, what, handle) in kinds {
let stage = target == usd::MetadataTargets::LAYERS;
let mut methods: BTreeMap<String, &tf::Token> = BTreeMap::new();
let mut fields = Vec::new();
for field in model
.metadata
.iter()
.filter(|field| field.applies_to.intersects(target))
{
let origin = format!("the {} metadata field `{}`", model.name, field.name);
let getter = names::method_name(field.name.as_str());
let setter = (!stage).then(|| format!("set_{}", names::snake_case(field.name.as_str())));
for method in iter::once(&getter).chain(&setter) {
if let Some(first) = methods.insert(method.clone(), &field.name) {
return Err(Error::MetadataMethodCollision {
library: model.name.clone(),
method: method.clone(),
first: first.to_string(),
second: field.name.to_string(),
});
}
}
let mut documentation = match &field.documentation {
Some(text) => format!("{}\n\n", doc::to_markdown(text, &doc::Symbols::default())),
None => String::new(),
};
documentation.push_str(&doc::wrap(&format!(
"The `{}` metadata field, declared `{}`.",
field.name,
field.type_name.as_str()
)));
if let Some(value) = &field.fallback
&& let Ok(text) = usda::TextWriter::value_to_string(value)
{
let fallback = match stage {
true => format!("Where no layer authors it, the stage reads `{text}`."),
false => format!(
"Its declared default, `{text}`, is not applied: like C++ `GetMetadata`, this reads what \
a layer authors or the schema declares, and `None` otherwise."
),
};
documentation.push_str(&format!("\n\n{}", doc::wrap(&fallback)));
}
fields.push(MetadataAccessor {
getter: identifier(&getter, &origin)?,
setter: setter.map(|setter| identifier(&setter, &origin)).transpose()?,
token: constant_of(by_value, &field.name),
rust_type: field
.type_name
.kind()
.and_then(types::rust_type)
.unwrap_or_else(|| syn::parse_quote! { ::openusd::sdf::Value }),
documentation,
});
}
if fields.is_empty() {
continue;
}
let handle = Ident::new(handle, Span::call_site());
traits.push(MetadataTrait {
name: Ident::new(name, Span::call_site()),
documentation: doc::wrap(&format!(
"The metadata fields the `{}` library registers for {what}.",
model.name
)),
handle: syn::parse_quote! { ::openusd::usd::#handle },
stage,
fields,
});
}
Ok(traits)
}
pub fn tokens(model: &Library, enums: &[TokenEnum]) -> Result<RustLibrary, Error> {
let tokens = constants(model)?;
let enums = lower_enums(model, enums, &by_value(&tokens))?;
Ok(RustLibrary {
library: model.name.clone(),
tokens,
enums,
classes: Vec::new(),
metadata: Vec::new(),
})
}
pub(super) fn by_value(tokens: &[Constant]) -> BTreeMap<&str, &Ident> {
tokens
.iter()
.map(|constant| (constant.value.as_str(), &constant.name))
.collect()
}
fn lower_enums(
model: &Library,
enums: &[TokenEnum],
by_value: &BTreeMap<&str, &Ident>,
) -> Result<Vec<RustEnum>, Error> {
let mut minted: BTreeSet<&str> = BTreeSet::new();
let mut lowered = Vec::new();
for declared in enums.iter().filter(|declared| declared.library == model.name) {
let name = declared.name.as_str();
let resolved = if !names::is_rust_identifier(name) {
Err(TokenEnumError::InvalidName)
} else if super::items::ALL.contains(&name) {
Err(TokenEnumError::ReservedName)
} else if !minted.insert(name) {
Err(TokenEnumError::Duplicate)
} else {
lower_enum(model, declared, by_value)
};
lowered.push(resolved.map_err(|cause| Error::TokenEnum {
name: name.to_owned(),
cause,
})?);
}
Ok(lowered)
}
fn lower_enum(
model: &Library,
declared: &TokenEnum,
by_value: &BTreeMap<&str, &Ident>,
) -> Result<RustEnum, TokenEnumError> {
let (class_name, property_name) = declared.source()?;
let class = model
.classes
.iter()
.find(|class| class.identifier.as_str() == class_name)
.ok_or_else(|| TokenEnumError::UnknownClass {
class: class_name.to_owned(),
})?;
let property = class
.properties
.iter()
.find(|property| property.name.as_str() == property_name)
.ok_or_else(|| TokenEnumError::UnknownProperty {
class: class_name.to_owned(),
property: property_name.to_owned(),
})?;
let allowed = property.allowed_tokens();
if allowed.is_empty() {
return Err(TokenEnumError::NoAllowedTokens {
class: class_name.to_owned(),
property: property_name.to_owned(),
});
}
if let Some(token) = declared
.variants
.keys()
.find(|token| !allowed.iter().any(|allowed| allowed.as_str() == token.as_str()))
{
return Err(TokenEnumError::UnknownVariantToken { token: token.clone() });
}
let variants = variants(declared, &allowed, by_value)?;
let mut default = None;
if declared.default {
let fallback = property
.fallback()
.and_then(sdf::Value::try_as_token_ref)
.ok_or_else(|| TokenEnumError::NoFallback {
class: class_name.to_owned(),
property: property_name.to_owned(),
})?;
let named = variants
.iter()
.find(|variant| variant.value == fallback.as_str())
.ok_or_else(|| TokenEnumError::UnknownFallback {
token: fallback.to_string(),
})?;
default = Some(named.name.clone());
}
Ok(RustEnum {
source: (class_name.to_owned(), property_name.to_owned()),
name: Ident::new(&declared.name, Span::call_site()),
documentation: enum_documentation(class_name, property_name, property),
variants,
default,
})
}
fn enum_documentation(class: &str, property: &str, source: &Property) -> String {
let declared = doc::wrap(&format!(
"The values `{class}.{property}` admits, as a Rust type. \
Read and written as the tokens themselves."
));
match source.documentation() {
Some(text) => format!("{}\n\n{declared}", doc::to_markdown(text, &doc::Symbols::default())),
None => declared,
}
}
fn variants(
declared: &TokenEnum,
allowed: &[tf::Token],
by_value: &BTreeMap<&str, &Ident>,
) -> Result<Vec<RustVariant>, TokenEnumError> {
let mut minted: BTreeMap<String, String> = BTreeMap::new();
let mut variants = Vec::with_capacity(allowed.len());
for token in allowed {
let value = token.as_str();
let spelled = declared.variants.get(value);
let name = spelled.cloned().unwrap_or_else(|| names::proper_case(value));
if !names::is_rust_identifier(&name) {
return Err(match spelled {
Some(_) => TokenEnumError::InvalidVariant {
token: value.to_owned(),
variant: name,
},
None => TokenEnumError::UnnameableToken {
token: value.to_owned(),
},
});
}
if let Some(first) = minted.insert(name.clone(), value.to_owned()) {
return Err(TokenEnumError::VariantCollision {
first,
second: value.to_owned(),
variant: name,
});
}
variants.push(RustVariant {
name: Ident::new(&name, Span::call_site()),
constant: constant_of(by_value, token),
value: value.to_owned(),
});
}
Ok(variants)
}
fn constants(model: &Library) -> Result<Vec<Constant>, Error> {
let mut minted: BTreeSet<String> = BTreeSet::new();
let mut constants = Vec::new();
for token in model.tokens()? {
let mut name = names::screaming_snake(&token.id);
while !minted.insert(name.clone()) {
name.push('_');
}
let origin = format!("token `{}`", token.id);
constants.push(Constant {
name: identifier(&name, &origin)?,
documentation: doc::wrap(&format!("`\"{}\"`: {}.", token.value, token.documentation.join(", "))),
value: token.value,
});
}
Ok(constants)
}
fn check_class_names(inventory: &Inventory<'_>, enums: &[RustEnum]) -> Result<(), Error> {
let mut seen: BTreeMap<String, &str> = super::items::ALL
.map(|name| (name.to_owned(), "the generated file"))
.into();
for generated in enums {
seen.insert(generated.name.to_string(), "a token enum");
}
for names in inventory.values() {
let identifier = names.class.identifier.as_str();
let mut minted = vec![names.view.clone()];
if names.qualifier != names.view {
minted.push(names.qualifier.clone());
}
for minted in minted {
if let Some(first) = seen.insert(minted.clone(), identifier)
&& first != identifier
{
return Err(names.class.violation(Violation::RustNameCollision {
name: minted,
first: first.to_owned(),
second: identifier.to_owned(),
}));
}
}
}
Ok(())
}
fn lower_class(
class: &Class,
model: &Library,
placed: &Placed<'_>,
by_value: &BTreeMap<&str, &Ident>,
sourced: &Sourced<'_>,
inventory: &Inventory<'_>,
library: &doc::Symbols<'_>,
) -> Result<RustClass, Error> {
let reflections = class.reflections(model);
check_methods(class, model, &reflections)?;
for property in class.local_properties().filter(|property| !offers(class, property)) {
if property.api.custom_get {
return Err(property.violation(Violation::CustomGetWithoutAccessor {
property: property.name.clone(),
}));
}
}
let elsewhere: BTreeMap<&tf::Token, ClassNames<'_>> = class
.bases
.iter()
.filter(|base| base.library != model.name)
.filter_map(|base| {
let Some(Ok(home)) = placed.get(base.library.as_str()) else {
return None;
};
let ancestor = model.find(&base.identifier)?;
Some((&base.identifier, ClassNames::of(ancestor, Some(home))))
})
.collect();
let chain = iter::once(&class.identifier).chain(class.bases.iter().map(|base| &base.identifier));
let owners = chain.filter_map(|owner| inventory.get(owner).or_else(|| elsewhere.get(owner)));
let links = class_symbols(owners, library);
let mine = inventory.get(&class.identifier).expect("a class of this library");
let accessors = mine
.accessors
.iter()
.map(|accessor| lower_accessor(class, accessor, by_value, sourced, &links))
.collect::<Result<_, _>>()?;
let inherited = class
.bases
.iter()
.map(|base| base_trait(class, base, model, placed))
.collect::<Result<Vec<_>, _>>()?;
let parent = match (class.bases.first(), inherited.first()) {
(_, Some(Some(path))) => path.clone(),
(Some(base), _) => root_trait(&base.identifier),
(None, _) => syn::parse_quote! { ::openusd::usd::SchemaBase },
};
let inherited: Vec<syn::Path> = inherited.into_iter().flatten().collect();
let mut accessors: Vec<RustAccessor> = accessors;
let mut reflected = Vec::new();
for reflection in &reflections {
let Reflection::Taken { schema, properties, .. } = reflection else {
continue;
};
let origin = schema.origin.describe();
let elsewhere;
let (theirs, home) = match inventory.get(&schema.identifier) {
Some(theirs) => (theirs, None),
None if schema.library == model.name => {
return Err(class.violation(Violation::Ungenerated {
class: schema.identifier.clone(),
}));
}
None => {
let home = reached(class, placed, &schema.library, &schema.identifier)?;
elsewhere = ClassNames::of(schema, Some(&home));
(&elsewhere, Some(home))
}
};
let links = class_symbols(iter::once(theirs), library);
let taken = theirs.accessors.iter().filter(|accessor| {
properties
.iter()
.any(|property| property.name == accessor.property.name)
});
for accessor in taken {
accessors.push(lower_accessor(schema, accessor, by_value, sourced, &links)?);
}
let view = identifier(&schema.metadata.class_name, &origin)?;
reflected.push(Reflected {
accessor: identifier(&names::snake_case(&schema.metadata.class_name), &origin)?,
view: match &home {
Some(home) => syn::parse_quote! { #home::#view },
None => syn::parse_quote! { #view },
},
});
}
let origin = class.origin.describe();
Ok(RustClass {
name: identifier(&mine.view, &origin)?,
kind: class.kind,
accessor_trait: match class.kind.is_applied_api_schema() {
true => None,
false => Some(AccessorTrait {
name: identifier(&mine.qualifier, &origin)?,
parent,
}),
},
inherited,
constant: constant_of(by_value, &class.identifier),
documentation: class
.documentation
.as_deref()
.map(|text| doc::to_markdown(text, &links)),
accessors,
reflected,
instancing: instancing(class),
})
}
fn instancing(class: &Class) -> Option<Instancing> {
let prefix = class.metadata.property_namespace_prefix.as_ref()?;
class.kind.is_multiple_apply_api_schema().then(|| Instancing {
prefix: prefix.to_string(),
base_names: class
.local_properties()
.map(|property| {
usd::SchemaRegistry::multiple_apply_name_template_base_name(property.schematics_name.as_str())
.to_string()
})
.collect(),
})
}
fn lower_accessor(
class: &Class,
accessor: &AccessorNames<'_>,
by_value: &BTreeMap<&str, &Ident>,
sourced: &Sourced<'_>,
symbols: &doc::Symbols<'_>,
) -> Result<RustAccessor, Error> {
let property = accessor.property;
let origin = property.origin.describe();
let kind = match property.shape.spec_type {
sdf::SpecType::Relationship => PropertyKind::Relationship,
_ => PropertyKind::Attribute {
type_constant: value_type(property)?,
},
};
let ty = match sourced.get(&(class.identifier.as_str(), property.name.as_str())) {
Some(name) => Some(syn::parse_quote! { #name }),
None => property
.type_name()
.and_then(|name| name.kind())
.and_then(types::rust_type),
};
let typed = match (&accessor.value, ty) {
(Some(value), Some(ty)) if !property.api.custom_get => Some(TypedRead {
value: identifier(value, &origin)?,
value_at: accessor
.value_at
.as_deref()
.map(|value_at| identifier(value_at, &origin))
.transpose()?,
ty,
}),
_ => None,
};
Ok(RustAccessor {
getter: identifier(&accessor.getter, &origin)?,
creator: identifier(&accessor.creator, &origin)?,
builder: identifier(&accessor.builder, &origin)?,
typed,
custom_get: property.api.custom_get,
token: constant_of(by_value, &property.schematics_name),
instanced: class.kind.is_multiple_apply_api_schema(),
kind,
custom: property.shape.custom,
uniform: property.shape.variability == sdf::Variability::Uniform
&& property.shape.spec_type != sdf::SpecType::Relationship,
documentation: documentation(property, symbols),
})
}
fn offers(class: &Class, property: &Property) -> bool {
class.kind.is_applied_api_schema() || !shadows_an_ancestor(class, property)
}
fn placed<'a>(externs: &'a Externs, model: &Library) -> Placed<'a> {
externs
.iter()
.filter(|(library, _)| model.foreign.iter().any(|foreign| &foreign.library == *library))
.map(|(library, text)| (library.as_str(), syn::parse_str(text).map_err(|_| text.as_str())))
.collect()
}
fn reached(class: &Class, placed: &Placed<'_>, library: &str, named: &tf::Token) -> Result<syn::Path, Error> {
match placed.get(library) {
Some(Ok(path)) => Ok(path.clone()),
Some(Err(text)) => Err(class.violation(Violation::UnreadableLibraryPath {
library: library.to_owned(),
path: (*text).to_owned(),
})),
None => Err(class.violation(Violation::UnknownLibrary {
library: library.to_owned(),
class: named.clone(),
})),
}
}
fn value_type(property: &Property) -> Result<syn::Path, Error> {
let named = property.type_name().and_then(|name| name.constant_ident());
let Some(constant) = named else {
return Err(property.violation(Violation::UnnameableType {
property: property.name.clone(),
type_name: property.type_name().map(|name| name.as_token()).unwrap_or_default(),
}));
};
let constant = Ident::new(constant, Span::call_site());
Ok(syn::parse_quote! { ::openusd::sdf::ValueTypeName::#constant })
}
pub(super) fn constant_of(by_value: &BTreeMap<&str, &Ident>, value: &tf::Token) -> TokenStream {
if let Some(name) = by_value.get(value.as_str()) {
let tokens = super::ident(super::items::TOKENS);
return quote! { #tokens::#name };
}
let literal = value.as_str();
quote! { #literal }
}
pub(super) fn identifier(name: &str, origin: &str) -> Result<Ident, Error> {
if !names::is_rust_identifier(name) {
return Err(Error::Definition {
origin: origin.to_owned(),
violation: Violation::NotAnIdentifier { name: name.to_owned() },
});
}
Ok(Ident::new(name, Span::call_site()))
}
struct Accessor {
getter: String,
creator: String,
builder: String,
value: Option<String>,
value_at: Option<String>,
}
fn named(api_name: Option<&str>, shape: &Shape) -> Option<Accessor> {
let api_name = api_name?;
let suffix = match shape.spec_type {
sdf::SpecType::Relationship => "rel",
_ => "attr",
};
let snake = names::snake_case(api_name);
let getter = format!("{snake}_{suffix}");
let value = names::method_name(api_name);
let attribute = shape.spec_type != sdf::SpecType::Relationship && !VIEW_METHODS.contains(&value.as_str());
Some(Accessor {
creator: format!("create_{getter}"),
builder: format!("{getter}_builder"),
getter,
value: attribute.then_some(value),
value_at: (attribute && shape.variability != sdf::Variability::Uniform).then(|| format!("{snake}_at")),
})
}
impl Accessor {
fn methods(self) -> impl Iterator<Item = String> {
[self.getter, self.creator, self.builder]
.into_iter()
.chain(self.value)
.chain(self.value_at)
}
}
fn offered(property: &Property) -> impl Iterator<Item = String> {
property
.sites
.iter()
.filter_map(|site| named(site.api_name.as_deref(), &property.shape))
.flat_map(Accessor::methods)
}
fn reflected_methods<'r>(reflections: &'r [Reflection<'r>]) -> impl Iterator<Item = (String, &'r tf::Token)> + 'r {
reflections
.iter()
.filter_map(|reflection| match reflection {
Reflection::Taken { schema, properties, .. } => Some((schema, properties)),
_ => None,
})
.flat_map(|(schema, properties)| {
properties
.iter()
.filter_map(|property| named(property.api_name(), &property.shape))
.flat_map(Accessor::methods)
.chain(iter::once(names::snake_case(&schema.metadata.class_name)))
.map(move |method| (method, &schema.identifier))
})
}
fn shadows_an_ancestor(class: &Class, property: &Property) -> bool {
let Some(mine) = named(property.api_name(), &property.shape) else {
return false;
};
property
.sites
.iter()
.filter(|site| site.class != class.identifier)
.filter_map(|site| named(site.api_name.as_deref(), &property.shape))
.any(|theirs| theirs.getter == mine.getter)
}
fn check_methods(class: &Class, model: &Library, reflections: &[Reflection<'_>]) -> Result<(), Error> {
let mut seen: BTreeMap<String, &tf::Token> = BTreeMap::new();
for property in &class.properties {
for method in offered(property) {
if let Some(first) = seen.insert(method.clone(), &property.name)
&& first != &property.name
{
return Err(property.violation(Violation::MethodCollision {
method,
first: first.clone(),
second: property.name.clone(),
}));
}
}
}
for base in &class.bases {
let Some(ancestor) = model.find(&base.identifier) else {
continue;
};
let theirs = ancestor.reflections(model);
for (method, _) in reflected_methods(&theirs) {
if let Some(first) = seen.insert(method.clone(), &ancestor.identifier) {
return Err(class.violation(Violation::MethodCollision {
method,
first: first.clone(),
second: ancestor.identifier.clone(),
}));
}
}
}
for (method, schema) in reflected_methods(reflections) {
if let Some(first) = seen.insert(method.clone(), schema) {
return Err(class.violation(Violation::MethodCollision {
method,
first: first.clone(),
second: schema.clone(),
}));
}
}
Ok(())
}
fn library_symbols(inventory: &Inventory<'_>, model: &Library, placed: &Placed<'_>) -> doc::Symbols<'static> {
let mut symbols = doc::Symbols::default();
let mut accessors: BTreeMap<String, Option<String>> = BTreeMap::new();
for class in inventory.values() {
for (cpp, markdown) in class.links() {
accessors
.entry(cpp)
.and_modify(|found| *found = None)
.or_insert(Some(markdown));
}
}
for (cpp, markdown) in accessors {
if let Some(markdown) = markdown {
symbols.insert(cpp, markdown);
}
}
for class in inventory.values() {
let view = &class.view;
let link = doc::link(view, None);
symbols.insert(class.qualified.clone(), link.clone());
if coined(view) {
symbols.insert(view.clone(), link);
}
}
let mut shared: BTreeMap<&str, usize> = BTreeMap::new();
for foreign in &model.foreign {
*shared.entry(foreign.class_name.as_str()).or_default() += 1;
}
for foreign in &model.foreign {
let Some(Ok(home)) = placed.get(foreign.library.as_str()) else {
continue;
};
let link = doc::link(&foreign.class_name, Some(&super::within(home, &foreign.class_name)));
symbols.insert(foreign.upstream_name.clone(), link.clone());
if coined(&foreign.class_name) && shared[foreign.class_name.as_str()] == 1 {
symbols.insert(foreign.class_name.clone(), link);
}
}
symbols
}
fn coined(name: &str) -> bool {
name.chars().skip(1).any(char::is_uppercase)
}
fn class_symbols<'a, 'i>(
owners: impl Iterator<Item = &'i ClassNames<'i>>,
library: &'a doc::Symbols<'a>,
) -> doc::Symbols<'a> {
let mut symbols = doc::Symbols::layered(library);
for owner in owners {
for (cpp, markdown) in owner.links() {
symbols.insert(cpp, markdown);
}
}
symbols
}
fn trait_name(class_name: &str) -> String {
format!("{class_name}Schema")
}
fn base_trait(class: &Class, base: &Base, model: &Library, placed: &Placed<'_>) -> Result<Option<syn::Path>, Error> {
if is_root(&base.identifier) {
return Ok(None);
}
let origin = class.origin.describe();
let inherited = identifier(&trait_name(&base.class_name), &origin)?;
if base.library == model.name {
return match base.generated {
true => Ok(Some(syn::parse_quote! { #inherited })),
false => Err(class.violation(Violation::Ungenerated {
class: base.identifier.clone(),
})),
};
}
let path = reached(class, placed, &base.library, &base.identifier)?;
Ok(Some(syn::parse_quote! { #path::#inherited }))
}
const VIEW_METHODS: &[&str] = &[
"prim",
"path",
"stage",
"is_concrete",
"is_typed",
"is_api_schema",
"is_applied_api_schema",
"is_multiple_apply_api_schema",
"from_prim",
"from_prim_unchecked",
"get",
"define",
"apply",
"can_apply",
"name",
"get_all",
"get_instance",
"instance_at_path",
"is_schema_property_base_name",
];
pub(super) fn schema_root(kind: usd::SchemaKind) -> syn::Path {
match kind {
usd::SchemaKind::ConcreteTyped | usd::SchemaKind::AbstractTyped => {
syn::parse_quote! { ::openusd::usd::Typed }
}
usd::SchemaKind::AbstractBase
| usd::SchemaKind::NonAppliedApi
| usd::SchemaKind::SingleApplyApi
| usd::SchemaKind::MultipleApplyApi => syn::parse_quote! { ::openusd::usd::APISchemaBase },
}
}
pub(super) fn kind_constant(kind: usd::SchemaKind) -> syn::Path {
let variant = Ident::new(
match kind {
usd::SchemaKind::AbstractBase => "AbstractBase",
usd::SchemaKind::AbstractTyped => "AbstractTyped",
usd::SchemaKind::ConcreteTyped => "ConcreteTyped",
usd::SchemaKind::NonAppliedApi => "NonAppliedApi",
usd::SchemaKind::SingleApplyApi => "SingleApplyApi",
usd::SchemaKind::MultipleApplyApi => "MultipleApplyApi",
},
Span::call_site(),
);
syn::parse_quote! { ::openusd::usd::SchemaKind::#variant }
}
fn root_trait(name: &tf::Token) -> syn::Path {
let (family, _) = usd::SchemaRegistry::parse_schema_family_and_version(name);
match family.as_str() {
TYPED => syn::parse_quote! { ::openusd::usd::Typed },
API_SCHEMA_BASE => syn::parse_quote! { ::openusd::usd::APISchemaBase },
_ => syn::parse_quote! { ::openusd::usd::SchemaBase },
}
}
fn documentation(property: &Property, symbols: &doc::Symbols<'_>) -> String {
let declared = doc::wrap(&declaration(property));
match property.documentation() {
Some(text) => format!("{}\n\n{declared}", doc::to_markdown(text, symbols)),
None => declared,
}
}
fn declaration(property: &Property) -> String {
let mut text = format!("Declared `{}`.", declared_as(property));
text.push_str(&allowed_values(&property.allowed_tokens()));
text
}
pub(super) fn allowed_values(allowed: &[tf::Token]) -> String {
if allowed.is_empty() {
return String::new();
}
let listed: Vec<String> = allowed
.iter()
.map(|token| match token.as_str().is_empty() {
true => "`\"\"`".to_owned(),
false => format!("`{token}`"),
})
.collect();
format!(" One of {}.", listed.join(", "))
}
fn declared_as(property: &Property) -> String {
let mut text = String::new();
if property.shape.custom {
text.push_str("custom ");
}
if property.shape.variability == sdf::Variability::Uniform
&& property.shape.spec_type != sdf::SpecType::Relationship
{
text.push_str("uniform ");
}
if property.shape.spec_type == sdf::SpecType::Relationship {
text.push_str("rel ");
} else {
let spelling = property
.type_name()
.map_or_else(|| "token".to_owned(), |name| name.serialization_name().to_string());
text.push_str(&spelling);
text.push(' ');
}
text.push_str(property.schematics_name.as_str());
if let Some(fallback) = property.fallback()
&& let Ok(value) = usda::TextWriter::value_to_string(fallback)
{
text.push_str(&format!(" = {value}"));
}
text
}