1use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9fn default_true() -> bool {
10 true
11}
12
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
17#[serde(rename_all = "snake_case")]
18pub enum OnBreach {
19 #[default]
23 Fail,
24 Quarantine,
26 Warn,
28}
29
30impl std::fmt::Display for OnBreach {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 f.write_str(match self {
33 OnBreach::Fail => "fail",
34 OnBreach::Quarantine => "quarantine",
35 OnBreach::Warn => "warn",
36 })
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
42#[serde(rename_all = "snake_case")]
43pub enum ContractFieldType {
44 String,
46 Integer,
48 Number,
50 Boolean,
52 Object,
56 Array,
58}
59
60impl std::fmt::Display for ContractFieldType {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.write_str(match self {
63 ContractFieldType::String => "string",
64 ContractFieldType::Integer => "integer",
65 ContractFieldType::Number => "number",
66 ContractFieldType::Boolean => "boolean",
67 ContractFieldType::Object => "object",
68 ContractFieldType::Array => "array",
69 })
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
77#[serde(deny_unknown_fields)]
78pub struct ContractSpec {
79 pub version: String,
85
86 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub description: Option<String>,
89
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub owner: Option<String>,
93
94 #[serde(default)]
98 pub on_breach: OnBreach,
99
100 #[serde(default = "default_true")]
103 pub allow_extra_fields: bool,
104
105 pub fields: Vec<FieldContract>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
111#[serde(deny_unknown_fields)]
112pub struct FieldContract {
113 pub name: String,
116
117 #[serde(rename = "type")]
120 pub field_type: ContractFieldType,
121
122 #[serde(default = "default_true")]
125 pub required: bool,
126
127 #[serde(default)]
130 pub nullable: bool,
131
132 #[serde(rename = "enum", default, skip_serializing_if = "Option::is_none")]
136 pub allowed_values: Option<Vec<Value>>,
137
138 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub pattern: Option<String>,
141
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub min: Option<f64>,
145
146 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub max: Option<f64>,
149
150 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub min_length: Option<usize>,
153
154 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub max_length: Option<usize>,
157
158 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub description: Option<String>,
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use serde_json::json;
167
168 #[test]
169 fn on_breach_defaults_to_fail() {
170 assert_eq!(OnBreach::default(), OnBreach::Fail);
171 }
172
173 #[test]
174 fn on_breach_serializes_snake_case() {
175 assert_eq!(
176 serde_json::to_string(&OnBreach::Quarantine).unwrap(),
177 "\"quarantine\""
178 );
179 let w: OnBreach = serde_json::from_str("\"warn\"").unwrap();
180 assert_eq!(w, OnBreach::Warn);
181 }
182
183 #[test]
184 fn field_type_round_trips() {
185 let t: ContractFieldType = serde_json::from_str("\"integer\"").unwrap();
186 assert_eq!(t, ContractFieldType::Integer);
187 assert_eq!(t.to_string(), "integer");
188 }
189
190 #[test]
191 fn parses_full_contract_block() {
192 let spec: ContractSpec = serde_json::from_value(json!({
193 "version": "1.2.0",
194 "description": "orders output",
195 "owner": "data-platform",
196 "on_breach": "quarantine",
197 "allow_extra_fields": false,
198 "fields": [
199 { "name": "id", "type": "integer", "min": 1 },
200 { "name": "status", "type": "string",
201 "enum": ["open", "closed"], "description": "lifecycle state" },
202 { "name": "email", "type": "string", "pattern": "^.+@.+$",
203 "required": false, "nullable": true,
204 "min_length": 3, "max_length": 254 }
205 ]
206 }))
207 .unwrap();
208 assert_eq!(spec.version, "1.2.0");
209 assert_eq!(spec.on_breach, OnBreach::Quarantine);
210 assert!(!spec.allow_extra_fields);
211 assert_eq!(spec.fields.len(), 3);
212 assert!(spec.fields[0].required, "required defaults to true");
213 assert!(!spec.fields[0].nullable, "nullable defaults to false");
214 assert!(!spec.fields[2].required);
215 assert!(spec.fields[2].nullable);
216 assert_eq!(
217 spec.fields[1].allowed_values.as_ref().unwrap(),
218 &vec![json!("open"), json!("closed")]
219 );
220 }
221
222 #[test]
223 fn rejects_unknown_keys() {
224 let err = serde_json::from_value::<ContractSpec>(json!({
225 "version": "1", "fields": [], "nope": true
226 }));
227 assert!(err.is_err());
228 }
229
230 #[test]
231 fn defaults_when_optional_fields_absent() {
232 let spec: ContractSpec = serde_json::from_value(json!({
233 "version": "1.0.0",
234 "fields": [{ "name": "id", "type": "string" }]
235 }))
236 .unwrap();
237 assert_eq!(spec.on_breach, OnBreach::Fail);
238 assert!(spec.allow_extra_fields);
239 assert!(spec.description.is_none());
240 assert!(spec.owner.is_none());
241 }
242}