use std::cmp::Reverse;
use std::collections::{HashMap, HashSet};
use std::mem;
use std::sync::{Arc, OnceLock, PoisonError, RwLock};
use crate::{ar, pcp, sdf, tf, usda};
use super::prim_definition::{self, FamilyVersions};
use super::{PrimDefinition, PrimTypeId, PrimTypeInfo, SchemaKind};
#[derive(Debug)]
pub struct SchemaRegistry {
infos: HashMap<tf::Token, SchemaInfo>,
families: HashMap<tf::Token, Vec<tf::Token>>,
concrete_defs: HashMap<tf::Token, Arc<PrimDefinition>>,
api_defs: HashMap<tf::Token, Arc<PrimDefinition>>,
empty_def: Arc<PrimDefinition>,
empty_type_info: Arc<PrimTypeInfo>,
type_infos: RwLock<HashMap<PrimTypeId, Arc<PrimTypeInfo>>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchemaInfo {
identifier: tf::Token,
family: tf::Token,
version: u32,
kind: SchemaKind,
bases: Vec<tf::Token>,
property_namespace_prefix: Option<tf::Token>,
auto_apply_to: Vec<tf::Token>,
can_only_apply_to: Vec<tf::Token>,
allowed_instance_names: Vec<tf::Token>,
}
#[derive(Debug)]
pub struct Schematics {
family: tf::Token,
resolved_location: Option<ar::ResolvedPath>,
data: sdf::Data,
}
#[derive(Debug, Clone, Copy)]
pub struct FamilySource<'a> {
pub name: &'a str,
pub manifest: &'a str,
pub schematics: &'a str,
pub resolved_location: Option<&'a ar::ResolvedPath>,
}
#[derive(Debug, Default)]
pub struct SchemaRegistryBuilder {
infos: HashMap<tf::Token, SchemaInfo>,
source_of: HashMap<tf::Token, Arc<Schematics>>,
extra_auto_apply: HashMap<tf::Token, Vec<tf::Token>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VersionFilter {
All,
GreaterThan(u32),
GreaterThanOrEqual(u32),
LessThan(u32),
LessThanOrEqual(u32),
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SchemaRegistryError {
#[error("Schema family {family} was registered with an empty resolved location")]
EmptyResolvedLocation {
family: tf::Token,
},
#[error("Unable to parse schematics for schema family {family}")]
Schematics {
family: tf::Token,
#[source]
source: usda::ParseError,
},
#[error("Unable to parse manifest for schema family {family}")]
Manifest {
family: tf::Token,
#[source]
source: usda::ParseError,
},
#[error("Unable to read schema {identifier} of family {family}: {cause}")]
Schema {
identifier: tf::Token,
family: tf::Token,
cause: Box<SchemaRegistryError>,
},
#[error("schemaKind is required")]
MissingSchemaKind,
#[error("Unknown schemaKind {kind}")]
UnknownSchemaKind {
kind: tf::Token,
},
#[error("Schema identifier {identifier} of family {family} is not a valid identifier")]
InvalidIdentifier {
identifier: tf::Token,
family: tf::Token,
},
#[error("Duplicate schema identifier {identifier} registering family {family}")]
DuplicateIdentifier {
identifier: tf::Token,
family: tf::Token,
},
#[error("No manifest entry for schema {identifier}")]
MissingManifestEntry {
identifier: tf::Token,
},
#[error("No schematics registered for schema {identifier}")]
MissingSchematics {
identifier: tf::Token,
},
#[error("No class prim for schema {identifier} in the schematics of family {family}")]
MissingClassPrim {
identifier: tf::Token,
family: tf::Token,
},
#[error(transparent)]
Path(#[from] sdf::PathParseError),
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ApplyApiError {
#[error("no prim at {path} to apply an API schema to")]
PrimNotValid {
path: sdf::Path,
},
#[error(transparent)]
Composition(#[from] pcp::QueryError),
#[error("{schema} is not an applied API schema")]
NotAppliedApi {
schema: tf::Token,
},
#[error("multiple-apply schema {schema} needs an instance name")]
MissingInstanceName {
schema: tf::Token,
},
#[error("single-apply schema {schema} takes no instance name, got {instance}")]
UnexpectedInstanceName {
schema: tf::Token,
instance: tf::Token,
},
#[error("{instance} is not an allowed instance name for {schema}")]
InstanceNameNotAllowed {
schema: tf::Token,
instance: tf::Token,
},
#[error("{schema} can only be applied to {allowed:?}")]
PrimTypeNotAllowed {
schema: tf::Token,
allowed: Vec<tf::Token>,
},
}
impl SchemaRegistry {
pub fn global() -> &'static Arc<SchemaRegistry> {
static GLOBAL: OnceLock<Arc<SchemaRegistry>> = OnceLock::new();
GLOBAL.get_or_init(|| {
SchemaRegistryBuilder::compiled_in()
.build()
.expect("compiled-in schema data must parse")
})
}
pub fn builder() -> SchemaRegistryBuilder {
SchemaRegistryBuilder::default()
}
pub fn schema_info(&self, identifier: &tf::Token) -> Option<&SchemaInfo> {
self.infos.get(identifier)
}
pub fn schema_info_in_family(&self, family: &tf::Token, version: u32) -> Option<&SchemaInfo> {
Self::is_allowed_family(family)
.then(|| self.infos.get(&Self::make_identifier(family, version)))
.flatten()
}
pub fn schema_infos_in_family<'a>(
&'a self,
family: &tf::Token,
filter: VersionFilter,
) -> impl Iterator<Item = &'a SchemaInfo> + use<'a> {
self.families
.get(family)
.into_iter()
.flatten()
.filter_map(|identifier| self.infos.get(identifier))
.filter(move |info| filter.accepts(info.version))
}
pub fn version_in_family(&self, schema_type: &tf::Token, family: &tf::Token, filter: VersionFilter) -> Option<u32> {
self.schema_infos_in_family(family, filter)
.find(|info| self.is_a(schema_type, info.identifier()))
.map(SchemaInfo::version)
}
pub fn parse_allowed_identifier(identifier: &tf::Token) -> Option<(tf::Token, u32)> {
Self::is_allowed_identifier(identifier).then(|| Self::parse_identifier(identifier))
}
pub fn schema_infos(&self) -> impl Iterator<Item = &SchemaInfo> {
self.infos.values()
}
pub fn concrete_prim_definition(&self, type_name: &tf::Token) -> Option<&Arc<PrimDefinition>> {
self.concrete_defs.get(type_name)
}
pub fn api_prim_definition(&self, identifier: &tf::Token) -> Option<&Arc<PrimDefinition>> {
self.api_defs.get(identifier)
}
pub fn empty_prim_definition(&self) -> &Arc<PrimDefinition> {
&self.empty_def
}
pub fn is_empty(&self) -> bool {
self.infos.is_empty()
}
pub fn is_concrete_type(&self, type_name: &tf::Token) -> bool {
self.concrete_defs.contains_key(type_name)
}
pub fn is_a(&self, schema: &tf::Token, base: &tf::Token) -> bool {
if !self.infos.contains_key(base) {
return false;
}
let mut visited = HashSet::new();
let mut pending = vec![schema];
while let Some(next) = pending.pop() {
if next == base {
return true;
}
if !visited.insert(next) {
continue;
}
let Some(info) = self.infos.get(next) else {
continue;
};
pending.extend(&info.bases);
}
false
}
pub fn check_applied_name(
&self,
name: &tf::Token,
) -> Result<Option<(&SchemaInfo, Option<tf::Token>)>, ApplyApiError> {
check_applied_shape(&self.infos, name)
}
pub fn is_allowed_instance_name(&self, identifier: &tf::Token, instance: &tf::Token) -> bool {
let (Some(info), Some(definition)) = (self.infos.get(identifier), self.api_defs.get(identifier)) else {
return false;
};
if !instance.split(':').all(sdf::Path::is_valid_identifier) || info.kind != SchemaKind::MultipleApplyApi {
return false;
}
if !info.allowed_instance_names.is_empty() && !info.allowed_instance_names.contains(instance) {
return false;
}
let base = instance.rsplit_once(':').map_or(instance.as_str(), |(_, base)| base);
!definition
.property_names()
.iter()
.any(|property| name_template_base(property) == base)
}
pub fn is_disallowed_field(field: &str) -> bool {
const ARCS: [sdf::FieldKey; 6] = [
sdf::FieldKey::InheritPaths,
sdf::FieldKey::Payload,
sdf::FieldKey::References,
sdf::FieldKey::Specializes,
sdf::FieldKey::VariantSelection,
sdf::FieldKey::VariantSetNames,
];
const RESOLVED: [sdf::FieldKey; 10] = [
sdf::FieldKey::Active,
sdf::FieldKey::ConnectionPaths,
sdf::FieldKey::CustomData,
sdf::FieldKey::Instanceable,
sdf::FieldKey::Kind,
sdf::FieldKey::Specifier,
sdf::FieldKey::TargetPaths,
sdf::FieldKey::TimeSamples,
sdf::FieldKey::Clips,
sdf::FieldKey::ClipSets,
];
ARCS.iter().chain(RESOLVED.iter()).any(|key| key.as_str() == field) || sdf::is_children_field(field)
}
pub fn build_composed_prim_definition(&self, type_name: &tf::Token, applied: &[tf::Token]) -> Arc<PrimDefinition> {
let typed = self.concrete_defs.get(type_name).unwrap_or(&self.empty_def);
if applied.is_empty() {
return typed.clone();
}
let mut seen = FamilyVersions::new();
for name in typed.applied_api_schemas() {
let (identifier, instance) = split_instance_name(name);
if let Some(info) = self.infos.get(&identifier) {
seen.insert((info.family().clone(), instance), info.version());
}
}
let mut definition = PrimDefinition::clone(typed);
for name in applied {
let Ok(Some((info, instance))) = self.check_applied_name(name) else {
continue;
};
let Some(weaker) = self.api_defs.get(info.identifier()) else {
continue;
};
definition.compose_weaker_api(weaker, instance.as_ref(), &self.infos, &mut seen);
}
definition.finish_composition();
Arc::new(definition)
}
pub fn prim_type_info(&self, id: PrimTypeId) -> Arc<PrimTypeInfo> {
if id.is_empty() {
return self.empty_type_info.clone();
}
let cached = self.type_infos.read().unwrap_or_else(PoisonError::into_inner);
if let Some(info) = cached.get(&id) {
return info.clone();
}
drop(cached);
let schema_type_name = match self.is_concrete_type(id.lookup_name()) {
true => id.lookup_name().clone(),
false => tf::Token::default(),
};
let definition = self.build_composed_prim_definition(&schema_type_name, id.applied_api_schemas());
let info = Arc::new(PrimTypeInfo::new(id.clone(), schema_type_name, definition));
self.type_infos
.write()
.unwrap_or_else(PoisonError::into_inner)
.entry(id)
.or_insert(info)
.clone()
}
pub fn empty_prim_type_info(&self) -> &Arc<PrimTypeInfo> {
&self.empty_type_info
}
pub fn parse_identifier(identifier: &tf::Token) -> (tf::Token, u32) {
match version_delimiter(identifier.as_str()) {
Some(delim) => match identifier[delim + 1..].parse() {
Ok(version) => (tf::Token::from(&identifier[..delim]), version),
Err(_) => (identifier.clone(), 0),
},
None => (identifier.clone(), 0),
}
}
pub fn make_identifier(family: &tf::Token, version: u32) -> tf::Token {
match version {
0 => family.clone(),
_ => tf::Token::from(format!("{family}_{version}")),
}
}
pub fn is_allowed_family(family: &tf::Token) -> bool {
sdf::Path::is_valid_identifier(family.as_str()) && version_delimiter(family.as_str()).is_none()
}
pub fn is_allowed_identifier(identifier: &tf::Token) -> bool {
let (family, version) = Self::parse_identifier(identifier);
Self::is_allowed_family(&family) && &Self::make_identifier(&family, version) == identifier
}
}
impl VersionFilter {
pub fn accepts(self, version: u32) -> bool {
match self {
Self::All => true,
Self::GreaterThan(other) => version > other,
Self::GreaterThanOrEqual(other) => version >= other,
Self::LessThan(other) => version < other,
Self::LessThanOrEqual(other) => version <= other,
}
}
}
impl SchemaInfo {
pub fn identifier(&self) -> &tf::Token {
&self.identifier
}
pub fn family(&self) -> &tf::Token {
&self.family
}
pub fn version(&self) -> u32 {
self.version
}
pub fn kind(&self) -> SchemaKind {
self.kind
}
pub fn bases(&self) -> &[tf::Token] {
&self.bases
}
pub fn property_namespace_prefix(&self) -> Option<&tf::Token> {
self.property_namespace_prefix.as_ref()
}
pub fn auto_apply_to(&self) -> &[tf::Token] {
&self.auto_apply_to
}
pub fn can_only_apply_to(&self) -> &[tf::Token] {
&self.can_only_apply_to
}
pub fn allowed_instance_names(&self) -> &[tf::Token] {
&self.allowed_instance_names
}
pub fn is_applied_api(&self) -> bool {
matches!(self.kind, SchemaKind::SingleApplyApi | SchemaKind::MultipleApplyApi)
}
}
impl Schematics {
pub fn family(&self) -> &tf::Token {
&self.family
}
pub fn resolved_location(&self) -> Option<&ar::ResolvedPath> {
self.resolved_location.as_ref()
}
pub fn data(&self) -> &sdf::Data {
&self.data
}
}
impl SchemaRegistryBuilder {
pub fn compiled_in() -> Self {
Self::default()
}
pub fn family(mut self, source: FamilySource<'_>) -> Result<Self, SchemaRegistryError> {
let family = tf::Token::from(source.name);
if source.resolved_location.is_some_and(|location| location.is_empty()) {
return Err(SchemaRegistryError::EmptyResolvedLocation { family });
}
let schematics = Arc::new(Schematics {
family: family.clone(),
resolved_location: source.resolved_location.cloned(),
data: usda::parse(source.schematics).map_err(|source| SchemaRegistryError::Schematics {
family: family.clone(),
source,
})?,
});
let manifest = usda::parse(source.manifest).map_err(|source| SchemaRegistryError::Manifest {
family: family.clone(),
source,
})?;
for identifier in root_prims(&manifest) {
let info = read_schema_info(&manifest, &identifier).map_err(|cause| SchemaRegistryError::Schema {
identifier: identifier.clone(),
family: family.clone(),
cause: Box::new(cause),
})?;
if !SchemaRegistry::is_allowed_identifier(&identifier) {
return Err(SchemaRegistryError::InvalidIdentifier { identifier, family });
}
if self.infos.contains_key(&identifier) {
return Err(SchemaRegistryError::DuplicateIdentifier { identifier, family });
}
self.source_of.insert(identifier.clone(), schematics.clone());
self.infos.insert(identifier, info);
}
Ok(self)
}
pub fn auto_apply(
mut self,
api: impl Into<tf::Token>,
targets: impl IntoIterator<Item = impl Into<tf::Token>>,
) -> Self {
self.extra_auto_apply
.entry(api.into())
.or_default()
.extend(targets.into_iter().map(Into::into));
self
}
pub fn build(mut self) -> Result<Arc<SchemaRegistry>, SchemaRegistryError> {
for (api, targets) in mem::take(&mut self.extra_auto_apply) {
if let Some(info) = self.infos.get_mut(&api) {
info.auto_apply_to.extend(targets);
}
}
let auto_applied = self.compute_auto_applied();
let mut api_defs = HashMap::new();
for identifier in self.sorted_identifiers(SchemaInfo::is_applied_api) {
if api_defs.contains_key(&identifier) {
continue;
}
let mut expansion = Expansion::default();
self.expand_api_definition(&identifier, &auto_applied, &mut api_defs, &mut expansion)?;
expansion.provisional.remove(&identifier);
for identifier in expansion.provisional {
api_defs.remove(&identifier);
}
}
let mut concrete_defs = HashMap::new();
for identifier in self.sorted_identifiers(|info| info.kind == SchemaKind::ConcreteTyped) {
let definition = self.typed_definition(&identifier, &auto_applied, &api_defs)?;
concrete_defs.insert(identifier, Arc::new(definition));
}
let mut grouped: HashMap<tf::Token, Vec<(u32, tf::Token)>> = HashMap::new();
for (identifier, info) in &self.infos {
grouped
.entry(info.family.clone())
.or_default()
.push((info.version, identifier.clone()));
}
let families = grouped
.into_iter()
.map(|(family, mut versions)| {
versions.sort_unstable_by_key(|(version, _)| Reverse(*version));
(family, versions.into_iter().map(|(_, identifier)| identifier).collect())
})
.collect();
let empty_def = Arc::new(PrimDefinition::default());
Ok(Arc::new(SchemaRegistry {
infos: self.infos,
families,
concrete_defs,
api_defs,
empty_def: empty_def.clone(),
empty_type_info: Arc::new(PrimTypeInfo::new(
PrimTypeId::default(),
tf::Token::default(),
empty_def,
)),
type_infos: RwLock::default(),
}))
}
fn compute_auto_applied(&self) -> AutoApplied {
let mut derived: HashMap<&tf::Token, Vec<&tf::Token>> = HashMap::new();
for (identifier, info) in &self.infos {
for base in &info.bases {
derived.entry(base).or_default().push(identifier);
}
}
let mut auto_applied = AutoApplied::new();
for (api, info) in &self.infos {
if info.kind != SchemaKind::SingleApplyApi {
continue;
}
let mut pending: Vec<&tf::Token> = info
.auto_apply_to
.iter()
.filter(|target| self.infos.contains_key(*target))
.collect();
let mut reached = HashSet::new();
while let Some(target) = pending.pop() {
if !reached.insert(target) {
continue;
}
pending.extend(derived.get(target).into_iter().flatten().copied());
}
for target in reached {
auto_applied.entry(target.clone()).or_default().push(api.clone());
}
}
for names in auto_applied.values_mut() {
names.sort_unstable_by(|a, b| sdf::element_cmp(b, a));
}
auto_applied
}
fn sorted_identifiers(&self, wanted: impl Fn(&SchemaInfo) -> bool) -> Vec<tf::Token> {
let mut identifiers: Vec<tf::Token> = self
.infos
.iter()
.filter(|(_, info)| wanted(info))
.map(|(identifier, _)| identifier.clone())
.collect();
identifiers.sort_by(|a, b| sdf::element_cmp(a, b));
identifiers
}
fn expand_api_definition(
&self,
identifier: &tf::Token,
auto_applied: &AutoApplied,
api_defs: &mut HashMap<tf::Token, Arc<PrimDefinition>>,
expansion: &mut Expansion,
) -> Result<(), SchemaRegistryError> {
if api_defs.contains_key(identifier) {
return Ok(());
}
if !expansion.open.insert(identifier.clone()) {
expansion.truncated = true;
return Ok(());
}
let mut pending = self.begin_definition(identifier, auto_applied)?;
for name in mem::take(&mut pending.built_ins) {
let (built_in, instance) = split_instance_name(&name);
if !self.infos.contains_key(&built_in) {
continue;
}
self.expand_api_definition(&built_in, auto_applied, api_defs, expansion)?;
pending.compose_built_in(&built_in, instance.as_ref(), api_defs, &self.infos);
}
expansion.open.remove(identifier);
if expansion.truncated {
expansion.provisional.insert(identifier.clone());
}
api_defs.insert(identifier.clone(), Arc::new(pending.finish()));
Ok(())
}
fn typed_definition(
&self,
identifier: &tf::Token,
auto_applied: &AutoApplied,
api_defs: &HashMap<tf::Token, Arc<PrimDefinition>>,
) -> Result<PrimDefinition, SchemaRegistryError> {
let mut pending = self.begin_definition(identifier, auto_applied)?;
for name in mem::take(&mut pending.built_ins) {
let (built_in, instance) = split_instance_name(&name);
pending.compose_built_in(&built_in, instance.as_ref(), api_defs, &self.infos);
}
Ok(pending.finish())
}
fn begin_definition(
&self,
identifier: &tf::Token,
auto_applied: &AutoApplied,
) -> Result<PendingDefinition, SchemaRegistryError> {
let info = self
.infos
.get(identifier)
.ok_or_else(|| SchemaRegistryError::MissingManifestEntry {
identifier: identifier.clone(),
})?;
let schematics = self
.source_of
.get(identifier)
.ok_or_else(|| SchemaRegistryError::MissingSchematics {
identifier: identifier.clone(),
})?
.clone();
let class_prim = sdf::Path::abs_root().append_path(identifier.as_str())?;
let overrides = override_property_names(&schematics, &class_prim);
let applied_name = match info.kind {
SchemaKind::MultipleApplyApi => Some(make_name_template(identifier)),
SchemaKind::SingleApplyApi => Some(identifier.clone()),
_ => None,
};
let mut seen = FamilyVersions::new();
if info.is_applied_api() {
let instance = applied_name.as_ref().and_then(|name| split_instance_name(name).1);
seen.insert((info.family.clone(), instance), info.version);
}
Ok(PendingDefinition {
definition: PrimDefinition::from_class_prim(&schematics, identifier, applied_name, &overrides)?,
built_ins: self.direct_built_ins(&schematics, &class_prim, info, auto_applied),
schematics,
class_prim,
overrides,
seen,
})
}
fn direct_built_ins(
&self,
schematics: &Schematics,
class_prim: &sdf::Path,
info: &SchemaInfo,
auto_applied: &AutoApplied,
) -> Vec<tf::Token> {
let declared = class_prim_field(schematics, class_prim, sdf::FieldKey::ApiSchemas)
.and_then(|value| value.clone().try_as_token_list_op())
.map(|list_op| list_op.compose_over(&[]))
.unwrap_or_default();
let auto_applied = auto_applied.get(info.identifier()).into_iter().flatten().cloned();
let wants_templates = info.kind == SchemaKind::MultipleApplyApi;
declared
.into_iter()
.chain(auto_applied)
.filter(|name| is_name_template(name) == wants_templates)
.filter(|name| {
check_applied_shape(&self.infos, name).is_ok_and(|resolved| resolved.is_some())
})
.collect()
}
}
type AutoApplied = HashMap<tf::Token, Vec<tf::Token>>;
#[derive(Default)]
struct Expansion {
open: HashSet<tf::Token>,
truncated: bool,
provisional: HashSet<tf::Token>,
}
struct PendingDefinition {
definition: PrimDefinition,
built_ins: Vec<tf::Token>,
schematics: Arc<Schematics>,
class_prim: sdf::Path,
overrides: Vec<tf::Token>,
seen: FamilyVersions,
}
impl PendingDefinition {
fn compose_built_in(
&mut self,
built_in: &tf::Token,
instance: Option<&tf::Token>,
api_defs: &HashMap<tf::Token, Arc<PrimDefinition>>,
infos: &HashMap<tf::Token, SchemaInfo>,
) {
if let Some(weaker) = api_defs.get(built_in) {
self.definition
.compose_weaker_api(weaker, instance, infos, &mut self.seen);
}
}
fn finish(mut self) -> PrimDefinition {
for name in &self.overrides {
self.definition
.compose_override(name, &self.schematics, &self.class_prim);
}
self.definition.finish_composition();
self.definition
}
}
fn override_property_names(schematics: &Schematics, class_prim: &sdf::Path) -> Vec<tf::Token> {
const OVERRIDE_NAMES: &str = "apiSchemaOverridePropertyNames";
class_prim_field(schematics, class_prim, sdf::FieldKey::CustomData)
.and_then(|value| value.clone().try_as_dictionary())
.and_then(|mut custom_data| custom_data.remove(OVERRIDE_NAMES))
.and_then(sdf::Value::try_as_token_vec)
.unwrap_or_default()
}
fn class_prim_field<'a>(
schematics: &'a Schematics,
class_prim: &sdf::Path,
field: sdf::FieldKey,
) -> Option<&'a sdf::Value> {
schematics.data().spec(class_prim)?.get(field.as_str())
}
fn check_applied_shape<'a>(
infos: &'a HashMap<tf::Token, SchemaInfo>,
name: &tf::Token,
) -> Result<Option<(&'a SchemaInfo, Option<tf::Token>)>, ApplyApiError> {
let (schema, instance) = split_instance_name(name);
let Some(info) = infos.get(&schema) else {
return Ok(None);
};
if !info.is_applied_api() {
return Err(ApplyApiError::NotAppliedApi { schema });
}
match (info.kind == SchemaKind::MultipleApplyApi, instance) {
(true, None) => Err(ApplyApiError::MissingInstanceName { schema }),
(false, Some(instance)) => Err(ApplyApiError::UnexpectedInstanceName { schema, instance }),
(_, instance) => Ok(Some((info, instance))),
}
}
const INSTANCE_NAME_PLACEHOLDER: &str = "__INSTANCE_NAME__";
pub(super) fn split_instance_name(name: &tf::Token) -> (tf::Token, Option<tf::Token>) {
match name.split_once(':') {
Some((identifier, instance)) if !instance.is_empty() => {
(tf::Token::from(identifier), Some(tf::Token::from(instance)))
}
Some((identifier, _)) => (tf::Token::from(identifier), None),
None => (name.clone(), None),
}
}
pub(super) fn make_name_template(identifier: &tf::Token) -> tf::Token {
tf::Token::from(format!("{identifier}:{INSTANCE_NAME_PLACEHOLDER}"))
}
pub(super) fn make_instance_name(template: &tf::Token, instance: &tf::Token) -> tf::Token {
match placeholder_position(template) {
Some(start) => {
let mut name = String::with_capacity(template.len() + instance.len());
name.push_str(&template[..start]);
name.push_str(instance);
name.push_str(&template[start + INSTANCE_NAME_PLACEHOLDER.len()..]);
tf::Token::from(name)
}
None => template.clone(),
}
}
pub(super) fn name_template_base(name: &tf::Token) -> &str {
match placeholder_position(name) {
Some(start) => name
.get(start + INSTANCE_NAME_PLACEHOLDER.len() + 1..)
.unwrap_or_default(),
None => name,
}
}
pub(super) fn is_name_template(name: &tf::Token) -> bool {
placeholder_position(name).is_some()
}
fn placeholder_position(name: &tf::Token) -> Option<usize> {
let mut start = 0;
for component in name.split(':') {
if component == INSTANCE_NAME_PLACEHOLDER {
return Some(start);
}
start += component.len() + 1;
}
None
}
fn root_prims(data: &sdf::Data) -> Vec<tf::Token> {
prim_definition::child_names(data, &sdf::Path::abs_root(), sdf::ChildrenKey::PrimChildren)
}
fn read_schema_info(manifest: &sdf::Data, identifier: &tf::Token) -> Result<SchemaInfo, SchemaRegistryError> {
let prim = sdf::Path::abs_root().append_path(identifier.as_str())?;
let kind = manifest_token(manifest, &prim, "schemaKind").ok_or(SchemaRegistryError::MissingSchemaKind)?;
let kind = SchemaKind::from_token(kind.as_str()).ok_or_else(|| SchemaRegistryError::UnknownSchemaKind { kind })?;
let (family, version) = SchemaRegistry::parse_identifier(identifier);
Ok(SchemaInfo {
identifier: identifier.clone(),
family,
version,
kind,
bases: manifest_token_vec(manifest, &prim, "bases"),
property_namespace_prefix: manifest_token(manifest, &prim, "propertyNamespacePrefix"),
auto_apply_to: manifest_token_vec(manifest, &prim, "apiSchemaAutoApplyTo"),
can_only_apply_to: manifest_token_vec(manifest, &prim, "apiSchemaCanOnlyApplyTo"),
allowed_instance_names: manifest_token_vec(manifest, &prim, "allowedInstanceNames"),
})
}
fn manifest_token(manifest: &sdf::Data, prim: &sdf::Path, attribute: &str) -> Option<tf::Token> {
manifest_default(manifest, prim, attribute)?.clone().try_as_token()
}
fn manifest_token_vec(manifest: &sdf::Data, prim: &sdf::Path, attribute: &str) -> Vec<tf::Token> {
manifest_default(manifest, prim, attribute)
.and_then(|value| value.clone().try_as_token_vec())
.unwrap_or_default()
}
fn manifest_default<'a>(manifest: &'a sdf::Data, prim: &sdf::Path, attribute: &str) -> Option<&'a sdf::Value> {
let path = prim.append_property(attribute).ok()?;
manifest.spec(&path)?.get(sdf::FieldKey::Default.as_str())
}
fn version_delimiter(identifier: &str) -> Option<usize> {
let stem = identifier.trim_end_matches(|c: char| c.is_ascii_digit());
if stem.len() == identifier.len() {
return None;
}
stem.strip_suffix('_').map(str::len)
}
#[cfg(test)]
impl SchemaRegistry {
const TEST_MANIFEST: &'static str = r#"#usda 1.0
def "APISchemaBase"
{
uniform token schemaKind = "abstractBase"
}
def "Typed"
{
uniform token schemaKind = "abstractBase"
}
def "CollectionAPI"
{
uniform token schemaKind = "multipleApplyAPI"
uniform token[] bases = ["APISchemaBase"]
}
def "SlotAPI"
{
uniform token schemaKind = "multipleApplyAPI"
uniform token[] bases = ["APISchemaBase"]
uniform token[] allowedInstanceNames = ["left", "right"]
uniform token[] apiSchemaCanOnlyApplyTo = ["NonboundableLightBase"]
}
def "LightAPI"
{
uniform token schemaKind = "singleApplyAPI"
uniform token[] bases = ["APISchemaBase"]
uniform token[] apiSchemaCanOnlyApplyTo = ["DistantLight"]
}
def "LightAPI_2"
{
uniform token schemaKind = "singleApplyAPI"
uniform token[] bases = ["APISchemaBase"]
}
def "NonboundableLightBase"
{
uniform token schemaKind = "abstractTyped"
uniform token[] bases = ["Typed"]
}
def "DistantLight"
{
uniform token schemaKind = "concreteTyped"
uniform token[] bases = ["NonboundableLightBase"]
}
def "DomeLight"
{
uniform token schemaKind = "concreteTyped"
uniform token[] bases = ["NonboundableLightBase"]
}
def "DomeLight_1"
{
uniform token schemaKind = "concreteTyped"
uniform token[] bases = ["NonboundableLightBase"]
}
"#;
const TEST_SCHEMATICS: &'static str = r#"#usda 1.0
class "APISchemaBase"
{
}
class "Typed"
{
}
class "CollectionAPI"
{
uniform token collection:__INSTANCE_NAME__:expansionRule = "expandPrims" (
allowedTokens = ["explicitOnly", "expandPrims", "expandPrimsAndProperties"]
)
uniform bool collection:__INSTANCE_NAME__:includeRoot
rel collection:__INSTANCE_NAME__:includes
}
class "LightAPI" (
apiSchemas = ["CollectionAPI:lightLink"]
customData = {
token[] apiSchemaOverridePropertyNames = ["collection:lightLink:includeRoot"]
}
)
{
uniform bool collection:lightLink:includeRoot = 1
float inputs:intensity = 1
uniform token light:shaderId = ""
}
class "SlotAPI"
{
float slot:__INSTANCE_NAME__:depth = 0
}
class "LightAPI_2"
{
float inputs:intensity = 2
}
class "NonboundableLightBase"
{
}
class DistantLight "DistantLight" (
apiSchemas = ["LightAPI"]
customData = {
token[] apiSchemaOverridePropertyNames = ["inputs:intensity", "light:shaderId"]
}
)
{
float inputs:angle = 0.53
float inputs:intensity = 50000
uniform token light:shaderId = "DistantLight"
}
class DomeLight "DomeLight"
{
float inputs:intensity = 1
}
class DomeLight_1 "DomeLight_1"
{
float inputs:intensity = 1
uniform token poleAxis = "scene"
}
"#;
pub(crate) fn test_registry() -> Arc<SchemaRegistry> {
Self::test_family(Self::TEST_MANIFEST, Self::TEST_SCHEMATICS)
}
pub(crate) fn test_family(manifest: &str, schematics: &str) -> Arc<SchemaRegistry> {
Self::builder()
.family(FamilySource {
name: "test",
manifest,
schematics,
resolved_location: None,
})
.expect("test family registers")
.build()
.expect("test registry builds")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn manifest_infos() {
let registry = SchemaRegistry::test_registry();
let light = registry.schema_info(&tf::Token::new("LightAPI")).expect("LightAPI");
assert_eq!(light.kind(), SchemaKind::SingleApplyApi);
assert_eq!(light.bases(), [tf::Token::new("APISchemaBase")]);
assert_eq!(light.can_only_apply_to(), [tf::Token::new("DistantLight")]);
assert!(light.is_applied_api());
let collection = registry
.schema_info(&tf::Token::new("CollectionAPI"))
.expect("CollectionAPI");
assert_eq!(collection.kind(), SchemaKind::MultipleApplyApi);
assert!(collection.auto_apply_to().is_empty());
assert!(registry.schema_info(&tf::Token::new("Nonexistent")).is_none());
}
#[test]
fn is_a_walks_bases() {
let registry = SchemaRegistry::test_registry();
let distant = tf::Token::new("DistantLight");
assert!(registry.is_a(&distant, &distant));
assert!(registry.is_a(&distant, &tf::Token::new("NonboundableLightBase")));
assert!(registry.is_a(&distant, &tf::Token::new("Typed")));
assert!(!registry.is_a(&distant, &tf::Token::new("DomeLight_1")));
assert!(!registry.is_a(&distant, &tf::Token::new("APISchemaBase")));
assert!(!registry.is_a(&tf::Token::new("Bogus"), &tf::Token::new("Typed")));
assert!(!registry.is_a(&tf::Token::new("Bogus"), &tf::Token::new("Bogus")));
assert!(!registry.is_a(&distant, &tf::Token::default()));
}
#[test]
fn allowed_instance_names() {
let registry = SchemaRegistry::test_registry();
let slot = tf::Token::new("SlotAPI");
let collection = tf::Token::new("CollectionAPI");
assert!(registry.is_allowed_instance_name(&slot, &tf::Token::new("left")));
assert!(!registry.is_allowed_instance_name(&slot, &tf::Token::new("middle")));
assert!(registry.is_allowed_instance_name(&collection, &tf::Token::new("anything")));
assert!(!registry.is_allowed_instance_name(&collection, &tf::Token::default()));
assert!(!registry.is_allowed_instance_name(&tf::Token::new("LightAPI"), &tf::Token::new("x")));
assert!(!registry.is_allowed_instance_name(&tf::Token::new("Bogus"), &tf::Token::new("x")));
}
#[test]
fn family_version_filters() {
let registry = SchemaRegistry::test_registry();
let dome = tf::Token::new("DomeLight");
let identifiers = |family, filter| {
registry
.schema_infos_in_family(family, filter)
.map(|info| info.identifier().to_string())
.collect::<Vec<_>>()
};
assert_eq!(identifiers(&dome, VersionFilter::All), ["DomeLight_1", "DomeLight"]);
assert_eq!(identifiers(&dome, VersionFilter::GreaterThan(0)), ["DomeLight_1"]);
assert_eq!(
identifiers(&dome, VersionFilter::GreaterThanOrEqual(1)),
["DomeLight_1"]
);
assert_eq!(identifiers(&dome, VersionFilter::LessThan(1)), ["DomeLight"]);
assert_eq!(
identifiers(&dome, VersionFilter::LessThanOrEqual(1)),
["DomeLight_1", "DomeLight"]
);
assert!(identifiers(&dome, VersionFilter::GreaterThan(1)).is_empty());
assert!(identifiers(&tf::Token::new("Bogus"), VersionFilter::All).is_empty());
}
#[test]
fn identifier_places_family() {
let placed = |name| SchemaRegistry::parse_allowed_identifier(&tf::Token::new(name));
let family = |name| placed(name).map(|(family, _)| family.to_string());
assert_eq!(placed("DomeLight_1"), Some((tf::Token::new("DomeLight"), 1)));
assert_eq!(placed("DistantLight"), Some((tf::Token::new("DistantLight"), 0)));
assert_eq!(family("Bogus_2").as_deref(), Some("Bogus"));
assert_eq!(placed("DomeLight_01"), None);
assert_eq!(placed("DomeLight_1_2"), None);
assert_eq!(placed("1Light"), None);
let registry = SchemaRegistry::test_registry();
assert!(
registry
.schema_info_in_family(&tf::Token::new("DomeLight_1"), 0)
.is_none()
);
}
#[test]
fn instance_name_identifier() {
let registry = SchemaRegistry::test_registry();
let collection = tf::Token::new("CollectionAPI");
for bad in ["", "my instance", "render:", ":render", "a::b", "1st", "in.dot"] {
let instance = tf::Token::from(bad);
assert!(
!registry.is_allowed_instance_name(&collection, &instance),
"accepted {bad:?}"
);
}
assert!(registry.is_allowed_instance_name(&collection, &tf::Token::new("a:b")));
}
#[test]
fn instance_name_property_collision() {
let registry = SchemaRegistry::test_registry();
let collection = tf::Token::new("CollectionAPI");
assert!(!registry.is_allowed_instance_name(&collection, &tf::Token::new("includeRoot")));
assert!(!registry.is_allowed_instance_name(&collection, &tf::Token::new("a:includeRoot")));
assert!(registry.is_allowed_instance_name(&collection, &tf::Token::new("includeRoot:a")));
}
#[test]
fn name_template_bases() {
let cases = [
("collection:__INSTANCE_NAME__:includeRoot", "includeRoot"),
("collection:__INSTANCE_NAME__", ""),
("inputs:intensity", "inputs:intensity"),
];
for (name, base) in cases {
assert_eq!(name_template_base(&tf::Token::new(name)), base, "basing {name}");
}
}
#[test]
fn versioned_identifier() {
let registry = SchemaRegistry::test_registry();
let dome = registry
.schema_info(&tf::Token::new("DomeLight_1"))
.expect("DomeLight_1");
assert_eq!(dome.family().as_str(), "DomeLight");
assert_eq!(dome.version(), 1);
let family = tf::Token::new("DomeLight");
let versioned = registry.schema_info_in_family(&family, 1).expect("DomeLight version 1");
assert_eq!(versioned.identifier().as_str(), "DomeLight_1");
let bare = registry.schema_info_in_family(&family, 0).expect("DomeLight version 0");
assert_eq!(bare.identifier().as_str(), "DomeLight");
assert!(registry.schema_info_in_family(&family, 2).is_none());
}
#[test]
fn parse_identifier_cases() {
let cases = [
("DomeLight", "DomeLight", 0),
("DomeLight_1", "DomeLight", 1),
("DomeLight_12", "DomeLight", 12),
("DomeLight_", "DomeLight_", 0),
("Basis2Curves", "Basis2Curves", 0),
("Foo_1_2", "Foo_1", 2),
("_1", "", 1),
];
for (identifier, family, version) in cases {
let parsed = SchemaRegistry::parse_identifier(&tf::Token::new(identifier));
assert_eq!((parsed.0.as_str(), parsed.1), (family, version), "parsing {identifier}");
}
}
#[test]
fn allowed_identifiers() {
assert!(SchemaRegistry::is_allowed_identifier(&tf::Token::new("DomeLight")));
assert!(SchemaRegistry::is_allowed_identifier(&tf::Token::new("DomeLight_1")));
assert!(!SchemaRegistry::is_allowed_identifier(&tf::Token::new("DomeLight_01")));
assert!(!SchemaRegistry::is_allowed_identifier(&tf::Token::new("Foo_1_2")));
assert!(!SchemaRegistry::is_allowed_family(&tf::Token::new("2Foo")));
assert!(SchemaRegistry::is_allowed_family(&tf::Token::new("_Foo")));
}
#[test]
fn instance_name_math() {
let template = tf::Token::new("collection:__INSTANCE_NAME__:includeRoot");
assert!(is_name_template(&template));
assert_eq!(
make_instance_name(&template, &tf::Token::new("lightLink")),
tf::Token::new("collection:lightLink:includeRoot")
);
let nested = tf::Token::new("Other:__INSTANCE_NAME__:foo");
assert_eq!(
make_instance_name(&nested, &tf::Token::new("bar")),
tf::Token::new("Other:bar:foo")
);
let plain = tf::Token::new("inputs:intensity");
assert!(!is_name_template(&plain));
assert_eq!(make_instance_name(&plain, &tf::Token::new("x")), plain);
assert!(!is_name_template(&tf::Token::new("my__INSTANCE_NAME__thing")));
assert_eq!(
make_name_template(&tf::Token::new("CollectionAPI")),
tf::Token::new("CollectionAPI:__INSTANCE_NAME__")
);
}
#[test]
fn instance_name_split() {
let (identifier, instance) = split_instance_name(&tf::Token::new("CollectionAPI:lightLink"));
assert_eq!(identifier, tf::Token::new("CollectionAPI"));
assert_eq!(instance, Some(tf::Token::new("lightLink")));
let (identifier, instance) = split_instance_name(&tf::Token::new("CollectionAPI:a:b"));
assert_eq!(identifier, tf::Token::new("CollectionAPI"));
assert_eq!(instance, Some(tf::Token::new("a:b")));
let (identifier, instance) = split_instance_name(&tf::Token::new("LightAPI"));
assert_eq!(identifier, tf::Token::new("LightAPI"));
assert_eq!(instance, None);
let (identifier, instance) = split_instance_name(&tf::Token::new("CollectionAPI:"));
assert_eq!(identifier, tf::Token::new("CollectionAPI"));
assert_eq!(instance, None);
}
#[test]
fn composed_typeless_prim() {
let registry = SchemaRegistry::test_registry();
let definition =
registry.build_composed_prim_definition(&tf::Token::default(), &[tf::Token::new("CollectionAPI:render")]);
assert_eq!(
definition.attribute_fallback(&tf::Token::new("collection:render:expansionRule")),
Some(sdf::Value::token("expandPrims"))
);
assert_eq!(
definition.applied_api_schemas(),
[tf::Token::new("CollectionAPI:render")]
);
}
#[test]
fn composed_typed_beats_authored() {
let registry = SchemaRegistry::test_registry();
let definition = registry.build_composed_prim_definition(
&tf::Token::new("DistantLight"),
&[tf::Token::new("CollectionAPI:render")],
);
assert_eq!(
definition.attribute_fallback(&tf::Token::new("inputs:intensity")),
Some(sdf::Value::Float(50000.0))
);
assert_eq!(
definition.applied_api_schemas(),
[
tf::Token::new("LightAPI"),
tf::Token::new("CollectionAPI:lightLink"),
tf::Token::new("CollectionAPI:render"),
]
);
}
#[test]
fn composed_skips_duplicates_and_unknowns() {
let registry = SchemaRegistry::test_registry();
let definition = registry.build_composed_prim_definition(
&tf::Token::new("DistantLight"),
&[
tf::Token::new("CollectionAPI:lightLink"),
tf::Token::new("Unregistered"),
tf::Token::new("CollectionAPI"),
tf::Token::new("LightAPI:instance"),
],
);
assert_eq!(
definition.applied_api_schemas(),
[tf::Token::new("LightAPI"), tf::Token::new("CollectionAPI:lightLink")]
);
}
#[test]
fn composed_with_no_applied_shares_typed() {
let registry = SchemaRegistry::test_registry();
let type_name = tf::Token::new("DistantLight");
let composed = registry.build_composed_prim_definition(&type_name, &[]);
assert!(Arc::ptr_eq(
&composed,
registry.concrete_prim_definition(&type_name).expect("DistantLight")
));
let unknown = registry.build_composed_prim_definition(&tf::Token::new("Bogus"), &[]);
assert!(Arc::ptr_eq(&unknown, registry.empty_prim_definition()));
}
#[test]
fn kind_round_trip() {
let kinds = [
SchemaKind::AbstractBase,
SchemaKind::AbstractTyped,
SchemaKind::ConcreteTyped,
SchemaKind::NonAppliedApi,
SchemaKind::SingleApplyApi,
SchemaKind::MultipleApplyApi,
];
for kind in kinds {
assert_eq!(SchemaKind::from_token(kind.as_str()), Some(kind));
}
assert_eq!(SchemaKind::from_token("bogus"), None);
}
#[test]
fn global_is_shared_and_empty() {
assert!(Arc::ptr_eq(SchemaRegistry::global(), SchemaRegistry::global()));
assert_eq!(SchemaRegistry::global().schema_infos().count(), 0);
}
}