use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct QualityRule {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub rule_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dimension: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub business_impact: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub severity: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metric: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rule: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub must_be: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub must_not_be: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub must_be_greater_than: Option<serde_json::Value>,
#[serde(
rename = "mustBeGreaterOrEqualTo",
alias = "mustBeGreaterThanOrEqual",
alias = "mustBeGreaterThanOrEqualTo",
skip_serializing_if = "Option::is_none"
)]
pub must_be_greater_or_equal_to: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub must_be_less_than: Option<serde_json::Value>,
#[serde(
rename = "mustBeLessOrEqualTo",
alias = "mustBeLessThanOrEqual",
alias = "mustBeLessThanOrEqualTo",
skip_serializing_if = "Option::is_none"
)]
pub must_be_less_or_equal_to: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub must_be_between: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub must_not_be_between: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub must_be_in: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub must_not_be_in: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub query: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub engine: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub implementation: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scheduler: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schedule: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authoritative_definitions: Vec<AuthoritativeDefinition>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub custom_properties: Vec<CustomProperty>,
#[serde(flatten)]
pub extra: IndexMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CustomProperty {
pub property: String,
pub value: serde_json::Value,
}
impl CustomProperty {
pub fn new(property: impl Into<String>, value: serde_json::Value) -> Self {
Self {
property: property.into(),
value,
}
}
pub fn string(property: impl Into<String>, value: impl Into<String>) -> Self {
Self {
property: property.into(),
value: serde_json::Value::String(value.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AuthoritativeDefinition {
#[serde(rename = "type")]
pub definition_type: String,
pub url: String,
}
impl AuthoritativeDefinition {
pub fn new(definition_type: impl Into<String>, url: impl Into<String>) -> Self {
Self {
definition_type: definition_type.into(),
url: url.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SchemaRelationship {
#[serde(rename = "type")]
pub relationship_type: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub from_properties: Vec<String>,
pub to_schema: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub to_properties: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PropertyRelationship {
#[serde(rename = "type")]
pub relationship_type: String,
pub to: String,
}
impl PropertyRelationship {
pub fn new(relationship_type: impl Into<String>, to: impl Into<String>) -> Self {
Self {
relationship_type: relationship_type.into(),
to: to.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct LogicalTypeOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub min_length: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_length: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_minimum: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_maximum: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub precision: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scale: Option<i32>,
}
impl LogicalTypeOptions {
pub fn is_empty(&self) -> bool {
self.min_length.is_none()
&& self.max_length.is_none()
&& self.pattern.is_none()
&& self.format.is_none()
&& self.minimum.is_none()
&& self.maximum.is_none()
&& self.exclusive_minimum.is_none()
&& self.exclusive_maximum.is_none()
&& self.precision.is_none()
&& self.scale.is_none()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Team {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub members: Vec<TeamMember>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TeamMember {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Support {
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Server {
#[serde(skip_serializing_if = "Option::is_none")]
pub server: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub server_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub environment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub database: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub project: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub catalog: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dataset: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub account: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub host: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delimiter: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub topic: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Role {
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub principal: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub access: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ServiceLevel {
#[serde(skip_serializing_if = "Option::is_none")]
pub property: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub element: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub driver: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scheduler: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schedule: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Price {
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub currency: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub billing_frequency: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub price_model: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Terms {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limitations: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Link {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub link_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Description {
Simple(String),
Structured(StructuredDescription),
}
impl Default for Description {
fn default() -> Self {
Description::Simple(String::new())
}
}
impl Description {
pub fn as_string(&self) -> String {
match self {
Description::Simple(s) => s.clone(),
Description::Structured(d) => d.purpose.clone().unwrap_or_default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct StructuredDescription {
#[serde(skip_serializing_if = "Option::is_none")]
pub purpose: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limitations: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quality_rule_serialization() {
let rule = QualityRule {
dimension: Some("accuracy".to_string()),
must_be: Some(serde_json::json!(true)),
..Default::default()
};
let json = serde_json::to_string(&rule).unwrap();
assert!(json.contains("dimension"));
assert!(json.contains("accuracy"));
}
#[test]
fn test_custom_property() {
let prop = CustomProperty::string("source_format", "avro");
assert_eq!(prop.property, "source_format");
assert_eq!(prop.value, serde_json::json!("avro"));
}
#[test]
fn test_description_variants() {
let simple: Description = serde_json::from_str(r#""A simple description""#).unwrap();
assert_eq!(simple.as_string(), "A simple description");
let structured: Description =
serde_json::from_str(r#"{"purpose": "Data analysis", "usage": "Read-only"}"#).unwrap();
assert_eq!(structured.as_string(), "Data analysis");
}
#[test]
fn test_logical_type_options_is_empty() {
let empty = LogicalTypeOptions::default();
assert!(empty.is_empty());
let with_length = LogicalTypeOptions {
max_length: Some(100),
..Default::default()
};
assert!(!with_length.is_empty());
}
}