Skip to main content

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