1use std::fmt;
2
3use super::identifier_macros::{define_identifier, define_owned_identifier};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum IdentifierValidationError {
7 BlankJobType,
8 BlankWorkflowType,
9 BlankStepKey,
10}
11
12impl fmt::Display for IdentifierValidationError {
13 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14 match self {
15 Self::BlankJobType => write!(f, "job_type must be non-empty"),
16 Self::BlankWorkflowType => write!(f, "workflow_type must be non-empty"),
17 Self::BlankStepKey => write!(f, "step_key must be non-empty"),
18 }
19 }
20}
21
22impl std::error::Error for IdentifierValidationError {}
23
24fn validate_identifier(
25 value: &str,
26 blank_error: IdentifierValidationError,
27) -> Result<(), IdentifierValidationError> {
28 if value.trim().is_empty() {
29 Err(blank_error)
30 } else {
31 Ok(())
32 }
33}
34
35define_identifier!(JobType, BlankJobType);
36define_identifier!(WorkflowType, BlankWorkflowType);
37define_identifier!(StepKey, BlankStepKey);
38define_owned_identifier!(JobTypeName, JobType, BlankJobType);
39define_owned_identifier!(WorkflowTypeName, WorkflowType, BlankWorkflowType);
40define_owned_identifier!(StepKeyName, StepKey, BlankStepKey);
41
42#[cfg(feature = "sqlx-postgres")]
43mod sqlx_postgres {
44 use super::{
45 IdentifierValidationError, JobType, JobTypeName, StepKey, StepKeyName, WorkflowType,
46 WorkflowTypeName,
47 };
48 use sqlx::decode::Decode;
49 use sqlx::encode::{Encode, IsNull};
50 use sqlx::error::BoxDynError;
51 use sqlx::postgres::{PgArgumentBuffer, PgTypeInfo, PgValueRef};
52 use sqlx::{Postgres, Type};
53
54 macro_rules! impl_postgres_text_identifier {
55 ($identifier:ident) => {
56 impl<'q> Type<Postgres> for $identifier<'q> {
57 fn type_info() -> PgTypeInfo {
58 <&str as Type<Postgres>>::type_info()
59 }
60
61 fn compatible(ty: &PgTypeInfo) -> bool {
62 <&str as Type<Postgres>>::compatible(ty)
63 }
64 }
65
66 impl<'q> Encode<'q, Postgres> for $identifier<'q> {
67 fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
68 <&str as Encode<Postgres>>::encode(self.as_str(), buf)
69 }
70
71 fn size_hint(&self) -> usize {
72 self.as_str().len()
73 }
74 }
75 };
76 }
77
78 macro_rules! impl_postgres_owned_text_identifier {
79 ($owned_identifier:ident) => {
80 impl Type<Postgres> for $owned_identifier {
81 fn type_info() -> PgTypeInfo {
82 <String as Type<Postgres>>::type_info()
83 }
84
85 fn compatible(ty: &PgTypeInfo) -> bool {
86 <String as Type<Postgres>>::compatible(ty)
87 }
88 }
89
90 impl<'q> Encode<'q, Postgres> for $owned_identifier {
91 fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
92 <&str as Encode<Postgres>>::encode(self.as_str(), buf)
93 }
94
95 fn size_hint(&self) -> usize {
96 self.as_str().len()
97 }
98 }
99
100 impl<'r> Decode<'r, Postgres> for $owned_identifier {
101 fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
102 let value = <String as Decode<Postgres>>::decode(value)?;
103 Self::new(value).map_err(|error: IdentifierValidationError| error.into())
104 }
105 }
106 };
107 }
108
109 impl_postgres_text_identifier!(JobType);
110 impl_postgres_text_identifier!(WorkflowType);
111 impl_postgres_text_identifier!(StepKey);
112 impl_postgres_owned_text_identifier!(JobTypeName);
113 impl_postgres_owned_text_identifier!(WorkflowTypeName);
114 impl_postgres_owned_text_identifier!(StepKeyName);
115}
116
117#[cfg(test)]
118mod tests {
119 use std::collections::HashMap;
120
121 use super::{
122 IdentifierValidationError, JobType, JobTypeName, StepKey, StepKeyName, WorkflowType,
123 WorkflowTypeName,
124 };
125
126 #[test]
127 fn job_type_compares_with_str_and_string() {
128 let value = JobType::new("jobs.test");
129 let owned = "jobs.test".to_string();
130
131 assert_eq!(value, "jobs.test");
132 assert_eq!("jobs.test", value);
133 assert_eq!(value, owned);
134 assert_eq!(owned, value);
135 }
136
137 #[test]
138 fn job_type_try_new_rejects_blank_values() {
139 assert_eq!(
140 JobType::try_new(""),
141 Err(IdentifierValidationError::BlankJobType)
142 );
143 assert_eq!(
144 JobType::try_new(" "),
145 Err(IdentifierValidationError::BlankJobType)
146 );
147 }
148
149 #[test]
150 fn job_type_try_from_str_rejects_blank_values() {
151 assert_eq!(
152 JobType::try_from(""),
153 Err(IdentifierValidationError::BlankJobType)
154 );
155 }
156
157 #[test]
158 fn job_type_hash_map_supports_str_lookup() {
159 let mut values = HashMap::new();
160 values.insert(JobType::new("jobs.test"), 42);
161
162 assert_eq!(values.get("jobs.test"), Some(&42));
163 }
164
165 #[test]
166 fn owned_job_type_supports_str_lookup_and_borrowed_conversion() {
167 let mut values = HashMap::new();
168 values.insert(JobTypeName::new("jobs.test").expect("valid identifier"), 42);
169
170 assert_eq!(values.get("jobs.test"), Some(&42));
171 assert_eq!(
172 JobTypeName::new("jobs.test")
173 .expect("valid identifier")
174 .as_borrowed(),
175 JobType::new("jobs.test")
176 );
177 assert_eq!(
178 JobTypeName::try_from(JobType::new("jobs.test")),
179 Ok(JobTypeName::new("jobs.test").expect("valid identifier"))
180 );
181 }
182
183 #[test]
184 fn owned_job_type_try_from_borrowed_rejects_blank_values() {
185 assert_eq!(
186 JobTypeName::try_from(JobType::new(" ")),
187 Err(IdentifierValidationError::BlankJobType)
188 );
189 }
190
191 #[test]
192 fn owned_job_type_deserialize_rejects_blank_values() {
193 assert_eq!(
194 serde_json::from_str::<JobTypeName>("\"\"")
195 .expect_err("empty job type should fail")
196 .to_string(),
197 "job_type must be non-empty"
198 );
199 assert_eq!(
200 serde_json::from_str::<JobTypeName>("\" \"")
201 .expect_err("blank job type should fail")
202 .to_string(),
203 "job_type must be non-empty"
204 );
205 }
206
207 #[test]
208 fn owned_job_type_roundtrips_through_serde() {
209 let value = JobTypeName::new("jobs.test").expect("valid identifier");
210 let serialized = serde_json::to_string(&value).expect("serialize job type");
211 let deserialized =
212 serde_json::from_str::<JobTypeName>(&serialized).expect("deserialize job type");
213
214 assert_eq!(deserialized, value);
215 }
216
217 #[test]
218 fn workflow_type_compares_with_str_and_string() {
219 let value = WorkflowType::new("workflow.test");
220 let owned = "workflow.test".to_string();
221
222 assert_eq!(value, "workflow.test");
223 assert_eq!("workflow.test", value);
224 assert_eq!(value, owned);
225 assert_eq!(owned, value);
226 }
227
228 #[test]
229 fn workflow_type_try_new_rejects_blank_values() {
230 assert_eq!(
231 WorkflowType::try_new(""),
232 Err(IdentifierValidationError::BlankWorkflowType)
233 );
234 assert_eq!(
235 WorkflowType::try_new(" "),
236 Err(IdentifierValidationError::BlankWorkflowType)
237 );
238 }
239
240 #[test]
241 fn workflow_type_try_from_str_rejects_blank_values() {
242 assert_eq!(
243 WorkflowType::try_from(""),
244 Err(IdentifierValidationError::BlankWorkflowType)
245 );
246 }
247
248 #[test]
249 fn owned_workflow_type_rejects_blank_values() {
250 assert_eq!(
251 WorkflowTypeName::new(""),
252 Err(IdentifierValidationError::BlankWorkflowType)
253 );
254 }
255
256 #[test]
257 fn owned_workflow_type_try_from_borrowed_rejects_blank_values() {
258 assert_eq!(
259 WorkflowTypeName::try_from(WorkflowType::new(" ")),
260 Err(IdentifierValidationError::BlankWorkflowType)
261 );
262 }
263
264 #[test]
265 fn owned_workflow_type_deserialize_rejects_blank_values() {
266 assert_eq!(
267 serde_json::from_str::<WorkflowTypeName>("\"\"")
268 .expect_err("empty workflow type should fail")
269 .to_string(),
270 "workflow_type must be non-empty"
271 );
272 assert_eq!(
273 serde_json::from_str::<WorkflowTypeName>("\" \"")
274 .expect_err("blank workflow type should fail")
275 .to_string(),
276 "workflow_type must be non-empty"
277 );
278 }
279
280 #[test]
281 fn owned_workflow_type_roundtrips_through_serde() {
282 let value = WorkflowTypeName::new("workflow.test").expect("valid identifier");
283 let serialized = serde_json::to_string(&value).expect("serialize workflow type");
284 let deserialized = serde_json::from_str::<WorkflowTypeName>(&serialized)
285 .expect("deserialize workflow type");
286
287 assert_eq!(deserialized, value);
288 }
289
290 #[test]
291 fn step_key_compares_with_str_and_string() {
292 let value = StepKey::new("step.test");
293 let owned = "step.test".to_string();
294
295 assert_eq!(value, "step.test");
296 assert_eq!("step.test", value);
297 assert_eq!(value, owned);
298 assert_eq!(owned, value);
299 }
300
301 #[test]
302 fn step_key_try_new_rejects_blank_values() {
303 assert_eq!(
304 StepKey::try_new(""),
305 Err(IdentifierValidationError::BlankStepKey)
306 );
307 assert_eq!(
308 StepKey::try_new(" "),
309 Err(IdentifierValidationError::BlankStepKey)
310 );
311 }
312
313 #[test]
314 fn step_key_try_from_str_rejects_blank_values() {
315 assert_eq!(
316 StepKey::try_from(""),
317 Err(IdentifierValidationError::BlankStepKey)
318 );
319 }
320
321 #[test]
322 fn owned_step_key_rejects_blank_values() {
323 assert_eq!(
324 StepKeyName::new(""),
325 Err(IdentifierValidationError::BlankStepKey)
326 );
327 }
328
329 #[test]
330 fn owned_step_key_try_from_borrowed_rejects_blank_values() {
331 assert_eq!(
332 StepKeyName::try_from(StepKey::new(" ")),
333 Err(IdentifierValidationError::BlankStepKey)
334 );
335 }
336
337 #[test]
338 fn owned_step_key_deserialize_rejects_blank_values() {
339 assert_eq!(
340 serde_json::from_str::<StepKeyName>("\"\"")
341 .expect_err("empty step key should fail")
342 .to_string(),
343 "step_key must be non-empty"
344 );
345 assert_eq!(
346 serde_json::from_str::<StepKeyName>("\" \"")
347 .expect_err("blank step key should fail")
348 .to_string(),
349 "step_key must be non-empty"
350 );
351 }
352
353 #[test]
354 fn owned_step_key_roundtrips_through_serde() {
355 let value = StepKeyName::new("step.test").expect("valid identifier");
356 let serialized = serde_json::to_string(&value).expect("serialize step key");
357 let deserialized =
358 serde_json::from_str::<StepKeyName>(&serialized).expect("deserialize step key");
359
360 assert_eq!(deserialized, value);
361 }
362
363 #[test]
364 fn unchecked_new_preserves_blank_values() {
365 assert_eq!(JobType::new("").as_str(), "");
366 assert_eq!(WorkflowType::new(" ").as_str(), " ");
367 assert_eq!(StepKey::new("").as_str(), "");
368 }
369}