1use std::fmt;
7
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct IdError {
13 pub value: String,
14}
15
16impl fmt::Display for IdError {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 write!(
19 f,
20 "not an identity (`sha256:` and 64 lowercase hex digits): {:?}",
21 self.value
22 )
23 }
24}
25
26impl std::error::Error for IdError {}
27
28fn is_digest(s: &str) -> bool {
29 s.strip_prefix("sha256:").is_some_and(|hex| {
30 hex.len() == 64
31 && hex
32 .bytes()
33 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
34 })
35}
36
37macro_rules! identity {
38 ($(#[$doc:meta])* $name:ident) => {
39 $(#[$doc])*
40 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
41 pub struct $name(String);
42
43 impl $name {
44 pub fn parse(s: &str) -> Result<Self, IdError> {
46 if is_digest(s) {
47 Ok(Self(s.to_string()))
48 } else {
49 Err(IdError { value: s.chars().take(80).collect() })
50 }
51 }
52
53 pub fn as_str(&self) -> &str {
55 &self.0
56 }
57
58 #[allow(dead_code)]
60 pub(crate) fn from_digest(s: String) -> Self {
61 debug_assert!(is_digest(&s));
62 Self(s)
63 }
64 }
65
66 impl fmt::Display for $name {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 f.write_str(&self.0)
69 }
70 }
71
72 impl Serialize for $name {
73 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
74 s.serialize_str(&self.0)
75 }
76 }
77
78 impl<'de> Deserialize<'de> for $name {
79 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
80 let s = String::deserialize(d)?;
81 Self::parse(&s).map_err(serde::de::Error::custom)
82 }
83 }
84
85 impl schemars::JsonSchema for $name {
86 fn schema_name() -> std::borrow::Cow<'static, str> {
87 stringify!($name).into()
88 }
89
90 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
91 crate::schema_emit::digest_schema()
92 }
93 }
94 };
95}
96
97identity!(
98 DefinitionId
100);
101identity!(
102 ProfileId
104);
105identity!(
106 PlanId
108);
109identity!(
110 DescriptorId
112);
113identity!(
114 CalibrationId
116);
117identity!(
118 SnapshotId
120);
121identity!(
122 SourceId
125);
126identity!(
127 AcquisitionRequestId
129);
130identity!(
131 ExecutionPolicyId
133);
134identity!(
135 RequestId
138);
139identity!(
140 ContentDigest
144);
145identity!(
146 RedactionPolicyId
149);
150identity!(
151 ArtifactId
154);
155identity!(
156 DatasetId
158);
159identity!(
160 EvaluatorConfigId
162);
163identity!(
164 DynamicRequestId
167);
168identity!(
169 DynamicCapabilityId
172);
173identity!(
174 TemplateId
177);
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 const GOOD: &str = "sha256:0000000000000000000000000000000000000000000000000000000000000abc";
184
185 #[test]
186 fn identities_are_validated() {
187 assert!(ArtifactId::parse(GOOD).is_ok());
188 for bad in [
189 "",
190 "sha256:",
191 "sha256:ABC0000000000000000000000000000000000000000000000000000000000000",
192 "sha512:0000000000000000000000000000000000000000000000000000000000000abc",
193 "sha256:000000000000000000000000000000000000000000000000000000000000abc",
194 "sha256:00000000000000000000000000000000000000000000000000000000000000abc",
195 ] {
196 assert!(ArtifactId::parse(bad).is_err(), "{bad:?}");
197 }
198 }
199
200 #[test]
201 fn serde_round_trips_and_refuses_invalid() {
202 let id: DatasetId = serde_json::from_str(&format!("\"{GOOD}\"")).unwrap();
203 assert_eq!(serde_json::to_string(&id).unwrap(), format!("\"{GOOD}\""));
204 assert!(serde_json::from_str::<DatasetId>("\"sha256:xyz\"").is_err());
205 }
206}
207
208identity!(
209 PromptTemplateId
211);
212
213identity!(
214 LiveConfigId
216);