data_modelling_core/models/odcs/
supporting.rs

1//! Supporting types for ODCS native data structures
2//!
3//! These types are used across ODCSContract, SchemaObject, and Property
4//! to represent shared concepts like quality rules, custom properties, and relationships.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Quality rule for data validation (ODCS v3.1.0)
10///
11/// Quality rules can be defined at contract, schema, or property level.
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
13#[serde(rename_all = "camelCase")]
14pub struct QualityRule {
15    /// Type of quality rule (e.g., "sql", "custom", "library")
16    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
17    pub rule_type: Option<String>,
18    /// Quality dimension (e.g., "accuracy", "completeness", "timeliness")
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub dimension: Option<String>,
21    /// Business impact description
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub business_impact: Option<String>,
24    /// Metric name for the rule
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub metric: Option<String>,
27    /// Description of the quality rule
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub description: Option<String>,
30    /// Condition that must be true
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub must_be: Option<serde_json::Value>,
33    /// Condition that must be false
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub must_not_be: Option<serde_json::Value>,
36    /// Greater than condition
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub must_be_greater_than: Option<serde_json::Value>,
39    /// Less than condition
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub must_be_less_than: Option<serde_json::Value>,
42    /// Greater than or equal condition
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub must_be_greater_than_or_equal: Option<serde_json::Value>,
45    /// Less than or equal condition
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub must_be_less_than_or_equal: Option<serde_json::Value>,
48    /// Value must be in this set
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub must_be_in: Option<Vec<serde_json::Value>>,
51    /// Value must not be in this set
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub must_not_be_in: Option<Vec<serde_json::Value>>,
54    /// SQL query for validation
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub query: Option<String>,
57    /// Scheduler type for quality checks
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub scheduler: Option<String>,
60    /// Schedule expression
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub schedule: Option<String>,
63    /// Engine for running the quality check
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub engine: Option<String>,
66    /// URL to quality tool or dashboard
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub url: Option<String>,
69    /// Additional properties not explicitly modeled
70    #[serde(flatten)]
71    pub extra: HashMap<String, serde_json::Value>,
72}
73
74/// Custom property for format-specific metadata (ODCS v3.1.0)
75///
76/// Used to store metadata that doesn't fit into the standard ODCS fields,
77/// such as Avro-specific or Protobuf-specific attributes.
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
79#[serde(rename_all = "camelCase")]
80pub struct CustomProperty {
81    /// Property name
82    pub property: String,
83    /// Property value (flexible type)
84    pub value: serde_json::Value,
85}
86
87impl CustomProperty {
88    /// Create a new custom property
89    pub fn new(property: impl Into<String>, value: serde_json::Value) -> Self {
90        Self {
91            property: property.into(),
92            value,
93        }
94    }
95
96    /// Create a string custom property
97    pub fn string(property: impl Into<String>, value: impl Into<String>) -> Self {
98        Self {
99            property: property.into(),
100            value: serde_json::Value::String(value.into()),
101        }
102    }
103}
104
105/// Authoritative definition reference (ODCS v3.1.0)
106///
107/// Links to external authoritative sources for definitions.
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
109#[serde(rename_all = "camelCase")]
110pub struct AuthoritativeDefinition {
111    /// Type of the reference (e.g., "businessDefinition", "transformationImplementation")
112    #[serde(rename = "type")]
113    pub definition_type: String,
114    /// URL to the authoritative definition
115    pub url: String,
116}
117
118impl AuthoritativeDefinition {
119    /// Create a new authoritative definition
120    pub fn new(definition_type: impl Into<String>, url: impl Into<String>) -> Self {
121        Self {
122            definition_type: definition_type.into(),
123            url: url.into(),
124        }
125    }
126}
127
128/// Schema-level relationship (ODCS v3.1.0)
129///
130/// Represents relationships between schema objects (tables).
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
132#[serde(rename_all = "camelCase")]
133pub struct SchemaRelationship {
134    /// Relationship type (e.g., "foreignKey", "parent", "child")
135    #[serde(rename = "type")]
136    pub relationship_type: String,
137    /// Source properties (column names) in this schema
138    #[serde(default, skip_serializing_if = "Vec::is_empty")]
139    pub from_properties: Vec<String>,
140    /// Target schema object name
141    pub to_schema: String,
142    /// Target properties (column names) in the target schema
143    #[serde(default, skip_serializing_if = "Vec::is_empty")]
144    pub to_properties: Vec<String>,
145    /// Optional description
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub description: Option<String>,
148}
149
150/// Property-level relationship (ODCS v3.1.0)
151///
152/// Represents relationships from a property to other definitions.
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
154#[serde(rename_all = "camelCase")]
155pub struct PropertyRelationship {
156    /// Relationship type (e.g., "foreignKey", "parent", "child")
157    #[serde(rename = "type")]
158    pub relationship_type: String,
159    /// Target reference (e.g., "definitions/order_id", "schema/id/properties/id")
160    pub to: String,
161}
162
163impl PropertyRelationship {
164    /// Create a new property relationship
165    pub fn new(relationship_type: impl Into<String>, to: impl Into<String>) -> Self {
166        Self {
167            relationship_type: relationship_type.into(),
168            to: to.into(),
169        }
170    }
171}
172
173/// Logical type options for additional type metadata (ODCS v3.1.0)
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
175#[serde(rename_all = "camelCase")]
176pub struct LogicalTypeOptions {
177    /// Minimum length for strings
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub min_length: Option<i64>,
180    /// Maximum length for strings
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub max_length: Option<i64>,
183    /// Regex pattern for strings
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub pattern: Option<String>,
186    /// Format hint (e.g., "email", "uuid", "uri")
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub format: Option<String>,
189    /// Minimum value for numbers/dates
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub minimum: Option<serde_json::Value>,
192    /// Maximum value for numbers/dates
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub maximum: Option<serde_json::Value>,
195    /// Exclusive minimum for numbers
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub exclusive_minimum: Option<serde_json::Value>,
198    /// Exclusive maximum for numbers
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub exclusive_maximum: Option<serde_json::Value>,
201    /// Precision for decimals
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub precision: Option<i32>,
204    /// Scale for decimals
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub scale: Option<i32>,
207}
208
209impl LogicalTypeOptions {
210    /// Check if all options are empty/None
211    pub fn is_empty(&self) -> bool {
212        self.min_length.is_none()
213            && self.max_length.is_none()
214            && self.pattern.is_none()
215            && self.format.is_none()
216            && self.minimum.is_none()
217            && self.maximum.is_none()
218            && self.exclusive_minimum.is_none()
219            && self.exclusive_maximum.is_none()
220            && self.precision.is_none()
221            && self.scale.is_none()
222    }
223}
224
225/// Team information (ODCS v3.1.0)
226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
227#[serde(rename_all = "camelCase")]
228pub struct Team {
229    /// Team name
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub name: Option<String>,
232    /// Team members
233    #[serde(default, skip_serializing_if = "Vec::is_empty")]
234    pub members: Vec<TeamMember>,
235    /// Additional properties
236    #[serde(flatten)]
237    pub extra: HashMap<String, serde_json::Value>,
238}
239
240/// Team member information
241#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
242#[serde(rename_all = "camelCase")]
243pub struct TeamMember {
244    /// Member name
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub name: Option<String>,
247    /// Member email
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub email: Option<String>,
250    /// Member role
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub role: Option<String>,
253    /// Additional properties
254    #[serde(flatten)]
255    pub extra: HashMap<String, serde_json::Value>,
256}
257
258/// Support information (ODCS v3.1.0)
259#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
260#[serde(rename_all = "camelCase")]
261pub struct Support {
262    /// Support channel
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub channel: Option<String>,
265    /// Support URL
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub url: Option<String>,
268    /// Support email
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub email: Option<String>,
271    /// Additional properties
272    #[serde(flatten)]
273    pub extra: HashMap<String, serde_json::Value>,
274}
275
276/// Server configuration (ODCS v3.1.0)
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
278#[serde(rename_all = "camelCase")]
279pub struct Server {
280    /// Server name/identifier
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub server: Option<String>,
283    /// Server type (e.g., "BigQuery", "Snowflake", "S3")
284    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
285    pub server_type: Option<String>,
286    /// Server environment (e.g., "production", "development")
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub environment: Option<String>,
289    /// Server description
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub description: Option<String>,
292    /// Database name
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub database: Option<String>,
295    /// Project name (for cloud platforms)
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub project: Option<String>,
298    /// Schema name
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub schema: Option<String>,
301    /// Catalog name
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub catalog: Option<String>,
304    /// Dataset name (for BigQuery)
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub dataset: Option<String>,
307    /// Account name (for Snowflake)
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub account: Option<String>,
310    /// Host URL
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub host: Option<String>,
313    /// Location/Region
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub location: Option<String>,
316    /// Format for file-based servers
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub format: Option<String>,
319    /// Delimiter for CSV files
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub delimiter: Option<String>,
322    /// Topic name for streaming
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub topic: Option<String>,
325    /// Additional properties
326    #[serde(flatten)]
327    pub extra: HashMap<String, serde_json::Value>,
328}
329
330/// Role definition (ODCS v3.1.0)
331#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
332#[serde(rename_all = "camelCase")]
333pub struct Role {
334    /// Role name
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub role: Option<String>,
337    /// Role description
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub description: Option<String>,
340    /// Principal (user/group)
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub principal: Option<String>,
343    /// Access level
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub access: Option<String>,
346    /// Additional properties
347    #[serde(flatten)]
348    pub extra: HashMap<String, serde_json::Value>,
349}
350
351/// Service level definition (ODCS v3.1.0)
352#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
353#[serde(rename_all = "camelCase")]
354pub struct ServiceLevel {
355    /// Service level property name
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub property: Option<String>,
358    /// Value
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub value: Option<serde_json::Value>,
361    /// Unit of measurement
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub unit: Option<String>,
364    /// Element this applies to
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub element: Option<String>,
367    /// Driver for this SLA
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub driver: Option<String>,
370    /// Description
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub description: Option<String>,
373    /// Scheduler
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub scheduler: Option<String>,
376    /// Schedule expression
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub schedule: Option<String>,
379    /// Additional properties
380    #[serde(flatten)]
381    pub extra: HashMap<String, serde_json::Value>,
382}
383
384/// Price information (ODCS v3.1.0)
385#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
386#[serde(rename_all = "camelCase")]
387pub struct Price {
388    /// Price amount
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub amount: Option<serde_json::Value>,
391    /// Currency
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub currency: Option<String>,
394    /// Billing frequency
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub billing_frequency: Option<String>,
397    /// Price model type
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub price_model: Option<String>,
400    /// Additional properties
401    #[serde(flatten)]
402    pub extra: HashMap<String, serde_json::Value>,
403}
404
405/// Terms and conditions (ODCS v3.1.0)
406#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
407#[serde(rename_all = "camelCase")]
408pub struct Terms {
409    /// Terms description
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub description: Option<String>,
412    /// Usage limitations
413    #[serde(skip_serializing_if = "Option::is_none")]
414    pub limitations: Option<String>,
415    /// URL to full terms
416    #[serde(skip_serializing_if = "Option::is_none")]
417    pub url: Option<String>,
418    /// Additional properties
419    #[serde(flatten)]
420    pub extra: HashMap<String, serde_json::Value>,
421}
422
423/// Link to external resource (ODCS v3.1.0)
424#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
425#[serde(rename_all = "camelCase")]
426pub struct Link {
427    /// Link type
428    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
429    pub link_type: Option<String>,
430    /// URL
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub url: Option<String>,
433    /// Description
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub description: Option<String>,
436    /// Additional properties
437    #[serde(flatten)]
438    pub extra: HashMap<String, serde_json::Value>,
439}
440
441/// Description that can be string or structured object (ODCS v3.1.0)
442#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
443#[serde(untagged)]
444pub enum Description {
445    /// Simple string description
446    Simple(String),
447    /// Structured description object
448    Structured(StructuredDescription),
449}
450
451impl Default for Description {
452    fn default() -> Self {
453        Description::Simple(String::new())
454    }
455}
456
457impl Description {
458    /// Get the description as a simple string
459    pub fn as_string(&self) -> String {
460        match self {
461            Description::Simple(s) => s.clone(),
462            Description::Structured(d) => d.purpose.clone().unwrap_or_default(),
463        }
464    }
465}
466
467/// Structured description with multiple fields
468#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
469#[serde(rename_all = "camelCase")]
470pub struct StructuredDescription {
471    /// Purpose of the data
472    #[serde(skip_serializing_if = "Option::is_none")]
473    pub purpose: Option<String>,
474    /// Limitations of the data
475    #[serde(skip_serializing_if = "Option::is_none")]
476    pub limitations: Option<String>,
477    /// Usage guidelines
478    #[serde(skip_serializing_if = "Option::is_none")]
479    pub usage: Option<String>,
480    /// Additional properties
481    #[serde(flatten)]
482    pub extra: HashMap<String, serde_json::Value>,
483}
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488
489    #[test]
490    fn test_quality_rule_serialization() {
491        let rule = QualityRule {
492            dimension: Some("accuracy".to_string()),
493            must_be: Some(serde_json::json!(true)),
494            ..Default::default()
495        };
496        let json = serde_json::to_string(&rule).unwrap();
497        assert!(json.contains("dimension"));
498        assert!(json.contains("accuracy"));
499    }
500
501    #[test]
502    fn test_custom_property() {
503        let prop = CustomProperty::string("source_format", "avro");
504        assert_eq!(prop.property, "source_format");
505        assert_eq!(prop.value, serde_json::json!("avro"));
506    }
507
508    #[test]
509    fn test_description_variants() {
510        let simple: Description = serde_json::from_str(r#""A simple description""#).unwrap();
511        assert_eq!(simple.as_string(), "A simple description");
512
513        let structured: Description =
514            serde_json::from_str(r#"{"purpose": "Data analysis", "usage": "Read-only"}"#).unwrap();
515        assert_eq!(structured.as_string(), "Data analysis");
516    }
517
518    #[test]
519    fn test_logical_type_options_is_empty() {
520        let empty = LogicalTypeOptions::default();
521        assert!(empty.is_empty());
522
523        let with_length = LogicalTypeOptions {
524            max_length: Some(100),
525            ..Default::default()
526        };
527        assert!(!with_length.is_empty());
528    }
529}