227 lines
6.5 KiB
Rust
227 lines
6.5 KiB
Rust
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<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub prefix: Option<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
#[serde(default)]
|
|
pub required: bool,
|
|
#[serde(default)]
|
|
pub nullable: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub default_value: Option<Value>,
|
|
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
|
pub fields: BTreeMap<String, Schema>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub items: Option<Box<Schema>>,
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub enum_values: Vec<String>,
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub variants: Vec<Schema>,
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|