Skip to main content

oxide_batch_core/domain/
identity.rs

1use std::borrow::Borrow;
2use std::fmt;
3use std::num::NonZeroU64;
4
5use super::{DomainError, IdentifierKind, NameKind};
6
7const MAX_DOMAIN_NAME_BYTES: usize = 128;
8const MAX_PARAMETER_NAME_BYTES: usize = 128;
9const MAX_EXIT_CODE_BYTES: usize = 64;
10
11fn validate_name(value: &str, kind: NameKind, max_bytes: usize) -> Result<(), DomainError> {
12    if value.is_empty() {
13        return Err(DomainError::EmptyName { kind });
14    }
15    if value.len() > max_bytes {
16        return Err(DomainError::NameTooLong { kind, max_bytes });
17    }
18    if value.trim() != value {
19        return Err(DomainError::NameHasSurroundingWhitespace { kind });
20    }
21    if let Some((character_index, _)) = value
22        .chars()
23        .enumerate()
24        .find(|(_, character)| character.is_control())
25    {
26        return Err(DomainError::NameContainsControl {
27            kind,
28            character_index,
29        });
30    }
31    Ok(())
32}
33
34macro_rules! domain_name {
35    ($name:ident, $kind:expr, $max:expr, $docs:literal, $redact_debug:expr) => {
36        #[doc = $docs]
37        #[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
38        pub struct $name(String);
39
40        impl $name {
41            /// Validates and constructs the name.
42            ///
43            /// # Errors
44            ///
45            /// Returns [`DomainError`] when the value is empty, too long, has
46            /// surrounding whitespace, or contains a control character.
47            pub fn new(value: impl Into<String>) -> Result<Self, DomainError> {
48                let value = value.into();
49                validate_name(&value, $kind, $max)?;
50                Ok(Self(value))
51            }
52
53            /// Borrows the validated value.
54            #[must_use]
55            pub fn as_str(&self) -> &str {
56                &self.0
57            }
58
59            /// Returns the validated value.
60            #[must_use]
61            pub fn into_string(self) -> String {
62                self.0
63            }
64        }
65
66        impl fmt::Display for $name {
67            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68                formatter.write_str(&self.0)
69            }
70        }
71
72        impl fmt::Debug for $name {
73            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
74                if $redact_debug {
75                    formatter.write_str(concat!(stringify!($name), "(<redacted>)"))
76                } else {
77                    formatter
78                        .debug_tuple(stringify!($name))
79                        .field(&self.0)
80                        .finish()
81                }
82            }
83        }
84
85        impl TryFrom<String> for $name {
86            type Error = DomainError;
87
88            fn try_from(value: String) -> Result<Self, Self::Error> {
89                Self::new(value)
90            }
91        }
92
93        impl TryFrom<&str> for $name {
94            type Error = DomainError;
95
96            fn try_from(value: &str) -> Result<Self, Self::Error> {
97                Self::new(value)
98            }
99        }
100
101        impl AsRef<str> for $name {
102            fn as_ref(&self) -> &str {
103                self.as_str()
104            }
105        }
106
107        impl Borrow<str> for $name {
108            fn borrow(&self) -> &str {
109                self.as_str()
110            }
111        }
112    };
113}
114
115domain_name!(
116    JobName,
117    NameKind::Job,
118    MAX_DOMAIN_NAME_BYTES,
119    "A validated logical job-definition name.",
120    false
121);
122domain_name!(
123    StepName,
124    NameKind::Step,
125    MAX_DOMAIN_NAME_BYTES,
126    "A validated logical step-definition name.",
127    false
128);
129domain_name!(
130    ParameterName,
131    NameKind::Parameter,
132    MAX_PARAMETER_NAME_BYTES,
133    "A validated job-parameter name.\n\nIts `Debug` representation is redacted because parameter metadata is sensitive by default.",
134    true
135);
136domain_name!(
137    ExitCode,
138    NameKind::ExitCode,
139    MAX_EXIT_CODE_BYTES,
140    "A validated flow- and operator-facing exit code.",
141    false
142);
143
144impl ExitCode {
145    pub(crate) fn framework_owned(value: &'static str) -> Self {
146        Self(String::from(value))
147    }
148}
149
150macro_rules! opaque_id {
151    ($name:ident, $kind:expr, $docs:literal) => {
152        #[doc = $docs]
153        #[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
154        pub struct $name(NonZeroU64);
155
156        impl $name {
157            /// Constructs a nonzero opaque identifier.
158            ///
159            /// # Errors
160            ///
161            /// Returns [`DomainError::ZeroIdentifier`] for zero.
162            pub fn new(value: u64) -> Result<Self, DomainError> {
163                NonZeroU64::new(value)
164                    .map(Self)
165                    .ok_or(DomainError::ZeroIdentifier { kind: $kind })
166            }
167
168            /// Returns the underlying nonzero numeric value.
169            #[must_use]
170            pub const fn get(self) -> u64 {
171                self.0.get()
172            }
173        }
174
175        impl fmt::Display for $name {
176            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
177                self.get().fmt(formatter)
178            }
179        }
180
181        impl fmt::Debug for $name {
182            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
183                formatter
184                    .debug_tuple(stringify!($name))
185                    .field(&self.get())
186                    .finish()
187            }
188        }
189
190        impl TryFrom<u64> for $name {
191            type Error = DomainError;
192
193            fn try_from(value: u64) -> Result<Self, Self::Error> {
194                Self::new(value)
195            }
196        }
197
198        impl From<$name> for u64 {
199            fn from(value: $name) -> Self {
200                value.get()
201            }
202        }
203    };
204}
205
206opaque_id!(
207    JobInstanceId,
208    IdentifierKind::JobInstance,
209    "An opaque identifier for one logical job instance."
210);
211opaque_id!(
212    JobExecutionId,
213    IdentifierKind::JobExecution,
214    "An opaque identifier for one job launch or restart attempt."
215);
216opaque_id!(
217    StepExecutionId,
218    IdentifierKind::StepExecution,
219    "An opaque identifier for one step attempt."
220);
221opaque_id!(
222    RecoveryDecisionId,
223    IdentifierKind::RecoveryDecision,
224    "An opaque identifier for one append-only recovery decision."
225);
226opaque_id!(
227    OperatorRequestId,
228    IdentifierKind::OperatorRequest,
229    "An opaque identifier for one append-only operator request record."
230);
231opaque_id!(
232    RetentionActionId,
233    IdentifierKind::RetentionAction,
234    "An opaque identifier for one append-only retention audit record."
235);
236opaque_id!(
237    StepPartitionId,
238    IdentifierKind::StepPartition,
239    "An opaque identifier for one durable step partition."
240);
241opaque_id!(
242    FailureId,
243    IdentifierKind::Failure,
244    "An opaque identifier used to correlate a redacted failure."
245);