Skip to main content

hl7_2/
dictionary.rs

1//! The HL7 v2 dictionary: what a segment's fields mean, what a composite
2//! data type is made of, and how a message's segments group.
3//!
4//! This is the knowledge that turns `PID|1||241900||TEST^FOUAZ` from text
5//! into data — that PID-5 is an `XPN`, that an `XPN`'s first component is
6//! an `FN`, that an `ORU_R01` nests its `OBX` segments inside
7//! `PATIENT_RESULT.ORDER_OBSERVATION.OBSERVATION`. Every one of this
8//! crate's three modes reads it: generic mode names tree nodes from it,
9//! schema mode *is* it (a caller-supplied dictionary instead of a bundled
10//! one), and struct mode uses it to resolve the paths a `#[hl7(...)]`
11//! attribute names.
12//!
13//! A dictionary is JSON, and the same reader loads a bundled release and a
14//! dictionary a caller wrote for one vendor's dialect — see
15//! `spec/index.md` §3 for the format. Bundled dictionaries live in
16//! `schemas/` and are embedded at compile time; v2.5 is complete and every
17//! other release is expressed as a delta of it via `"inherits"`.
18
19use crate::json::{self, Value};
20use std::collections::BTreeMap;
21use std::fmt;
22
23/// The data type this crate uses for a field whose real type is carried in
24/// another field: OBX-5, whose type OBX-2 names. Callers that look up a
25/// field type will see this sentinel and should ask
26/// [`Dictionary::variable_type`] instead.
27pub const VARIABLE: &str = "VAR";
28
29/// The placeholder a sparse delta leaves in positions it did not mention —
30/// `{"MSH": {"12": "ID"}}` states field 12 and says nothing about fields
31/// after the end of the inherited list. [`Dictionary::field_type`] reports
32/// these as unknown, which is the same fallback an unlisted segment takes.
33const UNSTATED: &str = "";
34
35/// One entry in an abstract message structure: a segment, or a named group
36/// of entries, each carrying whether the standard makes it required and
37/// whether it may repeat.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum Item {
40    /// A segment, e.g. `MSH`.
41    Segment {
42        /// The three-character segment name.
43        name: String,
44        /// Whether the structure requires it.
45        required: bool,
46        /// Whether it may appear more than once here.
47        repeats: bool,
48    },
49    /// A named group, e.g. `ORDER_OBSERVATION`.
50    Group {
51        /// The group name, as HL7's structure tables spell it.
52        name: String,
53        /// Whether the structure requires it.
54        required: bool,
55        /// Whether it may appear more than once here.
56        repeats: bool,
57        /// What the group contains, in order.
58        items: Vec<Item>,
59    },
60}
61
62impl Item {
63    /// The segment or group name.
64    #[must_use]
65    pub fn name(&self) -> &str {
66        match self {
67            Item::Segment { name, .. } | Item::Group { name, .. } => name,
68        }
69    }
70
71    /// Whether the structure requires this item.
72    #[must_use]
73    pub fn required(&self) -> bool {
74        match self {
75            Item::Segment { required, .. } | Item::Group { required, .. } => *required,
76        }
77    }
78
79    /// Whether this item may appear more than once in a row.
80    #[must_use]
81    pub fn repeats(&self) -> bool {
82        match self {
83            Item::Segment { repeats, .. } | Item::Group { repeats, .. } => *repeats,
84        }
85    }
86
87    /// Can this item begin with a segment named `segment`?
88    ///
89    /// For a group this walks its leading optional items plus the first
90    /// required one — the group's FIRST set — because an optional leading
91    /// segment means a group can start at more than one segment name.
92    #[must_use]
93    pub fn can_start(&self, segment: &str) -> bool {
94        match self {
95            Item::Segment { name, .. } => name == segment,
96            Item::Group { items, .. } => {
97                for item in items {
98                    if item.can_start(segment) {
99                        return true;
100                    }
101                    if item.required() {
102                        return false;
103                    }
104                }
105                false
106            }
107        }
108    }
109}
110
111/// Segment field types, composite component types, and message structures
112/// for one HL7 release or one vendor dialect.
113///
114/// Build one with [`crate::Version::dictionary`] for a bundled release, or
115/// [`Dictionary::from_json`] for schema mode.
116#[derive(Debug, Clone, PartialEq, Eq, Default)]
117pub struct Dictionary {
118    name: String,
119    version: Option<String>,
120    types: BTreeMap<String, Vec<String>>,
121    segments: BTreeMap<String, Vec<String>>,
122    cardinality: BTreeMap<String, Vec<Cardinality>>,
123    structures: BTreeMap<String, Vec<Item>>,
124    aliases: BTreeMap<String, String>,
125}
126
127/// How many times a field may appear, and whether it has to.
128///
129/// A dictionary generated from XML Schema knows a field's `minOccurs` and
130/// `maxOccurs` as well as its data type, and both change what a conversion
131/// should emit: a required field is written even when the message leaves it
132/// empty, so the position stays visible, and a field that cannot repeat
133/// keeps its repetition separator as ordinary text rather than being split
134/// into several elements. Hand-written dictionaries usually say nothing
135/// about either, and then both default to false — see `spec/index.md` §3.2.
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
137pub struct Cardinality {
138    /// The schema requires this field to be present.
139    pub required: bool,
140    /// The schema lets this field appear more than once.
141    pub repeats: bool,
142}
143
144impl Dictionary {
145    /// An empty dictionary under which everything falls back to generic
146    /// positional names. Useful as a base to build on, and as the honest
147    /// answer for a message whose dialect is entirely unknown.
148    pub fn empty(name: impl Into<String>) -> Dictionary {
149        Dictionary {
150            name: name.into(),
151            ..Dictionary::default()
152        }
153    }
154
155    /// Where this dictionary came from, for error and diagnostic messages:
156    /// `"v2.5"` for a bundled release, or whatever name the caller gave.
157    #[must_use]
158    pub fn name(&self) -> &str {
159        &self.name
160    }
161
162    /// The HL7 release this dictionary declares in its `"version"` member,
163    /// if any. A vendor dialect need not declare one.
164    #[must_use]
165    pub fn version(&self) -> Option<&str> {
166        self.version.as_deref()
167    }
168
169    /// The component data types of composite type `data_type`, or `None`
170    /// when it is primitive (`ST`, `NM`, `DTM`, ...) or simply unknown —
171    /// the two are indistinguishable here on purpose, because both mean
172    /// "treat the value as a scalar".
173    pub fn composite_components(&self, data_type: &str) -> Option<&[String]> {
174        self.types.get(data_type).map(Vec::as_slice)
175    }
176
177    /// True when `data_type` is a composite this dictionary can break apart.
178    #[must_use]
179    pub fn is_composite(&self, data_type: &str) -> bool {
180        self.types.contains_key(data_type)
181    }
182
183    /// The field data types of `segment`, index 0 being field 1. `None` for
184    /// a segment the dictionary does not list, including Z-segments.
185    pub fn segment_fields(&self, segment: &str) -> Option<&[String]> {
186        self.segments.get(segment).map(Vec::as_slice)
187    }
188
189    /// The data type of `segment`-`field` (1-based), or `None` when either
190    /// the segment or the field number is outside the dictionary, or when a
191    /// sparse delta left this position unstated.
192    ///
193    /// May return the [`VARIABLE`] sentinel; see [`Dictionary::variable_type`].
194    pub fn field_type(&self, segment: &str, field: usize) -> Option<&str> {
195        let types = self.segment_fields(segment)?;
196        match types.get(field.checked_sub(1)?).map(String::as_str) {
197            Some(UNSTATED) | None => None,
198            found => found,
199        }
200    }
201
202    /// What the schema says about how often `segment`-`field` (1-based) may
203    /// appear.
204    ///
205    /// Defaults to optional and non-repeating, which is both the XML Schema
206    /// default for an unstated `maxOccurs` and the honest answer for a
207    /// dictionary that never mentioned cardinality at all.
208    #[must_use]
209    pub fn field_cardinality(&self, segment: &str, field: usize) -> Cardinality {
210        field
211            .checked_sub(1)
212            .and_then(|index| self.cardinality.get(segment)?.get(index).copied())
213            .unwrap_or_default()
214    }
215
216    /// Resolve a [`VARIABLE`] field's real type from the message.
217    ///
218    /// Only OBX-5 works this way: OBX-2 names the data type of the value in
219    /// OBX-5, so an `OBX|1|NM|...` carries a number where an `OBX|1|CE|...`
220    /// carries a coded element. Returns `None` when OBX-2 is empty or names
221    /// a type this dictionary does not know as a composite, in which case
222    /// the value is treated as a scalar.
223    #[must_use]
224    pub fn variable_type(&self, segment: &er7::Segment) -> Option<&str> {
225        let named = segment
226            .component(2, 1)?
227            .subcomponent(1)?
228            .raw
229            .trim()
230            .to_string();
231        self.types
232            .get_key_value(&named)
233            .map(|(key, _)| key.as_str())
234    }
235
236    /// The abstract message structure named `id`, e.g. `ORU_R01`.
237    pub fn structure(&self, id: &str) -> Option<&[Item]> {
238        self.structures.get(id).map(Vec::as_slice)
239    }
240
241    /// The message structure ID for a message-type code and trigger event,
242    /// e.g. `("ADT", "A04")` -> `ADT_A01`.
243    ///
244    /// Several trigger events share one structure — an A04 admit and an A08
245    /// update are both carried by `ADT_A01` — and which ones is dictionary
246    /// knowledge, so it lives in the `"aliases"` section rather than in
247    /// code. Resolution order: an alias, then a structure named
248    /// `CODE_TRIGGER`, then one named `CODE` (which is how `ACK^A01`
249    /// reaches `ACK`), then `CODE_TRIGGER` unresolved, so an unknown
250    /// message type still gets the name HL7 would give it.
251    #[must_use]
252    pub fn structure_id(&self, code: &str, trigger: &str) -> String {
253        if code.is_empty() {
254            return "HL7Message".to_string();
255        }
256        let joined = if trigger.is_empty() {
257            code.to_string()
258        } else {
259            format!("{code}_{trigger}")
260        };
261        if let Some(target) = self.aliases.get(&joined) {
262            return target.clone();
263        }
264        if self.structures.contains_key(&joined) {
265            return joined;
266        }
267        if self.structures.contains_key(code) {
268            return code.to_string();
269        }
270        joined
271    }
272
273    /// Every structure ID this dictionary defines, in name order.
274    pub fn structure_ids(&self) -> impl Iterator<Item = &str> {
275        self.structures.keys().map(String::as_str)
276    }
277
278    /// Every segment name this dictionary defines, in name order.
279    pub fn segment_names(&self) -> impl Iterator<Item = &str> {
280        self.segments.keys().map(String::as_str)
281    }
282
283    /// Every composite data type this dictionary defines, in name order.
284    pub fn type_names(&self) -> impl Iterator<Item = &str> {
285        self.types.keys().map(String::as_str)
286    }
287
288    /// Load a dictionary from JSON, resolving an `"inherits"` member
289    /// against this crate's bundled releases.
290    ///
291    /// This is schema mode's entry point: write the shape of the vendor's
292    /// messages as JSON, load it at runtime, and no recompile is needed
293    /// when the business adds a field.
294    ///
295    /// ```
296    /// let dictionary = hl7_2::Dictionary::from_json(r#"{
297    ///   "inherits": "2.5",
298    ///   "segments": { "ZPD": ["ST", "XPN", "TS"] }
299    /// }"#, "acme").unwrap();
300    /// assert_eq!(dictionary.field_type("ZPD", 2), Some("XPN"));
301    /// assert_eq!(dictionary.field_type("PID", 5), Some("XPN")); // inherited
302    /// ```
303    /// # Errors
304    ///
305    /// [`Error::Json`] when the text is not valid JSON, [`Error::Field`] or
306    /// [`Error::Missing`] when a member is the wrong shape or absent, and
307    /// [`Error::UnknownBase`] when `inherits` names a release this crate has
308    /// no dictionary for.
309    pub fn from_json(text: &str, name: impl Into<String>) -> Result<Dictionary, Error> {
310        Dictionary::from_json_resolving(text, name, |version| {
311            crate::Version::parse(version).map(crate::Version::dictionary)
312        })
313    }
314
315    /// Load a dictionary from JSON, layering it over `base` rather than
316    /// over a bundled release. An `"inherits"` member is ignored.
317    /// # Errors
318    ///
319    /// [`Error::Json`] when the text is not valid JSON, [`Error::Field`] or
320    /// [`Error::Missing`] when a member is the wrong shape or absent, and
321    /// [`Error::UnknownBase`] when `inherits` names a release this crate has
322    /// no dictionary for.
323    pub fn from_json_over(
324        text: &str,
325        name: impl Into<String>,
326        base: &Dictionary,
327    ) -> Result<Dictionary, Error> {
328        let name = name.into();
329        let value = json::parse(text).map_err(Error::Json)?;
330        let mut dictionary = base.clone();
331        dictionary.name = name;
332        dictionary.version = None;
333        dictionary.apply(&value)?;
334        Ok(dictionary)
335    }
336
337    /// Load a dictionary from JSON, resolving `"inherits"` through
338    /// `resolve`. Used internally to load the bundled releases (where
339    /// resolution must not recurse back through the public entry point) and
340    /// available to callers who keep their own set of base dictionaries.
341    /// # Errors
342    ///
343    /// [`Error::Json`] when the text is not valid JSON, [`Error::Field`] or
344    /// [`Error::Missing`] when a member is the wrong shape or absent, and
345    /// [`Error::UnknownBase`] when `inherits` names a release this crate has
346    /// no dictionary for.
347    pub fn from_json_resolving(
348        text: &str,
349        name: impl Into<String>,
350        resolve: impl Fn(&str) -> Option<std::sync::Arc<Dictionary>>,
351    ) -> Result<Dictionary, Error> {
352        let name = name.into();
353        let value = json::parse(text).map_err(Error::Json)?;
354        let mut dictionary = match value.get("inherits") {
355            None => Dictionary::empty(name.clone()),
356            Some(Value::String(base)) => match resolve(base) {
357                Some(base) => Dictionary {
358                    name: name.clone(),
359                    ..(*base).clone()
360                },
361                None => return Err(Error::UnknownBase(base.clone())),
362            },
363            Some(other) => {
364                return Err(Error::field("inherits", "a version string", other));
365            }
366        };
367        dictionary.apply(&value)?;
368        Ok(dictionary)
369    }
370
371    /// Layer one parsed dictionary document over `self`: listed entries
372    /// replace what was there, `null` entries remove it, and everything
373    /// unmentioned is inherited untouched. That is what makes a per-release
374    /// delta file small enough to read.
375    fn apply(&mut self, value: &Value) -> Result<(), Error> {
376        if value.as_object().is_none() {
377            return Err(Error::field("<document>", "an object", value));
378        }
379        if let Some(version) = value.get("version") {
380            match version.as_str() {
381                Some(text) => self.version = Some(text.to_string()),
382                None => return Err(Error::field("version", "a version string", version)),
383            }
384        }
385        for section in ["types", "segments"] {
386            let Some(members) = value.get(section) else {
387                continue;
388            };
389            let members = members
390                .as_object()
391                .ok_or_else(|| Error::field(section, "an object", members))?;
392            for (key, entry) in members {
393                let is_segments = section == "segments";
394                let table = if is_segments {
395                    &mut self.segments
396                } else {
397                    &mut self.types
398                };
399                if entry.is_null() {
400                    table.remove(key);
401                    if is_segments {
402                        self.cardinality.remove(key);
403                    }
404                    continue;
405                }
406                let inherited = table.get(key).cloned().unwrap_or_default();
407                let inherited_cardinality = if is_segments {
408                    self.cardinality.get(key).cloned().unwrap_or_default()
409                } else {
410                    Vec::new()
411                };
412                let (names, cardinality) = positions(
413                    entry,
414                    inherited,
415                    inherited_cardinality,
416                    &format!("{section}.{key}"),
417                )?;
418                table.insert(key.clone(), names);
419                // Composite components do not repeat and are not
420                // individually required, so cardinality is kept for
421                // segments only.
422                if is_segments {
423                    self.cardinality.insert(key.clone(), cardinality);
424                }
425            }
426        }
427        if let Some(aliases) = value.get("aliases") {
428            let members = aliases
429                .as_object()
430                .ok_or_else(|| Error::field("aliases", "an object", aliases))?;
431            for (key, entry) in members {
432                if entry.is_null() {
433                    self.aliases.remove(key);
434                    continue;
435                }
436                let target = entry.as_str().ok_or_else(|| {
437                    Error::field(&format!("aliases.{key}"), "a structure ID", entry)
438                })?;
439                self.aliases.insert(key.clone(), target.to_string());
440            }
441        }
442        if let Some(structures) = value.get("structures") {
443            let members = structures
444                .as_object()
445                .ok_or_else(|| Error::field("structures", "an object", structures))?;
446            for (key, entry) in members {
447                if entry.is_null() {
448                    self.structures.remove(key);
449                    continue;
450                }
451                let items = parse_items(entry, &format!("structures.{key}"))?;
452                self.structures.insert(key.clone(), items);
453            }
454        }
455        Ok(())
456    }
457}
458
459/// Read a list of data types in either of the two forms a dictionary may
460/// write it.
461///
462/// An array states the whole list and replaces what was inherited. An
463/// object states individual 1-based positions and leaves the rest of the
464/// inherited list alone — `{"12": "ID"}` is how v2.1 says "MSH-12 is a
465/// plain ID here" without restating the other twenty fields, and without
466/// claiming anything about which fields that release did or did not have.
467///
468/// Either form may write a position as an object rather than a bare name
469/// when the schema says more than the type — see [`entry_of`].
470fn positions(
471    value: &Value,
472    inherited: Vec<String>,
473    inherited_cardinality: Vec<Cardinality>,
474    path: &str,
475) -> Result<(Vec<String>, Vec<Cardinality>), Error> {
476    if let Some(list) = value.as_array() {
477        let mut names = Vec::with_capacity(list.len());
478        let mut cardinality = Vec::with_capacity(list.len());
479        for (index, item) in list.iter().enumerate() {
480            let (name, card) = entry_of(item, &format!("{path}[{index}]"))?;
481            names.push(name);
482            cardinality.push(card);
483        }
484        return Ok((names, cardinality));
485    }
486    let members = value
487        .as_object()
488        .ok_or_else(|| Error::field(path, "an array, or an object of position overrides", value))?;
489    let mut names = inherited;
490    let mut cardinality = inherited_cardinality;
491    for (key, entry) in members {
492        let path = format!("{path}.{key}");
493        let position: usize = key
494            .parse()
495            .ok()
496            .filter(|position| *position > 0)
497            .ok_or_else(|| Error::Field {
498                path: path.clone(),
499                expected: "a 1-based position number".to_string(),
500                found: format!("{key:?}"),
501            })?;
502        let (name, card) = entry_of(entry, &path)?;
503        if names.len() < position {
504            names.resize(position, UNSTATED.to_string());
505        }
506        if cardinality.len() < position {
507            cardinality.resize(position, Cardinality::default());
508        }
509        names[position - 1] = name;
510        cardinality[position - 1] = card;
511    }
512    // A sparse delta may state a position past the end of what it inherited,
513    // and the two tables are indexed together, so they stay the same length.
514    cardinality.resize(names.len(), Cardinality::default());
515    Ok((names, cardinality))
516}
517
518/// Read one position: a bare data type name, or an object that also carries
519/// what the schema said about how often the field may appear.
520///
521/// `"XPN"` and `{"type": "XPN"}` mean the same thing. The object form exists
522/// for dictionaries generated from XML Schema, which know `minOccurs` and
523/// `maxOccurs` as well: `{"type": "XTN", "repeats": true}`.
524fn entry_of(value: &Value, path: &str) -> Result<(String, Cardinality), Error> {
525    if let Some(name) = value.as_str() {
526        return Ok((name.to_string(), Cardinality::default()));
527    }
528    // Anything that is neither a name nor an object cannot be either form,
529    // so it is reported against the commoner one.
530    if value.as_object().is_none() {
531        return Err(Error::field(path, "a data type name", value));
532    }
533    let name = value
534        .get("type")
535        .ok_or_else(|| Error::missing(&format!("{path}.type")))?
536        .as_str()
537        .ok_or_else(|| {
538            Error::field(
539                &format!("{path}.type"),
540                "a data type name",
541                value.get("type").unwrap_or(value),
542            )
543        })?;
544    Ok((
545        name.to_string(),
546        Cardinality {
547            required: flag(value, "required", path)?,
548            repeats: flag(value, "repeats", path)?,
549        },
550    ))
551}
552
553/// Read a structure's item list: an array of segment names, segment
554/// objects, and group objects.
555fn parse_items(value: &Value, path: &str) -> Result<Vec<Item>, Error> {
556    let list = value
557        .as_array()
558        .ok_or_else(|| Error::field(path, "an array of structure items", value))?;
559    let mut items = Vec::with_capacity(list.len());
560    for (index, entry) in list.iter().enumerate() {
561        let path = format!("{path}[{index}]");
562        // Shorthand: a bare string is an optional, non-repeating segment,
563        // which is what most entries in a hand-written structure are.
564        if let Some(name) = entry.as_str() {
565            items.push(Item::Segment {
566                name: name.to_string(),
567                required: false,
568                repeats: false,
569            });
570            continue;
571        }
572        let required = flag(entry, "required", &path)?;
573        let repeats = flag(entry, "repeats", &path)?;
574        if let Some(name) = entry.get("segment") {
575            let name = name
576                .as_str()
577                .ok_or_else(|| Error::field(&format!("{path}.segment"), "a segment name", name))?;
578            items.push(Item::Segment {
579                name: name.to_string(),
580                required,
581                repeats,
582            });
583        } else if let Some(name) = entry.get("group") {
584            let name = name
585                .as_str()
586                .ok_or_else(|| Error::field(&format!("{path}.group"), "a group name", name))?;
587            let children = entry
588                .get("items")
589                .ok_or_else(|| Error::missing(&format!("{path}.items")))?;
590            items.push(Item::Group {
591                name: name.to_string(),
592                required,
593                repeats,
594                items: parse_items(children, &format!("{path}.items"))?,
595            });
596        } else {
597            return Err(Error::field(
598                &path,
599                "an item with a `segment` or `group` member",
600                entry,
601            ));
602        }
603    }
604    Ok(items)
605}
606
607/// Read an optional boolean member, defaulting to false.
608fn flag(entry: &Value, name: &str, path: &str) -> Result<bool, Error> {
609    match entry.get(name) {
610        None => Ok(false),
611        Some(value) => value
612            .as_bool()
613            .ok_or_else(|| Error::field(&format!("{path}.{name}"), "true or false", value)),
614    }
615}
616
617/// Why a dictionary could not be loaded.
618#[derive(Debug, Clone, PartialEq, Eq)]
619pub enum Error {
620    /// The text is not valid JSON.
621    Json(json::Error),
622    /// A member is present but the wrong shape.
623    Field {
624        /// Where in the document, e.g. `segments.PID[3]`.
625        path: String,
626        /// What was expected there.
627        expected: String,
628        /// What was found instead.
629        found: String,
630    },
631    /// A required member is absent.
632    Missing {
633        /// Where in the document the member was expected.
634        path: String,
635    },
636    /// `"inherits"` names a release this crate has no dictionary for.
637    UnknownBase(String),
638}
639
640impl Error {
641    fn field(path: &str, expected: &str, found: &Value) -> Error {
642        Error::Field {
643            path: path.to_string(),
644            expected: expected.to_string(),
645            found: found.kind().to_string(),
646        }
647    }
648
649    fn missing(path: &str) -> Error {
650        Error::Missing {
651            path: path.to_string(),
652        }
653    }
654}
655
656impl fmt::Display for Error {
657    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
658        match self {
659            Error::Json(error) => write!(f, "{error}"),
660            Error::Field {
661                path,
662                expected,
663                found,
664            } => write!(f, "{path}: expected {expected}, found {found}"),
665            Error::Missing { path } => write!(f, "{path}: required member is missing"),
666            Error::UnknownBase(base) => {
667                write!(f, "`inherits`: {base:?} is not a known HL7 version")
668            }
669        }
670    }
671}
672
673impl std::error::Error for Error {}
674
675#[cfg(test)]
676mod tests {
677    use super::*;
678    use crate::Version;
679
680    #[test]
681    fn reads_the_base_release() {
682        let dictionary = Version::V2_5.dictionary();
683        assert_eq!(dictionary.field_type("PID", 5), Some("XPN"));
684        assert_eq!(dictionary.field_type("MSH", 9), Some("MSG"));
685        assert_eq!(dictionary.field_type("OBX", 5), Some(VARIABLE));
686        assert_eq!(dictionary.field_type("PID", 999), None);
687        assert_eq!(dictionary.field_type("ZZZ", 1), None);
688        assert_eq!(
689            dictionary
690                .composite_components("XPN")
691                .map(|c| c[0].as_str()),
692            Some("FN")
693        );
694        assert!(!dictionary.is_composite("ST"));
695        assert!(dictionary.structure("ORU_R01").is_some());
696    }
697
698    #[test]
699    fn a_delta_adds_removes_and_inherits() {
700        let dictionary = Dictionary::from_json(
701            r#"{
702                "inherits": "2.5",
703                "types": { "TS": ["ST"], "XPN": null },
704                "segments": { "ZPD": ["ST", "CX"] },
705                "structures": { "ORU_R01": null }
706            }"#,
707            "test",
708        )
709        .unwrap();
710        assert_eq!(dictionary.composite_components("TS").unwrap(), ["ST"]); // replaced
711        assert_eq!(dictionary.composite_components("XPN"), None); // removed
712        assert_eq!(dictionary.field_type("ZPD", 2), Some("CX")); // added
713        assert_eq!(dictionary.field_type("PID", 5), Some("XPN")); // inherited
714        assert_eq!(dictionary.structure("ORU_R01"), None); // removed
715        assert!(dictionary.structure("ACK").is_some()); // inherited
716        assert_eq!(dictionary.name(), "test");
717    }
718
719    #[test]
720    fn a_sparse_delta_restates_one_position_and_keeps_the_rest() {
721        let dictionary = Dictionary::from_json(
722            r#"{"inherits": "2.5", "segments": {"MSH": {"12": "ID"}}}"#,
723            "test",
724        )
725        .unwrap();
726        assert_eq!(dictionary.field_type("MSH", 12), Some("ID")); // overridden
727        assert_eq!(dictionary.field_type("MSH", 9), Some("MSG")); // untouched
728        assert_eq!(dictionary.field_type("MSH", 21), Some("EI")); // untouched
729        // A position past the inherited end is stated; the gap before it is
730        // unknown rather than silently typed.
731        let dictionary =
732            Dictionary::from_json(r#"{"segments": {"ZZZ": {"3": "CX"}}}"#, "test").unwrap();
733        assert_eq!(dictionary.field_type("ZZZ", 3), Some("CX"));
734        assert_eq!(dictionary.field_type("ZZZ", 1), None);
735        let error =
736            Dictionary::from_json(r#"{"segments": {"ZZZ": {"0": "CX"}}}"#, "test").unwrap_err();
737        assert!(error.to_string().contains("1-based position"), "{error}");
738    }
739
740    #[test]
741    fn reads_structures_including_the_string_shorthand() {
742        let dictionary = Dictionary::from_json(
743            r#"{"structures": {"ZZZ_Z01": [
744                {"segment": "MSH", "required": true},
745                "NTE",
746                {"group": "ORDER", "repeats": true, "items": [{"segment": "ORC", "required": true}]}
747            ]}}"#,
748            "test",
749        )
750        .unwrap();
751        let items = dictionary.structure("ZZZ_Z01").unwrap();
752        assert!(matches!(&items[0], Item::Segment { name, required: true, .. } if name == "MSH"));
753        assert!(matches!(
754            &items[1],
755            Item::Segment {
756                required: false,
757                repeats: false,
758                ..
759            }
760        ));
761        assert!(items[2].repeats() && !items[2].required());
762        assert!(items[2].can_start("ORC"));
763        assert!(!items[2].can_start("OBX"));
764    }
765
766    #[test]
767    fn a_group_can_start_at_any_leading_optional_segment() {
768        // ORU_R01's PATIENT_RESULT starts at PID (optional PATIENT group)
769        // or at ORC/OBR (the required ORDER_OBSERVATION group).
770        let dictionary = Version::V2_5.dictionary();
771        let items = dictionary.structure("ORU_R01").unwrap();
772        let patient_result = &items[2];
773        assert_eq!(patient_result.name(), "PATIENT_RESULT");
774        assert!(patient_result.can_start("PID"));
775        assert!(patient_result.can_start("OBR"));
776        assert!(!patient_result.can_start("MSA"));
777    }
778
779    #[test]
780    fn resolves_obx_5_through_obx_2() {
781        let dictionary = Version::V2_5.dictionary();
782        let message = er7::parse("MSH|^~\\&|A||||1||ORU^R01|1|P|2.5\rOBX|1|CE|X||a^b").unwrap();
783        let obx = message.segment("OBX").unwrap();
784        assert_eq!(dictionary.variable_type(obx), Some("CE"));
785        let message = er7::parse("MSH|^~\\&|A||||1||ORU^R01|1|P|2.5\rOBX|1|NM|X||7").unwrap();
786        assert_eq!(
787            dictionary.variable_type(message.segment("OBX").unwrap()),
788            None
789        );
790    }
791
792    #[test]
793    fn a_field_may_state_its_cardinality_as_well_as_its_type() {
794        let dictionary = Dictionary::from_json(
795            r#"{"segments": {"PID": [
796                 "SI",
797                 {"type": "CX", "required": true},
798                 {"type": "XTN", "repeats": true},
799                 {"type": "ST", "required": true, "repeats": true}
800               ]}}"#,
801            "x",
802        )
803        .unwrap();
804        // The bare name and the object form name the same data type.
805        assert_eq!(dictionary.field_type("PID", 1), Some("SI"));
806        assert_eq!(dictionary.field_type("PID", 2), Some("CX"));
807        assert_eq!(
808            dictionary.field_cardinality("PID", 1),
809            Cardinality::default()
810        );
811        assert_eq!(
812            dictionary.field_cardinality("PID", 2),
813            Cardinality {
814                required: true,
815                repeats: false
816            }
817        );
818        assert_eq!(
819            dictionary.field_cardinality("PID", 3),
820            Cardinality {
821                required: false,
822                repeats: true
823            }
824        );
825        assert_eq!(
826            dictionary.field_cardinality("PID", 4),
827            Cardinality {
828                required: true,
829                repeats: true
830            }
831        );
832        // Off the end, and a segment that was never mentioned, both default.
833        assert_eq!(
834            dictionary.field_cardinality("PID", 99),
835            Cardinality::default()
836        );
837        assert_eq!(
838            dictionary.field_cardinality("ZZZ", 1),
839            Cardinality::default()
840        );
841        assert_eq!(
842            dictionary.field_cardinality("PID", 0),
843            Cardinality::default()
844        );
845    }
846
847    #[test]
848    fn cardinality_layers_and_is_removed_like_everything_else() {
849        // A sparse override states one position and leaves the rest alone.
850        let dictionary = Dictionary::from_json(
851            r#"{"inherits": "2.5", "segments": {"PID": {"13": {"type": "XTN", "repeats": true}}}}"#,
852            "x",
853        )
854        .unwrap();
855        assert!(dictionary.field_cardinality("PID", 13).repeats);
856        assert!(!dictionary.field_cardinality("PID", 5).repeats);
857        assert_eq!(dictionary.field_type("PID", 5), Some("XPN")); // still inherited
858
859        // Removing the segment removes what was said about its fields too.
860        let dictionary =
861            Dictionary::from_json(r#"{"inherits": "2.5", "segments": {"PID": null}}"#, "x")
862                .unwrap();
863        assert_eq!(
864            dictionary.field_cardinality("PID", 13),
865            Cardinality::default()
866        );
867    }
868
869    #[test]
870    fn reports_where_a_malformed_dictionary_is_wrong() {
871        let error = Dictionary::from_json(r#"{"segments": {"PID": [1]}}"#, "x").unwrap_err();
872        assert_eq!(
873            error.to_string(),
874            "segments.PID[0]: expected a data type name, found number"
875        );
876        let error = Dictionary::from_json(r#"{"inherits": "9.9"}"#, "x").unwrap_err();
877        assert!(matches!(error, Error::UnknownBase(_)), "{error}");
878        let error =
879            Dictionary::from_json(r#"{"structures": {"A": [{"group": "G"}]}}"#, "x").unwrap_err();
880        assert_eq!(
881            error.to_string(),
882            "structures.A[0].items: required member is missing"
883        );
884        assert!(matches!(
885            Dictionary::from_json("not json", "x"),
886            Err(Error::Json(_))
887        ));
888    }
889
890    #[test]
891    fn layering_over_an_explicit_base_ignores_inherits() {
892        let base = Dictionary::from_json(r#"{"segments": {"AAA": ["ST"]}}"#, "base").unwrap();
893        let over = Dictionary::from_json_over(
894            r#"{"inherits": "2.5", "segments": {"BBB": ["NM"]}}"#,
895            "over",
896            &base,
897        )
898        .unwrap();
899        assert_eq!(over.field_type("AAA", 1), Some("ST"));
900        assert_eq!(over.field_type("BBB", 1), Some("NM"));
901        assert_eq!(
902            over.field_type("PID", 5),
903            None,
904            "2.5 must not have been pulled in"
905        );
906    }
907}