Skip to main content

haste_hl7v2/
parser.rs

1use haste_fhir_model::r4::generated::resources::{
2    HL7V2, HL7V2Segments, HL7V2SegmentsFields, HL7V2SegmentsFieldsValue,
3    HL7V2SegmentsFieldsValueValue,
4};
5use haste_fhir_model::r4::generated::terminology::IssueType;
6use haste_fhir_model::r4::generated::types::{FHIRId, FHIRString};
7use haste_fhir_operation_error::OperationOutcomeError;
8
9#[derive(Debug, Clone)]
10pub struct ParsedHL7V2Message(pub HL7V2);
11
12/// Marks an element that holds an empty value. JSON drops an empty string, so
13/// without the marker an empty field, component or subcomponent would vanish
14/// and shift the positions after it. A value that is not empty needs none.
15const EMPTY_VALUE_MARKER: &str = "_non_empty";
16
17fn parse_empty_string(v: String) -> FHIRString {
18    let empty = v.is_empty();
19    let mut fhir_string: FHIRString = v.into();
20
21    if empty {
22        fhir_string.id = Some(EMPTY_VALUE_MARKER.to_string());
23    }
24    fhir_string
25}
26
27fn parse_empty_id(v: String) -> FHIRId {
28    let empty = v.is_empty();
29    let mut fhir_id: FHIRId = v.into();
30
31    if empty {
32        fhir_id.id = Some(EMPTY_VALUE_MARKER.to_string());
33    }
34    fhir_id
35}
36
37/// Splits one field on the repetition, component and subcomponent
38/// separators, in that order.
39fn parse_field(field: &str, encoding_characters: &str) -> HL7V2SegmentsFields {
40    let fields = field
41        .split(encoding_characters.chars().nth(1).unwrap_or('~'))
42        .map(|field| {
43            let components = field
44                .split(encoding_characters.chars().next().unwrap_or('^'))
45                .map(|component| {
46                    let subcomponent = component
47                        .split(encoding_characters.chars().nth(3).unwrap_or('&'))
48                        .collect::<Vec<_>>();
49                    if subcomponent.len() > 1 {
50                        HL7V2SegmentsFieldsValueValue {
51                            value: None,
52                            subcomponents: Some(
53                                subcomponent
54                                    .iter()
55                                    .map(|s| parse_empty_string(s.to_string()))
56                                    .collect(),
57                            ),
58                        }
59                    } else {
60                        HL7V2SegmentsFieldsValueValue {
61                            value: Some(Box::new(parse_empty_string(component.to_string()))),
62                            subcomponents: None,
63                        }
64                    }
65                })
66                .collect::<Vec<_>>();
67            if components.len() > 1 {
68                HL7V2SegmentsFieldsValue {
69                    value: None,
70                    components: Some(components),
71                }
72            } else {
73                HL7V2SegmentsFieldsValue {
74                    value: components.into_iter().next(),
75                    components: None,
76                }
77            }
78        })
79        .collect::<Vec<_>>();
80    if fields.len() > 1 {
81        HL7V2SegmentsFields {
82            value: None,
83            repetitions: Some(fields),
84        }
85    } else {
86        HL7V2SegmentsFields {
87            value: Some(fields.into_iter().next().unwrap()),
88            repetitions: None,
89        }
90    }
91}
92
93impl TryFrom<&str> for ParsedHL7V2Message {
94    type Error = OperationOutcomeError;
95
96    fn try_from(value: &str) -> Result<Self, Self::Error> {
97        let mut segments = vec![];
98
99        let segment_lines = value.split(['\r', '\n']).filter(|s| !s.is_empty());
100
101        // A malformed message is a caller error, so every rejection is
102        // `invalid` (HTTP 400).
103        if !value.starts_with("MSH") {
104            return Err(OperationOutcomeError::error(
105                IssueType::invalid(),
106                "Message does not start with MSH segment".to_string(),
107            ));
108        }
109
110        // `MSH|^~\&|...`: the 4th character is the field separator, and the
111        // encoding characters run up to the next one.
112        let mut header_chars = value.chars();
113        let field_seperator = header_chars.nth(3).ok_or_else(|| {
114            OperationOutcomeError::error(
115                IssueType::invalid(),
116                "Missing field separator".to_string(),
117            )
118        })?;
119
120        let encoding_characters: String =
121            header_chars.take_while(|&c| c != field_seperator).collect();
122        if encoding_characters.is_empty() {
123            return Err(OperationOutcomeError::error(
124                IssueType::invalid(),
125                "Missing encoding characters".to_string(),
126            ));
127        }
128
129        for segment in segment_lines {
130            let mut segment = segment.split(field_seperator);
131            let segment_id = segment.next().ok_or_else(|| {
132                OperationOutcomeError::error(IssueType::invalid(), "Missing segment ID".to_string())
133            })?;
134            let is_header = segment_id == "MSH";
135
136            let segment_fields = segment.enumerate().map(|(index, field)| {
137                // MSH-2 holds the encoding characters themselves, so it is
138                // kept as one value rather than split on them.
139                if is_header && index == 0 {
140                    return HL7V2SegmentsFields {
141                        value: Some(HL7V2SegmentsFieldsValue {
142                            value: Some(HL7V2SegmentsFieldsValueValue {
143                                value: Some(Box::new(parse_empty_string(field.to_string()))),
144                                subcomponents: None,
145                            }),
146                            components: None,
147                        }),
148                        repetitions: None,
149                    };
150                }
151
152                parse_field(field, &encoding_characters)
153            });
154
155            segments.push(HL7V2Segments {
156                id: Box::new(parse_empty_id(segment_id.to_string())),
157                fields: Some(segment_fields.collect()),
158            });
159        }
160
161        Ok(ParsedHL7V2Message(HL7V2 {
162            fieldSeparator: Box::new(parse_empty_string(field_seperator.to_string())),
163            segments: Some(segments),
164            ..Default::default()
165        }))
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use crate::serialize::SerializeMessage;
172
173    use super::*;
174
175    #[test]
176    fn test_parse_hl7v2_message() {
177        let input = std::fs::read_to_string("./test_data/message1.bin").unwrap();
178
179        let result = ParsedHL7V2Message::try_from(input.as_str());
180
181        assert!(result.is_ok());
182
183        let message = result.unwrap().0;
184        let segments = message.segments.expect("message should contain segments");
185        assert_eq!(segments.len(), 8);
186
187        assert_eq!(segments[1].id.value.as_deref(), Some("SCH"));
188
189        let sch_fields = segments[1]
190            .fields
191            .clone()
192            .expect("SCH should contain fields");
193        assert_eq!(sch_fields.len(), 25);
194        assert_eq!(
195            sch_fields[0]
196                .value
197                .clone()
198                .unwrap()
199                .components
200                .unwrap()
201                .into_iter()
202                .map(|c| c.value.unwrap().value.unwrap())
203                .collect::<Vec<_>>(),
204            vec!["10345".to_string(), "10345".to_string()]
205        );
206
207        assert_eq!(segments[2].id.value.as_deref(), Some("PID"));
208        assert_eq!(segments[3].id.value.as_deref(), Some("PV1"));
209        assert_eq!(segments[4].id.value.as_deref(), Some("RGS"));
210        assert_eq!(segments[5].id.value.as_deref(), Some("AIG"));
211        assert_eq!(segments[6].id.value.as_deref(), Some("AIL"));
212        assert_eq!(segments[7].id.value.as_deref(), Some("AIP"));
213    }
214
215    #[test]
216    fn round_trip() {
217        let input = std::fs::read_to_string("./test_data/message1.bin").unwrap();
218        let result = ParsedHL7V2Message::try_from(input.as_str());
219        assert!(result.is_ok());
220
221        let message = result.unwrap();
222        let serialized: String = (SerializeMessage(&message.0)).into();
223
224        pretty_assertions::assert_eq!(serialized, input);
225    }
226
227    #[test]
228    fn round_trip_json_serialize() {
229        let input = std::fs::read_to_string("./test_data/message1.bin").unwrap();
230        let result = ParsedHL7V2Message::try_from(input.as_str());
231        assert!(result.is_ok());
232
233        let message = result.unwrap();
234        let json = serde_json::to_string_pretty(&message.0).unwrap();
235        println!("{json}");
236        let deserialized: HL7V2 = serde_json::from_str(&json).unwrap();
237        let serialized: String = (SerializeMessage(&deserialized)).into();
238
239        pretty_assertions::assert_eq!(serialized, input);
240    }
241}