use crate::checker::types::TypeId;
use crate::diagnostics::{DiagnosticDefinition, DiagnosticMap};
use crate::state_accessors::StateAccessors;
use std::collections::HashMap;
pub const DIAG_NS_ENUM_NOT_DECLARATION: &str = "ns-enum-not-declaration";
pub const DIAG_INVALID_NS_DECLARATION_MEMBER: &str = "invalid-ns-declaration-member";
pub const DIAG_NS_MISSING_PREFIX: &str = "ns-missing-prefix";
pub const DIAG_PREFIX_NOT_ALLOWED: &str = "prefix-not-allowed";
pub const DIAG_NS_NOT_URI: &str = "ns-not-uri";
pub const STATE_ATTRIBUTE: &str = "TypeSpec.Xml.attribute";
pub const STATE_UNWRAPPED: &str = "TypeSpec.Xml.unwrapped";
pub const STATE_NS: &str = "TypeSpec.Xml.ns";
pub const STATE_NS_DECLARATION: &str = "TypeSpec.Xml.nsDeclaration";
pub const XML_NAMESPACE: &str = "TypeSpec.Xml";
#[derive(Debug, Clone, PartialEq)]
pub struct XmlNamespace {
pub namespace: String,
pub prefix: String,
}
impl XmlNamespace {
pub fn new(namespace: &str, prefix: &str) -> Self {
Self {
namespace: namespace.to_string(),
prefix: prefix.to_string(),
}
}
pub fn is_valid_uri(&self) -> bool {
self.namespace.contains(':') && self.namespace.len() > 3
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum XmlEncoding {
XmlDateTime,
XmlDate,
XmlTime,
XmlDuration,
XmlBase64Binary,
}
impl XmlEncoding {
pub fn as_typespec_name(&self) -> &'static str {
match self {
XmlEncoding::XmlDateTime => "TypeSpec.Xml.Encoding.xmlDateTime",
XmlEncoding::XmlDate => "TypeSpec.Xml.Encoding.xmlDate",
XmlEncoding::XmlTime => "TypeSpec.Xml.Encoding.xmlTime",
XmlEncoding::XmlDuration => "TypeSpec.Xml.Encoding.xmlDuration",
XmlEncoding::XmlBase64Binary => "TypeSpec.Xml.Encoding.xmlBase64Binary",
}
}
pub fn default_scalar_name(&self) -> &'static str {
match self {
XmlEncoding::XmlDateTime => "utcDateTime", XmlEncoding::XmlDate => "plainDate",
XmlEncoding::XmlTime => "plainTime",
XmlEncoding::XmlDuration => "duration",
XmlEncoding::XmlBase64Binary => "bytes",
}
}
pub fn xs_type_name(&self) -> &'static str {
match self {
XmlEncoding::XmlDateTime => "xs:dateTime",
XmlEncoding::XmlDate => "xs:date",
XmlEncoding::XmlTime => "xs:time",
XmlEncoding::XmlDuration => "xs:duration",
XmlEncoding::XmlBase64Binary => "xs:base64Binary",
}
}
pub fn from_typespec_name(name: &str) -> Option<XmlEncoding> {
match name {
"TypeSpec.Xml.Encoding.xmlDateTime" => Some(XmlEncoding::XmlDateTime),
"TypeSpec.Xml.Encoding.xmlDate" => Some(XmlEncoding::XmlDate),
"TypeSpec.Xml.Encoding.xmlTime" => Some(XmlEncoding::XmlTime),
"TypeSpec.Xml.Encoding.xmlDuration" => Some(XmlEncoding::XmlDuration),
"TypeSpec.Xml.Encoding.xmlBase64Binary" => Some(XmlEncoding::XmlBase64Binary),
_ => None,
}
}
}
pub fn get_default_xml_encoding(scalar_name: &str) -> Option<XmlEncoding> {
match scalar_name {
"utcDateTime" | "offsetDateTime" => Some(XmlEncoding::XmlDateTime),
"plainDate" => Some(XmlEncoding::XmlDate),
"plainTime" => Some(XmlEncoding::XmlTime),
"duration" => Some(XmlEncoding::XmlDuration),
"bytes" => Some(XmlEncoding::XmlBase64Binary),
_ => None,
}
}
pub fn get_xml_encoding(
encode_value: Option<&str>,
scalar_name: &str,
encode_type_name: Option<&str>,
) -> Option<XmlEncodeData> {
if let Some(encoding) = encode_value {
if let Some(xml_enc) = XmlEncoding::from_typespec_name(encoding) {
return Some(XmlEncodeData {
encoding: Some(xml_enc),
type_name: encode_type_name.unwrap_or("string").to_string(),
});
}
}
let default = get_default_xml_encoding(scalar_name)?;
Some(XmlEncodeData {
encoding: Some(default),
type_name: "string".to_string(),
})
}
#[derive(Debug, Clone)]
pub struct XmlEncodeData {
pub encoding: Option<XmlEncoding>,
pub type_name: String,
}
flag_decorator!(apply_attribute, is_attribute, STATE_ATTRIBUTE);
flag_decorator!(apply_unwrapped, is_unwrapped, STATE_UNWRAPPED);
flag_decorator!(
apply_ns_declarations,
is_ns_declarations,
STATE_NS_DECLARATION
);
pub fn apply_ns(
state: &mut StateAccessors,
target: TypeId,
namespace: &XmlNamespace,
) -> Result<(), &'static str> {
if !validate_namespace_uri(&namespace.namespace) {
return Err(DIAG_NS_NOT_URI);
}
state.set_state(
STATE_NS,
target,
format!("{}|{}", namespace.namespace, namespace.prefix),
);
Ok(())
}
pub fn get_ns(state: &StateAccessors, target: TypeId) -> Option<XmlNamespace> {
state.get_state(STATE_NS, target).and_then(|s| {
let parts: Vec<&str> = s.splitn(2, '|').collect();
if parts.len() == 2 {
Some(XmlNamespace::new(parts[0], parts[1]))
} else {
None
}
})
}
pub fn validate_namespace_uri(namespace: &str) -> bool {
namespace.contains("://") || namespace.starts_with("urn:")
}
pub fn create_xml_library() -> DiagnosticMap {
HashMap::from([
(
DIAG_NS_ENUM_NOT_DECLARATION.to_string(),
DiagnosticDefinition::error(
"Enum member used as namespace must be part of an enum marked with @nsDeclaration.",
),
),
(
DIAG_INVALID_NS_DECLARATION_MEMBER.to_string(),
DiagnosticDefinition::error(
"Enum member {name} must have a value that is the XML namespace url.",
),
),
(
DIAG_NS_MISSING_PREFIX.to_string(),
DiagnosticDefinition::error(
"When using a string namespace you must provide a prefix as the 2nd argument.",
),
),
(
DIAG_PREFIX_NOT_ALLOWED.to_string(),
DiagnosticDefinition::error(
"@ns decorator cannot have the prefix parameter set when using an enum member.",
),
),
(
DIAG_NS_NOT_URI.to_string(),
DiagnosticDefinition::error("Namespace {namespace} is not a valid URI."),
),
])
}
pub const XML_TYPES_TSP: &str = r#"
namespace TypeSpec.Xml;
/**
* Known Xml encodings
*/
enum Encoding {
/** Corresponds to a field of schema xs:dateTime */
xmlDateTime,
/** Correspond to a field of schema xs:date */
xmlDate,
/** Correspond to a field of schema xs:time */
xmlTime,
/** Correspond to a field of schema xs:duration */
xmlDuration,
/** Correspond to a field of schema xs:base64Binary */
xmlBase64Binary,
}
"#;
pub const XML_DECORATORS_TSP: &str = r#"
import "../dist/src/decorators.js";
using TypeSpec.Reflection;
namespace TypeSpec.Xml;
/**
* Provide the name of the XML element or attribute. This means the same thing as
* @encodedName("application/xml", value)
*
* @param name The name of the XML element or attribute
*/
extern dec name(target: unknown, name: valueof string);
/**
* Specify that the target property should be encoded as an XML attribute instead of node.
*/
extern dec attribute(target: ModelProperty);
/**
* Specify that the target property shouldn't create a wrapper node. This can be used to
* flatten list nodes into the model node or to include raw text in the model node.
* It cannot be used with `@attribute`.
*/
extern dec unwrapped(target: ModelProperty);
/**
* Specify the XML namespace for this element.
*
* @param ns The namespace URI or a member of an enum decorated with @nsDeclaration.
* @param prefix The namespace prefix. Required if the namespace parameter was passed as a string.
*/
extern dec ns(target: unknown, ns: string | EnumMember, prefix?: valueof string);
/**
* Mark an enum as declaring XML namespaces. See `@ns`
*/
extern dec nsDeclarations(target: Enum);
"#;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_xml_library() {
let diags = create_xml_library();
assert_eq!(diags.len(), 5);
let codes: Vec<&str> = diags.keys().map(|code| code.as_str()).collect();
assert!(codes.contains(&DIAG_NS_ENUM_NOT_DECLARATION));
assert!(codes.contains(&DIAG_NS_MISSING_PREFIX));
assert!(codes.contains(&DIAG_NS_NOT_URI));
}
#[test]
fn test_xml_namespace_constant() {
assert_eq!(XML_NAMESPACE, "TypeSpec.Xml");
}
#[test]
fn test_xml_encoding_roundtrip() {
for encoding in &[
XmlEncoding::XmlDateTime,
XmlEncoding::XmlDate,
XmlEncoding::XmlTime,
XmlEncoding::XmlDuration,
XmlEncoding::XmlBase64Binary,
] {
let scalar = encoding.default_scalar_name();
let roundtrip = get_default_xml_encoding(scalar);
assert_eq!(
roundtrip,
Some(*encoding),
"Failed roundtrip for {:?}",
encoding
);
}
}
#[test]
fn test_xml_encoding_no_default_for_unknown() {
assert_eq!(get_default_xml_encoding("string"), None);
assert_eq!(get_default_xml_encoding("int32"), None);
}
#[test]
fn test_xml_encoding_names() {
assert_eq!(
XmlEncoding::XmlDateTime.as_typespec_name(),
"TypeSpec.Xml.Encoding.xmlDateTime"
);
assert_eq!(
XmlEncoding::XmlBase64Binary.as_typespec_name(),
"TypeSpec.Xml.Encoding.xmlBase64Binary"
);
}
#[test]
fn test_xml_encoding_xs_types() {
assert_eq!(XmlEncoding::XmlDateTime.xs_type_name(), "xs:dateTime");
assert_eq!(XmlEncoding::XmlDate.xs_type_name(), "xs:date");
assert_eq!(XmlEncoding::XmlTime.xs_type_name(), "xs:time");
assert_eq!(XmlEncoding::XmlDuration.xs_type_name(), "xs:duration");
assert_eq!(
XmlEncoding::XmlBase64Binary.xs_type_name(),
"xs:base64Binary"
);
}
#[test]
fn test_xml_namespace_type() {
let ns = XmlNamespace::new("http://example.com", "ex");
assert_eq!(ns.namespace, "http://example.com");
assert_eq!(ns.prefix, "ex");
assert!(ns.is_valid_uri());
let bad_ns = XmlNamespace::new("not-a-uri", "x");
assert!(!bad_ns.is_valid_uri());
}
#[test]
fn test_xml_namespace_equality() {
let ns1 = XmlNamespace::new("http://example.com", "ex");
let ns2 = XmlNamespace::new("http://example.com", "ex");
let ns3 = XmlNamespace::new("http://other.com", "ex");
assert_eq!(ns1, ns2);
assert_ne!(ns1, ns3);
}
#[test]
fn test_tsp_sources_not_empty() {
assert!(!XML_TYPES_TSP.is_empty());
assert!(!XML_DECORATORS_TSP.is_empty());
assert!(XML_TYPES_TSP.contains("Encoding"));
assert!(XML_DECORATORS_TSP.contains("name"));
assert!(XML_DECORATORS_TSP.contains("attribute"));
assert!(XML_DECORATORS_TSP.contains("unwrapped"));
assert!(XML_DECORATORS_TSP.contains("ns"));
assert!(XML_DECORATORS_TSP.contains("nsDeclarations"));
}
#[test]
fn test_xml_encode_data() {
let data = XmlEncodeData {
encoding: Some(XmlEncoding::XmlDateTime),
type_name: "string".to_string(),
};
assert!(data.encoding.is_some());
assert_eq!(data.type_name, "string");
}
#[test]
fn test_is_attribute() {
let mut state = StateAccessors::new();
assert!(!is_attribute(&state, 1));
apply_attribute(&mut state, 1);
assert!(is_attribute(&state, 1));
assert!(!is_attribute(&state, 2));
}
#[test]
fn test_is_unwrapped() {
let mut state = StateAccessors::new();
assert!(!is_unwrapped(&state, 1));
apply_unwrapped(&mut state, 1);
assert!(is_unwrapped(&state, 1));
}
#[test]
fn test_is_ns_declarations() {
let mut state = StateAccessors::new();
assert!(!is_ns_declarations(&state, 1));
apply_ns_declarations(&mut state, 1);
assert!(is_ns_declarations(&state, 1));
}
#[test]
fn test_apply_ns_valid_uri() {
let mut state = StateAccessors::new();
let ns = XmlNamespace::new("http://example.com/schema", "ex");
let result = apply_ns(&mut state, 1, &ns);
assert!(result.is_ok());
let retrieved = get_ns(&state, 1);
assert!(retrieved.is_some());
let retrieved = retrieved.unwrap();
assert_eq!(retrieved.namespace, "http://example.com/schema");
assert_eq!(retrieved.prefix, "ex");
}
#[test]
fn test_apply_ns_invalid_uri() {
let mut state = StateAccessors::new();
let ns = XmlNamespace::new("not-a-uri", "x");
let result = apply_ns(&mut state, 1, &ns);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), DIAG_NS_NOT_URI);
}
#[test]
fn test_validate_namespace_uri() {
assert!(validate_namespace_uri("http://example.com"));
assert!(validate_namespace_uri("https://example.com/schema"));
assert!(validate_namespace_uri("urn:example:ns"));
assert!(!validate_namespace_uri("not-a-uri"));
assert!(!validate_namespace_uri(""));
}
#[test]
fn test_apply_ns_urn() {
let mut state = StateAccessors::new();
let ns = XmlNamespace::new("urn:example:namespace", "ex");
let result = apply_ns(&mut state, 1, &ns);
assert!(result.is_ok());
let retrieved = get_ns(&state, 1);
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().namespace, "urn:example:namespace");
}
#[test]
fn test_xml_encoding_from_typespec_name() {
assert_eq!(
XmlEncoding::from_typespec_name("TypeSpec.Xml.Encoding.xmlDateTime"),
Some(XmlEncoding::XmlDateTime)
);
assert_eq!(
XmlEncoding::from_typespec_name("TypeSpec.Xml.Encoding.xmlDuration"),
Some(XmlEncoding::XmlDuration)
);
assert_eq!(XmlEncoding::from_typespec_name("unknown"), None);
}
#[test]
fn test_get_xml_encoding_with_encode() {
let result = get_xml_encoding(
Some("TypeSpec.Xml.Encoding.xmlDateTime"),
"utcDateTime",
Some("string"),
);
assert!(result.is_some());
let data = result.unwrap();
assert_eq!(data.encoding, Some(XmlEncoding::XmlDateTime));
assert_eq!(data.type_name, "string");
}
#[test]
fn test_get_xml_encoding_default_fallback() {
let result = get_xml_encoding(None, "utcDateTime", None);
assert!(result.is_some());
let data = result.unwrap();
assert_eq!(data.encoding, Some(XmlEncoding::XmlDateTime));
assert_eq!(data.type_name, "string");
}
#[test]
fn test_get_xml_encoding_unknown_scalar() {
let result = get_xml_encoding(None, "string", None);
assert!(result.is_none());
}
#[test]
fn test_get_xml_encoding_custom_encode() {
let result = get_xml_encoding(Some("rfc7231"), "utcDateTime", Some("string"));
assert!(result.is_some());
let data = result.unwrap();
assert_eq!(data.encoding, Some(XmlEncoding::XmlDateTime));
}
}