agentics_domain/models/
ids.rs1use std::borrow::Cow;
4use std::fmt;
5use std::str::FromStr;
6
7use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use uuid::Uuid;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct UuidIdError {
14 message: &'static str,
15}
16
17impl UuidIdError {
18 const fn new(message: &'static str) -> Self {
19 Self { message }
20 }
21}
22
23impl fmt::Display for UuidIdError {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 f.write_str(self.message)
27 }
28}
29
30impl std::error::Error for UuidIdError {}
31
32macro_rules! define_uuid_id_type {
33 ($type_name:ident, $schema_name:literal, $message:literal) => {
34 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
35 pub struct $type_name(String);
36
37 impl $type_name {
38 pub fn generate() -> Self {
40 Self(Uuid::new_v4().to_string())
41 }
42
43 pub fn try_new(value: impl AsRef<str>) -> Result<Self, UuidIdError> {
45 let value = value.as_ref();
46 if value.trim() != value {
47 return Err(UuidIdError::new($message));
48 }
49 let canonical = value.to_ascii_lowercase();
50 let Ok(uuid) = Uuid::parse_str(&canonical) else {
51 return Err(UuidIdError::new($message));
52 };
53 if uuid.to_string() != canonical {
54 return Err(UuidIdError::new($message));
55 }
56 Ok(Self(canonical))
57 }
58
59 pub fn as_str(&self) -> &str {
61 &self.0
62 }
63 }
64
65 impl fmt::Display for $type_name {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 f.write_str(self.as_str())
69 }
70 }
71
72 impl AsRef<str> for $type_name {
73 fn as_ref(&self) -> &str {
75 self.as_str()
76 }
77 }
78
79 impl FromStr for $type_name {
80 type Err = UuidIdError;
81
82 fn from_str(value: &str) -> Result<Self, Self::Err> {
84 Self::try_new(value)
85 }
86 }
87
88 impl Serialize for $type_name {
89 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
91 where
92 S: Serializer,
93 {
94 serializer.serialize_str(self.as_str())
95 }
96 }
97
98 impl<'de> Deserialize<'de> for $type_name {
99 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
101 where
102 D: Deserializer<'de>,
103 {
104 let value = String::deserialize(deserializer)?;
105 Self::try_new(&value).map_err(serde::de::Error::custom)
106 }
107 }
108
109 impl JsonSchema for $type_name {
110 fn inline_schema() -> bool {
112 true
113 }
114
115 fn schema_name() -> Cow<'static, str> {
117 $schema_name.into()
118 }
119
120 fn json_schema(_: &mut SchemaGenerator) -> Schema {
122 json_schema!({
123 "type": "string",
124 "format": "uuid",
125 "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
126 })
127 }
128 }
129 };
130}
131
132define_uuid_id_type!(
133 AgentId,
134 "AgentId",
135 "agent_id must be a canonical UUID string"
136);
137define_uuid_id_type!(
138 AgentTokenId,
139 "AgentTokenId",
140 "agent_token_id must be a canonical UUID string"
141);
142define_uuid_id_type!(
143 HumanId,
144 "HumanId",
145 "human_id must be a canonical UUID string"
146);
147define_uuid_id_type!(
148 HumanSessionId,
149 "HumanSessionId",
150 "human_session_id must be a canonical UUID string"
151);
152define_uuid_id_type!(
153 AdminServiceTokenId,
154 "AdminServiceTokenId",
155 "admin_service_token_id must be a canonical UUID string"
156);
157define_uuid_id_type!(
158 CreatorApiTokenId,
159 "CreatorApiTokenId",
160 "creator_api_token_id must be a canonical UUID string"
161);
162define_uuid_id_type!(
163 PioneerCodeId,
164 "PioneerCodeId",
165 "pioneer_code_id must be a canonical UUID string"
166);
167define_uuid_id_type!(
168 ChallengeReviewRecordId,
169 "ChallengeReviewRecordId",
170 "challenge_review_record_id must be a canonical UUID string"
171);
172define_uuid_id_type!(
173 ChallengePrivateAssetId,
174 "ChallengePrivateAssetId",
175 "challenge_private_asset_id must be a canonical UUID string"
176);
177define_uuid_id_type!(
178 ChallengeReviewValidationRecordId,
179 "ChallengeReviewValidationRecordId",
180 "challenge_review_validation_record_id must be a canonical UUID string"
181);
182define_uuid_id_type!(
183 ChallengeReviewAuditEventId,
184 "ChallengeReviewAuditEventId",
185 "challenge_review_audit_event_id must be a canonical UUID string"
186);
187define_uuid_id_type!(
188 ChallengeReviewPublishClaimId,
189 "ChallengeReviewPublishClaimId",
190 "challenge_review_publish_claim_id must be a canonical UUID string"
191);
192define_uuid_id_type!(
193 ChallengeShortlistRevisionId,
194 "ChallengeShortlistRevisionId",
195 "challenge_shortlist_revision_id must be a canonical UUID string"
196);
197define_uuid_id_type!(
198 EvaluationJobId,
199 "EvaluationJobId",
200 "evaluation_job_id must be a canonical UUID string"
201);
202define_uuid_id_type!(
203 EvaluationId,
204 "EvaluationId",
205 "evaluation_id must be a canonical UUID string"
206);
207define_uuid_id_type!(
208 SolutionSubmissionId,
209 "SolutionSubmissionId",
210 "solution_submission_id must be a canonical UUID string"
211);
212
213#[cfg(test)]
214mod tests {
215 use super::{AgentId, AgentTokenId, ChallengeReviewRecordId, SolutionSubmissionId};
216
217 #[test]
219 fn validates_solution_submission_ids() {
220 let valid = "f47ac10b-58cc-4372-a567-0e02b2c3d479";
221 assert!(SolutionSubmissionId::try_new(valid).is_ok());
222 let canonical = SolutionSubmissionId::try_new("F47AC10B-58CC-4372-A567-0E02B2C3D479")
223 .expect("UUID hex case should canonicalize");
224 assert_eq!(canonical.as_str(), valid);
225 assert!(SolutionSubmissionId::try_new("submission-1").is_err());
226 assert!(SolutionSubmissionId::try_new(" f47ac10b-58cc-4372-a567-0e02b2c3d479").is_err());
227 assert!(SolutionSubmissionId::try_new("f47ac10b58cc4372a5670e02b2c3d479").is_err());
228 }
229
230 #[test]
232 fn serde_rejects_invalid_solution_submission_ids() {
233 let submission: SolutionSubmissionId =
234 serde_json::from_str("\"f47ac10b-58cc-4372-a567-0e02b2c3d479\"")
235 .expect("valid submission id should parse");
236 assert_eq!(submission.as_str(), "f47ac10b-58cc-4372-a567-0e02b2c3d479");
237 assert!(serde_json::from_str::<SolutionSubmissionId>("\"submission-1\"").is_err());
238 }
239
240 #[test]
242 fn generated_uuid_ids_canonicalize_hex_case() {
243 let canonical = "f47ac10b-58cc-4372-a567-0e02b2c3d479";
244 assert_eq!(
245 AgentId::try_new("F47AC10B-58CC-4372-A567-0E02B2C3D479")
246 .expect("UUID hex case should canonicalize")
247 .as_str(),
248 canonical
249 );
250 assert_eq!(
251 AgentTokenId::try_new(canonical)
252 .expect("agent token id should parse")
253 .as_str(),
254 canonical
255 );
256 assert_eq!(
257 ChallengeReviewRecordId::try_new(canonical)
258 .expect("challenge review record id should parse")
259 .as_str(),
260 canonical
261 );
262 assert!(ChallengeReviewRecordId::try_new(format!(" {canonical}")).is_err());
263 assert!(ChallengeReviewRecordId::try_new("f47ac10b58cc4372a5670e02b2c3d479").is_err());
264 }
265}