use std::fmt;
use super::{ErrorKind, XmlError, XmlResult};
pub const XML_NAMESPACE_URI: &str = "http://www.w3.org/XML/1998/namespace";
pub const XMLNS_NAMESPACE_URI: &str = "http://www.w3.org/2000/xmlns/";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NamespaceUri(String);
impl NamespaceUri {
pub fn new(uri: impl Into<String>) -> XmlResult<Self> {
let uri = uri.into();
if uri.is_empty() {
return Err(XmlError::new(
ErrorKind::InvalidNamespace,
"namespace URI cannot be empty",
));
}
Ok(Self(uri))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for NamespaceUri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<&str> for NamespaceUri {
type Error = XmlError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<String> for NamespaceUri {
type Error = XmlError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NamespacePrefix(String);
impl NamespacePrefix {
pub fn new(prefix: impl Into<String>) -> XmlResult<Self> {
let prefix = prefix.into();
validate_xml_name(&prefix)?;
if prefix == "xmlns" {
return Err(XmlError::new(
ErrorKind::InvalidNamespace,
"namespace prefix `xmlns` is reserved",
));
}
Ok(Self(prefix))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for NamespacePrefix {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<&str> for NamespacePrefix {
type Error = XmlError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<String> for NamespacePrefix {
type Error = XmlError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct QName {
prefix: Option<NamespacePrefix>,
local: String,
namespace_uri: Option<NamespaceUri>,
}
impl QName {
pub fn new(local: impl Into<String>) -> XmlResult<Self> {
let local = local.into();
validate_xml_name(&local)?;
Ok(Self {
prefix: None,
local,
namespace_uri: None,
})
}
pub fn qualified(
prefix: impl Into<String>,
local: impl Into<String>,
namespace_uri: impl Into<String>,
) -> XmlResult<Self> {
let prefix = prefix.into();
let local = local.into();
let namespace_uri = namespace_uri.into();
validate_namespace_binding(Some(&prefix), &namespace_uri)?;
let prefix = NamespacePrefix::new(prefix)?;
validate_xml_name(&local)?;
let namespace_uri = NamespaceUri::new(namespace_uri)?;
Ok(Self {
prefix: Some(prefix),
local,
namespace_uri: Some(namespace_uri),
})
}
pub fn namespaced(
local: impl Into<String>,
namespace_uri: impl Into<String>,
) -> XmlResult<Self> {
let local = local.into();
let namespace_uri = namespace_uri.into();
validate_namespace_binding(None, &namespace_uri)?;
validate_xml_name(&local)?;
Ok(Self {
prefix: None,
local,
namespace_uri: Some(NamespaceUri::new(namespace_uri)?),
})
}
pub fn prefix(&self) -> Option<&NamespacePrefix> {
self.prefix.as_ref()
}
pub fn local(&self) -> &str {
&self.local
}
pub fn namespace_uri(&self) -> Option<&NamespaceUri> {
self.namespace_uri.as_ref()
}
pub fn lexical_name(&self) -> String {
match &self.prefix {
Some(prefix) => format!("{}:{}", prefix.as_str(), self.local),
None => self.local.clone(),
}
}
}
impl fmt::Display for QName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.lexical_name())
}
}
pub fn validate_xml_name(name: &str) -> XmlResult<()> {
if name.is_empty() {
return Err(XmlError::invalid_name(name, "name cannot be empty"));
}
let mut chars = name.chars();
let first = chars.next().expect("name is not empty");
if !is_name_start_char(first) {
return Err(XmlError::invalid_name(
name,
"name must start with an ASCII letter or underscore",
));
}
if let Some(invalid) = chars.find(|ch| !is_name_char(*ch)) {
return Err(XmlError::invalid_name(
name,
format!("character `{invalid}` is not allowed"),
));
}
Ok(())
}
pub fn validate_namespace_binding(prefix: Option<&str>, uri: &str) -> XmlResult<()> {
match prefix {
Some("xml") if uri == XML_NAMESPACE_URI => Ok(()),
Some("xml") => Err(XmlError::new(
ErrorKind::InvalidNamespace,
"namespace prefix `xml` must be bound to the XML namespace URI",
)),
Some("xmlns") => Err(XmlError::new(
ErrorKind::InvalidNamespace,
"namespace prefix `xmlns` cannot be declared or used as a qualified name prefix",
)),
Some(_) | None if uri == XML_NAMESPACE_URI => Err(XmlError::new(
ErrorKind::InvalidNamespace,
"the XML namespace URI can only be bound to prefix `xml`",
)),
Some(_) | None if uri == XMLNS_NAMESPACE_URI => Err(XmlError::new(
ErrorKind::InvalidNamespace,
"the XMLNS namespace URI cannot be declared explicitly",
)),
_ => Ok(()),
}
}
fn is_name_start_char(ch: char) -> bool {
ch == '_' || ch.is_ascii_alphabetic()
}
fn is_name_char(ch: char) -> bool {
is_name_start_char(ch) || ch.is_ascii_digit() || matches!(ch, '-' | '.')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn qname_new_creates_unqualified_name() {
let name = QName::new("Invoice").expect("valid name");
assert_eq!(name.local(), "Invoice");
assert_eq!(name.prefix(), None);
assert_eq!(name.namespace_uri(), None);
assert_eq!(name.lexical_name(), "Invoice");
}
#[test]
fn qname_qualified_creates_prefixed_name() {
let name = QName::qualified("cbc", "ID", "urn:example:cbc").expect("valid name");
assert_eq!(name.prefix().map(NamespacePrefix::as_str), Some("cbc"));
assert_eq!(name.local(), "ID");
assert_eq!(
name.namespace_uri().map(NamespaceUri::as_str),
Some("urn:example:cbc")
);
assert_eq!(name.lexical_name(), "cbc:ID");
}
#[test]
fn qname_rejects_empty_local_name() {
let error = QName::new("").expect_err("empty local name must fail");
assert_eq!(error.kind(), &ErrorKind::InvalidName);
}
#[test]
fn qname_rejects_empty_prefix() {
let error = QName::qualified("", "ID", "urn:example").expect_err("empty prefix must fail");
assert_eq!(error.kind(), &ErrorKind::InvalidName);
}
#[test]
fn qname_rejects_colon_inside_structural_name_parts() {
let error = QName::new("cbc:ID").expect_err("colon must be structural");
assert_eq!(error.kind(), &ErrorKind::InvalidName);
}
#[test]
fn qname_accepts_reserved_xml_prefix_with_reserved_uri() {
let name = QName::qualified("xml", "lang", XML_NAMESPACE_URI).expect("xml name");
assert_eq!(name.lexical_name(), "xml:lang");
assert_eq!(
name.namespace_uri().map(NamespaceUri::as_str),
Some(XML_NAMESPACE_URI)
);
}
#[test]
fn qname_rejects_reserved_namespace_misuse() {
assert_eq!(
QName::qualified("xml", "lang", "urn:wrong")
.expect_err("xml prefix must use reserved URI")
.kind(),
&ErrorKind::InvalidNamespace
);
assert_eq!(
QName::qualified("doc", "ID", XML_NAMESPACE_URI)
.expect_err("xml URI must only use xml prefix")
.kind(),
&ErrorKind::InvalidNamespace
);
assert_eq!(
QName::qualified("xmlns", "ID", XMLNS_NAMESPACE_URI)
.expect_err("xmlns prefix is reserved")
.kind(),
&ErrorKind::InvalidNamespace
);
assert_eq!(
QName::namespaced("Root", XML_NAMESPACE_URI)
.expect_err("xml URI cannot be default namespace")
.kind(),
&ErrorKind::InvalidNamespace
);
}
#[test]
fn core_validates_basic_xml_names() {
assert!(validate_xml_name("_valid-Name.1").is_ok());
assert!(validate_xml_name("1invalid").is_err());
assert!(validate_xml_name("invalid name").is_err());
}
}