Skip to main content

rustev_contract/
ids.rs

1//! Identities (spec 002, 3.3.2 and 3.3.3): `sha256:` and 64 lowercase hex
2//! digits. Computed identities come from [`crate::Identified::id`] (and
3//! content digests from [`crate::Document::record_digest`]); supplied ones
4//! (`ArtifactId`, `DatasetId`, `EvaluatorConfigId`) are validated only.
5
6use std::fmt;
7
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10/// Why a string is not an identity.
11#[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            /// Validate `s` as an identity.
45            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            /// The textual form.
54            pub fn as_str(&self) -> &str {
55                &self.0
56            }
57
58            /// Only for digests this crate computed.
59            #[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    /// Identity of a canonical decision definition.
99    DefinitionId
100);
101identity!(
102    /// Identity of a canonical profile (spec 034, 3.5.1).
103    ProfileId
104);
105identity!(
106    /// Identity of a canonical compiled plan.
107    PlanId
108);
109identity!(
110    /// Identity of a canonical backend capability descriptor.
111    DescriptorId
112);
113identity!(
114    /// Identity of a canonical calibration artifact.
115    CalibrationId
116);
117identity!(
118    /// Identity of a canonical context snapshot.
119    SnapshotId
120);
121identity!(
122    /// Identity of a canonical context source descriptor,
123    /// `rustev.source/1` (spec 032, 3.3.1).
124    SourceId
125);
126identity!(
127    /// Identity of an acquisition request (spec 036, 3.3).
128    AcquisitionRequestId
129);
130identity!(
131    /// Identity of a canonical execution policy (spec 003, 3.2.2).
132    ExecutionPolicyId
133);
134identity!(
135    /// Identity of a canonical request document, `rustev.request/1`
136    /// (spec 004, 3.4.1).
137    RequestId
138);
139identity!(
140    /// The schema-tagged digest of a retained document's record canonical
141    /// bytes (spec 004, 3.1.6 and 5.6). It proves integrity relative to
142    /// expected bytes, never truth, authorization or producer identity.
143    ContentDigest
144);
145identity!(
146    /// Identity of a canonical redaction policy, `rustev.redaction/1`
147    /// (spec 028, 3.4.1).
148    RedactionPolicyId
149);
150identity!(
151    /// A backend artifact (model, tokenizer, preprocessing, truncation,
152    /// precision), identified by its producer.
153    ArtifactId
154);
155identity!(
156    /// A dataset, identified by its producer.
157    DatasetId
158);
159identity!(
160    /// An evaluator configuration, identified by its producer.
161    EvaluatorConfigId
162);
163identity!(
164    /// Identity of a canonical dynamic request, the tagged digest of its
165    /// `DynamicIdentity` (spec 019, 3.4).
166    DynamicRequestId
167);
168identity!(
169    /// Identity of a canonical dynamic capability declaration,
170    /// `rustev.dynamic-capability/1` (spec 019, 3.4).
171    DynamicCapabilityId
172);
173identity!(
174    /// A dynamic question's structured template, identified by its producer
175    /// (spec 019, 3.3).
176    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    /// Identity of an authored prompt template (spec 037).
210    PromptTemplateId
211);
212
213identity!(
214    /// Identity of a canonical live configuration (spec 043, 3.2).
215    LiveConfigId
216);