use crate::models::resource::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
fn default_schema_format() -> String {
SchemaFormat::JSON.to_string()
}
pub struct SchemaFormat;
impl SchemaFormat {
pub const AVRO: &'static str = "avro";
pub const JSON: &'static str = "json";
pub const XML: &'static str = "xml";
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SchemaDefinition {
#[serde(default = "default_schema_format")]
pub format: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource: Option<ExternalResourceDefinition>,
#[serde(skip_serializing_if = "Option::is_none")]
pub document: Option<Value>,
}
impl Default for SchemaDefinition {
fn default() -> Self {
SchemaDefinition {
format: default_schema_format(),
resource: None,
document: None,
}
}
}
impl SchemaDefinition {
pub fn with_document(format: &str, document: Value) -> Self {
Self {
format: format.to_string(),
resource: None,
document: Some(document),
}
}
pub fn with_resource(format: &str, resource: ExternalResourceDefinition) -> Self {
Self {
format: format.to_string(),
resource: Some(resource),
document: None,
}
}
pub fn validate(&self) -> Result<(), SchemaValidationError> {
match (&self.document, &self.resource) {
(Some(_), Some(_)) => Err(SchemaValidationError::BothSet),
(None, None) => Err(SchemaValidationError::NeitherSet),
_ => Ok(()),
}
}
pub fn is_document(&self) -> bool {
self.document.is_some() && self.resource.is_none()
}
pub fn is_resource(&self) -> bool {
self.resource.is_some() && self.document.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SchemaValidationError {
BothSet,
NeitherSet,
}
impl std::fmt::Display for SchemaValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SchemaValidationError::BothSet => write!(
f,
"Schema 'document' and 'resource' are mutually exclusive; only one can be set"
),
SchemaValidationError::NeitherSet => {
write!(f, "Schema must have either 'document' or 'resource' set")
}
}
}
}
impl std::error::Error for SchemaValidationError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_schema_default_format() {
let schema = SchemaDefinition::default();
assert_eq!(schema.format, "json");
}
#[test]
fn test_schema_with_document() {
let doc = serde_json::json!({"type": "object", "properties": {"key": {"type": "string"}}});
let schema = SchemaDefinition::with_document("json", doc.clone());
assert!(schema.is_document());
assert!(!schema.is_resource());
assert_eq!(schema.document, Some(doc));
}
#[test]
fn test_schema_with_resource() {
let resource = ExternalResourceDefinition {
name: Some("mySchema".to_string()),
endpoint: crate::models::resource::OneOfEndpointDefinitionOrUri::Uri(
"http://example.com/schema".to_string(),
),
};
let schema = SchemaDefinition::with_resource("json", resource);
assert!(schema.is_resource());
assert!(!schema.is_document());
}
#[test]
fn test_schema_validate_document() {
let schema = SchemaDefinition::with_document("json", serde_json::json!({}));
assert!(schema.validate().is_ok());
}
#[test]
fn test_schema_validate_neither() {
let schema = SchemaDefinition::default();
assert!(matches!(
schema.validate(),
Err(SchemaValidationError::NeitherSet)
));
}
#[test]
fn test_schema_deserialize_with_document() {
let json = r#"{
"format": "json",
"document": {"type": "object"}
}"#;
let schema: SchemaDefinition = serde_json::from_str(json).unwrap();
assert_eq!(schema.format, "json");
assert!(schema.document.is_some());
}
#[test]
fn test_schema_deserialize_with_resource() {
let json = r#"{
"format": "avro",
"resource": {
"name": "myAvroSchema",
"endpoint": "http://example.com/schema.avsc"
}
}"#;
let schema: SchemaDefinition = serde_json::from_str(json).unwrap();
assert_eq!(schema.format, "avro");
assert!(schema.resource.is_some());
}
#[test]
fn test_schema_roundtrip() {
let json = r#"{"format": "json", "document": {"type": "object"}}"#;
let schema: SchemaDefinition = serde_json::from_str(json).unwrap();
let serialized = serde_json::to_string(&schema).unwrap();
let deserialized: SchemaDefinition = serde_json::from_str(&serialized).unwrap();
assert_eq!(schema, deserialized);
}
#[test]
fn test_schema_default_format_on_deserialize() {
let json = r#"{"document": {"type": "object"}}"#;
let schema: SchemaDefinition = serde_json::from_str(json).unwrap();
assert_eq!(schema.format, "json");
}
#[test]
fn test_schema_format_with_version() {
let json = r#"{"format": "json:2020-12", "document": {"type": "object"}}"#;
let schema: SchemaDefinition = serde_json::from_str(json).unwrap();
assert_eq!(schema.format, "json:2020-12");
}
#[test]
fn test_schema_avro_format() {
let json = r#"{"format": "avro", "document": {"type": "record", "name": "Test"}}"#;
let schema: SchemaDefinition = serde_json::from_str(json).unwrap();
assert_eq!(schema.format, "avro");
}
#[test]
fn test_schema_validate_both_set() {
let mut schema = SchemaDefinition::with_document("json", serde_json::json!({}));
schema.resource = Some(ExternalResourceDefinition {
name: Some("test".to_string()),
endpoint: crate::models::resource::OneOfEndpointDefinitionOrUri::Uri(
"http://example.com".to_string(),
),
});
assert!(matches!(
schema.validate(),
Err(SchemaValidationError::BothSet)
));
}
#[test]
fn test_schema_with_resource_roundtrip() {
let json = r#"{
"format": "json",
"resource": {
"name": "mySchema",
"endpoint": "http://example.com/schema.json"
}
}"#;
let schema: SchemaDefinition = serde_json::from_str(json).unwrap();
let serialized = serde_json::to_string(&schema).unwrap();
let deserialized: SchemaDefinition = serde_json::from_str(&serialized).unwrap();
assert_eq!(schema, deserialized);
}
#[test]
fn test_schema_validation_error_display() {
let both = SchemaValidationError::BothSet;
assert!(both.to_string().contains("mutually exclusive"));
let neither = SchemaValidationError::NeitherSet;
assert!(neither.to_string().contains("must have either"));
}
}