1use alloc::format;
9use alloc::string::String;
10use alloc::vec::Vec;
11
12use crate::codec::FieldKind;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum XsdType {
17 Long,
19 Double,
21 Boolean,
23 String,
25 HexBinary,
27}
28
29impl XsdType {
30 #[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 #[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#[derive(Debug, Default, Clone, PartialEq, Eq)]
57pub struct XsdGenerator {
58 type_name: String,
59 fields: Vec<(String, XsdType, bool)>,
60}
61
62impl XsdGenerator {
63 #[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 #[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 #[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 #[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}