1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::{
5 DiagnosticCode, MutationSendState, OperationId, OperationName, RetryClass, TargetRef,
6 Timestamp, VerificationStatus,
7};
8
9const MAX_URI_CHARS: usize = 2_048;
10const MAX_MEDIA_TYPE_CHARS: usize = 256;
11const MAX_DIAGNOSTIC_CHARS: usize = 4_096;
12const MAX_REDACTION_LABEL_CHARS: usize = 128;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
16#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
17#[serde(rename_all = "snake_case")]
18#[non_exhaustive]
19pub enum OperationStatus {
20 Succeeded,
22 Failed,
24 Cancelled,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
30#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
31#[serde(rename_all = "snake_case")]
32#[non_exhaustive]
33pub enum DiagnosticSeverity {
34 Info,
36 Warning,
38 Error,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45pub struct Diagnostic {
46 code: DiagnosticCode,
47 severity: DiagnosticSeverity,
48 message: String,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 field: Option<String>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 next_action: Option<String>,
53}
54
55impl Diagnostic {
56 pub fn new(
58 code: impl Into<String>,
59 severity: DiagnosticSeverity,
60 message: impl Into<String>,
61 ) -> Result<Self, ResultError> {
62 let code = code.into();
63 let message = message.into();
64 let code = DiagnosticCode::new(code.clone()).map_err(|_| ResultError::InvalidCode(code))?;
65 validate_text("diagnostic message", &message, MAX_DIAGNOSTIC_CHARS)?;
66 Ok(Self {
67 code,
68 severity,
69 message,
70 field: None,
71 next_action: None,
72 })
73 }
74
75 pub fn with_field(mut self, field: impl Into<String>) -> Result<Self, ResultError> {
77 let field = field.into();
78 validate_code(&field)?;
79 self.field = Some(field);
80 Ok(self)
81 }
82
83 pub fn with_next_action(mut self, next_action: impl Into<String>) -> Result<Self, ResultError> {
85 let next_action = next_action.into();
86 validate_text("diagnostic next action", &next_action, MAX_DIAGNOSTIC_CHARS)?;
87 self.next_action = Some(next_action);
88 Ok(self)
89 }
90
91 #[must_use]
93 pub fn code(&self) -> &str {
94 self.code.as_str()
95 }
96
97 #[must_use]
99 pub fn code_id(&self) -> &DiagnosticCode {
100 &self.code
101 }
102
103 #[must_use]
105 pub const fn severity(&self) -> DiagnosticSeverity {
106 self.severity
107 }
108
109 #[must_use]
111 pub fn message(&self) -> &str {
112 &self.message
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
119pub struct ArtifactRef {
120 uri: String,
121 media_type: String,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
123 sha256: Option<String>,
124 protected: bool,
125}
126
127impl ArtifactRef {
128 pub fn new(
130 uri: impl Into<String>,
131 media_type: impl Into<String>,
132 protected: bool,
133 ) -> Result<Self, ResultError> {
134 let uri = uri.into();
135 let media_type = media_type.into();
136 validate_text("artifact URI", &uri, MAX_URI_CHARS)?;
137 validate_text("artifact media type", &media_type, MAX_MEDIA_TYPE_CHARS)?;
138 Ok(Self {
139 uri,
140 media_type,
141 sha256: None,
142 protected,
143 })
144 }
145
146 pub fn with_sha256(mut self, sha256: impl Into<String>) -> Result<Self, ResultError> {
148 let sha256 = sha256.into();
149 validate_sha256(&sha256)?;
150 self.sha256 = Some(sha256);
151 Ok(self)
152 }
153
154 #[must_use]
156 pub fn uri(&self) -> &str {
157 &self.uri
158 }
159
160 #[must_use]
162 pub const fn protected(&self) -> bool {
163 self.protected
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
169#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
170pub struct EvidenceRef {
171 kind: String,
172 reference: String,
173}
174
175impl EvidenceRef {
176 pub fn new(kind: impl Into<String>, reference: impl Into<String>) -> Result<Self, ResultError> {
178 let kind = kind.into();
179 let reference = reference.into();
180 validate_code(&kind)?;
181 validate_text("evidence reference", &reference, MAX_URI_CHARS)?;
182 Ok(Self { kind, reference })
183 }
184
185 #[must_use]
187 pub fn kind(&self) -> &str {
188 &self.kind
189 }
190
191 #[must_use]
193 pub fn reference(&self) -> &str {
194 &self.reference
195 }
196}
197
198#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
200#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
201pub struct RedactionMetadata {
202 applied: bool,
203 labels: Vec<String>,
204}
205
206impl RedactionMetadata {
207 #[must_use]
209 pub const fn none() -> Self {
210 Self {
211 applied: false,
212 labels: Vec::new(),
213 }
214 }
215
216 pub fn with_label(mut self, label: impl Into<String>) -> Result<Self, ResultError> {
218 let label = label.into();
219 validate_text("redaction label", &label, MAX_REDACTION_LABEL_CHARS)?;
220 self.applied = true;
221 if !self.labels.contains(&label) {
222 self.labels.push(label);
223 self.labels.sort();
224 }
225 Ok(self)
226 }
227
228 #[must_use]
230 pub const fn applied(&self) -> bool {
231 self.applied
232 }
233
234 #[must_use]
236 pub fn labels(&self) -> &[String] {
237 &self.labels
238 }
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
243#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
244pub struct VerificationResult {
245 status: VerificationStatus,
246 completed_at: Timestamp,
247 diagnostics: Vec<Diagnostic>,
248 evidence: Vec<EvidenceRef>,
249}
250
251impl VerificationResult {
252 #[must_use]
254 pub const fn new(status: VerificationStatus, completed_at: Timestamp) -> Self {
255 Self {
256 status,
257 completed_at,
258 diagnostics: Vec::new(),
259 evidence: Vec::new(),
260 }
261 }
262
263 #[must_use]
265 pub fn with_diagnostic(mut self, diagnostic: Diagnostic) -> Self {
266 self.diagnostics.push(diagnostic);
267 self
268 }
269
270 #[must_use]
272 pub fn with_evidence(mut self, evidence: EvidenceRef) -> Self {
273 self.evidence.push(evidence);
274 self
275 }
276
277 #[must_use]
279 pub const fn status(&self) -> VerificationStatus {
280 self.status
281 }
282
283 #[must_use]
285 pub const fn completed_at(&self) -> Timestamp {
286 self.completed_at
287 }
288
289 #[must_use]
291 pub fn evidence(&self) -> &[EvidenceRef] {
292 &self.evidence
293 }
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
298#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
299pub struct ExecutionMetadata {
300 started_at: Timestamp,
301 completed_at: Timestamp,
302 mutation_send_state: MutationSendState,
303 retry: RetryClass,
304}
305
306impl ExecutionMetadata {
307 pub fn new(
309 started_at: Timestamp,
310 completed_at: Timestamp,
311 mutation_send_state: MutationSendState,
312 retry: RetryClass,
313 ) -> Result<Self, ResultError> {
314 if completed_at < started_at {
315 return Err(ResultError::CompletionBeforeStart);
316 }
317 Ok(Self {
318 started_at,
319 completed_at,
320 mutation_send_state,
321 retry,
322 })
323 }
324
325 #[must_use]
327 pub const fn started_at(self) -> Timestamp {
328 self.started_at
329 }
330
331 #[must_use]
333 pub const fn completed_at(self) -> Timestamp {
334 self.completed_at
335 }
336
337 #[must_use]
339 pub const fn mutation_send_state(self) -> MutationSendState {
340 self.mutation_send_state
341 }
342
343 #[must_use]
345 pub const fn retry(self) -> RetryClass {
346 self.retry
347 }
348}
349
350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
352#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
353pub struct OperationResult {
354 operation_id: OperationId,
355 operation: OperationName,
356 target: TargetRef,
357 status: OperationStatus,
358 started_at: Timestamp,
359 completed_at: Timestamp,
360 mutation_send_state: MutationSendState,
361 retry: RetryClass,
362 #[serde(default, skip_serializing_if = "Option::is_none")]
363 output: Option<Value>,
364 artifacts: Vec<ArtifactRef>,
365 diagnostics: Vec<Diagnostic>,
366 evidence: Vec<EvidenceRef>,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
368 verification: Option<VerificationResult>,
369 #[serde(default)]
370 redaction: RedactionMetadata,
371}
372
373impl OperationResult {
374 pub fn new(
376 operation_id: OperationId,
377 operation: OperationName,
378 target: TargetRef,
379 status: OperationStatus,
380 execution: ExecutionMetadata,
381 ) -> Result<Self, ResultError> {
382 if status == OperationStatus::Succeeded && execution.retry() != RetryClass::Never {
383 return Err(ResultError::RetryOnSuccess);
384 }
385 Ok(Self {
386 operation_id,
387 operation,
388 target,
389 status,
390 started_at: execution.started_at(),
391 completed_at: execution.completed_at(),
392 mutation_send_state: execution.mutation_send_state(),
393 retry: execution.retry(),
394 output: None,
395 artifacts: Vec::new(),
396 diagnostics: Vec::new(),
397 evidence: Vec::new(),
398 verification: None,
399 redaction: RedactionMetadata::none(),
400 })
401 }
402
403 pub fn with_output(mut self, output: Value) -> Result<Self, ResultError> {
405 const MAX_INLINE_BYTES: usize = 256 * 1_024;
406 let encoded = serde_json::to_vec(&output)
407 .map_err(|error| ResultError::OutputSerialization(error.to_string()))?;
408 if encoded.len() > MAX_INLINE_BYTES {
409 return Err(ResultError::InlineOutputTooLarge {
410 bytes: encoded.len(),
411 max_bytes: MAX_INLINE_BYTES,
412 });
413 }
414 self.output = Some(output);
415 Ok(self)
416 }
417
418 #[must_use]
420 pub fn with_artifact(mut self, artifact: ArtifactRef) -> Self {
421 self.artifacts.push(artifact);
422 self
423 }
424
425 #[must_use]
427 pub fn with_diagnostic(mut self, diagnostic: Diagnostic) -> Self {
428 self.diagnostics.push(diagnostic);
429 self
430 }
431
432 #[must_use]
434 pub fn with_evidence(mut self, evidence: EvidenceRef) -> Self {
435 self.evidence.push(evidence);
436 self
437 }
438
439 pub fn with_verification(
441 mut self,
442 verification: VerificationResult,
443 ) -> Result<Self, ResultError> {
444 if verification.completed_at() < self.completed_at {
445 return Err(ResultError::VerificationBeforeCompletion);
446 }
447 self.verification = Some(verification);
448 Ok(self)
449 }
450
451 #[must_use]
453 pub fn with_redaction(mut self, redaction: RedactionMetadata) -> Self {
454 self.redaction = redaction;
455 self
456 }
457
458 pub fn validate(&self) -> Result<(), ResultError> {
460 if self.status != OperationStatus::Succeeded
461 && !self
462 .diagnostics
463 .iter()
464 .any(|diagnostic| diagnostic.severity == DiagnosticSeverity::Error)
465 {
466 return Err(ResultError::FailureWithoutErrorDiagnostic);
467 }
468 if self.status != OperationStatus::Succeeded
469 && self
470 .verification
471 .as_ref()
472 .is_some_and(|verification| verification.status() == VerificationStatus::Verified)
473 {
474 return Err(ResultError::FailedOperationVerified);
475 }
476 Ok(())
477 }
478
479 #[must_use]
481 pub fn operation_id(&self) -> &OperationId {
482 &self.operation_id
483 }
484
485 #[must_use]
487 pub fn operation(&self) -> &OperationName {
488 &self.operation
489 }
490
491 #[must_use]
493 pub fn target(&self) -> &TargetRef {
494 &self.target
495 }
496
497 #[must_use]
499 pub const fn status(&self) -> OperationStatus {
500 self.status
501 }
502
503 #[must_use]
505 pub const fn mutation_send_state(&self) -> MutationSendState {
506 self.mutation_send_state
507 }
508
509 #[must_use]
511 pub const fn retry(&self) -> RetryClass {
512 self.retry
513 }
514
515 #[must_use]
517 pub fn output(&self) -> Option<&Value> {
518 self.output.as_ref()
519 }
520
521 #[must_use]
523 pub fn artifacts(&self) -> &[ArtifactRef] {
524 &self.artifacts
525 }
526
527 #[must_use]
529 pub fn diagnostics(&self) -> &[Diagnostic] {
530 &self.diagnostics
531 }
532
533 #[must_use]
535 pub fn evidence(&self) -> &[EvidenceRef] {
536 &self.evidence
537 }
538
539 #[must_use]
541 pub fn verification(&self) -> Option<&VerificationResult> {
542 self.verification.as_ref()
543 }
544
545 #[must_use]
547 pub fn redaction(&self) -> &RedactionMetadata {
548 &self.redaction
549 }
550}
551
552#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
554#[non_exhaustive]
555pub enum ResultError {
556 #[error("operation completion cannot precede start")]
558 CompletionBeforeStart,
559 #[error("successful operations cannot carry retry advice")]
561 RetryOnSuccess,
562 #[error("failed or cancelled operation requires an error diagnostic")]
564 FailureWithoutErrorDiagnostic,
565 #[error("failed or cancelled operation cannot be verified successful")]
567 FailedOperationVerified,
568 #[error("verification cannot complete before operation execution")]
570 VerificationBeforeCompletion,
571 #[error("inline output is {bytes} bytes; maximum is {max_bytes}; use an artifact")]
573 InlineOutputTooLarge {
574 bytes: usize,
576 max_bytes: usize,
578 },
579 #[error("could not serialize inline output: {0}")]
581 OutputSerialization(String),
582 #[error("invalid {field}: expected 1..={max_chars} non-control characters")]
584 InvalidText {
585 field: &'static str,
587 max_chars: usize,
589 },
590 #[error("invalid result code: {0}")]
592 InvalidCode(String),
593 #[error("invalid SHA-256 digest")]
595 InvalidSha256,
596}
597
598fn validate_text(field: &'static str, value: &str, max_chars: usize) -> Result<(), ResultError> {
599 let chars = value.chars().count();
600 if chars == 0 || chars > max_chars || value.chars().any(char::is_control) {
601 return Err(ResultError::InvalidText { field, max_chars });
602 }
603 Ok(())
604}
605
606fn validate_code(value: &str) -> Result<(), ResultError> {
607 let mut chars = value.chars();
608 if !matches!(chars.next(), Some('a'..='z'))
609 || !chars.all(|character| {
610 character.is_ascii_lowercase()
611 || character.is_ascii_digit()
612 || matches!(character, '.' | '_' | '-')
613 })
614 || value.ends_with(['.', '_', '-'])
615 {
616 return Err(ResultError::InvalidCode(value.to_owned()));
617 }
618 Ok(())
619}
620
621fn validate_sha256(value: &str) -> Result<(), ResultError> {
622 if value.len() == 64
623 && value
624 .bytes()
625 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
626 {
627 Ok(())
628 } else {
629 Err(ResultError::InvalidSha256)
630 }
631}
632
633#[cfg(test)]
634#[path = "result_tests.rs"]
635mod tests;