use std::collections::HashMap;
use openusd::{sdf, tf, usd};
use crate::error::Error;
use crate::model::{API_SCHEMA_BASE, Class, Library, Property, Reflection, SCHEMA_BASE, Shape};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Violation {
#[error("inherits from `{name}`, which no layer declares")]
MissingBase {
name: tf::Token,
},
#[error("inherits from itself through {chain}")]
CyclicInheritance {
chain: String,
},
#[error("inherits from {count} classes; a schema has at most one base")]
MultipleBases {
count: usize,
},
#[error("an applied API schema must inherit directly from APISchemaBase, not from `{base}`")]
AppliedApiNotRooted {
base: tf::Token,
},
#[error("a non-applied API schema inherits APISchemaBase or another non-applied API, not `{base}`")]
NonAppliedApiNotRooted {
base: tf::Token,
},
#[error("inherits from `{base}`, which is the same schema family")]
InheritsWithinFamily {
base: tf::Token,
},
#[error("unknown apiSchemaType `{spelling}`")]
UnknownApiSchemaType {
spelling: String,
},
#[error("{key} does not apply to a {kind} schema")]
KindMismatch {
key: &'static str,
kind: &'static str,
},
#[error("a concrete schema must inherit from Typed")]
ConcreteNotTyped,
#[error("a typed schema cannot declare apiSchemaType")]
ApiSchemaTypeOnTyped,
#[error("`{identifier}` is not an allowed schema identifier")]
DisallowedIdentifier {
identifier: tf::Token,
},
#[error("typeName `{authored}` is not the schema's own identifier")]
TypeNameMismatch {
authored: tf::Token,
},
#[error("apiSchemas must be authored as a prepend")]
ApiSchemasNotPrepended,
#[error("`{first}` and `{second}` both reach `{name}` in the schematics")]
DuplicateSchematicsName {
name: tf::Token,
first: tf::Token,
second: tf::Token,
},
#[error("property `{property}` has unknown type `{type_name}`")]
UnknownPropertyType {
property: tf::Token,
type_name: tf::Token,
},
#[error("a multiple-apply schema needs propertyNamespacePrefix exactly when it has properties")]
MultipleApplyPrefix,
#[error("`{field}` is not a field a schema may declare")]
DisallowedField {
field: String,
},
#[error("`{property}` is an API schema override here, but `{class}` declares it outright")]
OverridesADeclaration {
property: tf::Token,
class: tf::Token,
},
#[error("`{property}` is {} on `{class}`", redeclared_as(.ours, .theirs))]
IncompatibleRedeclaration {
property: tf::Token,
class: tf::Token,
theirs: Box<Shape>,
ours: Box<Shape>,
},
#[error("token `{id}` would hold both \"{first}\" and \"{second}\"")]
TokenValueCollision {
id: String,
first: String,
second: String,
},
#[error("`{first}` and `{second}` both reach the name {name}")]
RustNameCollision {
name: String,
first: String,
second: String,
},
#[error("`{first}` and `{second}` both reach the method {method}")]
MethodCollision {
method: String,
first: tf::Token,
second: tf::Token,
},
#[error("`{name}` is not an identifier, so nothing generated can be called it")]
NotAnIdentifier {
name: String,
},
#[error(
"`{class}` is inherited from or reflected but not generated; declare it in the root layer or in a library of its own"
)]
Ungenerated {
class: tf::Token,
},
#[error("reflects `{schema}`, which no layer declares")]
UnknownReflectedSchema {
schema: tf::Token,
},
#[error("`{schema}` is reflected but not applied; add it to `prepend apiSchemas`")]
ReflectedNotApplied {
schema: tf::Token,
},
#[error("`{class}` belongs to the {library} library; name where its views live with Builder::extern_library")]
UnknownLibrary {
library: String,
class: tf::Token,
},
#[error("the {library} library's views were placed at `{path}`, which is no Rust path")]
UnreadableLibraryPath {
library: String,
path: String,
},
#[error("unknown apiGetImplementation `{spelling}`")]
UnknownApiGetImplementation {
spelling: String,
},
#[error("`{property}` asks for a custom accessor but generates none")]
CustomGetWithoutAccessor {
property: tf::Token,
},
#[error("`{property}` is a `{type_name}`, which has no value-type constant to declare it with")]
UnnameableType {
property: tf::Token,
type_name: tf::Token,
},
}
pub fn check(library: &Library) -> Result<Vec<String>, Error> {
let mut warnings = Vec::new();
for class in &library.classes {
check_identifier(class)?;
check_kind(class)?;
check_inheritance(class)?;
check_fields(class)?;
check_properties(class)?;
check_reflection(class, library, &mut warnings)?;
warnings.extend(conventions(class));
}
Ok(warnings)
}
fn check_identifier(class: &Class) -> Result<(), Error> {
if !usd::SchemaRegistry::is_allowed_schema_identifier(class.identifier.as_str()) {
return Err(class.violation(Violation::DisallowedIdentifier {
identifier: class.identifier.clone(),
}));
}
if let Some(authored) = &class.authored_type_name
&& authored != &class.identifier
{
return Err(class.violation(Violation::TypeNameMismatch {
authored: authored.clone(),
}));
}
Ok(())
}
fn check_kind(class: &Class) -> Result<(), Error> {
let kind = class.kind;
let metadata = &class.metadata;
let multiple_apply = kind == usd::SchemaKind::MultipleApplyApi;
for (mismatched, key) in [
(
kind != usd::SchemaKind::SingleApplyApi && !metadata.auto_apply_to.is_empty(),
"apiSchemaAutoApplyTo",
),
(
!kind.is_applied_api_schema() && !metadata.can_only_apply_to.is_empty(),
"apiSchemaCanOnlyApplyTo",
),
(
!multiple_apply && !metadata.allowed_instance_names.is_empty(),
"apiSchemaAllowedInstanceNames",
),
(
!multiple_apply && !metadata.instance_restrictions.is_empty(),
"apiSchemaInstances",
),
(
kind != usd::SchemaKind::ConcreteTyped && !metadata.fallback_types.is_empty(),
"fallbackTypes",
),
(
multiple_apply
&& class
.authored_fields
.iter()
.any(|field| field == sdf::FieldKey::PropertyOrder.as_str()),
sdf::FieldKey::PropertyOrder.as_str(),
),
] {
if mismatched {
return Err(class.violation(Violation::KindMismatch {
key,
kind: kind_name(kind),
}));
}
}
if kind.is_api_schema() && (class.is_typed || class.authored_type_name.is_some()) {
return Err(class.violation(Violation::ApiSchemaTypeOnTyped));
}
if multiple_apply {
let has_properties = class.local_properties().next().is_some();
if has_properties != metadata.property_namespace_prefix.is_some() {
return Err(class.violation(Violation::MultipleApplyPrefix));
}
} else if metadata.property_namespace_prefix.is_some() {
return Err(class.violation(Violation::KindMismatch {
key: "propertyNamespacePrefix",
kind: kind_name(kind),
}));
}
if kind == usd::SchemaKind::ConcreteTyped && !class.is_typed {
return Err(class.violation(Violation::ConcreteNotTyped));
}
Ok(())
}
fn check_inheritance(class: &Class) -> Result<(), Error> {
if class.authored_base_count > 1 {
return Err(class.violation(Violation::MultipleBases {
count: class.authored_base_count,
}));
}
for base in &class.bases {
let (family, _) = usd::SchemaRegistry::parse_schema_family_and_version(&base.identifier);
if family == class.family {
return Err(class.violation(Violation::InheritsWithinFamily {
base: base.identifier.clone(),
}));
}
}
if !class.kind.is_api_schema() {
return Ok(());
}
let base = class.direct_base.clone().unwrap_or_else(|| tf::Token::new(SCHEMA_BASE));
let inherits_root = base.as_str() == API_SCHEMA_BASE;
if class.kind.is_applied_api_schema() && !inherits_root {
return Err(class.violation(Violation::AppliedApiNotRooted { base }));
}
if class.kind == usd::SchemaKind::NonAppliedApi && !inherits_root {
let base_is_non_applied = class
.bases
.first()
.is_some_and(|base| base.kind == usd::SchemaKind::NonAppliedApi);
if !base_is_non_applied {
return Err(class.violation(Violation::NonAppliedApiNotRooted { base }));
}
}
Ok(())
}
fn check_fields(class: &Class) -> Result<(), Error> {
if let Some(field) = disallowed_field(&class.authored_fields) {
return Err(class.violation(Violation::DisallowedField { field: field.clone() }));
}
if let Some(op) = &class.api_schemas_op
&& (op.explicit
|| !op.explicit_items.is_empty()
|| !op.appended_items.is_empty()
|| !op.added_items.is_empty()
|| !op.deleted_items.is_empty()
|| !op.ordered_items.is_empty())
{
return Err(class.violation(Violation::ApiSchemasNotPrepended));
}
Ok(())
}
fn check_properties(class: &Class) -> Result<(), Error> {
let mut seen: HashMap<&tf::Token, &tf::Token> = HashMap::new();
for property in &class.properties {
if let Some(first) = seen.insert(&property.schematics_name, &property.name) {
return Err(property.violation(Violation::DuplicateSchematicsName {
name: property.schematics_name.clone(),
first: first.clone(),
second: property.name.clone(),
}));
}
}
for property in &class.properties {
if let Some(field) = disallowed_field(property.fields.keys()) {
return Err(property.violation(Violation::DisallowedField { field: field.clone() }));
}
if property.shape.spec_type == sdf::SpecType::Attribute && property.type_name().is_none() {
let declared = property
.fields
.get(sdf::FieldKey::TypeName.as_str())
.and_then(|value| value.clone().try_as_token())
.unwrap_or_default();
return Err(property.violation(Violation::UnknownPropertyType {
property: property.name.clone(),
type_name: declared,
}));
}
if property.api.custom_get && !property.has_accessor() {
return Err(property.violation(Violation::CustomGetWithoutAccessor {
property: property.name.clone(),
}));
}
if property.is_override()
&& let Some(site) = property.sites.iter().find(|site| !site.is_override)
{
return Err(Error::Definition {
origin: site.origin.describe(),
violation: Violation::OverridesADeclaration {
property: property.name.clone(),
class: site.class.clone(),
},
});
}
if let Some((mine, weaker)) = property.sites.split_first()
&& mine.class == class.identifier
&& let Some(site) = weaker.iter().find(|site| site.shape != mine.shape)
{
return Err(Error::Definition {
origin: mine.origin.describe(),
violation: Violation::IncompatibleRedeclaration {
property: property.name.clone(),
class: site.class.clone(),
theirs: Box::new(site.shape.clone()),
ours: Box::new(mine.shape.clone()),
},
});
}
}
Ok(())
}
fn check_reflection(class: &Class, library: &Library, warnings: &mut Vec<String>) -> Result<(), Error> {
for reflection in class.reflections(library) {
match reflection {
Reflection::Taken { schema, shadowed, .. } => {
for (property, first) in shadowed {
warnings.push(format!(
"{}: `{}` of `{}` is not reflected; `{first}` already declares it",
class.origin.describe(),
property.name,
schema.identifier
));
}
}
Reflection::NotSingleApply(schema) => warnings.push(format!(
"{}: `{}` is a {} schema, so it is not reflected; only a single-apply API schema can be",
class.origin.describe(),
schema.identifier,
kind_name(schema.kind)
)),
Reflection::NotApplied(schema) => {
return Err(class.violation(Violation::ReflectedNotApplied {
schema: schema.identifier.clone(),
}));
}
}
}
Ok(())
}
fn conventions(class: &Class) -> Vec<String> {
let mut warnings = Vec::new();
let ends_in_api = class.family.as_str().ends_with("API");
let is_api = class.kind.is_api_schema();
if is_api && !ends_in_api {
warnings.push(format!(
"{}: an API schema's name conventionally ends in `API`",
class.origin.describe()
));
}
if !is_api && ends_in_api {
warnings.push(format!(
"{}: only an API schema's name conventionally ends in `API`",
class.origin.describe()
));
}
warnings
}
fn disallowed_field<'a>(fields: impl IntoIterator<Item = &'a String>) -> Option<&'a String> {
let input = [
sdf::FieldKey::InheritPaths.as_str(),
sdf::FieldKey::CustomData.as_str(),
sdf::FieldKey::Specifier.as_str(),
sdf::ChildrenKey::PrimChildren.as_str(),
sdf::ChildrenKey::PropertyChildren.as_str(),
];
fields
.into_iter()
.find(|field| !input.contains(&field.as_str()) && usd::SchemaRegistry::is_disallowed_field(field))
}
fn redeclared_as(ours: &Shape, theirs: &Shape) -> String {
spelled(ours)
.into_iter()
.zip(spelled(theirs))
.find(|(ours, theirs)| ours != theirs)
.map_or_else(
|| "as it was".to_owned(),
|(ours, theirs)| format!("{ours} here and {theirs}"),
)
}
fn spelled(shape: &Shape) -> [String; 4] {
[
match shape.spec_type {
sdf::SpecType::Relationship => "a relationship",
_ => "an attribute",
}
.to_owned(),
shape
.type_name
.as_ref()
.map_or_else(|| "of no type".to_owned(), |name| format!("`{name}`")),
match shape.variability {
sdf::Variability::Uniform => "uniform",
sdf::Variability::Varying => "varying",
}
.to_owned(),
match shape.custom {
true => "custom",
false => "not custom",
}
.to_owned(),
]
}
fn kind_name(kind: usd::SchemaKind) -> &'static str {
match kind {
usd::SchemaKind::AbstractBase => "abstract base",
usd::SchemaKind::AbstractTyped => "abstract typed",
usd::SchemaKind::ConcreteTyped => "concrete typed",
usd::SchemaKind::NonAppliedApi => "non-applied API",
usd::SchemaKind::SingleApplyApi => "single-apply API",
usd::SchemaKind::MultipleApplyApi => "multiple-apply API",
}
}
impl Class {
pub(crate) fn violation(&self, violation: Violation) -> Error {
Error::Definition {
origin: self.origin.describe(),
violation,
}
}
}
impl Property {
pub(crate) fn violation(&self, violation: Violation) -> Error {
Error::Definition {
origin: self.origin.describe(),
violation,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{TAG_API, read_fixture, read_source, schema};
fn violation(name: &str) -> Violation {
match read_fixture(name) {
Err(Error::Definition { violation, .. }) => violation,
Err(other) => panic!("{name} was rejected, but not by a rule: {other}"),
Ok(_) => panic!("{name} was accepted"),
}
}
#[test]
fn multiple_apply_prefix() {
assert!(matches!(violation("schemaFail12.usda"), Violation::MultipleApplyPrefix));
assert!(matches!(violation("schemaFail13.usda"), Violation::MultipleApplyPrefix));
}
#[test]
fn kind_metadata_mismatch() {
for (fixture, key) in [
("schemaFail14.usda", "propertyNamespacePrefix"),
("schemaFail15.usda", "fallbackTypes"),
("schemaFail16.usda", "fallbackTypes"),
("schemaFail17.usda", "apiSchemaAutoApplyTo"),
("schemaFail18.usda", "apiSchemaAutoApplyTo"),
("schemaFail24.usda", "propertyOrder"),
] {
match violation(fixture) {
Violation::KindMismatch { key: found, .. } => assert_eq!(found, key, "{fixture}"),
other => panic!("{fixture}: {other}"),
}
}
}
#[test]
fn api_schemas_mode() {
assert!(matches!(
violation("schemaFail19.usda"),
Violation::ApiSchemasNotPrepended
));
assert!(matches!(
violation("schemaFail20.usda"),
Violation::ApiSchemasNotPrepended
));
}
#[test]
fn same_family_base() {
assert!(matches!(
violation("schemaFail21.usda"),
Violation::InheritsWithinFamily { .. }
));
}
#[test]
fn registry_would_refuse() {
assert!(matches!(
violation("schemaFail22.usda"),
Violation::DisallowedIdentifier { .. }
));
match violation("schemaFail23.usda") {
Violation::DisallowedField { field } => assert_eq!(field, "kind"),
other => panic!("{other}"),
}
}
#[test]
fn variant_set_refused() {
let dir = tempfile::tempdir().expect("tempdir");
let error = read_source(
dir.path(),
r#"#usda 1.0
def "GLOBAL" (
customData = {
string libraryName = "testVariants"
}
)
{
}
class "Typed" {}
class "Varied" (
inherits = </Typed>
) {
double plain = 1
variantSet "shading" = {
"red" {
double onlyInVariant = 2
}
}
}
"#,
)
.expect_err("a schema may not vary");
match error {
Error::Definition {
violation: Violation::DisallowedField { field },
..
} => assert_eq!(field, sdf::ChildrenKey::VariantSetChildren.as_str()),
other => panic!("{other}"),
}
}
#[test]
fn composition_arc_refused() {
let dir = tempfile::tempdir().expect("tempdir");
let error = read_source(
dir.path(),
r#"#usda 1.0
def "GLOBAL" (
customData = {
string libraryName = "testArc"
}
)
{
}
class "Typed" {}
class "Specializing" (
inherits = </Typed>
specializes = </Typed>
) {}
"#,
)
.expect_err("a schema may not compose");
match error {
Error::Definition {
violation: Violation::DisallowedField { field },
..
} => assert_eq!(field, "specializes"),
other => panic!("{other}"),
}
}
const OVERRIDES: &str = r#"#usda 1.0
def "GLOBAL" (
customData = {
string libraryName = "testOverrides"
}
)
{
}
class "Typed" {}
class "Base" (
inherits = </Typed>
) {
int plain = 1
}
class "Derived" (
inherits = </Base>
) {
int plain = 2 (
customData = {
bool apiSchemaOverride = true
}
)
}
"#;
#[test]
fn override_of_a_declaration() {
let dir = tempfile::tempdir().expect("tempdir");
let error = read_source(dir.path(), OVERRIDES).expect_err("a class may not delete what it inherits");
match error {
Error::Definition {
violation: Violation::OverridesADeclaration { property, class },
..
} => {
assert_eq!(property.as_str(), "plain");
assert_eq!(class.as_str(), "Base");
}
other => panic!("{other}"),
}
}
#[test]
fn declaration_over_an_override() {
let dir = tempfile::tempdir().expect("tempdir");
let library = read_source(
dir.path(),
r#"#usda 1.0
def "GLOBAL" (
customData = {
string libraryName = "testTakenOn"
}
)
{
}
class "Typed" {}
class "Base" (
inherits = </Typed>
) {
int plain = 1 (
customData = {
bool apiSchemaOverride = true
}
)
}
class "Derived" (
inherits = </Base>
) {
int plain = 2
}
"#,
)
.expect("a class may take an inherited override on");
let derived = library
.classes
.iter()
.find(|class| class.identifier.as_str() == "Derived")
.expect("the derived class");
assert_eq!(derived.override_properties().count(), 0);
}
#[test]
fn override_reaches_a_silent_class() {
let dir = tempfile::tempdir().expect("tempdir");
let library = read_source(
dir.path(),
r#"#usda 1.0
def "GLOBAL" (
customData = {
string libraryName = "testInheritedOverride"
}
)
{
}
class "Typed" {}
class "Base" (
inherits = </Typed>
) {
int plain = 1 (
customData = {
bool apiSchemaOverride = true
}
)
}
class "Derived" (
inherits = </Base>
) {}
"#,
)
.expect("resolves");
for identifier in ["Base", "Derived"] {
let class = library
.classes
.iter()
.find(|class| class.identifier.as_str() == identifier)
.expect("the class");
let names: Vec<&str> = class.override_properties().map(|p| p.name.as_str()).collect();
assert_eq!(names, vec!["plain"], "{identifier}");
}
}
fn redeclaring(base: &str, derived: &str) -> String {
schema(
"testRedeclare",
&format!(
r#"class "Base" (
inherits = </Typed>
) {{
{base}
}}
class "Derived" (
inherits = </Base>
) {{
{derived}
}}"#
),
)
}
fn incompatible(base: &str, derived: &str) -> (String, Shape, Shape) {
let dir = tempfile::tempdir().expect("tempdir");
match read_source(dir.path(), &redeclaring(base, derived)).expect_err("the redeclaration changes the property")
{
Error::Definition {
origin,
violation:
Violation::IncompatibleRedeclaration {
class, theirs, ours, ..
},
} => {
assert!(
origin.ends_with("/Derived.plain"),
"reported against the redeclaration: {origin}"
);
(class.to_string(), *theirs, *ours)
}
other => panic!("{other}"),
}
}
#[test]
fn redeclared_type_changes() {
let (class, theirs, ours) = incompatible("int plain = 1", "double plain = 2");
assert_eq!(class, "Base");
assert_eq!(theirs.type_name, Some(tf::Token::new("int")));
assert_eq!(ours.type_name, Some(tf::Token::new("double")));
}
#[test]
fn redeclared_adds_uniform() {
let (_, theirs, ours) = incompatible(r#"token plain = "a""#, r#"uniform token plain = "b""#);
assert_eq!(theirs.variability, sdf::Variability::Varying);
assert_eq!(ours.variability, sdf::Variability::Uniform);
}
#[test]
fn redeclared_adds_custom() {
let (_, theirs, ours) = incompatible("int plain = 1", "custom int plain = 2");
assert!(!theirs.custom);
assert!(ours.custom);
}
#[test]
fn redeclared_spelled() {
let base = Shape {
spec_type: sdf::SpecType::Attribute,
type_name: Some(tf::Token::new("int")),
variability: sdf::Variability::Varying,
custom: false,
};
let typed = Shape {
type_name: Some(tf::Token::new("double")),
..base.clone()
};
let uniform = Shape {
variability: sdf::Variability::Uniform,
..base.clone()
};
let custom = Shape {
custom: true,
..base.clone()
};
let related = Shape {
spec_type: sdf::SpecType::Relationship,
type_name: None,
..base.clone()
};
assert_eq!(redeclared_as(&typed, &base), "`double` here and `int`");
assert_eq!(redeclared_as(&uniform, &base), "uniform here and varying");
assert_eq!(redeclared_as(&custom, &base), "custom here and not custom");
assert_eq!(redeclared_as(&related, &base), "a relationship here and an attribute");
}
#[test]
fn redeclared_as_rel() {
let dir = tempfile::tempdir().expect("tempdir");
let error = read_source(dir.path(), &redeclaring("int plain = 1", "rel plain"))
.expect_err("the specs disagree on what the property is");
assert!(
matches!(&error, Error::Composition { diagnostic, .. } if diagnostic.contains("inconsistent spec types")),
"{error}"
);
}
#[test]
fn redeclared_renamed_rejected() {
let (_, theirs, ours) = incompatible(
"int plain = 1",
r#"double plain = 2 (
customData = { string apiName = "other" }
)"#,
);
assert_eq!(theirs.type_name, Some(tf::Token::new("int")));
assert_eq!(ours.type_name, Some(tf::Token::new("double")));
}
#[test]
fn redeclared_fallback_only() {
let dir = tempfile::tempdir().expect("tempdir");
read_source(
dir.path(),
&redeclaring("double3 plain = (1, 1, 1)", "double3 plain = (2, 2, 2)"),
)
.expect("a fallback may change");
}
#[test]
fn redeclared_omits_uniform() {
let dir = tempfile::tempdir().expect("tempdir");
let library = read_source(
dir.path(),
&redeclaring(r#"uniform token plain = "a""#, r#"token plain = "b""#),
)
.expect("omitting the keyword changes nothing");
let derived = library
.classes
.iter()
.find(|class| class.identifier.as_str() == "Derived")
.expect("the derived class");
let plain = derived
.local_properties()
.find(|property| property.name.as_str() == "plain")
.expect("the redeclared property");
assert_eq!(plain.shape.variability, sdf::Variability::Uniform);
}
fn reflecting(classes: &str) -> String {
schema("testReflect", classes)
}
#[test]
fn reflected_unknown() {
let dir = tempfile::tempdir().expect("tempdir");
let error = read_source(
dir.path(),
&reflecting(
r#"class "Thing" (
inherits = </Typed>
customData = { token[] reflectedAPISchemas = ["NowhereAPI"] }
) {}"#,
),
)
.expect_err("nothing declares it");
match error {
Error::Definition {
violation: Violation::UnknownReflectedSchema { schema },
..
} => assert_eq!(schema.as_str(), "NowhereAPI"),
other => panic!("{other}"),
}
}
#[test]
fn reflected_not_applied() {
let dir = tempfile::tempdir().expect("tempdir");
let error = read_source(
dir.path(),
&reflecting(&format!(
r#"{TAG_API}
class "Thing" (
inherits = </Typed>
customData = {{ token[] reflectedAPISchemas = ["TagAPI"] }}
) {{}}"#
)),
)
.expect_err("the schema is not applied");
match error {
Error::Definition {
violation: Violation::ReflectedNotApplied { schema },
..
} => assert_eq!(schema.as_str(), "TagAPI"),
other => panic!("{other}"),
}
}
#[test]
fn reflected_multi_apply_warns() {
let dir = tempfile::tempdir().expect("tempdir");
let library = read_source(
dir.path(),
&reflecting(
r#"class "MultiAPI" (
inherits = </APISchemaBase>
customData = {
token apiSchemaType = "multipleApply"
token propertyNamespacePrefix = "multi"
}
) {
int depth = 0
}
class "Thing" (
inherits = </Typed>
prepend apiSchemas = ["MultiAPI:foo"]
customData = { token[] reflectedAPISchemas = ["MultiAPI"] }
) {}"#,
),
)
.expect("set aside, not refused");
let warnings = check(&library).expect("set aside, not refused");
assert!(
warnings
.iter()
.any(|warning| warning.contains("`MultiAPI` is a multiple-apply API schema, so it is not reflected")),
"{warnings:?}"
);
let thing = library.find(&tf::Token::new("Thing")).expect("the class");
assert!(matches!(
thing.reflections(&library).as_slice(),
[Reflection::NotSingleApply(schema)] if schema.identifier.as_str() == "MultiAPI"
));
}
#[test]
fn reflected_duplicate_warns() {
let dir = tempfile::tempdir().expect("tempdir");
let library = read_source(
dir.path(),
&reflecting(&format!(
r#"{TAG_API}
class "LabelAPI" (
inherits = </APISchemaBase>
customData = {{ token apiSchemaType = "singleApply" }}
) {{
string label = ""
int dup = 0
}}
class "Thing" (
inherits = </Typed>
prepend apiSchemas = ["TagAPI", "LabelAPI"]
customData = {{ token[] reflectedAPISchemas = ["TagAPI", "LabelAPI"] }}
) {{}}"#
)),
)
.expect("the first keeps the name");
let warnings = check(&library).expect("the first keeps the name");
assert!(
warnings
.iter()
.any(|warning| warning.contains("`dup` of `LabelAPI` is not reflected; `TagAPI` already declares it")),
"{warnings:?}"
);
let thing = library.find(&tf::Token::new("Thing")).expect("the class");
let taken: Vec<String> = thing
.reflections(&library)
.iter()
.map(|reflection| match reflection {
Reflection::Taken {
schema,
properties,
shadowed,
} => {
let names: Vec<&str> = properties.iter().map(|p| p.name.as_str()).collect();
let shadowed: Vec<String> = shadowed
.iter()
.map(|(p, first)| format!("{} by {first}", p.name))
.collect();
format!("{} takes {names:?}, shadowed {shadowed:?}", schema.identifier)
}
other => panic!("{other:?}"),
})
.collect();
assert_eq!(
taken,
vec![
r#"TagAPI takes ["tag", "dup"], shadowed []"#,
r#"LabelAPI takes ["label"], shadowed ["dup by TagAPI"]"#,
]
);
}
#[test]
fn type_name_mismatch() {
assert!(matches!(
violation("schemaFail5.usda"),
Violation::TypeNameMismatch { .. }
));
}
#[test]
fn concrete_needs_typed() {
assert!(matches!(violation("schemaFail6.usda"), Violation::ConcreteNotTyped));
assert!(matches!(violation("schemaFail7.usda"), Violation::ConcreteNotTyped));
}
#[test]
fn applied_api_base() {
for fixture in [
"schemaFail3.usda",
"schemaFail4.usda",
"schemaFail8.usda",
"schemaFail10.usda",
"schemaFail11.usda",
] {
assert!(
matches!(violation(fixture), Violation::AppliedApiNotRooted { .. }),
"{fixture}"
);
}
}
#[test]
fn non_applied_base() {
assert!(matches!(
violation("schemaFail9.usda"),
Violation::NonAppliedApiNotRooted { .. }
));
}
#[test]
fn typed_cannot_be_api() {
let dir = tempfile::tempdir().expect("tempdir");
let error = read_source(
dir.path(),
r#"#usda 1.0
over "GLOBAL" (
customData = {
string libraryName = "testTypedApi"
}
)
{
}
class "Typed" {}
class Shape "Shape" (
inherits = </Typed>
customData = { token apiSchemaType = "singleApply" }
) {}
"#,
)
.expect_err("a schema cannot be both");
assert!(
matches!(
error,
Error::Definition {
violation: Violation::ApiSchemaTypeOnTyped,
..
}
),
"{error}"
);
}
#[test]
fn silent_base_is_not_non_applied() {
let dir = tempfile::tempdir().expect("tempdir");
let error = read_source(
dir.path(),
r#"#usda 1.0
over "GLOBAL" (
customData = {
string libraryName = "testSilentBase"
}
)
{
}
class "APISchemaBase" {}
class "SilentAPI" (
inherits = </APISchemaBase>
) {}
class "NonAppliedAPI" (
inherits = </SilentAPI>
customData = { token apiSchemaType = "nonApplied" }
) {}
"#,
)
.expect_err("the base is single-apply by default");
assert!(
matches!(
error,
Error::Definition {
violation: Violation::NonAppliedApiNotRooted { .. },
..
}
),
"{error}"
);
}
#[test]
fn schematics_names_collide() {
let dir = tempfile::tempdir().expect("tempdir");
let error = read_source(
dir.path(),
r#"#usda 1.0
over "GLOBAL" (
customData = {
string libraryName = "testCollide"
}
)
{
}
class "APISchemaBase" {}
class "CollideAPI" (
inherits = </APISchemaBase>
customData = {
token apiSchemaType = "multipleApply"
token propertyNamespacePrefix = "test"
}
) {
int foo
int test:__INSTANCE_NAME__:foo
}
"#,
)
.expect_err("both properties reach one schematics name");
assert!(
matches!(
error,
Error::Definition {
violation: Violation::DuplicateSchematicsName { .. },
..
}
),
"{error}"
);
}
#[test]
fn accepted_upstream_failures() {
for fixture in ["schemaFail.usda", "schemaFail2.usda"] {
assert!(
read_fixture(fixture).is_ok(),
"{fixture} is now rejected; move it to a rule test and drop it from this list"
);
}
}
#[test]
fn suffix_is_a_warning() {
let dir = tempfile::tempdir().expect("tempdir");
let library = read_source(
dir.path(),
r#"#usda 1.0
over "GLOBAL" (
customData = {
string libraryName = "testConvention"
}
)
{
}
class "APISchemaBase" {}
class "Applied" (
inherits = </APISchemaBase>
customData = { token apiSchemaType = "singleApply" }
) {}
"#,
)
.expect("nothing here breaks a rule");
let warnings = check(&library).expect("nothing here breaks a rule");
assert!(
warnings
.iter()
.any(|warning| warning.contains("conventionally ends in")),
"{warnings:?}"
);
}
}