oxide_batch_core/domain/
identity.rs1use 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 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 #[must_use]
55 pub fn as_str(&self) -> &str {
56 &self.0
57 }
58
59 #[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 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 #[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);