Skip to main content

alpha2_model/
ids.rs

1//! Identifiers of the items a project document names.
2
3use alpha2_media::IdError;
4use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
5use std::fmt;
6
7// Same shape as the macro behind `alpha2_media::SourceId`, sharing its `IdError`. An ID may
8// also forbid one character, which its schema excludes by `pattern`.
9macro_rules! define_id {
10    ($name:ident, $description:literal $(, without = $forbidden:literal, pattern = $pattern:literal)?) => {
11        #[doc = $description]
12        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
13        #[serde(transparent)]
14        pub struct $name(String);
15
16        impl<'de> Deserialize<'de> for $name {
17            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
18            where
19                D: Deserializer<'de>,
20            {
21                let value = String::deserialize(deserializer)?;
22                $(if value.contains($forbidden) {
23                    return Err(D::Error::custom(format!(
24                        "{} {value:?} has a `{}`",
25                        stringify!($name),
26                        $forbidden
27                    )));
28                })?
29                Self::new(value).map_err(D::Error::custom)
30            }
31        }
32
33        impl $name {
34            /// Creates an identifier from a non-empty, stable external value (a UUID or ULID).
35            ///
36            /// # Errors
37            ///
38            /// Returns [`IdError::Empty`] when `value` is empty.
39            pub fn new(value: impl Into<String>) -> Result<Self, IdError> {
40                let value = value.into();
41                if value.is_empty() {
42                    Err(IdError::Empty)
43                } else {
44                    Ok(Self(value))
45                }
46            }
47
48            /// Returns this identifier as its serialized string value.
49            #[must_use]
50            pub fn as_str(&self) -> &str {
51                &self.0
52            }
53        }
54
55        #[cfg(feature = "schema")]
56        impl schemars::JsonSchema for $name {
57            fn schema_name() -> std::borrow::Cow<'static, str> {
58                stringify!($name).into()
59            }
60
61            fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
62                #[allow(unused_mut)]
63                let mut schema = schemars::json_schema!({
64                    "description": $description,
65                    "type": "string",
66                    "minLength": 1,
67                });
68                $(schema.insert("pattern".into(), $pattern.into());)?
69                schema
70            }
71        }
72
73        impl fmt::Display for $name {
74            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75                formatter.write_str(&self.0)
76            }
77        }
78    };
79}
80
81define_id!(ProjectId, "A stable project identifier.");
82define_id!(EffectResourceId, "A stable effect-resource identifier.");
83define_id!(TrackId, "A stable timeline-track identifier.");
84define_id!(ClipId, "A stable timeline-clip identifier.");
85define_id!(CompositionId, "A stable composition-graph identifier.");
86define_id!(
87    NodeId,
88    "A stable identifier of a node within one composition graph. It has no `/`, which joins the IDs of the group and media nodes a step was drawn through.",
89    without = '/',
90    pattern = "^[^/]*$"
91);