openusd-build 0.8.0

Generates Rust schema views from OpenUSD schema.usda files, for use as a build dependency
Documentation
//! What generation can fail with.

use std::io;
use std::path::PathBuf;

use crate::validate::Violation;

/// What a configured [`TokenEnum`](crate::TokenEnum) asked for that could not
/// be honoured.
///
/// Every one of these is a configuration mistake rather than a schema one, and
/// each stops generation: an enum silently left out, or one quietly given a
/// variant name nobody chose, is worse than a build that stops and says which.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TokenEnumError {
    /// A source not spelled `Class.property`.
    #[error("names source `{spelling}`, which is not spelled `Class.property`")]
    MalformedSource {
        /// What was configured.
        spelling: String,
    },

    /// A class the library does not declare.
    #[error("names class `{class}`, which the library does not declare")]
    UnknownClass {
        /// The class named.
        class: String,
    },

    /// A property the class does not declare.
    #[error("names `{class}.{property}`, which the class does not declare")]
    UnknownProperty {
        /// The class that was found.
        class: String,
        /// The property that was not.
        property: String,
    },

    /// A source property declaring no `allowedTokens`, so there are no variants
    /// to generate.
    #[error("sources `{class}.{property}`, which declares no allowedTokens")]
    NoAllowedTokens {
        /// The class holding the property.
        class: String,
        /// The property named.
        property: String,
    },

    /// A default asked for from a property with no token fallback to take it
    /// from.
    #[error("asks for a default, but `{class}.{property}` declares no token fallback")]
    NoFallback {
        /// The class holding the property.
        class: String,
        /// The property named.
        property: String,
    },

    /// A default asked for from a fallback the allowed tokens do not admit.
    #[error("asks for a default of `{token}`, which is not among its allowed tokens")]
    UnknownFallback {
        /// The fallback the property declares.
        token: String,
    },

    /// A token no casing rule turns into a Rust name — an empty spelling, one
    /// opening with a digit, one that is punctuation alone.
    #[error("has no Rust name for token `{token}`; give one with TokenEnum::variant")]
    UnnameableToken {
        /// The token that could not be named.
        token: String,
    },

    /// Two tokens reaching one variant name. Suffixing one of them would put a
    /// name nobody chose into a public API, so the caller is asked instead.
    #[error("names both `{first}` and `{second}` `{variant}`; give one a name with TokenEnum::variant")]
    VariantCollision {
        /// The token that took the name.
        first: String,
        /// The token that wanted it too.
        second: String,
        /// The name they both reached.
        variant: String,
    },

    /// A type name the caller spelled that Rust would not take.
    #[error("is not a Rust identifier")]
    InvalidName,

    /// A type name the generated file already uses for something else.
    #[error("takes the name of another item the generated file declares")]
    ReservedName,

    /// A library neither generated by this run nor declared with
    /// [`extern_library`](crate::Builder::extern_library), which is a name
    /// nothing will ever match.
    #[error("names library `{library}`, which this build neither generates nor declares")]
    UnknownLibrary {
        /// The library named.
        library: String,
    },

    /// A variant name the caller spelled that Rust would not take.
    #[error("names token `{token}` `{variant}`, which is not a Rust identifier")]
    InvalidVariant {
        /// The token it was given for.
        token: String,
        /// The name that was given.
        variant: String,
    },

    /// A variant override for a token the source property does not admit,
    /// which is a name that would silently do nothing.
    #[error("names a variant for token `{token}`, which is not among its allowed tokens")]
    UnknownVariantToken {
        /// The token the override named.
        token: String,
    },

    /// Two enums of one library under one name.
    #[error("is declared more than once")]
    Duplicate,
}

