1use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5use std::collections::BTreeMap;
6use chrono::{DateTime, Utc};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ComplianceSpec {
11 pub claim: String,
13
14 pub system_hash: String,
16
17 pub constraints: BTreeMap<String, String>,
19
20 pub jurisdiction: String,
22
23 pub version: String,
25
26 pub expiry: DateTime<Utc>,
28
29 #[serde(default)]
31 pub metadata: BTreeMap<String, String>,
32
33 #[serde(default)]
36 pub disclosed_fields: Option<Vec<String>>,
37}
38
39impl ComplianceSpec {
40 #[must_use]
42 pub fn new(
43 claim: String,
44 system_hash: String,
45 constraints: BTreeMap<String, String>,
46 jurisdiction: String,
47 version: String,
48 expiry: DateTime<Utc>,
49 ) -> Self {
50 Self {
51 claim,
52 system_hash,
53 constraints,
54 jurisdiction,
55 version,
56 expiry,
57 metadata: BTreeMap::new(),
58 disclosed_fields: None,
59 }
60 }
61
62 #[must_use]
65 pub fn hash(&self) -> String {
66 let json = serde_json::to_string(self).expect("Failed to serialize spec");
67 let mut hasher = Sha256::new();
68 hasher.update(json.as_bytes());
69 hex::encode(hasher.finalize())
70 }
71
72 #[must_use]
74 pub fn is_expired(&self) -> bool {
75 Utc::now() > self.expiry
76 }
77
78 pub fn validate(&self) -> crate::Result<()> {
80 if self.claim.is_empty() {
81 return Err(crate::VceError::InvalidSpec("Claim cannot be empty".to_string()));
82 }
83
84 if self.system_hash.is_empty() {
85 return Err(crate::VceError::InvalidSpec(
86 "System hash cannot be empty".to_string(),
87 ));
88 }
89
90 if self.is_expired() {
91 return Err(crate::VceError::SpecExpired(
92 self.expiry.to_rfc3339(),
93 ));
94 }
95
96 Ok(())
97 }
98
99 pub fn from_json_file(path: &std::path::Path) -> crate::Result<Self> {
101 let content = std::fs::read_to_string(path)?;
102 let spec: Self = serde_json::from_str(&content)?;
103 spec.validate()?;
104 Ok(spec)
105 }
106
107 pub fn from_yaml_file(path: &std::path::Path) -> crate::Result<Self> {
109 let content = std::fs::read_to_string(path)?;
110 let spec: Self = serde_yaml::from_str(&content)?;
111 spec.validate()?;
112 Ok(spec)
113 }
114
115 pub fn to_json_file(&self, path: &std::path::Path) -> crate::Result<()> {
117 let json = serde_json::to_string_pretty(self)?;
118 std::fs::write(path, json)?;
119 Ok(())
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn test_spec_validation() {
129 let spec = ComplianceSpec::new(
130 "SOC2 control X verified".to_string(),
131 "abc123".to_string(),
132 BTreeMap::new(),
133 "US, SEC".to_string(),
134 "1.0".to_string(),
135 Utc::now() + chrono::Duration::days(365),
136 );
137
138 assert!(spec.validate().is_ok());
139 }
140
141 #[test]
142 fn test_spec_expiry() {
143 let spec = ComplianceSpec::new(
144 "Test claim".to_string(),
145 "abc123".to_string(),
146 BTreeMap::new(),
147 "US".to_string(),
148 "1.0".to_string(),
149 Utc::now() - chrono::Duration::days(1),
150 );
151
152 assert!(spec.is_expired());
153 assert!(spec.validate().is_err());
154 }
155}
156