use crate::common::helpers::{has_uri_unsafe_bytes, validate_optional_uri};
use crate::v3_2::spec::Spec;
use crate::validation::Options;
use crate::validation::{Context, PushError, ValidateWithContext};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct XML {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attribute: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub wrapped: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "nodeType")]
pub node_type: Option<String>,
#[serde(flatten)]
#[serde(with = "crate::common::extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
impl ValidateWithContext<Spec> for XML {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
validate_optional_uri(&self.namespace, ctx, format!("{path}.namespace"));
if let Some(ns) = &self.namespace
&& !ns.is_empty()
&& !ctx.is_option(Options::IgnoreInvalidUrls)
&& !has_uri_unsafe_bytes(ns)
{
let mut chars = ns.chars();
let first_ok = chars.next().is_some_and(|c| c.is_ascii_alphabetic());
let scheme_end = ns.find(':');
let scheme_ok = first_ok
&& scheme_end.is_some_and(|i| {
ns[..i]
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
});
if !scheme_ok {
ctx.error(
format!("{path}.namespace"),
format_args!("must be an absolute URI (with `<scheme>:` prefix), found `{ns}`"),
);
}
}
if let Some(nt) = &self.node_type {
const ALLOWED: &[&str] = &["element", "attribute", "text", "cdata", "none"];
if !ALLOWED.contains(&nt.as_str()) {
ctx.error(
format!("{path}.nodeType"),
format_args!(
"must be one of `element`, `attribute`, `text`, `cdata`, `none`, found `{nt}`"
),
);
}
if self.attribute.is_some() {
ctx.error(
path.clone(),
"`attribute` MUST NOT be present when `nodeType` is set",
);
}
if self.wrapped.is_some() {
ctx.error(
path.clone(),
"`wrapped` MUST NOT be present when `nodeType` is set",
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validation::Options;
#[test]
fn serialize() {
assert_eq!(
serde_json::to_string(&XML::default()).unwrap(),
"{}",
"empty object"
);
assert_eq!(
serde_json::to_value(&XML {
name: Some("name".to_owned()),
namespace: Some("https://example.com/schema/sample".to_owned()),
prefix: Some("sample".to_owned()),
attribute: Some(true),
wrapped: Some(true),
node_type: None,
extensions: {
let mut map = BTreeMap::new();
map.insert("x-internal-id".to_owned(), serde_json::Value::Null);
Some(map)
},
})
.unwrap(),
serde_json::json!({
"name": "name",
"namespace": "https://example.com/schema/sample",
"prefix": "sample",
"attribute": true,
"wrapped": true,
"x-internal-id": null,
}),
"all fields"
);
}
#[test]
fn deserialize() {
assert_eq!(
serde_json::from_value::<XML>(serde_json::json!({})).unwrap(),
XML::default(),
"empty object"
);
assert_eq!(
serde_json::from_value::<XML>(serde_json::json!({
"name": "name",
"namespace": "https://example.com/schema/sample",
"prefix": "sample",
"attribute": true,
"wrapped": true,
"x-internal-id": null,
}))
.unwrap(),
XML {
name: Some("name".to_owned()),
namespace: Some("https://example.com/schema/sample".to_owned()),
prefix: Some("sample".to_owned()),
attribute: Some(true),
wrapped: Some(true),
node_type: None,
extensions: {
let mut map = BTreeMap::new();
map.insert("x-internal-id".to_owned(), serde_json::Value::Null);
Some(map)
},
},
"all fields"
);
}
#[test]
fn validate() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Options::new());
XML {
name: Some("name".to_owned()),
namespace: Some("https://example.com/schema/sample".to_owned()),
prefix: Some("sample".to_owned()),
attribute: Some(true),
wrapped: Some(true),
node_type: None,
extensions: None,
}
.validate_with_context(&mut ctx, "xml".to_owned());
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
XML {
namespace: Some("https://example.com/schema/sample".to_owned()),
..Default::default()
}
.validate_with_context(&mut ctx, "xml".to_owned());
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
let mut ctx = Context::new(&spec, Options::new());
XML {
namespace: Some("urn:example:ns:1".to_owned()),
..Default::default()
}
.validate_with_context(&mut ctx, "xml".to_owned());
assert!(ctx.errors.is_empty(), "urn accepted: {:?}", ctx.errors);
for ns in ["not a uri", "tab\there", "ctrl\x01char"] {
let mut ctx = Context::new(&spec, Options::new());
XML {
namespace: Some(ns.to_owned()),
..Default::default()
}
.validate_with_context(&mut ctx, "xml".to_owned());
let valid_uri_errs = ctx
.errors
.iter()
.filter(|e| e.contains("must be a valid URI"))
.count();
let absolute_uri_errs = ctx
.errors
.iter()
.filter(|e| e.contains("must be an absolute URI"))
.count();
assert_eq!(
valid_uri_errs, 1,
"exactly one 'must be a valid URI' for `{ns}`: {:?}",
ctx.errors
);
assert_eq!(
absolute_uri_errs, 0,
"no redundant 'must be an absolute URI' for `{ns}`: {:?}",
ctx.errors
);
}
for rel in ["#/foo", "bar/baz", "/relative/path"] {
let mut ctx = Context::new(&spec, Options::new());
XML {
namespace: Some(rel.to_owned()),
..Default::default()
}
.validate_with_context(&mut ctx, "xml".to_owned());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("must be an absolute URI")),
"relative `{rel}` rejected: {:?}",
ctx.errors
);
}
}
#[test]
fn validate_node_type_valid_values() {
let spec = Spec::default();
for nt in ["element", "attribute", "text", "cdata", "none"] {
let mut ctx = Context::new(&spec, Options::new());
XML {
node_type: Some(nt.to_owned()),
..Default::default()
}
.validate_with_context(&mut ctx, "xml".to_owned());
assert!(
ctx.errors.is_empty(),
"nodeType `{nt}` should be valid: {:?}",
ctx.errors
);
}
}
#[test]
fn validate_node_type_invalid_value() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Options::new());
XML {
node_type: Some("invalid".to_owned()),
..Default::default()
}
.validate_with_context(&mut ctx, "xml".to_owned());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("must be one of") && e.contains("invalid")),
"invalid nodeType should error: {:?}",
ctx.errors
);
}
#[test]
fn validate_node_type_exclusive_with_attribute_and_wrapped() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Options::new());
XML {
node_type: Some("element".to_owned()),
attribute: Some(true),
..Default::default()
}
.validate_with_context(&mut ctx, "xml".to_owned());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("`attribute` MUST NOT be present when `nodeType` is set")),
"attribute + nodeType should error: {:?}",
ctx.errors
);
let mut ctx = Context::new(&spec, Options::new());
XML {
node_type: Some("element".to_owned()),
wrapped: Some(true),
..Default::default()
}
.validate_with_context(&mut ctx, "xml".to_owned());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("`wrapped` MUST NOT be present when `nodeType` is set")),
"wrapped + nodeType should error: {:?}",
ctx.errors
);
}
#[test]
fn serialize_deserialize_node_type() {
let xml = XML {
node_type: Some("text".to_owned()),
..Default::default()
};
let v = serde_json::to_value(&xml).unwrap();
assert_eq!(v["nodeType"], "text");
let back: XML = serde_json::from_value(v).unwrap();
assert_eq!(back, xml);
}
}