1use super::schema::SchemaObject;
6use super::supporting::{
7 AuthoritativeDefinition, CustomProperty, Description, Link, Price, QualityRule, Role, Server,
8 ServiceLevel, Support, Team, Terms,
9};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
35#[serde(rename_all = "camelCase")]
36pub struct ODCSContract {
37 pub api_version: String,
40 pub kind: String,
42 pub id: String,
44 pub version: String,
46 pub name: String,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
52 pub status: Option<String>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
57 pub domain: Option<String>,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub data_product: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub tenant: Option<String>,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
68 pub description: Option<Description>,
69
70 #[serde(default)]
73 pub schema: Vec<SchemaObject>,
74
75 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 pub servers: Vec<Server>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub team: Option<Team>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub support: Option<Support>,
85 #[serde(default, skip_serializing_if = "Vec::is_empty")]
87 pub roles: Vec<Role>,
88
89 #[serde(default, skip_serializing_if = "Vec::is_empty")]
92 pub service_levels: Vec<ServiceLevel>,
93 #[serde(default, skip_serializing_if = "Vec::is_empty")]
95 pub quality: Vec<QualityRule>,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
100 pub price: Option<Price>,
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub terms: Option<Terms>,
104
105 #[serde(default, skip_serializing_if = "Vec::is_empty")]
108 pub links: Vec<Link>,
109 #[serde(default, skip_serializing_if = "Vec::is_empty")]
111 pub authoritative_definitions: Vec<AuthoritativeDefinition>,
112
113 #[serde(default, skip_serializing_if = "Vec::is_empty")]
116 pub tags: Vec<String>,
117 #[serde(default, skip_serializing_if = "Vec::is_empty")]
119 pub custom_properties: Vec<CustomProperty>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
124 pub contract_created_ts: Option<String>,
125}
126
127impl Default for ODCSContract {
128 fn default() -> Self {
129 Self {
130 api_version: "v3.1.0".to_string(),
131 kind: "DataContract".to_string(),
132 id: String::new(),
133 version: "1.0.0".to_string(),
134 name: String::new(),
135 status: None,
136 domain: None,
137 data_product: None,
138 tenant: None,
139 description: None,
140 schema: Vec::new(),
141 servers: Vec::new(),
142 team: None,
143 support: None,
144 roles: Vec::new(),
145 service_levels: Vec::new(),
146 quality: Vec::new(),
147 price: None,
148 terms: None,
149 links: Vec::new(),
150 authoritative_definitions: Vec::new(),
151 tags: Vec::new(),
152 custom_properties: Vec::new(),
153 contract_created_ts: None,
154 }
155 }
156}
157
158impl ODCSContract {
159 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
161 Self {
162 name: name.into(),
163 version: version.into(),
164 id: uuid::Uuid::new_v4().to_string(),
165 ..Default::default()
166 }
167 }
168
169 pub fn new_with_id(
171 id: impl Into<String>,
172 name: impl Into<String>,
173 version: impl Into<String>,
174 ) -> Self {
175 Self {
176 id: id.into(),
177 name: name.into(),
178 version: version.into(),
179 ..Default::default()
180 }
181 }
182
183 pub fn with_api_version(mut self, api_version: impl Into<String>) -> Self {
185 self.api_version = api_version.into();
186 self
187 }
188
189 pub fn with_status(mut self, status: impl Into<String>) -> Self {
191 self.status = Some(status.into());
192 self
193 }
194
195 pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
197 self.domain = Some(domain.into());
198 self
199 }
200
201 pub fn with_data_product(mut self, data_product: impl Into<String>) -> Self {
203 self.data_product = Some(data_product.into());
204 self
205 }
206
207 pub fn with_tenant(mut self, tenant: impl Into<String>) -> Self {
209 self.tenant = Some(tenant.into());
210 self
211 }
212
213 pub fn with_description(mut self, description: impl Into<String>) -> Self {
215 self.description = Some(Description::Simple(description.into()));
216 self
217 }
218
219 pub fn with_structured_description(mut self, description: Description) -> Self {
221 self.description = Some(description);
222 self
223 }
224
225 pub fn with_schema(mut self, schema: SchemaObject) -> Self {
227 self.schema.push(schema);
228 self
229 }
230
231 pub fn with_schemas(mut self, schemas: Vec<SchemaObject>) -> Self {
233 self.schema = schemas;
234 self
235 }
236
237 pub fn with_server(mut self, server: Server) -> Self {
239 self.servers.push(server);
240 self
241 }
242
243 pub fn with_team(mut self, team: Team) -> Self {
245 self.team = Some(team);
246 self
247 }
248
249 pub fn with_support(mut self, support: Support) -> Self {
251 self.support = Some(support);
252 self
253 }
254
255 pub fn with_role(mut self, role: Role) -> Self {
257 self.roles.push(role);
258 self
259 }
260
261 pub fn with_service_level(mut self, service_level: ServiceLevel) -> Self {
263 self.service_levels.push(service_level);
264 self
265 }
266
267 pub fn with_quality_rule(mut self, rule: QualityRule) -> Self {
269 self.quality.push(rule);
270 self
271 }
272
273 pub fn with_price(mut self, price: Price) -> Self {
275 self.price = Some(price);
276 self
277 }
278
279 pub fn with_terms(mut self, terms: Terms) -> Self {
281 self.terms = Some(terms);
282 self
283 }
284
285 pub fn with_link(mut self, link: Link) -> Self {
287 self.links.push(link);
288 self
289 }
290
291 pub fn with_authoritative_definition(mut self, definition: AuthoritativeDefinition) -> Self {
293 self.authoritative_definitions.push(definition);
294 self
295 }
296
297 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
299 self.tags.push(tag.into());
300 self
301 }
302
303 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
305 self.tags = tags;
306 self
307 }
308
309 pub fn with_custom_property(mut self, custom_property: CustomProperty) -> Self {
311 self.custom_properties.push(custom_property);
312 self
313 }
314
315 pub fn with_contract_created_ts(mut self, timestamp: impl Into<String>) -> Self {
317 self.contract_created_ts = Some(timestamp.into());
318 self
319 }
320
321 pub fn schema_count(&self) -> usize {
323 self.schema.len()
324 }
325
326 pub fn get_schema(&self, name: &str) -> Option<&SchemaObject> {
328 self.schema.iter().find(|s| s.name == name)
329 }
330
331 pub fn get_schema_mut(&mut self, name: &str) -> Option<&mut SchemaObject> {
333 self.schema.iter_mut().find(|s| s.name == name)
334 }
335
336 pub fn schema_names(&self) -> Vec<&str> {
338 self.schema.iter().map(|s| s.name.as_str()).collect()
339 }
340
341 pub fn is_multi_table(&self) -> bool {
343 self.schema.len() > 1
344 }
345
346 pub fn first_schema(&self) -> Option<&SchemaObject> {
348 self.schema.first()
349 }
350
351 pub fn description_string(&self) -> Option<String> {
353 self.description.as_ref().map(|d| d.as_string())
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360 use crate::models::odcs::Property;
361
362 #[test]
363 fn test_contract_creation() {
364 let contract = ODCSContract::new("my-contract", "1.0.0")
365 .with_domain("retail")
366 .with_status("active");
367
368 assert_eq!(contract.name, "my-contract");
369 assert_eq!(contract.version, "1.0.0");
370 assert_eq!(contract.domain, Some("retail".to_string()));
371 assert_eq!(contract.status, Some("active".to_string()));
372 assert_eq!(contract.api_version, "v3.1.0");
373 assert_eq!(contract.kind, "DataContract");
374 assert!(!contract.id.is_empty()); }
376
377 #[test]
378 fn test_contract_with_schema() {
379 let contract = ODCSContract::new("order-contract", "2.0.0")
380 .with_schema(
381 SchemaObject::new("orders")
382 .with_physical_type("table")
383 .with_properties(vec![
384 Property::new("id", "integer").with_primary_key(true),
385 Property::new("customer_id", "integer"),
386 Property::new("total", "number"),
387 ]),
388 )
389 .with_schema(
390 SchemaObject::new("order_items")
391 .with_physical_type("table")
392 .with_properties(vec![
393 Property::new("id", "integer").with_primary_key(true),
394 Property::new("order_id", "integer"),
395 Property::new("product_id", "integer"),
396 ]),
397 );
398
399 assert_eq!(contract.schema_count(), 2);
400 assert!(contract.is_multi_table());
401 assert_eq!(contract.schema_names(), vec!["orders", "order_items"]);
402
403 let orders = contract.get_schema("orders");
404 assert!(orders.is_some());
405 assert_eq!(orders.unwrap().property_count(), 3);
406 }
407
408 #[test]
409 fn test_contract_serialization() {
410 let contract = ODCSContract::new_with_id(
411 "550e8400-e29b-41d4-a716-446655440000",
412 "test-contract",
413 "1.0.0",
414 )
415 .with_domain("test")
416 .with_status("draft")
417 .with_description("A test contract")
418 .with_tag("test")
419 .with_schema(SchemaObject::new("test_table").with_property(Property::new("id", "string")));
420
421 let json = serde_json::to_string_pretty(&contract).unwrap();
422
423 assert!(json.contains("\"apiVersion\": \"v3.1.0\""));
424 assert!(json.contains("\"kind\": \"DataContract\""));
425 assert!(json.contains("\"id\": \"550e8400-e29b-41d4-a716-446655440000\""));
426 assert!(json.contains("\"name\": \"test-contract\""));
427 assert!(json.contains("\"domain\": \"test\""));
428 assert!(json.contains("\"status\": \"draft\""));
429
430 assert!(json.contains("apiVersion"));
432 assert!(!json.contains("api_version"));
433 }
434
435 #[test]
436 fn test_contract_deserialization() {
437 let json = r#"{
438 "apiVersion": "v3.1.0",
439 "kind": "DataContract",
440 "id": "test-id-123",
441 "version": "2.0.0",
442 "name": "customer-contract",
443 "status": "active",
444 "domain": "customers",
445 "description": "Customer data contract",
446 "schema": [
447 {
448 "name": "customers",
449 "physicalType": "table",
450 "properties": [
451 {
452 "name": "id",
453 "logicalType": "integer",
454 "primaryKey": true
455 },
456 {
457 "name": "name",
458 "logicalType": "string",
459 "required": true
460 }
461 ]
462 }
463 ],
464 "tags": ["customer", "pii"]
465 }"#;
466
467 let contract: ODCSContract = serde_json::from_str(json).unwrap();
468 assert_eq!(contract.api_version, "v3.1.0");
469 assert_eq!(contract.kind, "DataContract");
470 assert_eq!(contract.id, "test-id-123");
471 assert_eq!(contract.version, "2.0.0");
472 assert_eq!(contract.name, "customer-contract");
473 assert_eq!(contract.status, Some("active".to_string()));
474 assert_eq!(contract.domain, Some("customers".to_string()));
475 assert_eq!(contract.schema_count(), 1);
476 assert_eq!(contract.tags, vec!["customer", "pii"]);
477
478 let customers = contract.get_schema("customers").unwrap();
479 assert_eq!(customers.property_count(), 2);
480 }
481
482 #[test]
483 fn test_structured_description() {
484 let json = r#"{
485 "apiVersion": "v3.1.0",
486 "kind": "DataContract",
487 "id": "test",
488 "version": "1.0.0",
489 "name": "test",
490 "description": {
491 "purpose": "Store customer information",
492 "usage": "Read-only access for analytics"
493 }
494 }"#;
495
496 let contract: ODCSContract = serde_json::from_str(json).unwrap();
497 assert_eq!(
498 contract.description_string(),
499 Some("Store customer information".to_string())
500 );
501 }
502}