1use crate::FiscalError;
2
3const ROOT_TAG_LIST: &[&str] = &[
8 "distDFeInt",
9 "resNFe",
10 "resEvento",
11 "envEvento",
12 "ConsCad",
13 "consSitNFe",
14 "consReciNFe",
15 "downloadNFe",
16 "enviNFe",
17 "inutNFe",
18 "admCscNFCe",
19 "consStatServ",
20 "retDistDFeInt",
21 "retEnvEvento",
22 "retConsCad",
23 "retConsSitNFe",
24 "retConsReciNFe",
25 "retDownloadNFe",
26 "retEnviNFe",
27 "retInutNFe",
28 "retAdmCscNFCe",
29 "retConsStatServ",
30 "procInutNFe",
31 "procEventoNFe",
32 "procNFe",
33 "nfeProc",
34 "NFe",
35];
36
37pub fn identify_xml_type(xml: &str) -> Result<String, FiscalError> {
48 let trimmed = xml.trim();
49 if trimmed.is_empty() {
50 return Err(FiscalError::XmlParsing("XML is empty.".into()));
51 }
52 if !trimmed.starts_with('<') {
53 return Err(FiscalError::XmlParsing(
54 "Invalid document: not valid XML.".into(),
55 ));
56 }
57
58 let reader = quick_xml::Reader::from_str(trimmed);
60 let _ = reader; if let Some(tag) = find_root_tag(trimmed) {
66 return Ok(tag.to_string());
67 }
68
69 Err(FiscalError::XmlParsing(
70 "Document does not belong to the NFe project.".into(),
71 ))
72}
73
74fn find_root_tag(xml: &str) -> Option<&'static str> {
76 for &tag in ROOT_TAG_LIST {
77 let pattern_space = format!("<{tag} ");
79 let pattern_close = format!("<{tag}>");
80 let pattern_newline = format!("<{tag}\n");
81 if xml.contains(&pattern_space)
82 || xml.contains(&pattern_close)
83 || xml.contains(&pattern_newline)
84 {
85 return Some(tag);
86 }
87 }
88 None
89}
90
91pub fn xml_to_json(xml: &str) -> Result<String, FiscalError> {
131 let value = xml_to_value(xml)?;
132
133 serde_json::to_string(&value)
134 .map_err(|e| FiscalError::XmlParsing(format!("JSON serialization failed: {e}")))
135}
136
137pub fn xml_to_value(xml: &str) -> Result<serde_json::Value, FiscalError> {
162 let root_tag = identify_xml_type(xml)?;
163 let full = xml_str_to_json_value(xml.trim())?;
164
165 match full {
168 serde_json::Value::Object(map) => {
169 if let Some(inner) = map.get(&root_tag) {
170 Ok(inner.clone())
171 } else {
172 Ok(serde_json::Value::Object(map))
174 }
175 }
176 other => Ok(other),
177 }
178}
179
180pub fn xml_to_map(xml: &str) -> Result<serde_json::Map<String, serde_json::Value>, FiscalError> {
203 let value = xml_to_value(xml)?;
204 match value {
205 serde_json::Value::Object(map) => Ok(map),
206 _ => Err(FiscalError::XmlParsing(
207 "Top-level XML value is not an object.".into(),
208 )),
209 }
210}
211
212fn xml_str_to_json_value(xml: &str) -> Result<serde_json::Value, FiscalError> {
218 use quick_xml::Reader;
219 use quick_xml::events::Event;
220 use serde_json::{Map, Value};
221
222 let mut reader = Reader::from_str(xml);
223 reader.config_mut().trim_text(true);
224
225 let mut stack: Vec<(String, Map<String, Value>)> = Vec::new();
226 let mut root_map = Map::new();
227
228 loop {
229 match reader.read_event() {
230 Ok(Event::Start(ref e)) => {
231 let raw_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
232 let local_name = strip_ns_prefix(&raw_name);
233
234 let mut attrs_map = Map::new();
235 for attr in e.attributes().flatten() {
236 let key = String::from_utf8_lossy(attr.key.as_ref()).to_string();
237 if is_xmlns_attr(&key) {
238 continue;
239 }
240 let val = String::from_utf8_lossy(&attr.value).to_string();
241 attrs_map.insert(key, Value::String(val));
242 }
243
244 stack.push((local_name, attrs_map));
245 }
246 Ok(Event::Empty(ref e)) => {
247 let raw_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
248 let local_name = strip_ns_prefix(&raw_name);
249
250 let mut attrs_map = Map::new();
251 for attr in e.attributes().flatten() {
252 let key = String::from_utf8_lossy(attr.key.as_ref()).to_string();
253 if is_xmlns_attr(&key) {
254 continue;
255 }
256 let val = String::from_utf8_lossy(&attr.value).to_string();
257 attrs_map.insert(key, Value::String(val));
258 }
259
260 let child_val = if attrs_map.is_empty() {
261 Value::String(String::new())
262 } else {
263 Value::Object(attrs_map)
264 };
265
266 if let Some((_name, map)) = stack.last_mut() {
267 insert_into_map(map, &local_name, child_val);
268 } else {
269 root_map.insert(local_name, child_val);
270 }
271 }
272 Ok(Event::Text(ref e)) => {
273 let text = e.decode().unwrap_or_default().to_string();
274 if !text.is_empty() {
275 if let Some((_name, map)) = stack.last_mut() {
276 map.insert("#text".to_string(), Value::String(text));
277 }
278 }
279 }
280 Ok(Event::End(_)) => {
281 if let Some((name, map)) = stack.pop() {
282 let child_val = if map.len() == 1 {
283 if let Some(text) = map.get("#text") {
284 text.clone()
285 } else {
286 Value::Object(map)
287 }
288 } else if map.is_empty() {
289 Value::String(String::new())
290 } else {
291 Value::Object(map)
292 };
293
294 if let Some((_parent_name, parent_map)) = stack.last_mut() {
295 insert_into_map(parent_map, &name, child_val);
296 } else {
297 root_map.insert(name, child_val);
298 }
299 }
300 }
301 Ok(Event::Decl(_)) | Ok(Event::Comment(_)) | Ok(Event::PI(_)) => {}
302 Ok(Event::CData(ref e)) => {
303 let text = String::from_utf8_lossy(e.as_ref()).to_string();
304 if let Some((_name, map)) = stack.last_mut() {
305 map.insert("#text".to_string(), Value::String(text));
306 }
307 }
308 Ok(Event::Eof) => break,
309 Err(e) => return Err(FiscalError::XmlParsing(format!("XML parse error: {e}"))),
310 _ => {}
311 }
312 }
313
314 Ok(Value::Object(root_map))
315}
316
317fn strip_ns_prefix(name: &str) -> String {
319 match name.find(':') {
320 Some(idx) => name[idx + 1..].to_string(),
321 None => name.to_string(),
322 }
323}
324
325fn is_xmlns_attr(key: &str) -> bool {
329 key == "xmlns" || key.starts_with("xmlns:")
330}
331
332fn insert_into_map(
334 map: &mut serde_json::Map<String, serde_json::Value>,
335 key: &str,
336 value: serde_json::Value,
337) {
338 use serde_json::Value;
339 if let Some(existing) = map.get_mut(key) {
340 match existing {
341 Value::Array(arr) => {
342 arr.push(value);
343 }
344 _ => {
345 let prev = existing.take();
346 *existing = Value::Array(vec![prev, value]);
347 }
348 }
349 } else {
350 map.insert(key.to_string(), value);
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn identify_nfe() {
360 let xml = r#"<?xml version="1.0"?><NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe/></NFe>"#;
361 assert_eq!(identify_xml_type(xml).unwrap(), "NFe");
362 }
363
364 #[test]
365 fn identify_nfe_proc() {
366 let xml = r#"<nfeProc versao="4.00"><NFe/><protNFe/></nfeProc>"#;
367 assert_eq!(identify_xml_type(xml).unwrap(), "nfeProc");
368 }
369
370 #[test]
371 fn empty_returns_err() {
372 assert!(identify_xml_type("").is_err());
373 }
374
375 #[test]
376 fn non_xml_returns_err() {
377 assert!(identify_xml_type("hello world").is_err());
378 }
379
380 #[test]
381 fn unknown_root_returns_err() {
382 let xml = "<other><data/></other>";
383 assert!(identify_xml_type(xml).is_err());
384 }
385
386 #[test]
389 fn identify_env_evento() {
390 let xml = r#"<envEvento versao="1.00"><evento/></envEvento>"#;
391 assert_eq!(identify_xml_type(xml).unwrap(), "envEvento");
392 }
393
394 #[test]
395 fn identify_ret_cons_sit_nfe() {
396 let xml = "<retConsSitNFe><cStat>100</cStat></retConsSitNFe>";
397 assert_eq!(identify_xml_type(xml).unwrap(), "retConsSitNFe");
398 }
399
400 #[test]
401 fn identify_cons_cad() {
402 let xml = r#"<ConsCad versao="2.00"><infCons/></ConsCad>"#;
403 assert_eq!(identify_xml_type(xml).unwrap(), "ConsCad");
404 }
405
406 #[test]
407 fn identify_inut_nfe() {
408 let xml = r#"<inutNFe versao="4.00"><infInut/></inutNFe>"#;
409 assert_eq!(identify_xml_type(xml).unwrap(), "inutNFe");
410 }
411
412 #[test]
413 fn identify_ret_env_evento() {
414 let xml = "<retEnvEvento><cStat>128</cStat></retEnvEvento>";
415 assert_eq!(identify_xml_type(xml).unwrap(), "retEnvEvento");
416 }
417
418 #[test]
419 fn identify_ret_inut_nfe() {
420 let xml = "<retInutNFe><infInut/></retInutNFe>";
421 assert_eq!(identify_xml_type(xml).unwrap(), "retInutNFe");
422 }
423
424 #[test]
425 fn identify_adm_csc_nfce() {
426 let xml = r#"<admCscNFCe versao="1.00"><data/></admCscNFCe>"#;
427 assert_eq!(identify_xml_type(xml).unwrap(), "admCscNFCe");
428 }
429
430 #[test]
431 fn identify_dist_dfe_int() {
432 let xml = r#"<distDFeInt versao="1.01"><data/></distDFeInt>"#;
433 assert_eq!(identify_xml_type(xml).unwrap(), "distDFeInt");
434 }
435
436 #[test]
437 fn identify_proc_evento_nfe() {
438 let xml = r#"<procEventoNFe versao="1.00"><evento/></procEventoNFe>"#;
439 assert_eq!(identify_xml_type(xml).unwrap(), "procEventoNFe");
440 }
441
442 #[test]
445 fn xml_to_json_empty_elements() {
446 let xml = r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe Id="NFe123"><empty/></infNFe></NFe>"#;
448 let json = xml_to_json(xml).unwrap();
449 assert!(json.contains("infNFe"));
450 assert!(!json.contains("xmlns"));
452 }
453
454 #[test]
455 fn xml_to_json_non_nfe_document_fails() {
456 let xml = "<garbage><data>hello</data></garbage>";
457 assert!(xml_to_json(xml).is_err());
458 }
459
460 #[test]
461 fn xml_to_json_empty_element_with_attrs() {
462 let xml = r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><tag attr="val"/></NFe>"#;
464 let json = xml_to_json(xml).unwrap();
465 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
466 assert!(v.get("tag").is_some());
467 assert!(v.get("NFe").is_none(), "root tag NFe must be unwrapped");
468 }
469
470 #[test]
471 fn xml_to_json_repeated_elements_become_array() {
472 let xml =
474 r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><det>a</det><det>b</det></NFe>"#;
475 let json = xml_to_json(xml).unwrap();
476 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
477 let det = v
478 .get("det")
479 .expect("det should be at top level after unwrap");
480 assert!(det.is_array());
481 assert_eq!(det.as_array().expect("is array").len(), 2);
482 }
483
484 #[test]
485 fn xml_to_json_element_with_only_attrs_no_text() {
486 let xml =
488 r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><item a="1" b="2"></item></NFe>"#;
489 let json = xml_to_json(xml).unwrap();
490 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
491 assert!(v.get("item").is_some());
492 }
493
494 #[test]
495 fn strip_ns_prefix_with_colon() {
496 assert_eq!(strip_ns_prefix("nfe:NFe"), "NFe");
497 }
498
499 #[test]
500 fn strip_ns_prefix_without_colon() {
501 assert_eq!(strip_ns_prefix("NFe"), "NFe");
502 }
503
504 #[test]
505 fn is_xmlns_attr_cases() {
506 assert!(is_xmlns_attr("xmlns"));
507 assert!(is_xmlns_attr("xmlns:nfe"));
508 assert!(!is_xmlns_attr("versao"));
509 assert!(!is_xmlns_attr("Id"));
510 }
511
512 #[test]
513 fn xml_to_json_basic() {
514 let xml = r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe Id="NFe123"><ide><cUF>35</cUF></ide></infNFe></NFe>"#;
516 let json = xml_to_json(xml).unwrap();
517 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
518 assert!(v.get("infNFe").is_some(), "infNFe should be at top level");
519 assert!(v.get("NFe").is_none(), "NFe root should be unwrapped");
520 }
521
522 #[test]
523 fn xml_to_json_attributes_inline() {
524 let xml = r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe versao="4.00" Id="NFe123"><ide><cUF>35</cUF></ide></infNFe></NFe>"#;
526 let json = xml_to_json(xml).unwrap();
527 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
528 let inf_nfe = v.get("infNFe").expect("infNFe at top level");
529 assert_eq!(inf_nfe.get("versao").and_then(|v| v.as_str()), Some("4.00"));
531 assert_eq!(inf_nfe.get("Id").and_then(|v| v.as_str()), Some("NFe123"));
532 assert!(
533 inf_nfe.get("attributes").is_none(),
534 "should NOT have @attributes/attributes key"
535 );
536 }
537
538 #[test]
539 fn xml_to_json_cdata_in_qrcode() {
540 let xml = concat!(
542 r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe">"#,
543 r#"<infNFe versao="4.00" Id="NFe123"><ide><cUF>35</cUF></ide></infNFe>"#,
544 r#"<infNFeSupl>"#,
545 r#"<qrCode><![CDATA[http://example.com/nfce?p=123&x=456]]></qrCode>"#,
546 r#"<urlChave>http://example.com/nfce/consulta</urlChave>"#,
547 r#"</infNFeSupl>"#,
548 r#"</NFe>"#,
549 );
550 let json = xml_to_json(xml).unwrap();
551 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
552 let supl = v.get("infNFeSupl").expect("infNFeSupl at top level");
553 assert_eq!(
554 supl.get("qrCode").and_then(|v| v.as_str()),
555 Some("http://example.com/nfce?p=123&x=456"),
556 "CDATA content should be preserved without mangling"
557 );
558 assert_eq!(
559 supl.get("urlChave").and_then(|v| v.as_str()),
560 Some("http://example.com/nfce/consulta"),
561 );
562 }
563
564 #[test]
565 fn xml_to_json_xmlns_stripped() {
566 let xml = r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe" xmlns:nfe="http://www.portalfiscal.inf.br/nfe"><infNFe versao="4.00"><ide><cUF>35</cUF></ide></infNFe></NFe>"#;
568 let json = xml_to_json(xml).unwrap();
569 assert!(
570 !json.contains("xmlns"),
571 "xmlns must not appear in JSON output"
572 );
573 assert!(json.contains("versao"), "non-xmlns attributes must be kept");
574 }
575
576 #[test]
579 fn xml_to_value_navigable_fields() {
580 let xml = r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe versao="4.00" Id="NFe35..."><ide><cUF>35</cUF><nNF>12345</nNF></ide></infNFe></NFe>"#;
584 let value = xml_to_value(xml).unwrap();
585
586 assert_eq!(value["infNFe"]["ide"]["cUF"].as_str(), Some("35"));
588 assert_eq!(value["infNFe"]["ide"]["nNF"].as_str(), Some("12345"));
589 assert_eq!(value["infNFe"]["versao"].as_str(), Some("4.00"));
591 assert_eq!(value["infNFe"]["Id"].as_str(), Some("NFe35..."));
592 assert!(value.get("xmlns").is_none(), "xmlns must not appear");
594 }
595
596 #[test]
597 fn xml_to_value_ret_cons_sit_nfe() {
598 let xml = concat!(
600 r#"<retConsSitNFe versao="4.00">"#,
601 "<cStat>100</cStat>",
602 "<xMotivo>Autorizado o uso da NF-e</xMotivo>",
603 "<chNFe>35200612345678901234550010000000011000000019</chNFe>",
604 "</retConsSitNFe>"
605 );
606 let value = xml_to_value(xml).unwrap();
607
608 assert_eq!(value["cStat"].as_str(), Some("100"));
609 assert_eq!(value["xMotivo"].as_str(), Some("Autorizado o uso da NF-e"));
610 assert_eq!(
611 value["chNFe"].as_str(),
612 Some("35200612345678901234550010000000011000000019")
613 );
614 assert_eq!(value["versao"].as_str(), Some("4.00"));
616 }
617
618 #[test]
619 fn xml_to_value_empty_xml_fails() {
620 assert!(xml_to_value("").is_err());
621 }
622
623 #[test]
624 fn xml_to_value_non_nfe_fails() {
625 assert!(xml_to_value("<other/>").is_err());
626 }
627
628 #[test]
629 fn xml_to_value_cdata_preserved() {
630 let xml = concat!(
632 r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe">"#,
633 r#"<infNFeSupl>"#,
634 r#"<qrCode><![CDATA[http://example.com?a=1&b=2]]></qrCode>"#,
635 r#"</infNFeSupl>"#,
636 r#"</NFe>"#,
637 );
638 let value = xml_to_value(xml).unwrap();
639 assert_eq!(
640 value["infNFeSupl"]["qrCode"].as_str(),
641 Some("http://example.com?a=1&b=2")
642 );
643 }
644
645 #[test]
648 fn xml_to_map_returns_top_level_keys() {
649 let xml = r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe><ide><cUF>35</cUF></ide></infNFe></NFe>"#;
651 let map = xml_to_map(xml).unwrap();
652
653 assert!(
654 map.contains_key("infNFe"),
655 "top-level map must contain 'infNFe' key (root NFe unwrapped)"
656 );
657 assert_eq!(map["infNFe"]["ide"]["cUF"].as_str(), Some("35"));
658 }
659
660 #[test]
661 fn xml_to_map_nfe_proc() {
662 let xml = concat!(
664 r#"<nfeProc versao="4.00">"#,
665 r#"<NFe><infNFe><ide><cUF>31</cUF></ide></infNFe></NFe>"#,
666 r#"<protNFe><infProt><cStat>100</cStat></infProt></protNFe>"#,
667 r#"</nfeProc>"#
668 );
669 let map = xml_to_map(xml).unwrap();
670
671 assert_eq!(map["versao"].as_str(), Some("4.00"));
672 assert_eq!(map["NFe"]["infNFe"]["ide"]["cUF"].as_str(), Some("31"));
673 assert_eq!(map["protNFe"]["infProt"]["cStat"].as_str(), Some("100"));
674 }
675
676 #[test]
677 fn xml_to_map_empty_xml_fails() {
678 assert!(xml_to_map("").is_err());
679 }
680
681 #[test]
682 fn xml_to_map_non_nfe_fails() {
683 assert!(xml_to_map("<garbage/>").is_err());
684 }
685
686 #[test]
687 fn xml_to_map_repeated_elements() {
688 let xml = concat!(
690 r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe">"#,
691 r#"<det nItem="1"><prod><cProd>001</cProd></prod></det>"#,
692 r#"<det nItem="2"><prod><cProd>002</cProd></prod></det>"#,
693 r#"</NFe>"#
694 );
695 let map = xml_to_map(xml).unwrap();
696 let det = &map["det"];
697 assert!(det.is_array(), "repeated elements must become an array");
698 let arr = det.as_array().expect("is array");
699 assert_eq!(arr.len(), 2);
700 assert_eq!(arr[0]["prod"]["cProd"].as_str(), Some("001"));
701 assert_eq!(arr[1]["prod"]["cProd"].as_str(), Some("002"));
702 }
703
704 #[test]
705 fn xml_to_value_and_json_produce_equivalent_output() {
706 let xml = r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe versao="4.00"><ide><cUF>35</cUF></ide></infNFe></NFe>"#;
708 let value = xml_to_value(xml).unwrap();
709 let json_str = xml_to_json(xml).unwrap();
710 let from_json: serde_json::Value = serde_json::from_str(&json_str).unwrap();
711 assert_eq!(value, from_json);
712 }
713
714 #[test]
717 fn xml_to_value_matches_php_to_std_nfe() {
718 let xml = r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe versao="4.00" Id="NFe123"><ide><cUF>35</cUF></ide></infNFe></NFe>"#;
722 let value = xml_to_value(xml).unwrap();
723
724 assert!(value.get("NFe").is_none(), "NFe wrapper must be removed");
726 assert!(value.get("xmlns").is_none(), "xmlns must be stripped");
728 assert!(value.get("infNFe").is_some(), "infNFe must be at top level");
730 assert_eq!(value["infNFe"]["versao"].as_str(), Some("4.00"));
731 assert_eq!(value["infNFe"]["Id"].as_str(), Some("NFe123"));
732 assert_eq!(value["infNFe"]["ide"]["cUF"].as_str(), Some("35"));
733 }
734
735 #[test]
736 fn xml_to_value_matches_php_to_std_ret_envi_nfe() {
737 let xml = concat!(
739 r#"<retEnviNFe versao="4.00">"#,
740 "<tpAmb>2</tpAmb>",
741 "<cStat>103</cStat>",
742 "<xMotivo>Lote recebido com sucesso</xMotivo>",
743 "</retEnviNFe>"
744 );
745 let value = xml_to_value(xml).unwrap();
746
747 assert!(value.get("retEnviNFe").is_none(), "root must be unwrapped");
748 assert_eq!(value["versao"].as_str(), Some("4.00"));
749 assert_eq!(value["tpAmb"].as_str(), Some("2"));
750 assert_eq!(value["cStat"].as_str(), Some("103"));
751 assert_eq!(value["xMotivo"].as_str(), Some("Lote recebido com sucesso"));
752 }
753
754 #[test]
755 fn xml_to_value_matches_php_to_std_nfe_proc() {
756 let xml = concat!(
759 r#"<nfeProc versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">"#,
760 r#"<NFe><infNFe><ide><cUF>35</cUF></ide></infNFe></NFe>"#,
761 r#"<protNFe><infProt><cStat>100</cStat></infProt></protNFe>"#,
762 r#"</nfeProc>"#
763 );
764 let value = xml_to_value(xml).unwrap();
765
766 assert!(
767 value.get("nfeProc").is_none(),
768 "root nfeProc must be unwrapped"
769 );
770 assert!(value.get("xmlns").is_none(), "xmlns must not appear");
771 assert_eq!(value["versao"].as_str(), Some("4.00"));
772 assert!(value.get("NFe").is_some());
773 assert!(value.get("protNFe").is_some());
774 assert_eq!(value["protNFe"]["infProt"]["cStat"].as_str(), Some("100"));
775 }
776}