Skip to main content

hl7v2_query/
lib.rs

1//! HL7 v2 path-based field access and query functionality.
2//!
3//! This crate provides query functionality for HL7 v2 messages,
4//! including:
5//! - Path-based field access via [`get`]
6//! - Presence semantics via [`get_presence`]
7//!
8//! # Path Format
9//!
10//! Paths use the format: `SEGMENT.FIELD\[REP\].COMPONENT`
11//!
12//! Examples:
13//! - `PID.5.1` - First component of 5th field in PID segment (first repetition)
14//! - `PID.5[2].1` - First component of 5th field, second repetition
15//! - `MSH.9` - 9th field of MSH segment
16//! - `MSH.9.1` - First component of 9th field of MSH segment
17//!
18//! # Example
19//!
20//! ```
21//! use hl7v2_model::Message;
22//! use hl7v2_query::get;
23//!
24//! // Assuming you have a parsed Message from hl7v2-parser
25//! // let message = hl7v2_parser::parse(hl7_bytes).unwrap();
26//! // let last_name = get(&message, "PID.5.1").unwrap();
27//! ```
28
29use hl7v2_model::{Atom, Message, Presence, Segment};
30
31/// Get value at path (e.g., `PID.5[1].1`)
32///
33/// # Arguments
34///
35/// * `msg` - The message to query
36/// * `path` - The path to the field (e.g., `PID.5.1`, `PID.5[1].1`, `MSH.9`)
37///
38/// # Returns
39///
40/// The value at the path, or `None` if not found
41///
42/// # Example
43///
44/// ```
45/// use hl7v2_query::get;
46/// use hl7v2_model::{Message, Segment, Field, Rep, Comp, Atom, Delims};
47///
48/// // Create a minimal message for testing
49/// let message = Message {
50///     delims: Delims::default(),
51///     segments: vec![],
52///     charsets: vec![],
53/// };
54///
55/// // Returns None for missing segment
56/// assert!(get(&message, "PID.5.1").is_none());
57/// ```
58pub fn get<'a>(msg: &'a Message, path: &str) -> Option<&'a str> {
59    // Parse the path
60    // Format: SEGMENT.FIELD\[REP\].COMPONENT
61    // Examples: `PID.5.1`, `PID.5[1].1`, `MSH.9`
62
63    let mut parts = path.split('.');
64    let segment_id = parts.next()?;
65
66    // Find the segment
67    let segment = msg
68        .segments
69        .iter()
70        .find(|s| std::str::from_utf8(&s.id) == Ok(segment_id))?;
71
72    // Parse field index (1-based)
73    let field_part = parts.next()?;
74    let (field_index, rep_index) = parse_field_and_rep(field_part)?;
75
76    // Special handling for MSH segments
77    if segment_id == "MSH" {
78        get_msh_field(msg, segment, field_index, rep_index, parts)
79    } else {
80        get_field(segment, field_index, rep_index, parts)
81    }
82}
83
84/// Get presence semantics for a field at path.
85///
86/// Presence semantics distinguish between:
87/// - `Presence::Value(String)` - Field exists with a value
88/// - `Presence::Empty` - Field exists but is empty
89/// - `Presence::Null` - Field is explicitly null (HL7 null value: "")
90/// - `Presence::Missing` - Field does not exist
91///
92/// # Arguments
93///
94/// * `msg` - The message to query
95/// * `path` - The path to the field
96///
97/// # Returns
98///
99/// The presence status of the field
100///
101/// # Example
102///
103/// ```
104/// use hl7v2_query::get_presence;
105/// use hl7v2_model::{Message, Delims, Presence};
106///
107/// let message = Message {
108///     delims: Delims::default(),
109///     segments: vec![],
110///     charsets: vec![],
111/// };
112///
113/// assert!(matches!(get_presence(&message, "PID.5.1"), Presence::Missing));
114/// ```
115pub fn get_presence(msg: &Message, path: &str) -> Presence {
116    // Parse the path
117    let mut parts = path.split('.');
118    let segment_id = match parts.next() {
119        Some(id) => id,
120        None => return Presence::Missing,
121    };
122
123    // Find the segment
124    let segment = match msg
125        .segments
126        .iter()
127        .find(|s| std::str::from_utf8(&s.id) == Ok(segment_id))
128    {
129        Some(seg) => seg,
130        None => return Presence::Missing,
131    };
132
133    // Parse field index (1-based)
134    let field_part = match parts.next() {
135        Some(part) => part,
136        None => return Presence::Missing,
137    };
138
139    let (field_index, rep_index) = match parse_field_and_rep(field_part) {
140        Some(indices) => indices,
141        None => return Presence::Missing,
142    };
143
144    // Special handling for MSH segments
145    if segment_id == "MSH" {
146        get_msh_field_presence(msg, segment, field_index, rep_index, parts)
147    } else {
148        get_field_presence(segment, field_index, rep_index, parts)
149    }
150}
151
152// ============================================================================
153// Internal helper functions
154// ============================================================================
155
156/// Parse field and repetition indices from a string like "5" or "5[1]"
157fn parse_field_and_rep(field_str: &str) -> Option<(usize, usize)> {
158    if let Some(bracket_pos) = field_str.find('[') {
159        // Has repetition index
160        let field_index = field_str[..bracket_pos].parse::<usize>().ok()?;
161        let rep_part = &field_str[bracket_pos + 1..];
162        if let Some(end_bracket) = rep_part.find(']') {
163            let rep_index = rep_part[..end_bracket].parse::<usize>().ok()?;
164            Some((field_index, rep_index))
165        } else {
166            None
167        }
168    } else {
169        // No repetition index, default to 1
170        let field_index = field_str.parse::<usize>().ok()?;
171        Some((field_index, 1))
172    }
173}
174
175/// Get field value from a non-MSH segment
176fn get_field<'a>(
177    segment: &'a Segment,
178    field_index: usize,
179    rep_index: usize,
180    mut parts: std::str::Split<char>,
181) -> Option<&'a str> {
182    // Convert to 0-based indexing
183    if field_index == 0 {
184        return None;
185    }
186    let zero_based_field_index = field_index - 1;
187
188    // Get the field
189    if zero_based_field_index >= segment.fields.len() {
190        return None;
191    }
192    let field = &segment.fields[zero_based_field_index];
193
194    // Get the repetition
195    if rep_index == 0 || rep_index > field.reps.len() {
196        return None;
197    }
198    let rep = &field.reps[rep_index - 1];
199
200    // Parse component index if provided
201    let comp_index = if let Some(comp_part) = parts.next() {
202        comp_part.parse::<usize>().ok()?
203    } else {
204        1 // Default to first component
205    };
206
207    // Get the component
208    if comp_index == 0 || comp_index > rep.comps.len() {
209        return None;
210    }
211    let comp = &rep.comps[comp_index - 1];
212
213    // Get the first subcomponent as text
214    if comp.subs.is_empty() {
215        return None;
216    }
217
218    match &comp.subs[0] {
219        Atom::Text(text) => Some(text.as_str()),
220        Atom::Null => None,
221    }
222}
223
224/// Get field value from an MSH segment
225fn get_msh_field<'a>(
226    _msg: &'a Message,
227    segment: &'a Segment,
228    field_index: usize,
229    rep_index: usize,
230    mut parts: std::str::Split<char>,
231) -> Option<&'a str> {
232    if field_index == 1 {
233        // MSH-1 is the field separator character
234        None // We can't return a reference to a temporary
235    } else if field_index == 2 {
236        // MSH-2 is the encoding characters
237        if segment.fields.is_empty() {
238            return None;
239        }
240        let field = &segment.fields[0];
241        if rep_index == 0 || rep_index > field.reps.len() {
242            return None;
243        }
244        let rep = &field.reps[rep_index - 1];
245        let comp_index = if let Some(comp_part) = parts.next() {
246            comp_part.parse::<usize>().ok()?
247        } else {
248            1
249        };
250        if comp_index == 0 || comp_index > rep.comps.len() {
251            return None;
252        }
253        let comp = &rep.comps[comp_index - 1];
254        if comp.subs.is_empty() {
255            return None;
256        }
257        match &comp.subs[0] {
258            Atom::Text(text) => Some(text.as_str()),
259            Atom::Null => None,
260        }
261    } else {
262        // MSH-3 and beyond
263        let adjusted_field_index = field_index - 2;
264        if adjusted_field_index >= segment.fields.len() {
265            return None;
266        }
267        let field = &segment.fields[adjusted_field_index];
268        if rep_index == 0 || rep_index > field.reps.len() {
269            return None;
270        }
271        let rep = &field.reps[rep_index - 1];
272        let comp_index = if let Some(comp_part) = parts.next() {
273            comp_part.parse::<usize>().ok()?
274        } else {
275            1
276        };
277        if comp_index == 0 || comp_index > rep.comps.len() {
278            return None;
279        }
280        let comp = &rep.comps[comp_index - 1];
281        if comp.subs.is_empty() {
282            return None;
283        }
284        match &comp.subs[0] {
285            Atom::Text(text) => Some(text.as_str()),
286            Atom::Null => None,
287        }
288    }
289}
290
291/// Get field presence from a non-MSH segment
292fn get_field_presence(
293    segment: &Segment,
294    field_index: usize,
295    rep_index: usize,
296    mut parts: std::str::Split<char>,
297) -> Presence {
298    if field_index == 0 {
299        return Presence::Missing;
300    }
301    let zero_based_field_index = field_index - 1;
302
303    if zero_based_field_index >= segment.fields.len() {
304        return Presence::Missing;
305    }
306    let field = &segment.fields[zero_based_field_index];
307
308    if rep_index == 0 || rep_index > field.reps.len() {
309        return Presence::Missing;
310    }
311    let rep = &field.reps[rep_index - 1];
312
313    let comp_index = if let Some(comp_part) = parts.next() {
314        match comp_part.parse::<usize>() {
315            Ok(index) => index,
316            Err(_) => return Presence::Missing,
317        }
318    } else {
319        1
320    };
321
322    if comp_index == 0 || comp_index > rep.comps.len() {
323        return Presence::Missing;
324    }
325    let comp = &rep.comps[comp_index - 1];
326
327    if comp.subs.is_empty() {
328        return Presence::Missing;
329    }
330
331    match &comp.subs[0] {
332        Atom::Text(text) => {
333            if text.is_empty() {
334                Presence::Empty
335            } else {
336                Presence::Value(text.clone())
337            }
338        }
339        Atom::Null => Presence::Null,
340    }
341}
342
343/// Get field presence from an MSH segment
344fn get_msh_field_presence(
345    msg: &Message,
346    segment: &Segment,
347    field_index: usize,
348    rep_index: usize,
349    mut parts: std::str::Split<char>,
350) -> Presence {
351    if field_index == 1 {
352        // MSH-1 is the field separator character
353        Presence::Value(msg.delims.field.to_string())
354    } else if field_index == 2 {
355        if segment.fields.is_empty() {
356            return Presence::Missing;
357        }
358        let field = &segment.fields[0];
359        if rep_index == 0 || rep_index > field.reps.len() {
360            return Presence::Missing;
361        }
362        let rep = &field.reps[rep_index - 1];
363        let comp_index = if let Some(comp_part) = parts.next() {
364            match comp_part.parse::<usize>() {
365                Ok(index) => index,
366                Err(_) => return Presence::Missing,
367            }
368        } else {
369            1
370        };
371        if comp_index == 0 || comp_index > rep.comps.len() {
372            return Presence::Missing;
373        }
374        let comp = &rep.comps[comp_index - 1];
375        if comp.subs.is_empty() {
376            return Presence::Missing;
377        }
378        match &comp.subs[0] {
379            Atom::Text(text) => {
380                if text.is_empty() {
381                    Presence::Empty
382                } else {
383                    Presence::Value(text.clone())
384                }
385            }
386            Atom::Null => Presence::Null,
387        }
388    } else {
389        let adjusted_field_index = field_index - 2;
390        if adjusted_field_index >= segment.fields.len() {
391            return Presence::Missing;
392        }
393        let field = &segment.fields[adjusted_field_index];
394        if rep_index == 0 || rep_index > field.reps.len() {
395            return Presence::Missing;
396        }
397        let rep = &field.reps[rep_index - 1];
398        let comp_index = if let Some(comp_part) = parts.next() {
399            match comp_part.parse::<usize>() {
400                Ok(index) => index,
401                Err(_) => return Presence::Missing,
402            }
403        } else {
404            1
405        };
406        if comp_index == 0 || comp_index > rep.comps.len() {
407            return Presence::Missing;
408        }
409        let comp = &rep.comps[comp_index - 1];
410        if comp.subs.is_empty() {
411            return Presence::Missing;
412        }
413        match &comp.subs[0] {
414            Atom::Text(text) => {
415                if text.is_empty() {
416                    Presence::Empty
417                } else {
418                    Presence::Value(text.clone())
419                }
420            }
421            Atom::Null => Presence::Null,
422        }
423    }
424}
425
426#[cfg(test)]
427mod tests;