Skip to main content

data_modelling_core/models/
odps.rs

1//! ODPS (Open Data Product Standard) models
2//!
3//! Defines structures for ODPS Data Products that link to ODCS Tables.
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use std::str::FromStr;
8
9use super::tag::Tag;
10
11/// ODPS API version
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum ODPSApiVersion {
15    #[serde(rename = "v0.9.0")]
16    V0_9_0,
17    #[serde(rename = "v1.0.0")]
18    V1_0_0,
19}
20
21/// ODPS status
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23pub enum ODPSStatus {
24    #[serde(rename = "proposed")]
25    Proposed,
26    #[serde(rename = "draft")]
27    Draft,
28    #[serde(rename = "active")]
29    Active,
30    #[serde(rename = "deprecated")]
31    Deprecated,
32    #[serde(rename = "retired")]
33    Retired,
34}
35
36/// Authoritative definition
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38#[serde(rename_all = "camelCase")]
39pub struct ODPSAuthoritativeDefinition {
40    /// Type of definition
41    pub r#type: String,
42    /// URL to the authority
43    pub url: String,
44    /// Optional description
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub description: Option<String>,
47}
48
49/// Custom property
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
51#[serde(rename_all = "camelCase")]
52pub struct ODPSCustomProperty {
53    /// Property name
54    pub property: String,
55    /// Property value
56    pub value: serde_json::Value,
57    /// Optional description
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub description: Option<String>,
60}
61
62/// ODPS description
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
64#[serde(rename_all = "camelCase")]
65pub struct ODPSDescription {
66    /// Intended purpose
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub purpose: Option<String>,
69    /// Limitations
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub limitations: Option<String>,
72    /// Recommended usage
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub usage: Option<String>,
75    /// Authoritative definitions
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub authoritative_definitions: Option<Vec<ODPSAuthoritativeDefinition>>,
78    /// Custom properties
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub custom_properties: Option<Vec<ODPSCustomProperty>>,
81}
82
83/// Input port
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
85#[serde(rename_all = "camelCase")]
86pub struct ODPSInputPort {
87    /// Port name
88    pub name: String,
89    /// Port version
90    pub version: String,
91    /// Contract ID (links to ODCS Table)
92    pub contract_id: String,
93    /// Tags
94    #[serde(
95        default,
96        skip_serializing_if = "Vec::is_empty",
97        deserialize_with = "deserialize_tags"
98    )]
99    pub tags: Vec<Tag>,
100    /// Custom properties
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub custom_properties: Option<Vec<ODPSCustomProperty>>,
103    /// Authoritative definitions
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub authoritative_definitions: Option<Vec<ODPSAuthoritativeDefinition>>,
106}
107
108/// SBOM (Software Bill of Materials)
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
110#[serde(rename_all = "camelCase")]
111pub struct ODPSSBOM {
112    /// SBOM type
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub r#type: Option<String>,
115    /// URL to SBOM
116    pub url: String,
117}
118
119/// Input contract dependency
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
121#[serde(rename_all = "camelCase")]
122pub struct ODPSInputContract {
123    /// Contract ID
124    pub id: String,
125    /// Contract version
126    pub version: String,
127}
128
129/// Output port
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
131#[serde(rename_all = "camelCase")]
132pub struct ODPSOutputPort {
133    /// Port name
134    pub name: String,
135    /// Port description
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub description: Option<String>,
138    /// Port type
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub r#type: Option<String>,
141    /// Port version
142    pub version: String,
143    /// Contract ID (links to ODCS Table)
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub contract_id: Option<String>,
146    /// SBOM array
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub sbom: Option<Vec<ODPSSBOM>>,
149    /// Input contracts (dependencies)
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub input_contracts: Option<Vec<ODPSInputContract>>,
152    /// Tags
153    #[serde(
154        default,
155        skip_serializing_if = "Vec::is_empty",
156        deserialize_with = "deserialize_tags"
157    )]
158    pub tags: Vec<Tag>,
159    /// Custom properties
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub custom_properties: Option<Vec<ODPSCustomProperty>>,
162    /// Authoritative definitions
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub authoritative_definitions: Option<Vec<ODPSAuthoritativeDefinition>>,
165}
166
167/// Management port
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
169#[serde(rename_all = "camelCase")]
170pub struct ODPSManagementPort {
171    /// Port name
172    pub name: String,
173    /// Content type
174    pub content: String,
175    /// Port type (rest or topic)
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub r#type: Option<String>,
178    /// URL to access endpoint
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub url: Option<String>,
181    /// Channel name
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub channel: Option<String>,
184    /// Description
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub description: Option<String>,
187    /// Tags
188    #[serde(
189        default,
190        skip_serializing_if = "Vec::is_empty",
191        deserialize_with = "deserialize_tags"
192    )]
193    pub tags: Vec<Tag>,
194    /// Custom properties
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub custom_properties: Option<Vec<ODPSCustomProperty>>,
197    /// Authoritative definitions
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub authoritative_definitions: Option<Vec<ODPSAuthoritativeDefinition>>,
200}
201
202/// Support channel
203#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
204#[serde(rename_all = "camelCase")]
205pub struct ODPSSupport {
206    /// Channel name
207    pub channel: String,
208    /// Access URL
209    pub url: String,
210    /// Description
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub description: Option<String>,
213    /// Tool name
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub tool: Option<String>,
216    /// Scope
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub scope: Option<String>,
219    /// Invitation URL
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub invitation_url: Option<String>,
222    /// Tags
223    #[serde(
224        default,
225        skip_serializing_if = "Vec::is_empty",
226        deserialize_with = "deserialize_tags"
227    )]
228    pub tags: Vec<Tag>,
229    /// Custom properties
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub custom_properties: Option<Vec<ODPSCustomProperty>>,
232    /// Authoritative definitions
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub authoritative_definitions: Option<Vec<ODPSAuthoritativeDefinition>>,
235}
236
237/// Team member
238#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
239#[serde(rename_all = "camelCase")]
240pub struct ODPSTeamMember {
241    /// Username or email
242    pub username: String,
243    /// Member name
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub name: Option<String>,
246    /// Description
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub description: Option<String>,
249    /// Role
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub role: Option<String>,
252    /// Date joined
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub date_in: Option<String>,
255    /// Date left
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub date_out: Option<String>,
258    /// Replaced by username
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub replaced_by_username: Option<String>,
261    /// Tags
262    #[serde(
263        default,
264        skip_serializing_if = "Vec::is_empty",
265        deserialize_with = "deserialize_tags"
266    )]
267    pub tags: Vec<Tag>,
268    /// Custom properties
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub custom_properties: Option<Vec<ODPSCustomProperty>>,
271    /// Authoritative definitions
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub authoritative_definitions: Option<Vec<ODPSAuthoritativeDefinition>>,
274}
275
276/// Team
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
278#[serde(rename_all = "camelCase")]
279pub struct ODPSTeam {
280    /// Team name
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub name: Option<String>,
283    /// Team description
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub description: Option<String>,
286    /// Team members
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub members: Option<Vec<ODPSTeamMember>>,
289    /// Tags
290    #[serde(
291        default,
292        skip_serializing_if = "Vec::is_empty",
293        deserialize_with = "deserialize_tags"
294    )]
295    pub tags: Vec<Tag>,
296    /// Custom properties
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub custom_properties: Option<Vec<ODPSCustomProperty>>,
299    /// Authoritative definitions
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub authoritative_definitions: Option<Vec<ODPSAuthoritativeDefinition>>,
302}
303
304/// Data Product - main structure
305#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
306#[serde(rename_all = "camelCase")]
307pub struct ODPSDataProduct {
308    /// API version
309    pub api_version: String,
310    /// Kind (always "DataProduct")
311    pub kind: String,
312    /// Unique identifier
313    pub id: String,
314    /// Product name
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub name: Option<String>,
317    /// Product version
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub version: Option<String>,
320    /// Status
321    pub status: ODPSStatus,
322    /// Business domain
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub domain: Option<String>,
325    /// Tenant/organization
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub tenant: Option<String>,
328    /// Authoritative definitions
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub authoritative_definitions: Option<Vec<ODPSAuthoritativeDefinition>>,
331    /// Description
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub description: Option<ODPSDescription>,
334    /// Custom properties
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub custom_properties: Option<Vec<ODPSCustomProperty>>,
337    /// Tags
338    #[serde(
339        default,
340        skip_serializing_if = "Vec::is_empty",
341        deserialize_with = "deserialize_tags"
342    )]
343    pub tags: Vec<Tag>,
344    /// Input ports
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub input_ports: Option<Vec<ODPSInputPort>>,
347    /// Output ports
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub output_ports: Option<Vec<ODPSOutputPort>>,
350    /// Management ports
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub management_ports: Option<Vec<ODPSManagementPort>>,
353    /// Support channels
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub support: Option<Vec<ODPSSupport>>,
356    /// Team
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub team: Option<ODPSTeam>,
359    /// Product creation timestamp
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub product_created_ts: Option<String>,
362    /// Creation timestamp (internal)
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub created_at: Option<DateTime<Utc>>,
365    /// Last update timestamp (internal)
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub updated_at: Option<DateTime<Utc>>,
368}
369
370/// Deserialize tags with backward compatibility (supports Vec<String> and Vec<Tag>)
371fn deserialize_tags<'de, D>(deserializer: D) -> Result<Vec<Tag>, D::Error>
372where
373    D: serde::Deserializer<'de>,
374{
375    // Accept either Vec<String> (backward compatibility) or Vec<Tag>
376    struct TagVisitor;
377
378    impl<'de> serde::de::Visitor<'de> for TagVisitor {
379        type Value = Vec<Tag>;
380
381        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
382            formatter.write_str("a vector of tags (strings or Tag objects)")
383        }
384
385        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
386        where
387            A: serde::de::SeqAccess<'de>,
388        {
389            let mut tags = Vec::new();
390            while let Some(item) = seq.next_element::<serde_json::Value>()? {
391                match item {
392                    serde_json::Value::String(s) => {
393                        // Backward compatibility: parse string as Tag
394                        if let Ok(tag) = Tag::from_str(&s) {
395                            tags.push(tag);
396                        }
397                    }
398                    _ => {
399                        // Try to deserialize as Tag directly (if it's a string in JSON)
400                        if let serde_json::Value::String(s) = item
401                            && let Ok(tag) = Tag::from_str(&s)
402                        {
403                            tags.push(tag);
404                        }
405                    }
406                }
407            }
408            Ok(tags)
409        }
410    }
411
412    deserializer.deserialize_seq(TagVisitor)
413}