1use alloc::collections::BTreeMap;
10use alloc::format;
11use alloc::string::{String, ToString};
12use alloc::vec::Vec;
13
14use crate::emitter::{EmitError, XmlEmitter};
15use crate::parser::{Event, ParseError, XmlParser};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum FieldKind {
20 Integer,
22 Float,
24 Bool,
26 String,
28 Bytes,
30}
31
32#[derive(Debug, Clone, PartialEq)]
34pub enum FieldValue {
35 Integer(i64),
37 Float(f64),
39 Bool(bool),
41 String(String),
43 Bytes(Vec<u8>),
45}
46
47impl FieldValue {
48 #[must_use]
50 pub fn kind(&self) -> FieldKind {
51 match self {
52 Self::Integer(_) => FieldKind::Integer,
53 Self::Float(_) => FieldKind::Float,
54 Self::Bool(_) => FieldKind::Bool,
55 Self::String(_) => FieldKind::String,
56 Self::Bytes(_) => FieldKind::Bytes,
57 }
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum CodecError {
64 Parse(ParseError),
66 Emit(EmitError),
68 InvalidValue {
70 field: String,
72 kind: FieldKind,
74 got: String,
76 },
77 NoSampleRoot,
79 InvalidHex,
81}
82
83impl core::fmt::Display for CodecError {
84 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
85 match self {
86 Self::Parse(e) => write!(f, "parse: {e}"),
87 Self::Emit(e) => write!(f, "emit: {e}"),
88 Self::InvalidValue { field, kind, got } => {
89 write!(f, "field `{field}`: expected {kind:?}, got `{got}`")
90 }
91 Self::NoSampleRoot => f.write_str("no sample root element"),
92 Self::InvalidHex => f.write_str("invalid hex string"),
93 }
94 }
95}
96
97#[cfg(feature = "std")]
98impl std::error::Error for CodecError {}
99
100impl From<ParseError> for CodecError {
101 fn from(e: ParseError) -> Self {
102 Self::Parse(e)
103 }
104}
105
106impl From<EmitError> for CodecError {
107 fn from(e: EmitError) -> Self {
108 Self::Emit(e)
109 }
110}
111
112pub fn encode_to_xml(
117 type_name: &str,
118 fields: &BTreeMap<String, FieldValue>,
119) -> Result<String, CodecError> {
120 let mut e = XmlEmitter::new();
121 e.start_element(type_name, &[])?;
122 for (k, v) in fields {
123 e.start_element(k, &[("xsi:type", kind_xsi_name(v.kind()))])?;
124 match v {
125 FieldValue::Integer(i) => e.text(&format!("{i}")),
126 FieldValue::Float(x) => e.text(&format!("{x}")),
127 FieldValue::Bool(b) => e.text(if *b { "true" } else { "false" }),
128 FieldValue::String(s) => e.text(s),
129 FieldValue::Bytes(b) => e.text(&hex_encode(b)),
130 }
131 e.end_element()?;
132 }
133 e.end_element()?;
134 Ok(e.finish())
135}
136
137pub fn decode_xml(xml: &str) -> Result<(String, BTreeMap<String, FieldValue>), CodecError> {
142 let parser = XmlParser::new(xml);
143 let mut events: Vec<Event> = Vec::new();
144 for ev in parser {
145 events.push(ev?);
146 }
147 let mut iter = events.into_iter().peekable();
148 while let Some(Event::Declaration(_)) = iter.peek() {
150 iter.next();
151 }
152 let type_name = match iter.next() {
153 Some(Event::StartElement { name, .. }) => name,
154 _ => return Err(CodecError::NoSampleRoot),
155 };
156
157 let mut fields = BTreeMap::new();
158 while let Some(ev) = iter.next() {
159 match ev {
160 Event::StartElement { name, attrs } => {
161 let kind_attr = attrs
162 .iter()
163 .find(|(k, _)| k == "xsi:type")
164 .map(|(_, v)| v.as_str())
165 .unwrap_or("xs:string");
166 let text = match iter.next() {
167 Some(Event::Text(t)) => t,
168 Some(Event::EndElement(_)) => {
169 fields.insert(name.clone(), FieldValue::String(String::new()));
171 continue;
172 }
173 _ => String::new(),
174 };
175 while let Some(next) = iter.peek() {
177 if matches!(next, Event::EndElement(n) if n == &name) {
178 iter.next();
179 break;
180 }
181 iter.next();
182 }
183 let value = parse_value(&name, kind_attr, &text)?;
184 fields.insert(name, value);
185 }
186 Event::EndElement(n) if n == type_name => break,
187 _ => {}
188 }
189 }
190 Ok((type_name, fields))
191}
192
193fn kind_xsi_name(k: FieldKind) -> &'static str {
194 match k {
195 FieldKind::Integer => "xs:long",
196 FieldKind::Float => "xs:double",
197 FieldKind::Bool => "xs:boolean",
198 FieldKind::String => "xs:string",
199 FieldKind::Bytes => "xs:hexBinary",
200 }
201}
202
203fn parse_value(field: &str, xsi_type: &str, text: &str) -> Result<FieldValue, CodecError> {
204 match xsi_type {
205 "xs:long" | "xs:int" | "xs:short" | "xs:byte" => text
206 .parse::<i64>()
207 .map(FieldValue::Integer)
208 .map_err(|_| CodecError::InvalidValue {
209 field: field.into(),
210 kind: FieldKind::Integer,
211 got: text.into(),
212 }),
213 "xs:double" | "xs:float" => {
214 text.parse::<f64>()
215 .map(FieldValue::Float)
216 .map_err(|_| CodecError::InvalidValue {
217 field: field.into(),
218 kind: FieldKind::Float,
219 got: text.into(),
220 })
221 }
222 "xs:boolean" => match text {
223 "true" | "1" => Ok(FieldValue::Bool(true)),
224 "false" | "0" => Ok(FieldValue::Bool(false)),
225 _ => Err(CodecError::InvalidValue {
226 field: field.into(),
227 kind: FieldKind::Bool,
228 got: text.into(),
229 }),
230 },
231 "xs:hexBinary" => hex_decode(text).map(FieldValue::Bytes),
232 _ => Ok(FieldValue::String(text.to_string())),
233 }
234}
235
236fn hex_encode(b: &[u8]) -> String {
237 let mut out = String::with_capacity(b.len() * 2);
238 for byte in b {
239 let hi = (byte >> 4) & 0x0f;
240 let lo = byte & 0x0f;
241 out.push(hex_digit(hi));
242 out.push(hex_digit(lo));
243 }
244 out
245}
246
247fn hex_digit(n: u8) -> char {
248 match n {
249 0..=9 => char::from(b'0' + n),
250 10..=15 => char::from(b'A' + n - 10),
251 _ => '?',
252 }
253}
254
255fn hex_decode(s: &str) -> Result<Vec<u8>, CodecError> {
256 if s.len() % 2 != 0 {
257 return Err(CodecError::InvalidHex);
258 }
259 let mut out = Vec::with_capacity(s.len() / 2);
260 let bytes = s.as_bytes();
261 let mut i = 0;
262 while i < bytes.len() {
263 let hi = hex_value(bytes[i])?;
264 let lo = hex_value(bytes[i + 1])?;
265 out.push((hi << 4) | lo);
266 i += 2;
267 }
268 Ok(out)
269}
270
271fn hex_value(c: u8) -> Result<u8, CodecError> {
272 match c {
273 b'0'..=b'9' => Ok(c - b'0'),
274 b'a'..=b'f' => Ok(c - b'a' + 10),
275 b'A'..=b'F' => Ok(c - b'A' + 10),
276 _ => Err(CodecError::InvalidHex),
277 }
278}
279
280#[cfg(test)]
281#[allow(
282 clippy::expect_used,
283 clippy::unwrap_used,
284 clippy::panic,
285 clippy::float_cmp
286)]
287mod tests {
288 use super::*;
289
290 fn sample() -> BTreeMap<String, FieldValue> {
291 let mut m = BTreeMap::new();
292 m.insert("id".into(), FieldValue::Integer(42));
293 m.insert("price".into(), FieldValue::Float(99.5));
294 m.insert("active".into(), FieldValue::Bool(true));
295 m.insert("symbol".into(), FieldValue::String("AAPL".into()));
296 m.insert("blob".into(), FieldValue::Bytes(alloc::vec![0xCA, 0xFE]));
297 m
298 }
299
300 #[test]
301 fn round_trip_preserves_all_fields() {
302 let s = sample();
303 let xml = encode_to_xml("Trade", &s).unwrap();
304 let (type_name, decoded) = decode_xml(&xml).unwrap();
305 assert_eq!(type_name, "Trade");
306 assert_eq!(decoded.get("id"), Some(&FieldValue::Integer(42)));
307 assert_eq!(decoded.get("price"), Some(&FieldValue::Float(99.5)));
308 assert_eq!(decoded.get("active"), Some(&FieldValue::Bool(true)));
309 assert_eq!(
310 decoded.get("symbol"),
311 Some(&FieldValue::String("AAPL".into()))
312 );
313 assert_eq!(
314 decoded.get("blob"),
315 Some(&FieldValue::Bytes(alloc::vec![0xCA, 0xFE]))
316 );
317 }
318
319 #[test]
320 fn encode_uses_type_name_as_root() {
321 let xml = encode_to_xml("MyTopic", &sample()).unwrap();
322 assert!(xml.starts_with("<MyTopic"));
323 assert!(xml.ends_with("</MyTopic>"));
324 }
325
326 #[test]
327 fn invalid_integer_rejected() {
328 let xml = r#"<T><id xsi:type="xs:long">notanumber</id></T>"#;
329 let err = decode_xml(xml).unwrap_err();
330 assert!(matches!(err, CodecError::InvalidValue { .. }));
331 }
332
333 #[test]
334 fn invalid_bool_rejected() {
335 let xml = r#"<T><b xsi:type="xs:boolean">maybe</b></T>"#;
336 let err = decode_xml(xml).unwrap_err();
337 assert!(matches!(
338 err,
339 CodecError::InvalidValue {
340 kind: FieldKind::Bool,
341 ..
342 }
343 ));
344 }
345
346 #[test]
347 fn missing_xsi_type_defaults_to_string() {
348 let xml = "<T><name>Alice</name></T>";
349 let (_, decoded) = decode_xml(xml).unwrap();
350 assert_eq!(
351 decoded.get("name"),
352 Some(&FieldValue::String("Alice".into()))
353 );
354 }
355
356 #[test]
357 fn empty_body_yields_empty_string() {
358 let xml = "<T><name></name></T>";
359 let (_, decoded) = decode_xml(xml).unwrap();
360 assert_eq!(
361 decoded.get("name"),
362 Some(&FieldValue::String(String::new()))
363 );
364 }
365
366 #[test]
367 fn hex_round_trip() {
368 let bytes = alloc::vec![0xDE, 0xAD, 0xBE, 0xEF];
369 let s = hex_encode(&bytes);
370 assert_eq!(s, "DEADBEEF");
371 let back = hex_decode(&s).unwrap();
372 assert_eq!(back, bytes);
373 }
374
375 #[test]
376 fn hex_decode_rejects_odd_length() {
377 assert!(hex_decode("ABC").is_err());
378 }
379
380 #[test]
381 fn hex_decode_rejects_non_hex_char() {
382 assert!(hex_decode("ZZ").is_err());
383 }
384
385 #[test]
386 fn boolean_accepts_zero_and_one() {
387 let xml = r#"<T><b xsi:type="xs:boolean">1</b></T>"#;
388 let (_, decoded) = decode_xml(xml).unwrap();
389 assert_eq!(decoded.get("b"), Some(&FieldValue::Bool(true)));
390 }
391}