rite_model/ir/ids.rs
1//! Type-safe identifier newtypes for the ceremony IR.
2//!
3//! These wrapper types prevent accidentally mixing up different kinds of IDs
4//! (e.g., passing a `RoleId` where a `StepId` is expected).
5//!
6//! Each ID wraps a `String` for easy debugging and display.
7
8use std::fmt;
9use std::hash::Hash;
10
11macro_rules! define_id {
12 ($(#[$meta:meta])* $name:ident) => {
13 #[derive(Clone, Eq, PartialEq, Hash, Debug, serde::Serialize, serde::Deserialize)]
14 #[cfg_attr(test, derive(schemars::JsonSchema))]
15 $(#[$meta])*
16 #[serde(transparent)]
17 pub struct $name(String);
18
19 impl $name {
20 /// Create a new ID from any string-like value.
21 pub fn new(s: impl Into<String>) -> Self {
22 Self(s.into())
23 }
24
25 /// Get the underlying string.
26 pub fn as_str(&self) -> &str {
27 &self.0
28 }
29
30 /// Consume and return the underlying string.
31 pub fn into_string(self) -> String {
32 self.0
33 }
34 }
35
36 impl fmt::Display for $name {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 write!(f, "{}", self.0)
39 }
40 }
41
42 impl From<String> for $name {
43 fn from(s: String) -> Self {
44 Self(s)
45 }
46 }
47
48 impl From<&str> for $name {
49 fn from(s: &str) -> Self {
50 Self(s.to_string())
51 }
52 }
53
54 impl AsRef<str> for $name {
55 fn as_ref(&self) -> &str {
56 &self.0
57 }
58 }
59 };
60}
61
62define_id!(
63 /// Unique identifier for a role in the ceremony.
64 #[cfg_attr(test, schemars(description = "A role's identifier, as written in the ceremony."))]
65 RoleId
66);
67
68define_id!(
69 /// Unique identifier for a step in the ceremony.
70 #[cfg_attr(test, schemars(description = "A step's identifier, as written in the ceremony."))]
71 StepId
72);
73
74define_id!(
75 /// Unique identifier for a section in the ceremony.
76 SectionId
77);
78
79define_id!(
80 /// Unique identifier for an act in the ceremony.
81 #[cfg_attr(test, schemars(description = "An act's identifier, as written in the ceremony."))]
82 ActId
83);
84
85define_id!(
86 /// Unique identifier for a parameter in the ceremony.
87 #[cfg_attr(
88 test,
89 schemars(description = "A parameter's identifier, as written in the ceremony.")
90 )]
91 ParamId
92);
93
94define_id!(
95 /// Unique identifier for a material in the ceremony.
96 #[cfg_attr(
97 test,
98 schemars(description = "A material's identifier, as written in the ceremony.")
99 )]
100 MaterialId
101);
102
103define_id!(
104 /// Unique identifier for an artifact produced during the ceremony.
105 ///
106 /// Artifacts are runtime values produced by step actions and stored in execution state.
107 /// They can be referenced by subsequent steps (e.g., `${artifact.keypair.private}`)
108 /// or declared as outputs for file/stdout destinations.
109 ///
110 /// Not all artifacts are outputs; intermediate artifacts may be produced and consumed
111 /// without ever being written to disk.
112 ///
113 /// See also: [`OutputId`] for output declarations.
114 ArtifactId
115);
116
117define_id!(
118 /// Unique identifier for an output declaration in the ceremony.
119 ///
120 /// Outputs are ceremony declarations that specify the type and format of artifacts
121 /// produced during the ceremony. They are resolved at ceremony load time.
122 ///
123 /// # Relationship to `ArtifactId`
124 ///
125 /// `OutputId` and `ArtifactId` are **separate types** that **match by name**:
126 ///
127 /// ```yaml
128 /// # Ceremony declares outputs (OutputId)
129 /// output:
130 /// public_key: # OutputId("public_key")
131 /// type: public_key
132 ///
133 /// # Steps produce artifacts (ArtifactId)
134 /// steps:
135 /// - id: export
136 /// produces: ${artifact.public_key} # ArtifactId("public_key")
137 /// ```
138 ///
139 /// At runtime, the executor looks up: "for this `ArtifactId`, is there a declared `OutputId`?"
140 ///
141 /// They remain separate types because:
142 /// - **Different lifespans**: `OutputId` exists from resolution, `ArtifactId` from execution
143 /// - **Different scopes**: Not all artifacts are outputs (intermediate values)
144 /// - **Type safety**: Prevents mixing declarations vs runtime values
145 ///
146 /// See also: [`ArtifactId`] for runtime artifacts.
147 OutputId
148);
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use std::collections::HashMap;
154
155 #[test]
156 fn ids_are_type_safe() {
157 let role_id = RoleId::new("witness");
158 let step_id = StepId::new("witness"); // Same string, different type.
159
160 // These are different types, can't be compared directly.
161 assert_eq!(role_id.as_str(), step_id.as_str());
162 }
163
164 #[test]
165 fn ids_work_as_hash_keys() {
166 let mut map: HashMap<RoleId, String> = HashMap::new();
167 map.insert(RoleId::new("admin"), "Alice".to_string());
168 map.insert(RoleId::new("witness"), "Bob".to_string());
169
170 assert_eq!(map.get(&RoleId::new("admin")), Some(&"Alice".to_string()));
171 }
172
173 #[test]
174 fn ids_display_nicely() {
175 let id = RoleId::new("ceremony_admin");
176 assert_eq!(format!("{id}"), "ceremony_admin");
177 assert_eq!(format!("{id:?}"), r#"RoleId("ceremony_admin")"#);
178 }
179}