Skip to main content

edifact_primitives/
delimiters.rs

1/// Error when parsing a UNA service string advice segment.
2#[derive(Debug, Clone, PartialEq, Eq)]
3pub enum UnaParseError {
4    /// UNA segment must be exactly 9 bytes.
5    InvalidLength { expected: usize, actual: usize },
6    /// UNA segment must start with "UNA".
7    InvalidPrefix,
8}
9
10impl std::fmt::Display for UnaParseError {
11    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
12        match self {
13            Self::InvalidLength { expected, actual } => {
14                write!(
15                    f,
16                    "UNA segment must be exactly {expected} bytes, got {actual}"
17                )
18            }
19            Self::InvalidPrefix => write!(f, "UNA segment must start with 'UNA'"),
20        }
21    }
22}
23
24impl std::error::Error for UnaParseError {}
25
26/// EDIFACT delimiter characters.
27///
28/// The six characters that control EDIFACT message structure. When no UNA
29/// service string advice is present, the standard defaults apply:
30/// - Component separator: `:` (colon)
31/// - Element separator: `+` (plus)
32/// - Decimal mark: `.` (period)
33/// - Release character: `?` (question mark)
34/// - Segment terminator: `'` (apostrophe)
35/// - Reserved: ` ` (space)
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub struct EdifactDelimiters {
38    /// Component data element separator (default: `:`).
39    pub component: u8,
40    /// Data element separator (default: `+`).
41    pub element: u8,
42    /// Decimal mark (default: `.`).
43    pub decimal: u8,
44    /// Release character / escape (default: `?`).
45    pub release: u8,
46    /// Segment terminator (default: `'`).
47    pub segment: u8,
48    /// Reserved for future use (default: ` `).
49    pub reserved: u8,
50}
51
52impl Default for EdifactDelimiters {
53    fn default() -> Self {
54        Self {
55            component: b':',
56            element: b'+',
57            decimal: b'.',
58            release: b'?',
59            segment: b'\'',
60            reserved: b' ',
61        }
62    }
63}
64
65impl EdifactDelimiters {
66    /// Standard EDIFACT delimiters (when no UNA segment is present).
67    pub const STANDARD: Self = Self {
68        component: b':',
69        element: b'+',
70        decimal: b'.',
71        release: b'?',
72        segment: b'\'',
73        reserved: b' ',
74    };
75
76    /// Parse delimiters from a UNA service string advice segment.
77    ///
78    /// The UNA segment is exactly 9 bytes: `UNA` followed by 6 delimiter characters.
79    /// Format: `UNA<component><element><decimal><release><reserved><terminator>`
80    ///
81    /// # Errors
82    ///
83    /// Returns an error if the input is not exactly 9 bytes or does not start with `UNA`.
84    pub fn from_una(una: &[u8]) -> Result<Self, UnaParseError> {
85        if una.len() != 9 {
86            return Err(UnaParseError::InvalidLength {
87                expected: 9,
88                actual: una.len(),
89            });
90        }
91
92        if &una[0..3] != b"UNA" {
93            return Err(UnaParseError::InvalidPrefix);
94        }
95
96        // UNA format positions:
97        // 0-2: "UNA"
98        // 3: component separator
99        // 4: element separator
100        // 5: decimal mark
101        // 6: release character
102        // 7: reserved
103        // 8: segment terminator
104        Ok(Self {
105            component: una[3],
106            element: una[4],
107            decimal: una[5],
108            release: una[6],
109            reserved: una[7],
110            segment: una[8],
111        })
112    }
113
114    /// Detect delimiters from an EDIFACT message.
115    ///
116    /// If the message starts with a UNA segment, parses delimiters from it.
117    /// Otherwise, returns the standard defaults.
118    ///
119    /// Returns `(has_una, delimiters)`.
120    pub fn detect(input: &[u8]) -> (bool, Self) {
121        if input.len() >= 9 && &input[0..3] == b"UNA" {
122            match Self::from_una(&input[0..9]) {
123                Ok(d) => (true, d),
124                Err(_) => (false, Self::default()),
125            }
126        } else {
127            (false, Self::default())
128        }
129    }
130
131    /// Formats the delimiters as a UNA service string advice segment.
132    ///
133    /// Returns the 9-byte UNA string: `UNA:+.? '`
134    pub fn to_una_string(&self) -> String {
135        format!(
136            "UNA{}{}{}{}{}{}",
137            self.component as char,
138            self.element as char,
139            self.decimal as char,
140            self.release as char,
141            self.reserved as char,
142            self.segment as char,
143        )
144    }
145}
146
147impl std::fmt::Display for EdifactDelimiters {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        write!(
150            f,
151            "UNA{}{}{}{}{}{}",
152            self.component as char,
153            self.element as char,
154            self.decimal as char,
155            self.release as char,
156            self.reserved as char,
157            self.segment as char,
158        )
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_default_delimiters() {
168        let d = EdifactDelimiters::default();
169        assert_eq!(d.component, b':');
170        assert_eq!(d.element, b'+');
171        assert_eq!(d.decimal, b'.');
172        assert_eq!(d.release, b'?');
173        assert_eq!(d.segment, b'\'');
174        assert_eq!(d.reserved, b' ');
175    }
176
177    #[test]
178    fn test_delimiters_equality() {
179        let a = EdifactDelimiters::default();
180        let b = EdifactDelimiters::default();
181        assert_eq!(a, b);
182    }
183
184    #[test]
185    fn test_delimiters_debug() {
186        let d = EdifactDelimiters::default();
187        let debug = format!("{:?}", d);
188        assert!(debug.contains("EdifactDelimiters"));
189    }
190
191    #[test]
192    fn test_from_una_standard() {
193        let una = b"UNA:+.? '";
194        let d = EdifactDelimiters::from_una(una).unwrap();
195        assert_eq!(d, EdifactDelimiters::default());
196    }
197
198    #[test]
199    fn test_from_una_custom_delimiters() {
200        let una = b"UNA;*.# |";
201        let d = EdifactDelimiters::from_una(una).unwrap();
202        assert_eq!(d.component, b';');
203        assert_eq!(d.element, b'*');
204        assert_eq!(d.decimal, b'.');
205        assert_eq!(d.release, b'#');
206        assert_eq!(d.reserved, b' ');
207        assert_eq!(d.segment, b'|');
208    }
209
210    #[test]
211    fn test_from_una_too_short() {
212        let una = b"UNA:+.";
213        assert!(EdifactDelimiters::from_una(una).is_err());
214    }
215
216    #[test]
217    fn test_from_una_wrong_prefix() {
218        let una = b"XXX:+.? '";
219        assert!(EdifactDelimiters::from_una(una).is_err());
220    }
221
222    #[test]
223    fn test_detect_with_una() {
224        let input = b"UNA:+.? 'UNB+UNOC:3+sender+recipient'";
225        let (has_una, delimiters) = EdifactDelimiters::detect(input);
226        assert!(has_una);
227        assert_eq!(delimiters, EdifactDelimiters::default());
228    }
229
230    #[test]
231    fn test_detect_without_una() {
232        let input = b"UNB+UNOC:3+sender+recipient'";
233        let (has_una, delimiters) = EdifactDelimiters::detect(input);
234        assert!(!has_una);
235        assert_eq!(delimiters, EdifactDelimiters::default());
236    }
237
238    #[test]
239    fn test_detect_empty_input() {
240        let input = b"";
241        let (has_una, delimiters) = EdifactDelimiters::detect(input);
242        assert!(!has_una);
243        assert_eq!(delimiters, EdifactDelimiters::default());
244    }
245
246    #[test]
247    fn test_una_roundtrip() {
248        let original = EdifactDelimiters {
249            component: b';',
250            element: b'*',
251            decimal: b',',
252            release: b'#',
253            segment: b'!',
254            reserved: b' ',
255        };
256        let una_string = original.to_una_string();
257        let parsed = EdifactDelimiters::from_una(una_string.as_bytes()).unwrap();
258        assert_eq!(original, parsed);
259    }
260}