1use std::collections::BTreeMap;
2use std::fmt;
3
4use sha2::{Digest, Sha256};
5
6use super::{DomainError, JobName, ParameterName};
7
8const MAX_PARAMETER_STRING_BYTES: usize = 64 * 1024;
9
10#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12#[non_exhaustive]
13pub enum ParameterValueKind {
14 String,
16 I64,
18 U64,
20 Bool,
22}
23
24impl ParameterValueKind {
25 #[must_use]
30 pub const fn as_str(self) -> &'static str {
31 match self {
32 Self::String => "STRING",
33 Self::I64 => "I64",
34 Self::U64 => "U64",
35 Self::Bool => "BOOL",
36 }
37 }
38}
39
40impl fmt::Display for ParameterValueKind {
41 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42 formatter.write_str(self.as_str())
43 }
44}
45
46#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
52pub struct ParameterValue(ParameterValueInner);
53
54#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
55enum ParameterValueInner {
56 String(String),
57 I64(i64),
58 U64(u64),
59 Bool(bool),
60}
61
62impl ParameterValue {
63 pub fn string(value: impl Into<String>) -> Result<Self, DomainError> {
70 let value = value.into();
71 if value.len() > MAX_PARAMETER_STRING_BYTES {
72 return Err(DomainError::ParameterStringTooLong {
73 max_bytes: MAX_PARAMETER_STRING_BYTES,
74 });
75 }
76 Ok(Self(ParameterValueInner::String(value)))
77 }
78
79 #[must_use]
81 pub const fn kind(&self) -> ParameterValueKind {
82 match self {
83 Self(ParameterValueInner::String(_)) => ParameterValueKind::String,
84 Self(ParameterValueInner::I64(_)) => ParameterValueKind::I64,
85 Self(ParameterValueInner::U64(_)) => ParameterValueKind::U64,
86 Self(ParameterValueInner::Bool(_)) => ParameterValueKind::Bool,
87 }
88 }
89
90 #[must_use]
92 pub fn as_str(&self) -> Option<&str> {
93 match self {
94 Self(ParameterValueInner::String(value)) => Some(value),
95 _ => None,
96 }
97 }
98
99 #[must_use]
101 pub const fn as_i64(&self) -> Option<i64> {
102 match self {
103 Self(ParameterValueInner::I64(value)) => Some(*value),
104 _ => None,
105 }
106 }
107
108 #[must_use]
110 pub const fn as_u64(&self) -> Option<u64> {
111 match self {
112 Self(ParameterValueInner::U64(value)) => Some(*value),
113 _ => None,
114 }
115 }
116
117 #[must_use]
119 pub const fn as_bool(&self) -> Option<bool> {
120 match self {
121 Self(ParameterValueInner::Bool(value)) => Some(*value),
122 _ => None,
123 }
124 }
125}
126
127impl From<i64> for ParameterValue {
128 fn from(value: i64) -> Self {
129 Self(ParameterValueInner::I64(value))
130 }
131}
132
133impl TryFrom<String> for ParameterValue {
134 type Error = DomainError;
135
136 fn try_from(value: String) -> Result<Self, Self::Error> {
137 Self::string(value)
138 }
139}
140
141impl TryFrom<&str> for ParameterValue {
142 type Error = DomainError;
143
144 fn try_from(value: &str) -> Result<Self, Self::Error> {
145 Self::string(value)
146 }
147}
148
149impl From<u64> for ParameterValue {
150 fn from(value: u64) -> Self {
151 Self(ParameterValueInner::U64(value))
152 }
153}
154
155impl From<bool> for ParameterValue {
156 fn from(value: bool) -> Self {
157 Self(ParameterValueInner::Bool(value))
158 }
159}
160
161impl fmt::Debug for ParameterValue {
162 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
163 formatter
164 .debug_tuple(match self.kind() {
165 ParameterValueKind::String => "String",
166 ParameterValueKind::I64 => "I64",
167 ParameterValueKind::U64 => "U64",
168 ParameterValueKind::Bool => "Bool",
169 })
170 .field(&Redacted)
171 .finish()
172 }
173}
174
175impl fmt::Display for ParameterValue {
176 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
177 formatter.write_str("<redacted>")
178 }
179}
180
181struct Redacted;
182
183impl fmt::Debug for Redacted {
184 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
185 formatter.write_str("<redacted>")
186 }
187}
188
189#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
191pub enum ParameterRole {
192 Identifying,
194 NonIdentifying,
196}
197
198#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
200pub struct JobParameter {
201 value: ParameterValue,
202 role: ParameterRole,
203}
204
205impl JobParameter {
206 #[must_use]
208 pub const fn new(value: ParameterValue, role: ParameterRole) -> Self {
209 Self { value, role }
210 }
211
212 #[must_use]
214 pub const fn value(&self) -> &ParameterValue {
215 &self.value
216 }
217
218 #[must_use]
220 pub const fn role(&self) -> ParameterRole {
221 self.role
222 }
223
224 #[must_use]
226 pub const fn is_identifying(&self) -> bool {
227 matches!(self.role, ParameterRole::Identifying)
228 }
229}
230
231impl fmt::Debug for JobParameter {
232 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
233 formatter
234 .debug_struct("JobParameter")
235 .field("kind", &self.value.kind())
236 .field("role", &self.role)
237 .field("value", &Redacted)
238 .finish()
239 }
240}
241
242#[derive(Clone, Default, Eq, PartialEq)]
244pub struct JobParameters {
245 values: BTreeMap<ParameterName, JobParameter>,
246}
247
248impl JobParameters {
249 #[must_use]
251 pub const fn new() -> Self {
252 Self {
253 values: BTreeMap::new(),
254 }
255 }
256
257 pub fn insert(
264 &mut self,
265 name: ParameterName,
266 parameter: JobParameter,
267 ) -> Result<(), DomainError> {
268 if self.values.contains_key(&name) {
269 return Err(DomainError::DuplicateParameter);
270 }
271 self.values.insert(name, parameter);
272 Ok(())
273 }
274
275 pub fn try_from_iter(
282 parameters: impl IntoIterator<Item = (ParameterName, JobParameter)>,
283 ) -> Result<Self, DomainError> {
284 let mut result = Self::new();
285 for (name, parameter) in parameters {
286 result.insert(name, parameter)?;
287 }
288 Ok(result)
289 }
290
291 #[must_use]
293 pub fn get(&self, name: &ParameterName) -> Option<&JobParameter> {
294 self.values.get(name)
295 }
296
297 #[must_use]
299 pub fn iter(&self) -> impl ExactSizeIterator<Item = (&ParameterName, &JobParameter)> {
300 self.values.iter()
301 }
302
303 #[must_use]
305 pub fn len(&self) -> usize {
306 self.values.len()
307 }
308
309 #[must_use]
311 pub fn is_empty(&self) -> bool {
312 self.values.is_empty()
313 }
314
315 #[must_use]
317 pub fn identifying_len(&self) -> usize {
318 self.values
319 .values()
320 .filter(|parameter| parameter.is_identifying())
321 .count()
322 }
323
324 #[must_use]
330 pub fn flow_input_digest(&self) -> [u8; 32] {
331 let mut hash = Sha256::new();
332 hash.update(b"oxide-batch.flow-parameters.v1\0");
333 for (name, parameter) in &self.values {
334 hash_parameter_field(&mut hash, name.as_str().as_bytes());
335 hash.update([match parameter.role() {
336 ParameterRole::Identifying => 1,
337 ParameterRole::NonIdentifying => 0,
338 }]);
339 match ¶meter.value.0 {
340 ParameterValueInner::String(value) => {
341 hash.update([1]);
342 hash_parameter_field(&mut hash, value.as_bytes());
343 }
344 ParameterValueInner::I64(value) => {
345 hash.update([2]);
346 hash.update(value.to_be_bytes());
347 }
348 ParameterValueInner::U64(value) => {
349 hash.update([3]);
350 hash.update(value.to_be_bytes());
351 }
352 ParameterValueInner::Bool(value) => {
353 hash.update([4, u8::from(*value)]);
354 }
355 }
356 }
357 hash.finalize().into()
358 }
359}
360
361fn hash_parameter_field(hash: &mut Sha256, value: &[u8]) {
362 hash.update(u64::try_from(value.len()).unwrap_or(u64::MAX).to_be_bytes());
363 hash.update(value);
364}
365
366impl fmt::Debug for JobParameters {
367 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
368 formatter
369 .debug_struct("JobParameters")
370 .field("parameter_count", &self.len())
371 .field("identifying_count", &self.identifying_len())
372 .finish_non_exhaustive()
373 }
374}
375
376#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
381pub struct JobInstanceKey {
382 job_name: JobName,
383 identifying_parameters: BTreeMap<ParameterName, ParameterValue>,
384}
385
386impl JobInstanceKey {
387 #[must_use]
389 pub fn new(job_name: JobName, parameters: &JobParameters) -> Self {
390 let identifying_parameters = parameters
391 .iter()
392 .filter(|(_, parameter)| parameter.is_identifying())
393 .map(|(name, parameter)| (name.clone(), parameter.value().clone()))
394 .collect();
395
396 Self {
397 job_name,
398 identifying_parameters,
399 }
400 }
401
402 #[must_use]
404 pub const fn job_name(&self) -> &JobName {
405 &self.job_name
406 }
407
408 #[must_use]
410 pub fn identifying_parameter_count(&self) -> usize {
411 self.identifying_parameters.len()
412 }
413
414 #[must_use]
417 pub fn identifying_value(&self, name: &ParameterName) -> Option<&ParameterValue> {
418 self.identifying_parameters.get(name)
419 }
420
421 #[must_use]
424 pub fn identifying_fields(
425 &self,
426 ) -> impl ExactSizeIterator<Item = (&ParameterName, ParameterValueKind)> {
427 self.identifying_parameters
428 .iter()
429 .map(|(name, value)| (name, value.kind()))
430 }
431
432 #[must_use]
440 pub fn digest(&self) -> [u8; 32] {
441 let mut encoded = Vec::new();
442 encoded.push(1);
443 push_length_prefixed(&mut encoded, self.job_name.as_str().as_bytes());
444 for (name, value) in &self.identifying_parameters {
445 push_length_prefixed(&mut encoded, name.as_str().as_bytes());
446 encoded.push(parameter_tag(value.kind()));
447 match &value.0 {
448 ParameterValueInner::String(value) => {
449 push_length_prefixed(&mut encoded, value.as_bytes());
450 }
451 ParameterValueInner::I64(value) => {
452 encoded.extend_from_slice(&value.to_be_bytes());
453 }
454 ParameterValueInner::U64(value) => {
455 encoded.extend_from_slice(&value.to_be_bytes());
456 }
457 ParameterValueInner::Bool(value) => encoded.push(u8::from(*value)),
458 }
459 }
460 Sha256::digest(encoded).into()
461 }
462}
463
464fn push_length_prefixed(target: &mut Vec<u8>, value: &[u8]) {
465 let length = u32::try_from(value.len()).unwrap_or(u32::MAX);
466 target.extend_from_slice(&length.to_be_bytes());
467 target.extend_from_slice(value);
468}
469
470const fn parameter_tag(kind: ParameterValueKind) -> u8 {
471 match kind {
472 ParameterValueKind::String => 1,
473 ParameterValueKind::I64 => 2,
474 ParameterValueKind::U64 => 3,
475 ParameterValueKind::Bool => 4,
476 }
477}
478
479impl fmt::Debug for JobInstanceKey {
480 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
481 formatter
482 .debug_struct("JobInstanceKey")
483 .field("job_name", &self.job_name)
484 .field(
485 "identifying_parameter_count",
486 &self.identifying_parameter_count(),
487 )
488 .finish_non_exhaustive()
489 }
490}