Skip to main content

zerodds_xml_wire/
xsd.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! XSD-Schema-Generation aus DDS-Topic-Type-Definitions.
4//!
5//! Spec DDS-XML 1.0 §6.5: aus einem IDL-Type wird ein XSD-Schema
6//! generiert, das alle Caller validieren koennen.
7
8use alloc::format;
9use alloc::string::String;
10use alloc::vec::Vec;
11
12use crate::codec::FieldKind;
13
14/// XSD-Type-Reference.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum XsdType {
17    /// `xs:long`.
18    Long,
19    /// `xs:double`.
20    Double,
21    /// `xs:boolean`.
22    Boolean,
23    /// `xs:string`.
24    String,
25    /// `xs:hexBinary`.
26    HexBinary,
27}
28
29impl XsdType {
30    /// Liefert den Spec-konformen XSD-Namen.
31    #[must_use]
32    pub fn xsd_name(self) -> &'static str {
33        match self {
34            Self::Long => "xs:long",
35            Self::Double => "xs:double",
36            Self::Boolean => "xs:boolean",
37            Self::String => "xs:string",
38            Self::HexBinary => "xs:hexBinary",
39        }
40    }
41
42    /// Mapping `FieldKind` → `XsdType`.
43    #[must_use]
44    pub fn from_field_kind(k: FieldKind) -> Self {
45        match k {
46            FieldKind::Integer => Self::Long,
47            FieldKind::Float => Self::Double,
48            FieldKind::Bool => Self::Boolean,
49            FieldKind::String => Self::String,
50            FieldKind::Bytes => Self::HexBinary,
51        }
52    }
53}
54
55/// XSD-Generator-Builder. Spec §6.5.
56#[derive(Debug, Default, Clone, PartialEq, Eq)]
57pub struct XsdGenerator {
58    type_name: String,
59    fields: Vec<(String, XsdType, bool)>,
60}
61
62impl XsdGenerator {
63    /// Konstruktor mit Type-Name (wird Root-Element-Name).
64    #[must_use]
65    pub fn new(type_name: &str) -> Self {
66        Self {
67            type_name: type_name.into(),
68            fields: Vec::new(),
69        }
70    }
71
72    /// Fuegt ein Feld hinzu. `optional=true` mappt zu
73    /// `minOccurs="0"`.
74    #[must_use]
75    pub fn field(mut self, name: &str, kind: XsdType, optional: bool) -> Self {
76        self.fields.push((name.into(), kind, optional));
77        self
78    }
79
80    /// Render zu XSD-XML-String. Spec §6.5 Appendix A.
81    #[must_use]
82    pub fn render(&self) -> String {
83        let mut out = String::new();
84        out.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
85        out.push('\n');
86        out.push_str(
87            r#"<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">"#,
88        );
89        out.push('\n');
90        out.push_str(&format!(
91            "  <xs:element name=\"{}\">\n    <xs:complexType>\n      <xs:sequence>\n",
92            self.type_name
93        ));
94        for (name, kind, optional) in &self.fields {
95            let occurs = if *optional { r#" minOccurs="0""# } else { "" };
96            out.push_str(&format!(
97                "        <xs:element name=\"{name}\" type=\"{}\"{occurs}/>\n",
98                kind.xsd_name()
99            ));
100        }
101        out.push_str(
102            "      </xs:sequence>\n    </xs:complexType>\n  </xs:element>\n</xs:schema>\n",
103        );
104        out
105    }
106
107    /// Anzahl Felder.
108    #[must_use]
109    pub fn field_count(&self) -> usize {
110        self.fields.len()
111    }
112}
113
114#[cfg(test)]
115#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn builder_renders_root_element() {
121        let xsd = XsdGenerator::new("Trade")
122            .field("id", XsdType::Long, false)
123            .field("symbol", XsdType::String, false)
124            .render();
125        assert!(xsd.contains(r#"<xs:element name="Trade">"#));
126        assert!(xsd.contains(r#"<xs:element name="id" type="xs:long"/>"#));
127        assert!(xsd.contains(r#"<xs:element name="symbol" type="xs:string"/>"#));
128    }
129
130    #[test]
131    fn optional_field_gets_min_occurs_zero() {
132        let xsd = XsdGenerator::new("T")
133            .field("opt", XsdType::Long, true)
134            .render();
135        assert!(xsd.contains(r#"minOccurs="0""#));
136    }
137
138    #[test]
139    fn from_field_kind_round_trip() {
140        assert_eq!(XsdType::from_field_kind(FieldKind::Integer), XsdType::Long);
141        assert_eq!(XsdType::from_field_kind(FieldKind::Float), XsdType::Double);
142        assert_eq!(XsdType::from_field_kind(FieldKind::Bool), XsdType::Boolean);
143        assert_eq!(XsdType::from_field_kind(FieldKind::String), XsdType::String);
144        assert_eq!(
145            XsdType::from_field_kind(FieldKind::Bytes),
146            XsdType::HexBinary
147        );
148    }
149
150    #[test]
151    fn declaration_starts_xsd() {
152        let xsd = XsdGenerator::new("X").render();
153        assert!(xsd.starts_with("<?xml"));
154        assert!(xsd.contains("xs:schema"));
155    }
156
157    #[test]
158    fn field_count_tracks_additions() {
159        let g = XsdGenerator::new("T")
160            .field("a", XsdType::Long, false)
161            .field("b", XsdType::String, false);
162        assert_eq!(g.field_count(), 2);
163    }
164
165    #[test]
166    fn empty_type_renders_empty_sequence() {
167        let xsd = XsdGenerator::new("Empty").render();
168        assert!(xsd.contains("<xs:sequence>"));
169        assert!(xsd.contains("</xs:sequence>"));
170    }
171
172    #[test]
173    fn xsd_names_are_spec_conform() {
174        assert_eq!(XsdType::Long.xsd_name(), "xs:long");
175        assert_eq!(XsdType::HexBinary.xsd_name(), "xs:hexBinary");
176        assert_eq!(XsdType::Boolean.xsd_name(), "xs:boolean");
177    }
178}