use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaDefinition {
pub schema_id: String,
pub version: u8,
pub fields: IndexMap<String, FieldDefinition>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldDefinition {
#[serde(rename = "type")]
pub field_type: FieldType,
#[serde(default)]
pub required: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fields: Option<IndexMap<String, FieldDefinition>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum FieldType {
#[serde(rename = "string")]
String,
#[serde(rename = "bool")]
Bool,
#[serde(rename = "int")]
Int,
#[serde(rename = "float")]
Float,
#[serde(rename = "[string]")]
StringArray,
#[serde(rename = "[int]")]
IntArray,
#[serde(rename = "table")]
Table,
}
impl SchemaDefinition {
pub fn from_file(path: &std::path::Path) -> Result<Self, crate::error::GermanicError> {
let content = std::fs::read_to_string(path)?;
let schema: Self = serde_json::from_str(&content)?;
Ok(schema)
}
pub fn to_file(&self, path: &std::path::Path) -> Result<(), crate::error::GermanicError> {
let json = serde_json::to_string_pretty(self)?;
std::fs::write(path, json)?;
Ok(())
}
pub fn field_count(&self) -> usize {
self.fields.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_restaurant_schema() -> SchemaDefinition {
let mut fields = IndexMap::new();
fields.insert(
"name".into(),
FieldDefinition {
field_type: FieldType::String,
required: true,
default: None,
fields: None,
},
);
fields.insert(
"cuisine".into(),
FieldDefinition {
field_type: FieldType::String,
required: false,
default: None,
fields: None,
},
);
fields.insert(
"rating".into(),
FieldDefinition {
field_type: FieldType::Float,
required: false,
default: None,
fields: None,
},
);
fields.insert(
"tags".into(),
FieldDefinition {
field_type: FieldType::StringArray,
required: false,
default: None,
fields: None,
},
);
let mut addr_fields = IndexMap::new();
addr_fields.insert(
"street".into(),
FieldDefinition {
field_type: FieldType::String,
required: true,
default: None,
fields: None,
},
);
addr_fields.insert(
"city".into(),
FieldDefinition {
field_type: FieldType::String,
required: true,
default: None,
fields: None,
},
);
addr_fields.insert(
"country".into(),
FieldDefinition {
field_type: FieldType::String,
required: false,
default: Some("DE".into()),
fields: None,
},
);
fields.insert(
"address".into(),
FieldDefinition {
field_type: FieldType::Table,
required: true,
default: None,
fields: Some(addr_fields),
},
);
SchemaDefinition {
schema_id: "de.dining.restaurant.v1".into(),
version: 1,
fields,
}
}
#[test]
fn test_schema_serialize_roundtrip() {
let schema = sample_restaurant_schema();
let json = serde_json::to_string_pretty(&schema).unwrap();
let parsed: SchemaDefinition = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.schema_id, "de.dining.restaurant.v1");
assert_eq!(parsed.fields.len(), 5);
let keys: Vec<&String> = parsed.fields.keys().collect();
assert_eq!(keys, &["name", "cuisine", "rating", "tags", "address"]);
}
#[test]
fn test_field_type_serde() {
let json = r#"{"type": "string", "required": true}"#;
let field: FieldDefinition = serde_json::from_str(json).unwrap();
assert_eq!(field.field_type, FieldType::String);
assert!(field.required);
let json = r#"{"type": "[string]"}"#;
let field: FieldDefinition = serde_json::from_str(json).unwrap();
assert_eq!(field.field_type, FieldType::StringArray);
}
#[test]
fn test_nested_table_fields() {
let schema = sample_restaurant_schema();
let addr = &schema.fields["address"];
assert_eq!(addr.field_type, FieldType::Table);
let nested = addr.fields.as_ref().unwrap();
assert_eq!(nested.len(), 3);
assert!(nested["street"].required);
}
}