1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use thiserror::Error;
9
10pub const RESULT_SCHEMA_VERSION: u32 = 1;
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18pub struct ResultSchemaV1 {
19 pub ok: bool,
21
22 pub status: String,
24
25 pub latency_ms: u64,
27
28 pub bytes: u64,
30
31 pub capability_digest: String,
33
34 pub commit: String,
36
37 pub layer: String,
39
40 pub name: String,
42
43 pub mode: String,
45
46 pub exp_id: String,
48
49 pub idem_key: String,
51
52 #[serde(default)]
55 pub x_meta: HashMap<String, serde_json::Value>,
56}
57
58impl ResultSchemaV1 {
59 #[allow(clippy::too_many_arguments)]
61 pub fn new(
62 ok: bool,
63 status: String,
64 latency_ms: u64,
65 bytes: u64,
66 capability_digest: String,
67 commit: String,
68 layer: String,
69 name: String,
70 mode: String,
71 exp_id: String,
72 idem_key: String,
73 ) -> Self {
74 Self {
75 ok,
76 status,
77 latency_ms,
78 bytes,
79 capability_digest,
80 commit,
81 layer,
82 name,
83 mode,
84 exp_id,
85 idem_key,
86 x_meta: HashMap::new(),
87 }
88 }
89
90 pub fn with_meta(mut self, key: String, value: serde_json::Value) -> Self {
92 self.x_meta.insert(key, value);
93 self
94 }
95
96 pub fn get_meta(&self, key: &str) -> Option<&serde_json::Value> {
98 self.x_meta.get(key)
99 }
100
101 pub fn validate(&self) -> Result<(), ResultSchemaError> {
103 if self.status.is_empty() {
105 return Err(ResultSchemaError::EmptyRequiredField("status".to_string()));
106 }
107 if self.capability_digest.is_empty() {
108 return Err(ResultSchemaError::EmptyRequiredField(
109 "capability_digest".to_string(),
110 ));
111 }
112 if self.commit.is_empty() {
113 return Err(ResultSchemaError::EmptyRequiredField("commit".to_string()));
114 }
115 if self.layer.is_empty() {
116 return Err(ResultSchemaError::EmptyRequiredField("layer".to_string()));
117 }
118 if self.name.is_empty() {
119 return Err(ResultSchemaError::EmptyRequiredField("name".to_string()));
120 }
121 if self.mode.is_empty() {
122 return Err(ResultSchemaError::EmptyRequiredField("mode".to_string()));
123 }
124 if self.exp_id.is_empty() {
125 return Err(ResultSchemaError::EmptyRequiredField("exp_id".to_string()));
126 }
127 if self.idem_key.is_empty() {
128 return Err(ResultSchemaError::EmptyRequiredField(
129 "idem_key".to_string(),
130 ));
131 }
132
133 if !matches!(self.layer.as_str(), "atom" | "macro" | "playbook") {
135 return Err(ResultSchemaError::InvalidFieldValue {
136 field: "layer".to_string(),
137 value: self.layer.clone(),
138 allowed: vec![
139 "atom".to_string(),
140 "macro".to_string(),
141 "playbook".to_string(),
142 ],
143 });
144 }
145
146 if !matches!(self.mode.as_str(), "strict" | "explore" | "shadow") {
147 return Err(ResultSchemaError::InvalidFieldValue {
148 field: "mode".to_string(),
149 value: self.mode.clone(),
150 allowed: vec![
151 "strict".to_string(),
152 "explore".to_string(),
153 "shadow".to_string(),
154 ],
155 });
156 }
157
158 if self.capability_digest.len() != 64
160 || !self
161 .capability_digest
162 .chars()
163 .all(|c| c.is_ascii_hexdigit())
164 {
165 return Err(ResultSchemaError::InvalidFieldFormat {
166 field: "capability_digest".to_string(),
167 expected: "64-character SHA256 hex string".to_string(),
168 actual: self.capability_digest.clone(),
169 });
170 }
171
172 if self.commit.len() < 7
174 || self.commit.len() > 40
175 || !self.commit.chars().all(|c| c.is_ascii_hexdigit())
176 {
177 return Err(ResultSchemaError::InvalidFieldFormat {
178 field: "commit".to_string(),
179 expected: "7-40 character git commit hex string".to_string(),
180 actual: self.commit.clone(),
181 });
182 }
183
184 if !self.idem_key.starts_with("idem_") || self.idem_key.len() != 21 {
186 return Err(ResultSchemaError::InvalidFieldFormat {
187 field: "idem_key".to_string(),
188 expected: "idem_<16-hex-chars> format".to_string(),
189 actual: self.idem_key.clone(),
190 });
191 }
192
193 Ok(())
194 }
195}
196
197#[derive(Debug, Error)]
199pub enum ResultSchemaError {
200 #[error("Empty required field: {0}")]
201 EmptyRequiredField(String),
202
203 #[error("Invalid value for field {field}: '{value}', allowed: {allowed:?}")]
204 InvalidFieldValue {
205 field: String,
206 value: String,
207 allowed: Vec<String>,
208 },
209
210 #[error("Invalid format for field {field}: expected {expected}, got '{actual}'")]
211 InvalidFieldFormat {
212 field: String,
213 expected: String,
214 actual: String,
215 },
216
217 #[error("Unknown field detected: {field}. Only x_meta extensions are allowed.")]
218 UnknownField { field: String },
219
220 #[error("Schema version mismatch: expected v{expected}, got v{actual}")]
221 VersionMismatch { expected: u32, actual: u32 },
222
223 #[error("Deserialization failed: {0}")]
224 DeserializationFailed(String),
225}
226
227pub struct ResultSchemaValidator;
229
230impl ResultSchemaValidator {
231 pub fn validate_json(json_str: &str) -> Result<ResultSchemaV1, ResultSchemaError> {
236 let json_value: serde_json::Value = serde_json::from_str(json_str)
238 .map_err(|e| ResultSchemaError::DeserializationFailed(e.to_string()))?;
239
240 if let Some(obj) = json_value.as_object() {
241 let allowed_fields = &[
243 "ok",
244 "status",
245 "latency_ms",
246 "bytes",
247 "capability_digest",
248 "commit",
249 "layer",
250 "name",
251 "mode",
252 "exp_id",
253 "idem_key",
254 "x_meta",
255 ];
256
257 for field_name in obj.keys() {
258 if !allowed_fields.contains(&field_name.as_str()) {
259 return Err(ResultSchemaError::UnknownField {
260 field: field_name.clone(),
261 });
262 }
263 }
264 }
265
266 let result: ResultSchemaV1 = serde_json::from_str(json_str)
268 .map_err(|e| ResultSchemaError::DeserializationFailed(e.to_string()))?;
269
270 result.validate()?;
272
273 Ok(result)
274 }
275
276 pub fn validate_struct(result: &ResultSchemaV1) -> Result<(), ResultSchemaError> {
278 result.validate()
279 }
280
281 pub fn check_backward_compatibility(
283 _old_result: &ResultSchemaV1,
284 _new_result: &ResultSchemaV1,
285 ) -> Result<(), ResultSchemaError> {
286 Ok(())
291 }
292
293 pub fn schema_hash() -> String {
295 use sha2::{Digest, Sha256};
296
297 let schema_repr = format!(
299 "RESULT_SCHEMA_V{}_FIELDS:ok:bool,status:string,latency_ms:u64,bytes:u64,capability_digest:string,commit:string,layer:string,name:string,mode:string,exp_id:string,idem_key:string,x_meta:map",
300 RESULT_SCHEMA_VERSION
301 );
302
303 let mut hasher = Sha256::new();
304 hasher.update(schema_repr.as_bytes());
305 format!("{:x}", hasher.finalize())
306 }
307}
308
309pub mod builders {
311 use super::*;
312
313 #[allow(clippy::too_many_arguments)]
315 pub fn success(
316 latency_ms: u64,
317 bytes: u64,
318 capability_digest: String,
319 commit: String,
320 name: String,
321 mode: String,
322 exp_id: String,
323 idem_key: String,
324 ) -> ResultSchemaV1 {
325 ResultSchemaV1::new(
326 true,
327 "success".to_string(),
328 latency_ms,
329 bytes,
330 capability_digest,
331 commit,
332 "atom".to_string(), name,
334 mode,
335 exp_id,
336 idem_key,
337 )
338 }
339
340 #[allow(clippy::too_many_arguments)]
342 pub fn error(
343 error_message: String,
344 latency_ms: u64,
345 capability_digest: String,
346 commit: String,
347 name: String,
348 mode: String,
349 exp_id: String,
350 idem_key: String,
351 ) -> ResultSchemaV1 {
352 ResultSchemaV1::new(
353 false,
354 format!("error: {}", error_message),
355 latency_ms,
356 0, capability_digest,
358 commit,
359 "atom".to_string(),
360 name,
361 mode,
362 exp_id,
363 idem_key,
364 )
365 }
366
367 pub fn timeout(
369 capability_digest: String,
370 commit: String,
371 name: String,
372 mode: String,
373 exp_id: String,
374 idem_key: String,
375 ) -> ResultSchemaV1 {
376 ResultSchemaV1::new(
377 false,
378 "timeout".to_string(),
379 0, 0, capability_digest,
382 commit,
383 "atom".to_string(),
384 name,
385 mode,
386 exp_id,
387 idem_key,
388 )
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395 use serde_json::json;
396
397 fn create_valid_result() -> ResultSchemaV1 {
398 ResultSchemaV1::new(
399 true,
400 "success".to_string(),
401 150,
402 1024,
403 "a".repeat(64), "abc123f".to_string(), "atom".to_string(),
406 "fs.read.v1".to_string(),
407 "strict".to_string(),
408 "exp_123".to_string(),
409 "idem_1234567890abcdef".to_string(),
410 )
411 }
412
413 #[test]
414 fn test_valid_result_creation() {
415 let result = create_valid_result();
416 assert!(result.validate().is_ok());
417 assert!(result.ok);
418 assert_eq!(result.status, "success");
419 assert_eq!(result.latency_ms, 150);
420 assert_eq!(result.bytes, 1024);
421 }
422
423 #[test]
424 fn test_result_with_metadata() {
425 let result = create_valid_result()
426 .with_meta("custom_field".to_string(), json!("custom_value"))
427 .with_meta("debug_info".to_string(), json!({"details": "test"}));
428
429 assert!(result.validate().is_ok());
430 assert_eq!(
431 result.get_meta("custom_field"),
432 Some(&json!("custom_value"))
433 );
434 assert_eq!(
435 result.get_meta("debug_info"),
436 Some(&json!({"details": "test"}))
437 );
438 assert_eq!(result.get_meta("nonexistent"), None);
439 }
440
441 #[test]
442 fn test_validation_empty_fields() {
443 let mut result = create_valid_result();
444 result.status = "".to_string();
445
446 let error = result.validate().unwrap_err();
447 assert!(matches!(error, ResultSchemaError::EmptyRequiredField(_)));
448 }
449
450 #[test]
451 fn test_validation_invalid_layer() {
452 let mut result = create_valid_result();
453 result.layer = "invalid_layer".to_string();
454
455 let error = result.validate().unwrap_err();
456 assert!(matches!(error, ResultSchemaError::InvalidFieldValue { .. }));
457 }
458
459 #[test]
460 fn test_validation_invalid_mode() {
461 let mut result = create_valid_result();
462 result.mode = "invalid_mode".to_string();
463
464 let error = result.validate().unwrap_err();
465 assert!(matches!(error, ResultSchemaError::InvalidFieldValue { .. }));
466 }
467
468 #[test]
469 fn test_validation_invalid_capability_digest() {
470 let mut result = create_valid_result();
471 result.capability_digest = "invalid_digest".to_string();
472
473 let error = result.validate().unwrap_err();
474 assert!(matches!(
475 error,
476 ResultSchemaError::InvalidFieldFormat { .. }
477 ));
478 }
479
480 #[test]
481 fn test_validation_invalid_commit() {
482 let mut result = create_valid_result();
483 result.commit = "x".to_string(); let error = result.validate().unwrap_err();
486 assert!(matches!(
487 error,
488 ResultSchemaError::InvalidFieldFormat { .. }
489 ));
490 }
491
492 #[test]
493 fn test_validation_invalid_idem_key() {
494 let mut result = create_valid_result();
495 result.idem_key = "invalid_key".to_string();
496
497 let error = result.validate().unwrap_err();
498 assert!(matches!(
499 error,
500 ResultSchemaError::InvalidFieldFormat { .. }
501 ));
502 }
503
504 #[test]
505 fn test_json_validation_success() {
506 let valid_json = json!({
507 "ok": true,
508 "status": "success",
509 "latency_ms": 150,
510 "bytes": 1024,
511 "capability_digest": "a".repeat(64),
512 "commit": "abc123f",
513 "layer": "atom",
514 "name": "fs.read.v1",
515 "mode": "strict",
516 "exp_id": "exp_123",
517 "idem_key": "idem_1234567890abcdef",
518 "x_meta": {
519 "custom": "value"
520 }
521 })
522 .to_string();
523
524 let result = ResultSchemaValidator::validate_json(&valid_json).unwrap();
525 assert!(result.ok);
526 assert_eq!(result.get_meta("custom"), Some(&json!("value")));
527 }
528
529 #[test]
530 fn test_json_validation_unknown_field() {
531 let invalid_json = json!({
532 "ok": true,
533 "status": "success",
534 "latency_ms": 150,
535 "bytes": 1024,
536 "capability_digest": "a".repeat(64),
537 "commit": "abc123f",
538 "layer": "atom",
539 "name": "fs.read.v1",
540 "mode": "strict",
541 "exp_id": "exp_123",
542 "idem_key": "idem_1234567890abcdef",
543 "unknown_field": "not allowed" })
545 .to_string();
546
547 let error = ResultSchemaValidator::validate_json(&invalid_json).unwrap_err();
548 assert!(matches!(error, ResultSchemaError::UnknownField { .. }));
549 }
550
551 #[test]
552 fn test_serialization_roundtrip() {
553 let original = create_valid_result().with_meta("test".to_string(), json!("metadata"));
554
555 let json = serde_json::to_string(&original).unwrap();
556 let deserialized = ResultSchemaValidator::validate_json(&json).unwrap();
557
558 assert_eq!(original, deserialized);
559 }
560
561 #[test]
562 fn test_builder_functions() {
563 let success = builders::success(
564 100,
565 512,
566 "a".repeat(64),
567 "abcdef123".to_string(), "test.capability".to_string(),
569 "explore".to_string(),
570 "exp_456".to_string(),
571 "idem_abcdef1234567890".to_string(),
572 );
573 assert!(success.validate().is_ok());
574 assert!(success.ok);
575
576 let error = builders::error(
577 "test error".to_string(),
578 50,
579 "a".repeat(64),
580 "abc123fed".to_string(), "test.capability".to_string(),
582 "strict".to_string(),
583 "exp_789".to_string(),
584 "idem_fedcba0987654321".to_string(),
585 );
586 assert!(error.validate().is_ok());
587 assert!(!error.ok);
588 assert!(error.status.contains("error: test error"));
589 }
590
591 #[test]
592 fn test_schema_hash() {
593 let hash1 = ResultSchemaValidator::schema_hash();
594 let hash2 = ResultSchemaValidator::schema_hash();
595
596 assert_eq!(hash1, hash2);
598
599 assert_eq!(hash1.len(), 64);
601 assert!(hash1.chars().all(|c| c.is_ascii_hexdigit()));
602 }
603
604 #[test]
605 fn test_backward_compatibility_check() {
606 let old_result = create_valid_result();
607 let new_result =
608 create_valid_result().with_meta("new_field".to_string(), json!("new_value"));
609
610 assert!(
612 ResultSchemaValidator::check_backward_compatibility(&old_result, &new_result).is_ok()
613 );
614
615 assert!(
617 ResultSchemaValidator::check_backward_compatibility(&old_result, &old_result).is_ok()
618 );
619 }
620
621 #[test]
622 fn test_result_schema_error_display() {
623 let empty_field_error = ResultSchemaError::EmptyRequiredField("status".to_string());
624 let format_error = ResultSchemaError::InvalidFieldFormat {
625 field: "commit".to_string(),
626 expected: "9-character hex string".to_string(),
627 actual: "abc".to_string(),
628 };
629 let unknown_field_error = ResultSchemaError::UnknownField {
630 field: "unknown".to_string(),
631 };
632 let version_error = ResultSchemaError::VersionMismatch {
633 expected: 1,
634 actual: 2,
635 };
636 let deserialization_error =
637 ResultSchemaError::DeserializationFailed("JSON parse error".to_string());
638
639 assert!(format!("{}", empty_field_error).contains("Empty required field"));
641 assert!(format!("{}", format_error).contains("Invalid format for field commit"));
642 assert!(format!("{}", unknown_field_error).contains("Unknown field detected"));
643 assert!(format!("{}", version_error).contains("Schema version mismatch"));
644 assert!(format!("{}", deserialization_error).contains("Deserialization failed"));
645
646 let debug_str = format!("{:?}", empty_field_error);
648 assert!(debug_str.contains("EmptyRequiredField"));
649 }
650
651 #[test]
652 fn test_json_validation_malformed() {
653 let malformed_json = "{ invalid json";
654 let result = ResultSchemaValidator::validate_json(malformed_json);
655 assert!(result.is_err());
656 }
657
658 #[test]
659 fn test_json_validation_missing_required_fields() {
660 let incomplete_json = json!({
661 "ok": true,
662 })
664 .to_string();
665
666 let result = ResultSchemaValidator::validate_json(&incomplete_json);
667 assert!(result.is_err());
668 }
669
670 #[test]
671 fn test_validation_invalid_commit_formats() {
672 let invalid_commits = vec![
673 "".to_string(), "abc".to_string(), "a".repeat(41), "abcdefGHI".to_string(), "abcdef12@".to_string(), ];
679
680 for commit in invalid_commits {
681 let mut result = create_valid_result();
682 result.commit = commit.clone();
683
684 let error = result.validate();
685 assert!(error.is_err(), "Commit '{}' should be invalid", commit);
686 }
687 }
688
689 #[test]
690 fn test_validation_invalid_capability_digest_formats() {
691 let invalid_digests = vec![
692 "".to_string(), "a".repeat(63), "a".repeat(65), "g".repeat(64), format!("{}@", "a".repeat(63)), ];
698
699 for digest in invalid_digests {
700 let mut result = create_valid_result();
701 result.capability_digest = digest.clone();
702
703 let error = result.validate();
704 assert!(
705 error.is_err(),
706 "Capability digest '{}' should be invalid",
707 digest
708 );
709 }
710 }
711
712 #[test]
713 fn test_validation_invalid_exp_id_formats() {
714 let invalid_exp_ids = vec![
715 "", ];
717
718 for exp_id in invalid_exp_ids {
719 let mut result = create_valid_result();
720 result.exp_id = exp_id.to_string();
721
722 let error = result.validate();
723 assert!(error.is_err(), "Exp ID '{}' should be invalid", exp_id);
724 }
725 }
726
727 #[test]
728 fn test_validation_invalid_idem_key_formats() {
729 let invalid_idem_keys = vec![
730 "", "idem", "idem_", "invalid_123abc", "idem_", "idem_ghi", ];
737
738 for idem_key in invalid_idem_keys {
739 let mut result = create_valid_result();
740 result.idem_key = idem_key.to_string();
741
742 let error = result.validate();
743 assert!(error.is_err(), "Idem key '{}' should be invalid", idem_key);
744 }
745 }
746
747 #[test]
748 fn test_validation_invalid_modes() {
749 let invalid_modes = vec![
750 "", "invalid", "STRICT", "explore_mode", ];
755
756 for mode in invalid_modes {
757 let mut result = create_valid_result();
758 result.mode = mode.to_string();
759
760 let error = result.validate();
761 assert!(error.is_err(), "Mode '{}' should be invalid", mode);
762 }
763 }
764
765 #[test]
766 fn test_validation_invalid_layers() {
767 let invalid_layers = vec![
768 "", "invalid", "ATOM", "atom_layer", ];
773
774 for layer in invalid_layers {
775 let mut result = create_valid_result();
776 result.layer = layer.to_string();
777
778 let error = result.validate();
779 assert!(error.is_err(), "Layer '{}' should be invalid", layer);
780 }
781 }
782
783 #[test]
784 fn test_builder_success_with_zero_values() {
785 let result = builders::success(
786 0, 0, "a".repeat(64),
789 "abc123def".to_string(),
790 "test.capability".to_string(),
791 "strict".to_string(),
792 "exp_test".to_string(),
793 "idem_1234567890abcdef".to_string(),
794 );
795
796 assert!(result.validate().is_ok());
797 assert!(result.ok);
798 assert_eq!(result.latency_ms, 0);
799 assert_eq!(result.bytes, 0);
800 }
801
802 #[test]
803 fn test_builder_error_empty_message() {
804 let result = builders::error(
805 "".to_string(), 100,
807 "a".repeat(64),
808 "abc123def".to_string(),
809 "test.capability".to_string(),
810 "strict".to_string(),
811 "exp_test".to_string(),
812 "idem_1234567890abcdef".to_string(),
813 );
814
815 assert!(result.validate().is_ok()); assert!(!result.ok);
817 assert!(result.status.contains("error:"));
818 }
819
820 #[test]
821 fn test_result_metadata_operations() {
822 let mut result = create_valid_result();
823
824 result = result
826 .with_meta("key1".to_string(), json!("value1"))
827 .with_meta("key2".to_string(), json!({"nested": "object"}))
828 .with_meta("key3".to_string(), json!([1, 2, 3]));
829
830 assert_eq!(result.get_meta("key1"), Some(&json!("value1")));
831 assert_eq!(result.get_meta("key2"), Some(&json!({"nested": "object"})));
832 assert_eq!(result.get_meta("key3"), Some(&json!([1, 2, 3])));
833 assert_eq!(result.get_meta("nonexistent"), None);
834
835 result = result.with_meta("key1".to_string(), json!("new_value"));
837 assert_eq!(result.get_meta("key1"), Some(&json!("new_value")));
838 }
839
840 #[test]
841 fn test_json_round_trip_with_complex_meta() {
842 let original = create_valid_result()
843 .with_meta("string".to_string(), json!("test"))
844 .with_meta("number".to_string(), json!(42))
845 .with_meta("boolean".to_string(), json!(true))
846 .with_meta("null".to_string(), json!(null))
847 .with_meta("array".to_string(), json!([1, "two", 3.0]))
848 .with_meta("object".to_string(), json!({"nested": {"deep": "value"}}));
849
850 let json = serde_json::to_string(&original).unwrap();
851 let deserialized = ResultSchemaValidator::validate_json(&json).unwrap();
852
853 assert_eq!(original, deserialized);
854 assert_eq!(deserialized.get_meta("string"), Some(&json!("test")));
855 assert_eq!(deserialized.get_meta("number"), Some(&json!(42)));
856 assert_eq!(deserialized.get_meta("boolean"), Some(&json!(true)));
857 assert_eq!(deserialized.get_meta("null"), Some(&json!(null)));
858 assert_eq!(
859 deserialized.get_meta("array"),
860 Some(&json!([1, "two", 3.0]))
861 );
862 assert_eq!(
863 deserialized.get_meta("object"),
864 Some(&json!({"nested": {"deep": "value"}}))
865 );
866 }
867
868 #[test]
869 fn test_schema_hash_consistency() {
870 let hashes: Vec<String> = (0..10)
872 .map(|_| ResultSchemaValidator::schema_hash())
873 .collect();
874
875 for hash in &hashes {
877 assert_eq!(hash, &hashes[0]);
878 }
879
880 assert_eq!(hashes[0].len(), 64);
882 assert!(hashes[0].chars().all(|c| c.is_ascii_hexdigit()));
883 }
884}