openusd 0.8.0

Rust native USD library
Documentation
//! Carrying a schema's token-valued enum through [`Value`](super::Value).
//!
//! A schema declares a closed set of tokens for a property — an axis, a
//! subdivision scheme, an interpolation — and the Rust enum naming them is
//! either generated by `openusd-build` from the property's `allowedTokens` or
//! written by hand where no single property declares the set. This is what
//! makes such an enum a value: the conversion is about [`Value`](super::Value)
//! and [`CastError`](super::CastError), which this crate owns, so every token
//! enum reaches it the same way.

/// Carries a token-valued schema enum through [`Value`](crate::sdf::Value), in
/// both directions.
///
/// [`From`] authors a [`Value::Token`](crate::sdf::Value::Token), so the enum
/// passes straight to [`Attribute::set`](crate::usd::Attribute::set)
/// (`attr.set(Axis::X)?`); [`TryFrom`] decodes one, so
/// [`Attribute::get`](crate::usd::Attribute::get) extracts it
/// (`attr.get::<Axis>()?`). These attributes are `token`-valued, so only a
/// `Value::Token` decodes. The enum's [`FromValue`](crate::sdf::FromValue)
/// accepts that kind alone, so a typed read of any other kind is no value. A
/// token the enum does not spell, or a direct `try_from` of another kind, is a
/// [`CastError::TypeMismatch`](crate::sdf::CastError::TypeMismatch) naming the
/// enum.
///
/// Each enum must expose `fn as_token(self) -> &'static str` and
/// `fn from_token(impl AsRef<str>) -> Option<Self>`. Both are what
/// `openusd-build` generates beside the enums it writes, which invoke this
/// macro themselves.
///
/// ```
/// use openusd::sdf;
///
/// #[derive(Clone, Copy, Debug, PartialEq, Eq)]
/// pub enum Axis {
///     X,
///     Y,
///     Z,
/// }
///
/// impl Axis {
///     pub const fn as_token(self) -> &'static str {
///         match self {
///             Self::X => "X",
///             Self::Y => "Y",
///             Self::Z => "Z",
///         }
///     }
///
///     pub fn from_token(token: impl AsRef<str>) -> Option<Self> {
///         match token.as_ref() {
///             "X" => Some(Self::X),
///             "Y" => Some(Self::Y),
///             "Z" => Some(Self::Z),
///             _ => None,
///         }
///     }
/// }
///
/// sdf::impl_token_value!(Axis);
///
/// assert_eq!(sdf::Value::from(Axis::Y), sdf::Value::token("Y"));
/// assert_eq!(Axis::try_from(sdf::Value::token("Z")), Ok(Axis::Z));
/// assert!(Axis::try_from(sdf::Value::Bool(true)).is_err());
/// ```
// `#[macro_export]` binds at the crate root whatever module a macro lives in,
// and this crate's root holds only `Error` and `Result`. The binding is hidden
// so `sdf::impl_token_value` is the one path documented for it.
#[doc(hidden)]
#[macro_export]
macro_rules! impl_token_value {
    ($($ty:ty),+ $(,)?) => {$(
        impl ::std::convert::From<$ty> for $crate::sdf::Value {
            fn from(value: $ty) -> Self {
                // The token is `'static`, so it is held rather than copied.
                $crate::sdf::Value::Token($crate::tf::Token::new(value.as_token()))
            }
        }

        impl ::std::convert::TryFrom<$crate::sdf::Value> for $ty {
            type Error = $crate::sdf::CastError;

            // The error is named outright rather than as `Self::Error`: a token
            // spelled `error` gives the enum an `Error` variant, and
            // `Self::Error` would then name two things.
            fn try_from(
                value: $crate::sdf::Value,
            ) -> ::std::result::Result<Self, $crate::sdf::CastError> {
                match &value {
                    $crate::sdf::Value::Token(token) => <$ty>::from_token(token.as_str()),
                    _ => ::std::option::Option::None,
                }
                .ok_or_else(|| $crate::sdf::CastError::TypeMismatch {
                    target: ::std::stringify!($ty),
                    actual: ::std::convert::Into::into(&value),
                })
            }
        }

        impl $crate::sdf::FromValue for $ty {
            fn accepts_kind(kind: $crate::sdf::ValueKind) -> bool {
                kind == $crate::sdf::ValueKind::Token
            }
        }
    )+};
}

#[cfg(test)]
mod tests {
    use crate::sdf;

    /// A token set admitting `error`, which names a variant that an associated
    /// `Error` type would otherwise be confused with.
    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    enum Outcome {
        Ok,
        Error,
    }

    impl Outcome {
        const fn as_token(self) -> &'static str {
            match self {
                Self::Ok => "ok",
                Self::Error => "error",
            }
        }

        fn from_token(token: impl AsRef<str>) -> Option<Self> {
            match token.as_ref() {
                "ok" => Some(Self::Ok),
                "error" => Some(Self::Error),
                _ => None,
            }
        }
    }

    sdf::impl_token_value!(Outcome);

    /// An `Error` variant and the `TryFrom` associated type coexist, which the
    /// signature naming `CastError` outright is what allows.
    #[test]
    fn error_variant_admitted() -> Result<(), sdf::CastError> {
        assert_eq!(Outcome::try_from(sdf::Value::token("error"))?, Outcome::Error);
        Ok(())
    }

    /// The authored value holds the static token rather than a copy of it.
    #[test]
    fn authors_a_token() {
        assert_eq!(sdf::Value::from(Outcome::Ok), sdf::Value::token("ok"));
    }

    /// A token the enum does not name, and a value that is no token at all,
    /// both fail against the enum's own name.
    #[test]
    fn unknown_value_reports_the_enum() {
        for value in [sdf::Value::token("maybe"), sdf::Value::Bool(true)] {
            let error = Outcome::try_from(value).expect_err("not an Outcome");
            assert!(error.to_string().contains("Outcome"), "{error}");
        }
    }
}