Skip to main content

runledger_core/jobs/
identifiers.rs

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}