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 that defines `alpha2_media::SourceId`, sharing its `IdError`. An ID may
8// also forbid one character, which its schema then 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.
35            ///
36            /// UUIDs and ULIDs are both suitable values. The engine does not
37            /// generate IDs because identity belongs to the document author.
38            ///
39            /// # Errors
40            ///
41            /// Returns [`IdError::Empty`] when `value` is empty.
42            pub fn new(value: impl Into<String>) -> Result<Self, IdError> {
43                let value = value.into();
44                if value.is_empty() {
45                    Err(IdError::Empty)
46                } else {
47                    Ok(Self(value))
48                }
49            }
50
51            /// Returns this identifier as its serialized string value.
52            #[must_use]
53            pub fn as_str(&self) -> &str {
54                &self.0
55            }
56        }
57
58        #[cfg(feature = "schema")]
59        impl schemars::JsonSchema for $name {
60            fn schema_name() -> std::borrow::Cow<'static, str> {
61                stringify!($name).into()
62            }
63
64            fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
65                #[allow(unused_mut)]
66                let mut schema = schemars::json_schema!({
67                    "description": $description,
68                    "type": "string",
69                    "minLength": 1,
70                });
71                $(schema.insert("pattern".into(), $pattern.into());)?
72                schema
73            }
74        }
75
76        impl fmt::Display for $name {
77            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78                formatter.write_str(&self.0)
79            }
80        }
81    };
82}
83
84define_id!(ProjectId, "A stable project identifier.");
85define_id!(EffectResourceId, "A stable effect-resource identifier.");
86define_id!(TrackId, "A stable timeline-track identifier.");
87define_id!(ClipId, "A stable timeline-clip identifier.");
88define_id!(CompositionId, "A stable composition-graph identifier.");
89define_id!(
90    NodeId,
91    "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.",
92    without = '/',
93    pattern = "^[^/]*$"
94);