Skip to main content

mig_assembly/
renderer.rs

1//! EDIFACT string renderer from disassembled segments.
2//!
3//! Converts a list of `DisassembledSegment` values back into a valid
4//! EDIFACT string using the provided delimiters.
5
6use crate::disassembler::DisassembledSegment;
7use edifact_primitives::EdifactDelimiters;
8
9/// Render a list of disassembled segments into an EDIFACT string.
10///
11/// Follows the same rendering rules as `RawSegment::to_raw_string`:
12/// - Elements separated by element separator (`+`)
13/// - Components separated by component separator (`:`)
14/// - Trailing empty elements are trimmed
15/// - Each segment terminated by segment terminator (`'`)
16pub fn render_edifact(segments: &[DisassembledSegment], delimiters: &EdifactDelimiters) -> String {
17    let mut out = String::new();
18
19    for seg in segments {
20        render_segment(seg, delimiters, &mut out);
21    }
22
23    out
24}
25
26fn render_segment(seg: &DisassembledSegment, delimiters: &EdifactDelimiters, out: &mut String) {
27    let elem_sep = delimiters.element as char;
28    let comp_sep = delimiters.component as char;
29    let seg_term = delimiters.segment as char;
30
31    out.push_str(&seg.tag);
32
33    for element in &seg.elements {
34        out.push(elem_sep);
35        // Preserve ALL components including trailing empty ones for roundtrip fidelity.
36        for (j, component) in element.iter().enumerate() {
37            if j > 0 {
38                out.push(comp_sep);
39            }
40            escape_component(component, delimiters, out);
41        }
42    }
43
44    out.push(seg_term);
45}
46
47/// Write a component value, escaping any delimiter characters with the release character.
48///
49/// All values — whether parsed from EDIFACT or synthetically created — are stored
50/// without escape sequences. The tokenizer unescapes during `OwnedSegment` creation,
51/// and the renderer re-escapes here when writing EDIFACT output.
52fn escape_component(value: &str, delimiters: &EdifactDelimiters, out: &mut String) {
53    delimiters.escape_into(value, out);
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn test_render_segments_to_edifact() {
62        let segments = vec![
63            DisassembledSegment {
64                tag: "UNH".to_string(),
65                elements: vec![
66                    vec!["1".to_string()],
67                    vec!["UTILMD".to_string(), "D".to_string(), "11A".to_string()],
68                ],
69            },
70            DisassembledSegment {
71                tag: "BGM".to_string(),
72                elements: vec![vec!["E01".to_string()]],
73            },
74        ];
75
76        let delimiters = EdifactDelimiters::default();
77        let rendered = render_edifact(&segments, &delimiters);
78
79        assert_eq!(rendered, "UNH+1+UTILMD:D:11A'BGM+E01'");
80    }
81
82    #[test]
83    fn test_render_empty_segments() {
84        let delimiters = EdifactDelimiters::default();
85        let rendered = render_edifact(&[], &delimiters);
86        assert_eq!(rendered, "");
87    }
88
89    #[test]
90    fn test_render_segment_with_empty_components() {
91        let segments = vec![DisassembledSegment {
92            tag: "CAV".to_string(),
93            elements: vec![vec![
94                "SA".to_string(),
95                String::new(),
96                String::new(),
97                String::new(),
98            ]],
99        }];
100
101        let delimiters = EdifactDelimiters::default();
102        let rendered = render_edifact(&segments, &delimiters);
103
104        // Trailing empty components should be preserved
105        assert_eq!(rendered, "CAV+SA:::'");
106    }
107
108    #[test]
109    fn test_render_multiple_elements() {
110        let segments = vec![DisassembledSegment {
111            tag: "DTM".to_string(),
112            elements: vec![vec![
113                "137".to_string(),
114                "20250101".to_string(),
115                "102".to_string(),
116            ]],
117        }];
118
119        let delimiters = EdifactDelimiters::default();
120        let rendered = render_edifact(&segments, &delimiters);
121
122        assert_eq!(rendered, "DTM+137:20250101:102'");
123    }
124
125    #[test]
126    fn test_render_escapes_delimiter_chars_in_values() {
127        // Timestamps with timezone offset contain '+' which must be escaped as '?+'
128        let segments = vec![DisassembledSegment {
129            tag: "DTM".to_string(),
130            elements: vec![vec![
131                "137".to_string(),
132                "202603021433+00".to_string(),
133                "303".to_string(),
134            ]],
135        }];
136
137        let delimiters = EdifactDelimiters::default();
138        let rendered = render_edifact(&segments, &delimiters);
139
140        assert_eq!(rendered, "DTM+137:202603021433?+00:303'");
141    }
142
143    #[test]
144    fn test_render_escapes_multiple_special_chars() {
145        // Value with ':', '+', and '?' all needing escape
146        let segments = vec![DisassembledSegment {
147            tag: "FTX".to_string(),
148            elements: vec![
149                vec!["ABO".to_string()],
150                vec![],
151                vec![],
152                vec!["hello?+world:test".to_string()],
153            ],
154        }];
155
156        let delimiters = EdifactDelimiters::default();
157        let rendered = render_edifact(&segments, &delimiters);
158
159        assert_eq!(rendered, "FTX+ABO+++hello???+world?:test'");
160    }
161
162    #[test]
163    fn test_render_no_escape_needed_for_plain_values() {
164        let segments = vec![DisassembledSegment {
165            tag: "BGM".to_string(),
166            elements: vec![vec!["312".to_string()], vec!["DOC001".to_string()]],
167        }];
168
169        let delimiters = EdifactDelimiters::default();
170        let rendered = render_edifact(&segments, &delimiters);
171
172        assert_eq!(rendered, "BGM+312+DOC001'");
173    }
174}