x12_delimiters/
lib.rs

1pub mod errors;
2
3use errors::DelimiterError;
4
5const DEFAULT_SEGMENT_TERMINATOR: u8 = b'~';
6const DEFAULT_ELEMENT_SEPARATOR: u8 = b'*';
7const DEFAULT_SUB_ELEMENT_SEPARATOR: u8 = b':';
8
9const ISA_MIN_LENGTH: usize = 106;
10const ISA_ELEMENT_SEPARATOR_INDEX: usize = 3;
11const ISA_SUB_ELEMENT_SEPARATOR_INDEX: usize = 104;
12const ISA_SEGMENT_TERMINATOR_INDEX: usize = 105;
13
14/// Represents the three delimiter types used in X12 EDI transactions.
15///
16/// X12 delimiters control how segments, elements, and sub-elements are separated in the EDI data.
17/// The standard default delimiters are:
18/// - Segment terminator: `~`
19/// - Element separator: `*`
20/// - Sub-element separator: `:`
21#[derive(Debug, PartialEq, Eq, Clone, Copy)]
22pub struct Delimiters {
23    segment_terminator: u8,
24    element_separator: u8,
25    sub_element_separator: u8,
26}
27
28impl Delimiters {
29    /// Creates a new Delimiters instance with the specified values.
30    ///
31    /// # Arguments
32    /// * `segment_terminator` - Character used to terminate segments
33    /// * `element_separator` - Character used to separate elements
34    /// * `sub_element_separator` - Character used to separate sub-elements
35    pub fn new(segment_terminator: u8, element_separator: u8, sub_element_separator: u8) -> Self {
36        Delimiters {
37            segment_terminator,
38            element_separator,
39            sub_element_separator,
40        }
41    }
42
43    /// Extracts delimiters from an ISA segment.
44    ///
45    /// The ISA segment is the first segment in an X12 file and contains the delimiter information.
46    /// - Element separator is at position 3
47    /// - Sub-element separator is at position 104
48    /// - Segment terminator is at position 105
49    ///
50    /// # Arguments
51    /// * `isa_segment` - Byte slice containing the ISA segment
52    ///
53    /// # Returns
54    /// * `Result<Delimiters, DelimiterError>` - Delimiters on success, error on failure
55    ///
56    /// # Errors
57    /// Returns `DelimiterError::InvalidIsaLength` if the ISA segment is too short
58    pub fn from_isa(isa_segment: &[u8]) -> Result<Self, DelimiterError> {
59        if isa_segment.len() < ISA_MIN_LENGTH {
60            return Err(DelimiterError::InvalidIsaLength);
61        }
62
63        let element_separator = isa_segment[ISA_ELEMENT_SEPARATOR_INDEX];
64        let sub_element_separator = isa_segment[ISA_SUB_ELEMENT_SEPARATOR_INDEX];
65        let segment_terminator = isa_segment[ISA_SEGMENT_TERMINATOR_INDEX];
66
67        Ok(Delimiters {
68            element_separator,
69            sub_element_separator,
70            segment_terminator,
71        })
72    }
73
74    /// Returns the segment terminator character.
75    pub fn segment_terminator(&self) -> u8 {
76        self.segment_terminator
77    }
78
79    /// Returns the element separator character.
80    pub fn element_separator(&self) -> u8 {
81        self.element_separator
82    }
83
84    /// Returns the sub-element separator character.
85    pub fn sub_element_separator(&self) -> u8 {
86        self.sub_element_separator
87    }
88
89    /// Validates that all three delimiters are distinct.
90    ///
91    /// In X12 EDI, all delimiters must be different characters to avoid ambiguity.
92    ///
93    /// # Returns
94    /// * `bool` - True if all delimiters are unique, false otherwise
95    pub fn are_valid(&self) -> bool {
96        self.segment_terminator != self.element_separator &&
97        self.segment_terminator != self.sub_element_separator &&
98        self.element_separator != self.sub_element_separator
99    }
100}
101
102impl Default for Delimiters {
103    /// Creates a Delimiters instance with the standard default values.
104    fn default() -> Self {
105        Delimiters {
106            segment_terminator: DEFAULT_SEGMENT_TERMINATOR,
107            element_separator: DEFAULT_ELEMENT_SEPARATOR,
108            sub_element_separator: DEFAULT_SUB_ELEMENT_SEPARATOR,
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    const SAMPLE_ISA_SEGMENT_STANDARD: &[u8] = b"ISA*00*          *00*          *ZZ*SENDERID       *ZZ*RECEIVERID     *250403*0856*U*00501*000000001*0*P*:~";
118    const SAMPLE_ISA_SEGMENT_ALT: &[u8] = b"ISA^00^          ^00^          ^ZZ^SENDERID       ^ZZ^RECEIVERID     ^250403^0856^U^00401^000000002^1^T^>}";
119    const TOO_SHORT_ISA: &[u8] = b"ISA*00*";
120
121    #[test]
122    fn test_default_delimiters() {
123        let delimiters = Delimiters::default();
124        assert_eq!(delimiters.segment_terminator(), b'~');
125        assert_eq!(delimiters.element_separator(), b'*');
126        assert_eq!(delimiters.sub_element_separator(), b':');
127    }
128
129    #[test]
130    fn test_new_delimiters() {
131        let delimiters = Delimiters::new(b'!', b'@', b'#');
132        assert_eq!(delimiters.segment_terminator(), b'!');
133        assert_eq!(delimiters.element_separator(), b'@');
134        assert_eq!(delimiters.sub_element_separator(), b'#');
135    }
136
137    #[test]
138    fn test_from_isa_standard() {
139        let result = Delimiters::from_isa(SAMPLE_ISA_SEGMENT_STANDARD);
140        assert!(result.is_ok());
141        let delimiters = result.unwrap();
142        assert_eq!(delimiters.segment_terminator(), b'~');
143        assert_eq!(delimiters.element_separator(), b'*');
144        assert_eq!(delimiters.sub_element_separator(), b':');
145    }
146
147    #[test]
148    fn test_from_isa_alternative() {
149        let result = Delimiters::from_isa(SAMPLE_ISA_SEGMENT_ALT);
150        assert!(result.is_ok());
151        let delimiters = result.unwrap();
152        assert_eq!(delimiters.segment_terminator(), b'}');
153        assert_eq!(delimiters.element_separator(), b'^');
154        assert_eq!(delimiters.sub_element_separator(), b'>');
155    }
156
157    #[test]
158    fn test_from_isa_too_short() {
159        let result = Delimiters::from_isa(TOO_SHORT_ISA);
160        assert!(result.is_err());
161        assert_eq!(result.err().unwrap(), DelimiterError::InvalidIsaLength);
162    }
163
164    #[test]
165    fn test_from_isa_exact_length() {
166        let exact_len_isa = SAMPLE_ISA_SEGMENT_STANDARD[..ISA_MIN_LENGTH].to_vec();
167        assert_eq!(exact_len_isa.len(), ISA_MIN_LENGTH);
168        
169        let result = Delimiters::from_isa(&exact_len_isa);
170        assert!(result.is_ok());
171        let delimiters = result.unwrap();
172        assert_eq!(delimiters.segment_terminator(), b'~');
173        assert_eq!(delimiters.element_separator(), b'*');
174        assert_eq!(delimiters.sub_element_separator(), b':');
175    }
176
177    #[test]
178    fn test_getters() {
179        let delimiters = Delimiters::new(b'A', b'B', b'C');
180        assert_eq!(delimiters.segment_terminator(), b'A');
181        assert_eq!(delimiters.element_separator(), b'B');
182        assert_eq!(delimiters.sub_element_separator(), b'C');
183    }
184
185    #[test]
186    fn test_are_valid() {
187        let valid_delimiters = Delimiters::new(b'~', b'*', b':');
188        assert!(valid_delimiters.are_valid());
189        
190        let invalid_delimiters1 = Delimiters::new(b'*', b'*', b':'); 
191        assert!(!invalid_delimiters1.are_valid());
192        
193        let invalid_delimiters2 = Delimiters::new(b'~', b'*', b'*');
194        assert!(!invalid_delimiters2.are_valid());
195        
196        let invalid_delimiters3 = Delimiters::new(b'~', b'~', b':');
197        assert!(!invalid_delimiters3.are_valid());
198    }
199
200    use proptest::prelude::*;
201
202    fn valid_delimiter() -> impl Strategy<Value = u8> {
203        (33..=126u8).prop_filter("Avoiding whitespace", |&c| c != b' ' && c != b'\t' && c != b'\n' && c != b'\r')
204    }
205
206    fn distinct_delimiters() -> impl Strategy<Value = (u8, u8, u8)> {
207        (valid_delimiter(), valid_delimiter(), valid_delimiter())
208            .prop_filter("Delimiters must be distinct", |(a, b, c)| a != b && b != c && a != c)
209    }
210
211    fn isa_segment_with_delimiters() -> impl Strategy<Value = (Vec<u8>, u8, u8, u8)> {
212        distinct_delimiters().prop_flat_map(|(elem_sep, sub_elem_sep, seg_term)| {
213            let mut isa = Vec::with_capacity(ISA_MIN_LENGTH);
214            isa.extend_from_slice(b"ISA");
215            isa.push(elem_sep);
216            
217            for i in 4..ISA_SUB_ELEMENT_SEPARATOR_INDEX {
218                if i % 2 == 0 {
219                    isa.push(elem_sep);
220                } else {
221                    isa.push(b'X');
222                }
223            }
224            
225            while isa.len() < ISA_SUB_ELEMENT_SEPARATOR_INDEX {
226                isa.push(b'X');
227            }
228            
229            isa.push(sub_elem_sep);
230            isa.push(seg_term);
231            
232            Just((isa, elem_sep, sub_elem_sep, seg_term))
233        })
234    }
235
236    fn isa_segment_extended() -> impl Strategy<Value = (Vec<u8>, u8, u8, u8)> {
237        isa_segment_with_delimiters().prop_flat_map(|(isa, elem_sep, sub_elem_sep, seg_term)| {
238            (0..=10).prop_map(move |n| {
239                let mut extended_isa = isa.clone();
240                for _ in 0..n {
241                    extended_isa.push(b'X');
242                }
243                (extended_isa, elem_sep, sub_elem_sep, seg_term)
244            })
245        })
246    }
247
248    fn invalid_length_isa() -> impl Strategy<Value = Vec<u8>> {
249        (1..ISA_MIN_LENGTH).prop_map(|len| {
250            let mut isa = Vec::with_capacity(len);
251            isa.extend_from_slice(b"ISA*"); 
252            while isa.len() < len {
253                isa.push(b'X');
254            }
255            isa
256        })
257    }
258
259    proptest! {
260        #[test]
261        fn prop_from_isa_extracts_correct_delimiters(
262            (isa, elem_sep, sub_elem_sep, seg_term) in isa_segment_with_delimiters()
263        ) {
264            let result = Delimiters::from_isa(&isa);
265            prop_assert!(result.is_ok(), "from_isa should succeed on valid ISA segment");
266            
267            let delimiters = result.unwrap();
268            prop_assert_eq!(delimiters.element_separator(), elem_sep);
269            prop_assert_eq!(delimiters.sub_element_separator(), sub_elem_sep);
270            prop_assert_eq!(delimiters.segment_terminator(), seg_term);
271        }
272
273        #[test]
274        fn prop_from_isa_works_with_extended_segments(
275            (isa, elem_sep, sub_elem_sep, seg_term) in isa_segment_extended()
276        ) {
277            let result = Delimiters::from_isa(&isa);
278            prop_assert!(result.is_ok(), "from_isa should succeed on extended ISA segment");
279            
280            let delimiters = result.unwrap();
281            prop_assert_eq!(delimiters.element_separator(), elem_sep);
282            prop_assert_eq!(delimiters.sub_element_separator(), sub_elem_sep);
283            prop_assert_eq!(delimiters.segment_terminator(), seg_term);
284        }
285
286        #[test]
287        fn prop_new_delimiters_preserves_values(
288            (seg_term, elem_sep, sub_elem_sep) in distinct_delimiters()
289        ) {
290            let delimiters = Delimiters::new(seg_term, elem_sep, sub_elem_sep);
291            prop_assert_eq!(delimiters.segment_terminator(), seg_term);
292            prop_assert_eq!(delimiters.element_separator(), elem_sep);
293            prop_assert_eq!(delimiters.sub_element_separator(), sub_elem_sep);
294        }
295
296        #[test]
297        fn prop_delimiter_roundtrip(
298            (seg_term, elem_sep, sub_elem_sep) in distinct_delimiters()
299        ) {
300            let delimiters1 = Delimiters::new(seg_term, elem_sep, sub_elem_sep);
301            
302            let delimiters2 = Delimiters::new(
303                delimiters1.segment_terminator(),
304                delimiters1.element_separator(),
305                delimiters1.sub_element_separator()
306            );
307            
308            prop_assert_eq!(delimiters1, delimiters2);
309        }
310
311        #[test]
312        fn prop_delimiter_equality(
313            (s1, e1, se1) in distinct_delimiters(), 
314            (s2, e2, se2) in distinct_delimiters()
315        ) {
316            let d1 = Delimiters::new(s1, e1, se1);
317            let d2 = Delimiters::new(s1, e1, se1);
318            let d3 = Delimiters::new(s2, e2, se2);
319
320            prop_assert_eq!(d1, d2);
321            
322            if s1 != s2 || e1 != e2 || se1 != se2 {
323                prop_assert_ne!(d1, d3);
324            }
325        }
326
327        #[test]
328        fn prop_invalid_length_isa_returns_error(
329            isa in invalid_length_isa()
330        ) {
331            let result = Delimiters::from_isa(&isa);
332            prop_assert!(result.is_err());
333            prop_assert_eq!(result.err().unwrap(), DelimiterError::InvalidIsaLength);
334        }
335
336        #[test]
337        fn prop_valid_delimiters_check(
338            (seg_term, elem_sep, sub_elem_sep) in distinct_delimiters()
339        ) {
340            let valid = Delimiters::new(seg_term, elem_sep, sub_elem_sep);
341            prop_assert!(valid.are_valid());
342            
343            let invalid1 = Delimiters::new(seg_term, seg_term, sub_elem_sep);
344            prop_assert!(!invalid1.are_valid());
345            
346            let invalid2 = Delimiters::new(seg_term, elem_sep, seg_term);
347            prop_assert!(!invalid2.are_valid());
348            
349            let invalid3 = Delimiters::new(seg_term, elem_sep, elem_sep);
350            prop_assert!(!invalid3.are_valid());
351        }
352    }
353}