use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use serde_json::Value; #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum XmlNodeKind { Element, Attribute, Text, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct XmlQualifiedName { pub local_name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub namespace_uri: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub prefix: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct XmlSchemaBinding { pub node_kind: XmlNodeKind, pub name: XmlQualifiedName, #[serde(default)] pub repeated: bool, #[serde(default)] pub nillable: bool, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum SchemaKind { Object, Array, String, Integer, Number, Boolean, Enum, #[serde(rename = "null")] Null, Oneof, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Schema { #[serde(rename = "type")] pub kind: SchemaKind, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(default)] pub required: bool, #[serde(default)] pub nullable: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub default_value: Option, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub fields: BTreeMap, #[serde(default, skip_serializing_if = "Option::is_none")] pub items: Option>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub enum_values: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub variants: Vec, } impl Schema { pub fn is_object(&self) -> bool { self.kind == SchemaKind::Object } pub fn field(&self, name: &str) -> Option<&Schema> { self.fields.get(name) } pub fn has_required_fields(&self) -> bool { self.fields.values().any(|field| field.required) } pub fn expected_type_name(&self) -> &'static str { match self.kind { SchemaKind::Object => "object", SchemaKind::Array => "array", SchemaKind::String => "string", SchemaKind::Integer => "integer", SchemaKind::Number => "number", SchemaKind::Boolean => "boolean", SchemaKind::Enum => "enum", SchemaKind::Null => "null", SchemaKind::Oneof => "oneof", } } } #[cfg(test)] mod tests { use std::collections::BTreeMap; use super::{Schema, SchemaKind, XmlNodeKind, XmlQualifiedName, XmlSchemaBinding}; #[test] fn object_schema_exposes_fields() { let mut fields = BTreeMap::new(); fields.insert( "email".to_owned(), Schema { kind: SchemaKind::String, description: None, required: true, nullable: false, default_value: None, fields: BTreeMap::new(), items: None, enum_values: Vec::new(), variants: Vec::new(), }, ); let schema = Schema { kind: SchemaKind::Object, description: Some("User input".to_owned()), required: true, nullable: false, default_value: None, fields, items: None, enum_values: Vec::new(), variants: Vec::new(), }; assert!(schema.is_object()); assert!(schema.has_required_fields()); assert_eq!( schema.field("email").map(|field| field.required), Some(true) ); } #[test] fn schema_serializes_type_field() { let schema = Schema { kind: SchemaKind::Array, description: None, required: false, nullable: false, default_value: None, fields: BTreeMap::new(), items: Some(Box::new(Schema { kind: SchemaKind::String, description: None, required: false, nullable: false, default_value: None, fields: BTreeMap::new(), items: None, enum_values: Vec::new(), variants: Vec::new(), })), enum_values: Vec::new(), variants: Vec::new(), }; let value = serde_json::to_value(schema).unwrap(); assert_eq!(value["type"], "array"); assert_eq!(value["items"]["type"], "string"); } #[test] fn schema_roundtrips_through_yaml() { let schema = Schema { kind: SchemaKind::Object, description: Some("Lead output".to_owned()), required: true, nullable: false, default_value: None, fields: BTreeMap::from([( "id".to_owned(), Schema { kind: SchemaKind::String, description: None, required: true, nullable: false, default_value: None, fields: BTreeMap::new(), items: None, enum_values: Vec::new(), variants: Vec::new(), }, )]), items: None, enum_values: Vec::new(), variants: Vec::new(), }; let yaml = serde_yaml::to_string(&schema).unwrap(); let restored: Schema = serde_yaml::from_str(&yaml).unwrap(); assert!(yaml.contains("type: object")); assert_eq!(restored, schema); } #[test] fn xml_schema_binding_roundtrips() { let binding = XmlSchemaBinding { node_kind: XmlNodeKind::Element, name: XmlQualifiedName { local_name: "CreateLeadRequest".to_owned(), namespace_uri: Some("urn:crm".to_owned()), prefix: Some("crm".to_owned()), }, repeated: false, nillable: true, }; let value = serde_json::to_value(&binding).unwrap(); let restored: XmlSchemaBinding = serde_json::from_value(value.clone()).unwrap(); assert_eq!(value["node_kind"], "element"); assert_eq!(value["name"]["local_name"], "CreateLeadRequest"); assert_eq!(restored, binding); } }