openusd-build 0.8.0

Generates Rust schema views from OpenUSD schema.usda files, for use as a build dependency
Documentation
//! What a schema's value types become in generated code.
//!
//! Two answers per property, and only one of them lives here. [`rust_type`] is
//! the type its accessor reads and writes, so a `point3f` attribute hands back
//! a `gf::Vec3f`. The constant its creator declares it with comes from
//! [`sdf::ValueTypeName::constant_ident`], since `sdf` is what names its own
//! constants.

use openusd::sdf;

/// The Rust type a value of `kind` reads back as, or `None` for a kind no
/// attribute can hold — `opaque`, which carries no value, and the metadata-only
/// kinds.
///
/// Paths are spelled the way generated code spells them: in full, since the
/// generated file is included bare into a consumer's module and carries no
/// `use` block of its own.
pub fn rust_type(kind: sdf::ValueKind) -> Option<syn::Type> {
    let spelled = match kind.element_kind() {
        Some(element) => format!("::std::vec::Vec<{}>", scalar_type(element)?),
        None => scalar_type(kind)?.to_owned(),
    };
    Some(syn::parse_str(&spelled).expect("every spelling below is a Rust type"))
}

/// The Rust type one scalar value reads back as. A role does not change it: a
/// point, a normal and a colour are all one vector type, as they are one kind.
///
/// The pairing restates what `sdf::Value` declares, [`sdf::ValueKind`] being
/// derived from it, as the module-qualified source text a generated file
/// spells. That spelling is the part `sdf` has no reason to know.
fn scalar_type(kind: sdf::ValueKind) -> Option<&'static str> {
    Some(match kind {
        sdf::ValueKind::Bool => "bool",
        sdf::ValueKind::Uchar => "u8",
        sdf::ValueKind::Int => "i32",
        sdf::ValueKind::Uint => "u32",
        sdf::ValueKind::Int64 => "i64",
        sdf::ValueKind::Uint64 => "u64",
        sdf::ValueKind::Half => "::openusd::gf::f16",
        sdf::ValueKind::Float => "f32",
        sdf::ValueKind::Double => "f64",
        sdf::ValueKind::String => "::std::string::String",
        sdf::ValueKind::Token => "::openusd::tf::Token",
        sdf::ValueKind::AssetPath => "::openusd::sdf::AssetPath",
        sdf::ValueKind::TimeCode => "::openusd::sdf::TimeCode",
        sdf::ValueKind::PathExpression => "::openusd::sdf::PathExpression",

        sdf::ValueKind::Vec2h => "::openusd::gf::Vec2h",
        sdf::ValueKind::Vec2f => "::openusd::gf::Vec2f",
        sdf::ValueKind::Vec2d => "::openusd::gf::Vec2d",
        sdf::ValueKind::Vec2i => "::openusd::gf::Vec2i",
        sdf::ValueKind::Vec3h => "::openusd::gf::Vec3h",
        sdf::ValueKind::Vec3f => "::openusd::gf::Vec3f",
        sdf::ValueKind::Vec3d => "::openusd::gf::Vec3d",
        sdf::ValueKind::Vec3i => "::openusd::gf::Vec3i",
        sdf::ValueKind::Vec4h => "::openusd::gf::Vec4h",
        sdf::ValueKind::Vec4f => "::openusd::gf::Vec4f",
        sdf::ValueKind::Vec4d => "::openusd::gf::Vec4d",
        sdf::ValueKind::Vec4i => "::openusd::gf::Vec4i",

        sdf::ValueKind::Quath => "::openusd::gf::Quath",
        sdf::ValueKind::Quatf => "::openusd::gf::Quatf",
        sdf::ValueKind::Quatd => "::openusd::gf::Quatd",

        sdf::ValueKind::Matrix2d => "::openusd::gf::Mat2d",
        sdf::ValueKind::Matrix3d => "::openusd::gf::Mat3d",
        sdf::ValueKind::Matrix4d => "::openusd::gf::Matrix4d",

        // `opaque` carries no value, and the rest are metadata kinds no
        // attribute is declared with.
        _ => return None,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    use quote::ToTokens;

    /// `kind`'s Rust type as source text, spaces removed.
    fn spelled(kind: sdf::ValueKind) -> Option<String> {
        rust_type(kind).map(|ty| ty.to_token_stream().to_string().replace(' ', ""))
    }

    /// Every type an attribute can be declared with has a Rust type to read
    /// it back as, and an array reads back as a `Vec` of its element's.
    #[test]
    fn every_attribute_type_maps() {
        for type_name in sdf::ValueTypeName::all() {
            let Some(kind) = type_name.kind() else { continue };
            if kind == sdf::ValueKind::Opaque {
                continue;
            }
            assert!(
                rust_type(kind).is_some(),
                "{} has no Rust type",
                type_name.serialization_name()
            );
        }

        assert_eq!(spelled(sdf::ValueKind::Float).as_deref(), Some("f32"));
        assert_eq!(
            spelled(sdf::ValueKind::Vec3fVec).as_deref(),
            Some("::std::vec::Vec<::openusd::gf::Vec3f>")
        );
        assert_eq!(
            spelled(sdf::ValueKind::TokenVec).as_deref(),
            Some("::std::vec::Vec<::openusd::tf::Token>")
        );
    }

    /// A role does not change the Rust type: a point, a normal and a colour
    /// are all one vector type.
    #[test]
    fn roles_share_a_type() {
        for spelling in ["float3", "point3f", "normal3f", "vector3f", "color3f"] {
            let kind = sdf::ValueTypeName::find(spelling).and_then(|type_name| type_name.kind());
            assert_eq!(
                kind.and_then(spelled).as_deref(),
                Some("::openusd::gf::Vec3f"),
                "{spelling}"
            );
        }
    }

    /// `opaque` carries no value, so it has no type to read.
    #[test]
    fn opaque_has_no_type() {
        assert!(rust_type(sdf::ValueKind::Opaque).is_none());
    }
}