use crate::common::helpers::{
Context, PushError, ValidateWithContext, has_uri_unsafe_bytes, validate_optional_uri,
};
use crate::v3_1::spec::Spec;
use crate::validation::Options;
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(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}`"),
);
}
}
}
}
#[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),
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),
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),
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);
let mut ctx = Context::new(&spec, Options::new());
XML {
namespace: Some("not a uri".to_owned()),
..Default::default()
}
.validate_with_context(&mut ctx, "xml".to_owned());
assert!(
ctx.errors.iter().any(|e| e.contains("must be a valid URI")),
"invalid URI: {:?}",
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
);
}
}
}