/// A failure while generating a schema library.
///
/// A build script runs this, so a variant that has a file to name names it:
/// the message is all a contributor sees when `cargo build` stops.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    /// Generation had schemas to build but nowhere to write them.
    #[error("no output directory: call Builder::out_dir, or run this from a build script, which sets OUT_DIR")]
    NoOutDir,

    /// Reading a schema through `openusd` failed.
    #[error(transparent)]
    Core(#[from] openusd::Error),

    /// A schema's layers did not compose, so what it declares cannot be read.
    #[error("{schema} does not compose: {diagnostic}")]
    Composition {
        /// The schema that was being read.
        schema: PathBuf,
        /// What composition reported. An unresolved sublayer usually wants a
        /// `search_path`.
        diagnostic: String,
    },

    /// A schema library did not name itself, so nothing can refer to it.
    #[error("{schema}: no /GLOBAL prim declares customData.libraryName")]
    NoLibraryName {
        /// The schema that was being read.
        schema: PathBuf,
    },

    /// A schema declares something the generator cannot honour.
    #[error("{origin}: {violation}")]
    Definition {
        /// Where the offending declaration lives, layer and path.
        origin: String,
        /// Which rule it broke.
        violation: Violation,
    },

    /// A configured [`TokenEnum`](crate::TokenEnum) the schema cannot supply.
    ///
    /// Configuration is what asked for it, so this names the enum rather than a
    /// layer: nothing in the schema is wrong, and the file to open is the build
    /// script.
    #[error("token enum `{name}` {cause}")]
    TokenEnum {
        /// The enum the configuration asked for.
        name: String,
        /// What it asked for that could not be honoured.
        cause: TokenEnumError,
    },

    /// A field holds a value the generated declaration has no Rust literal for.
    ///
    /// Only a fallback or a piece of metadata reaches the table, so this is a
    /// value type nothing generated has needed yet rather than an unusual
    /// schema. It stops the build: a fallback that went missing would leave the
    /// generated schemas answering differently from the ones they came from.
    #[error("{schema}.{field} holds a {kind} value, which the generated declaration cannot express")]
    UnwritableValue {
        /// The schema declaring it.
        schema: String,
        /// The field on it.
        field: String,
        /// The value type that has no literal.
        kind: &'static str,
    },

    /// The generator wrote Rust that does not parse.
    ///
    /// A defect in this crate rather than anything a schema can ask for, but it
    /// surfaces in a consumer's build, so it says which schema was being
    /// generated and what the parser made of it.
    #[error("generated Rust for {schema} does not parse: {cause}")]
    Malformed {
        /// The schema being generated.
        schema: String,
        /// What parsing the emitted tokens reported.
        cause: String,
    },

    /// Two schemas declare the same `libraryName`, so both would be written to
    /// one `<library>.rs` and the second would silently replace the first.
    #[error("{first} and {second} both declare libraryName `{library}`, so both would write {library}.rs")]
    DuplicateLibrary {
        /// The library name they share.
        library: String,
        /// The schema configured first.
        first: PathBuf,
        /// The schema configured second.
        second: PathBuf,
    },

    /// A shader-definition layer does not declare its nodes in a form this
    /// crate generates views of.
    #[error("invalid shader definitions {path}: {cause}")]
    ShaderDefs {
        /// The layer.
        path: PathBuf,
        /// What is wrong with it.
        cause: String,
    },

    /// A `plugInfo.json` does not declare its metadata fields or kinds in a
    /// form this crate reads.
    #[error("invalid plugInfo {path}: {cause}")]
    PlugInfo {
        /// The file.
        path: PathBuf,
        /// What is wrong with it.
        cause: String,
    },

    /// A `plugInfo.json` declares a plugin that is neither a configured
    /// schema's `libraryName` nor a requested declaration family, so what it
    /// declares would reach no generated file.
    #[error("{path} declares plugin `{plugin}`, which is neither a configured schema library nor a declaration family")]
    UnknownPlugin {
        /// The plugin name.
        plugin: String,
        /// The file declaring it.
        path: PathBuf,
    },

    /// A declaration family's name is not an identifier, so it could not name
    /// the file generated for it.
    #[error("declaration family `{family}` is not a valid identifier")]
    InvalidDeclarationFamily {
        /// The requested family.
        family: String,
    },

    /// A declaration family was requested for a plugin no configured
    /// `plugInfo.json` declares.
    #[error("no configured plugInfo declares a plugin called `{family}`, requested as a declaration family")]
    UnknownDeclarationFamily {
        /// The requested family.
        family: String,
    },

    /// Two libraries declare one kind, so the registry would refuse the
    /// second.
    #[error("kind `{kind}` is declared by both {first} and {second}")]
    DuplicateKind {
        /// The kind.
        kind: String,
        /// The library declaring it first.
        first: String,
        /// The library declaring it again.
        second: String,
    },

    /// Two libraries declare one metadata field, so the registry would refuse
    /// the second.
    #[error("metadata field `{field}` is declared by both {first} and {second}")]
    DuplicateMetadataField {
        /// The field.
        field: String,
        /// The library declaring it first.
        first: String,
        /// The library declaring it again.
        second: String,
    },

    /// Two of a library's metadata fields would be read or written by one
    /// method of the trait they share.
    #[error("metadata fields `{first}` and `{second}` of {library} both need a method called `{method}`")]
    MetadataMethodCollision {
        /// The library declaring them.
        library: String,
        /// The method both need.
        method: String,
        /// The field named first.
        first: String,
        /// The field named second.
        second: String,
    },

    /// A file or directory could not be read, written or created.
    #[error("cannot access {path}")]
    Io {
        /// What was being read or written.
        path: PathBuf,
        /// What the filesystem reported.
        #[source]
        source: io::Error,
    },
}

impl Error {
    /// The first diagnostic `stage` raised, named against the schema it came
    /// from.
    ///
    /// Composition reports many; the first is the one a contributor fixes,
    /// and the rest usually follow from it.
    pub(crate) fn composition(schema: &std::path::Path, stage: &openusd::usd::Stage) -> Option<Self> {
        let diagnostic = stage.composition_errors().first()?.to_string();
        Some(Error::Composition {
            schema: schema.to_path_buf(),
            diagnostic,
        })
    }
}

/// A malformed path names nothing to read.
///
/// Written out rather than derived: `#[from]` builds a conversion into the
/// variant holding that type, and this belongs in [`Error::Core`] with every
/// other core failure. Its own variant would let one failure arrive two ways —
/// as itself, or nested in a `Core` — and leave a caller matching in two
/// places for it. `openusd::Error` already tells them apart.
impl From<openusd::sdf::PathParseError> for Error {
    fn from(source: openusd::sdf::PathParseError) -> Self {
        Error::Core(source.into())
    }
}

/// Authoring the layers this crate writes goes through the same door every
/// other edit does. Written out for the reason above.
impl From<openusd::sdf::EditError> for Error {
    fn from(source: openusd::sdf::EditError) -> Self {
        Error::Core(source.into())
    }
}

/// A composed query a reader asks of a stage. Written out for the reason
/// above.
impl From<openusd::pcp::QueryError> for Error {
    fn from(source: openusd::pcp::QueryError) -> Self {
        Error::Core(source.into())
    }
}

/// A field that will not decode is schema data this crate cannot use. Written
/// out for the reason above.
impl From<openusd::sdf::DataError> for Error {
    fn from(source: openusd::sdf::DataError) -> Self {
        Error::Core(source.into())
    }
}