1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4use crate::common::{ContractStatus, Severity, Stability};
5use crate::version::SchemaVersion;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Contract {
15 pub schema_version: SchemaVersion,
16 pub id: String,
18 pub title: String,
19 #[serde(default)]
21 pub description: String,
22 pub status: ContractStatus,
23 pub stability: Stability,
24 pub scope: String,
26 #[serde(default)]
28 pub capabilities: Vec<String>,
29 #[serde(default)]
31 pub invariants: Vec<Invariant>,
32 #[serde(default)]
34 pub required_semantics: Vec<Semantic>,
35 #[serde(default)]
37 pub forbidden_semantics: Vec<Semantic>,
38 #[serde(default)]
40 pub edge_cases: Vec<EdgeCase>,
41 #[serde(default)]
43 pub examples: Vec<Example>,
44 #[serde(default)]
46 pub non_goals: Vec<String>,
47 #[serde(default)]
49 pub implementation_notes: Vec<String>,
50 #[serde(default)]
52 pub test_expectations: Vec<String>,
53 #[serde(default)]
55 pub expected_api: Vec<String>,
56 #[serde(default)]
58 pub history: Vec<HistoryEntry>,
59 pub created_at: DateTime<Utc>,
60 pub updated_at: DateTime<Utc>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct Invariant {
66 pub id: String,
68 pub description: String,
69 #[serde(default)]
70 pub severity: Severity,
71 #[serde(default)]
73 pub test_tags: Vec<String>,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct Semantic {
79 pub id: String,
81 pub description: String,
82 #[serde(default)]
84 pub test_tags: Vec<String>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct EdgeCase {
90 pub id: String,
91 pub scenario: String,
92 pub expected_behavior: String,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct Example {
98 pub title: String,
99 pub description: String,
100 #[serde(default)]
101 pub code: Option<String>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct HistoryEntry {
107 pub version: String,
108 pub date: DateTime<Utc>,
109 pub description: String,
110 pub author: String,
111}
112
113impl Contract {
114 pub fn new_draft(id: String, title: String, scope: String) -> Self {
116 let now = Utc::now();
117 Self {
118 schema_version: SchemaVersion::CURRENT,
119 id,
120 title,
121 description: String::new(),
122 status: ContractStatus::Draft,
123 stability: Stability::Experimental,
124 scope,
125 capabilities: Vec::new(),
126 invariants: Vec::new(),
127 required_semantics: Vec::new(),
128 forbidden_semantics: Vec::new(),
129 edge_cases: Vec::new(),
130 examples: Vec::new(),
131 non_goals: Vec::new(),
132 implementation_notes: Vec::new(),
133 test_expectations: Vec::new(),
134 expected_api: Vec::new(),
135 history: Vec::new(),
136 created_at: now,
137 updated_at: now,
138 }
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_contract_toml_roundtrip() {
148 let mut contract = Contract::new_draft(
149 "key-value-store".to_string(),
150 "Key-Value Store Contract".to_string(),
151 "Defines the behavior of a basic key-value store".to_string(),
152 );
153 contract.capabilities.push("get/set/delete operations".to_string());
154 contract.invariants.push(Invariant {
155 id: "inv-001".to_string(),
156 description: "A key set with a value must return that value on get".to_string(),
157 severity: Severity::Required,
158 test_tags: vec!["conformance".to_string()],
159 });
160 contract.required_semantics.push(Semantic {
161 id: "req-001".to_string(),
162 description: "get(key) returns None for missing keys".to_string(),
163 test_tags: vec!["conformance".to_string(), "basic".to_string()],
164 });
165 contract.forbidden_semantics.push(Semantic {
166 id: "forbid-001".to_string(),
167 description: "Must not panic on missing key lookup".to_string(),
168 test_tags: vec!["safety".to_string()],
169 });
170
171 let toml_str = toml::to_string_pretty(&contract).unwrap();
172 let parsed: Contract = toml::from_str(&toml_str).unwrap();
173 assert_eq!(parsed.id, "key-value-store");
174 assert_eq!(parsed.invariants.len(), 1);
175 assert_eq!(parsed.required_semantics.len(), 1);
176 assert_eq!(parsed.forbidden_semantics.len(), 1);
177 }
178
179 #[test]
180 fn test_contract_defaults() {
181 let c = Contract::new_draft(
182 "test".to_string(),
183 "Test".to_string(),
184 "scope".to_string(),
185 );
186 assert_eq!(c.status, ContractStatus::Draft);
187 assert_eq!(c.stability, Stability::Experimental);
188 assert!(c.invariants.is_empty());
189 }
190}