use std::collections::{BTreeMap, HashMap};
use std::iter;
use std::path::{Path, PathBuf};
use openusd::{ar, sdf, tf, usd};
use crate::Builder;
use crate::error::Error;
use crate::model::Origin;
use crate::names;
const GLOBAL: &str = "/GLOBAL";
pub struct Source {
pub stage: usd::Stage,
pub library: String,
pub use_literal_identifiers: bool,
pub skip_code_generation: bool,
pub declarations: Vec<Declaration>,
pub tokens: Vec<DeclaredToken>,
}
pub struct Declaration {
pub name: tf::Token,
pub library: String,
pub prefix: String,
pub generated: bool,
pub bases: Vec<tf::Token>,
pub custom_data: CustomData,
pub type_name: Option<tf::Token>,
pub api_schemas: Option<sdf::TokenListOp>,
pub documentation: Option<String>,
pub properties: Vec<PropertyDeclaration>,
pub fields: Vec<String>,
pub origin: Origin,
}
pub struct PropertyDeclaration {
pub name: tf::Token,
pub custom_data: CustomData,
pub origin: Origin,
}
pub fn open_stage(builder: &Builder, path: &Path) -> Result<usd::Stage, Error> {
let resolver = ar::DefaultResolver::with_search_paths(builder.search_paths.clone());
let registry = usd::SchemaRegistryBuilder::empty()
.build()
.map_err(|source| Error::Core(source.into()))?;
let stage = usd::Stage::builder()
.resolver(resolver)
.schema_registry(registry)
.open(&path.to_string_lossy())?;
match Error::composition(path, &stage) {
Some(error) => Err(error),
None => Ok(stage),
}
}
pub fn watched_layers(stage: &usd::Stage) -> Vec<PathBuf> {
let root = stage.root_layer().identifier.clone();
let identifiers = stage.layer_identifiers();
iter::once(&root)
.chain(identifiers.iter().filter(|id| **id != root))
.filter_map(|identifier| stage.layer(identifier)?.resolved_path().map(watched_path))
.collect()
}
pub fn open(builder: &Builder, schema: &Path) -> Result<Source, Error> {
let stage = open_stage(builder, schema)?;
let identifiers = stage.layer_stack();
let root = stage.root_layer().identifier.clone();
let mut globals = Vec::new();
for identifier in iter::once(&root).chain(identifiers.iter().filter(|id| **id != root)) {
let Some(layer) = stage.layer(identifier) else {
continue;
};
globals.push((identifier, read_global(&layer)));
}
let skip_code_generation = globals.iter().any(|(_, global)| global.skip_code_generation);
let use_literal_identifiers = globals.first().is_none_or(|(_, global)| global.use_literal_identifiers);
let tokens = globals
.first()
.map(|(_, global)| global.tokens.clone())
.unwrap_or_default();
let owners: HashMap<&String, Owner<'_>> = globals
.iter()
.filter_map(|(identifier, global)| Some((*identifier, global.owner()?)))
.collect();
let root_owner = owners.get(&root).ok_or_else(|| Error::NoLibraryName {
schema: schema.to_path_buf(),
})?;
let mut declarations = Vec::new();
for (identifier, _) in &globals {
let Some(layer) = stage.layer(identifier) else {
continue;
};
let owner = owners.get(identifier).unwrap_or(root_owner);
declarations.extend(read_classes(&layer, identifier, owner, **identifier == root));
}
Ok(Source {
stage,
library: root_owner.library.to_owned(),
use_literal_identifiers,
skip_code_generation,
declarations,
tokens,
})
}
#[derive(Debug, Clone)]
pub struct DeclaredToken {
pub id: String,
pub value: String,
pub documentation: Option<String>,
}
struct Global {
library: Option<String>,
prefix: Option<String>,
use_literal_identifiers: bool,
skip_code_generation: bool,
tokens: Vec<DeclaredToken>,
}
struct Owner<'a> {
library: &'a str,
prefix: String,
}
impl Global {
fn owner(&self) -> Option<Owner<'_>> {
let library = self.library.as_deref()?;
Some(Owner {
library,
prefix: self.prefix.clone().unwrap_or_else(|| names::proper_case(library)),
})
}
}
fn watched_path(resolved: &str) -> PathBuf {
match resolved.strip_prefix(r"\?\") {
Some(rest) if rest.as_bytes().get(1) == Some(&b':') => PathBuf::from(rest),
_ => PathBuf::from(resolved),
}
}
fn read_global(layer: &sdf::Layer) -> Global {
let custom_data = layer
.prim(GLOBAL)
.ok()
.flatten()
.map(|prim| CustomData::read(prim.get::<sdf::Value>(sdf::FieldKey::CustomData)))
.unwrap_or_default();
Global {
library: custom_data.string("libraryName"),
prefix: custom_data.string("libraryPrefix"),
use_literal_identifiers: custom_data.flag("useLiteralIdentifier").unwrap_or(true),
skip_code_generation: custom_data.flag("skipCodeGeneration").unwrap_or(false),
tokens: custom_data.token_declarations("libraryTokens"),
}
}
fn read_classes(layer: &sdf::Layer, identifier: &str, owner: &Owner<'_>, generated: bool) -> Vec<Declaration> {
let Some(root) = layer.pseudo_root() else {
return Vec::new();
};
root.prim_children()
.unwrap_or_default()
.into_iter()
.filter_map(|name| {
let path = sdf::Path::abs_root().append_path(name.as_str()).ok()?;
let prim = layer.prim(&path).ok().flatten()?;
if prim.specifier() != Some(sdf::Specifier::Class) {
return None;
}
let origin = Origin {
layer: identifier.to_owned(),
path: path.clone(),
};
Some(Declaration {
name,
library: owner.library.to_owned(),
prefix: owner.prefix.clone(),
generated,
bases: inherit_names(&prim),
custom_data: CustomData::read(prim.get::<sdf::Value>(sdf::FieldKey::CustomData)),
type_name: prim.type_name(),
api_schemas: prim.api_schemas(),
documentation: prim.get(sdf::FieldKey::Documentation),
properties: read_properties(layer, &prim, &path, identifier),
fields: prim.fields(),
origin,
})
})
.collect()
}
fn inherit_names(prim: &sdf::PrimSpecRef<'_>) -> Vec<tf::Token> {
let Some(list_op) = prim.get::<sdf::PathListOp>(sdf::FieldKey::InheritPaths) else {
return Vec::new();
};
list_op
.iter()
.filter_map(|target| target.name().map(tf::Token::from))
.collect()
}
fn read_properties(
layer: &sdf::Layer,
prim: &sdf::PrimSpecRef<'_>,
prim_path: &sdf::Path,
identifier: &str,
) -> Vec<PropertyDeclaration> {
prim.property_children()
.unwrap_or_default()
.into_iter()
.filter_map(|name| {
let path = prim_path.append_property(name.as_str()).ok()?;
let custom_data = layer
.attribute(&path)
.ok()
.flatten()
.map(|attribute| attribute.get::<sdf::Value>(sdf::FieldKey::CustomData))
.or_else(|| {
layer
.relationship(&path)
.ok()
.flatten()
.map(|relationship| relationship.get::<sdf::Value>(sdf::FieldKey::CustomData))
})?;
Some(PropertyDeclaration {
name,
custom_data: CustomData::read(custom_data),
origin: Origin {
layer: identifier.to_owned(),
path,
},
})
})
.collect()
}
#[derive(Default)]
pub struct CustomData(sdf::Dictionary);
impl CustomData {
fn read(value: Option<sdf::Value>) -> Self {
Self(value.and_then(sdf::Value::try_as_dictionary).unwrap_or_default())
}
pub fn string(&self, key: &str) -> Option<String> {
self.0.get(key).and_then(sdf::Value::as_str).map(str::to_owned)
}
pub fn flag(&self, key: &str) -> Option<bool> {
self.0.get(key).and_then(sdf::Value::try_as_bool_ref).copied()
}
pub fn tokens(&self, key: &str) -> Vec<tf::Token> {
let Some(value) = self.0.get(key) else {
return Vec::new();
};
if let Some(tokens) = value.try_as_token_vec_ref() {
return tokens.clone();
}
value
.try_as_string_vec_ref()
.map(|strings| strings.iter().map(|text| tf::Token::from(text.as_str())).collect())
.unwrap_or_default()
}
pub fn token_declarations(&self, key: &str) -> Vec<DeclaredToken> {
self.0
.get(key)
.and_then(sdf::Value::try_as_dictionary_ref)
.into_iter()
.flatten()
.map(|(id, entry)| {
let entry = CustomData(entry.try_as_dictionary_ref().cloned().unwrap_or_default());
DeclaredToken {
value: entry.string("value").unwrap_or_else(|| id.clone()),
documentation: entry.string("doc"),
id: id.clone(),
}
})
.collect()
}
pub fn instance_restrictions(&self) -> BTreeMap<tf::Token, Vec<tf::Token>> {
self.0
.get("apiSchemaInstances")
.and_then(sdf::Value::try_as_dictionary_ref)
.into_iter()
.flatten()
.filter_map(|(instance, entry)| {
let allowed = CustomData(entry.try_as_dictionary_ref()?.clone()).tokens("apiSchemaCanOnlyApplyTo");
(!allowed.is_empty()).then(|| (tf::Token::from(instance.as_str()), allowed))
})
.collect()
}
}
#[cfg(test)]
mod tests {
use crate::error::Error;
use crate::tests::read_fixture;
#[test]
fn library_name_required() {
let error = read_fixture("schema_sublayer.usda").expect_err("a sublayer declares no library of its own");
assert!(matches!(error, Error::NoLibraryName { .. }), "{error}");
}
#[test]
fn codeless_flag() {
let library = read_fixture("codeless_schema.usda").expect("resolves");
assert!(library.skip_code_generation);
let library = read_fixture("schema.usda").expect("resolves");
assert!(!library.skip_code_generation);
}
#[test]
fn sublayers_reported() {
let library = read_fixture("schema.usda").expect("resolves");
assert!(
library
.source_layers
.iter()
.any(|layer| layer.ends_with("schema_sublayer.usda")),
"{:?}",
library.source_layers
);
}
}