Skip to main content

fhir_core/
meta.rs

1//! The shape of the per-element metadata table, shared by every release.
2//!
3//! Each release generates its own table of [`ElementMeta`] — the facts the
4//! specification states about an element that the Rust types cannot carry, such
5//! as whether a repeating field was `0..*` or `1..*`, which value set a code is
6//! bound to, and which resources a `Reference` may point at. The *types* in
7//! that table, and the lookups over it, do not vary by release, so they are
8//! defined once here and used by [`r4::meta`](crate::r4::meta) and
9//! [`r5::meta`](crate::r5::meta).
10//!
11//! ```
12//! use fhir::r5::meta;
13//!
14//! let gender = meta::element("Patient.gender").unwrap();
15//! assert_eq!(gender.binding.unwrap().strength, fhir::meta::BindingStrength::Required);
16//! ```
17
18use std::collections::HashMap;
19
20/// Binding strength for a coded element
21/// (`ElementDefinition.binding.strength`).
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum BindingStrength {
24    /// The value must come from the bound value set.
25    Required,
26    /// Codes from the value set should be used; others allowed if none fit.
27    Extensible,
28    /// The value set is a suggestion.
29    Preferred,
30    /// The value set is illustrative only.
31    Example,
32}
33
34impl BindingStrength {
35    /// Parse a FHIR strength token (`"required"`, …); unknown tokens map to
36    /// [`Example`](Self::Example).
37    #[must_use]
38    pub fn from_token(token: &str) -> Self {
39        match token {
40            "required" => Self::Required,
41            "extensible" => Self::Extensible,
42            "preferred" => Self::Preferred,
43            _ => Self::Example,
44        }
45    }
46}
47
48/// A value-set binding on a coded element.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct BindingMeta {
51    /// How strictly the value set applies.
52    pub strength: BindingStrength,
53    /// Canonical `ValueSet` URL (may carry a `|version` suffix), if declared.
54    pub value_set: Option<&'static str>,
55}
56
57/// One allowed type for an element (an entry of `ElementDefinition.type`).
58///
59/// A `value[x]` choice element has one `TypeRef` per allowed type; a reference
60/// element carries its allowed target resource profiles.
61#[derive(Debug, Clone, Copy)]
62pub struct TypeRef {
63    /// FHIR type code, e.g. `"Quantity"`, `"string"`, `"Reference"`.
64    pub code: &'static str,
65    /// For `Reference`/`canonical` types, the allowed target resource profiles
66    /// as canonical URLs; empty otherwise.
67    pub target_profiles: &'static [&'static str],
68}
69
70impl TypeRef {
71    /// The bare target resource names (final path segment of each profile URL).
72    ///
73    /// ```
74    /// use fhir::r5::meta;
75    /// let subject = meta::element("Observation.subject").unwrap();
76    /// let targets: Vec<_> = subject.types[0].target_names().collect();
77    /// assert!(targets.contains(&"Patient"));
78    /// ```
79    pub fn target_names(&self) -> impl Iterator<Item = &'static str> {
80        self.target_profiles
81            .iter()
82            .map(|url| url.rsplit(['/', '#']).next().unwrap_or(url))
83    }
84}
85
86/// Metadata for one element of a FHIR resource or datatype, keyed by its full
87/// `ElementDefinition` path.
88#[derive(Debug, Clone, Copy)]
89pub struct ElementMeta {
90    /// Full FHIR path, e.g. `"Patient.gender"` or `"Observation.value[x]"`.
91    pub path: &'static str,
92    /// Minimum cardinality.
93    pub min: u32,
94    /// Maximum cardinality as the raw FHIR token: `"0"`, `"1"`, `"*"`, or a
95    /// number.
96    pub max: &'static str,
97    /// Whether the element is part of the summary view
98    /// (`ElementDefinition.isSummary`).
99    pub is_summary: bool,
100    /// Coded-value binding, if any.
101    pub binding: Option<BindingMeta>,
102    /// Allowed types; more than one for a `value[x]` choice element.
103    pub types: &'static [TypeRef],
104    /// The element path whose content defines this one, for the recursive
105    /// backbones FHIR expresses with `contentReference`.
106    ///
107    /// `Questionnaire.item.item` does not restate an item's elements; it points
108    /// at `Questionnaire.item`. The target is not always an ancestor —
109    /// `TestScript.test.action.operation` refers to
110    /// `TestScript.setup.action.operation` — so it cannot be recovered from the
111    /// path and has to be carried here.
112    pub content_reference: Option<&'static str>,
113}
114
115impl ElementMeta {
116    /// Whether the element is mandatory (minimum cardinality ≥ 1).
117    #[must_use]
118    pub fn is_required(&self) -> bool {
119        self.min >= 1
120    }
121
122    /// Whether the element repeats (maximum cardinality greater than one).
123    #[must_use]
124    pub fn is_multiple(&self) -> bool {
125        self.max != "0" && self.max != "1"
126    }
127
128    /// Whether the element is a `value[x]`-style choice element.
129    #[must_use]
130    pub fn is_choice(&self) -> bool {
131        self.path.ends_with("[x]")
132    }
133
134    /// The FHIR type codes allowed for this element.
135    pub fn type_codes(&self) -> impl Iterator<Item = &'static str> {
136        self.types.iter().map(|t| t.code)
137    }
138}
139
140/// Look up an element by full FHIR path in a release's table.
141///
142/// The table is generated sorted by path, so this is a binary search.
143#[must_use]
144pub fn find(table: &'static [ElementMeta], path: &str) -> Option<&'static ElementMeta> {
145    table
146        .binary_search_by(|e| e.path.cmp(path))
147        .ok()
148        .map(|i| &table[i])
149}
150
151/// Look up an element, resolving a `value[x]` choice key to its choice element.
152///
153/// `path` is the literal path being looked up (`"Observation.valueQuantity"`),
154/// `context` the path or datatype name the element sits in (`"Observation"`),
155/// and `name` the JSON/XML key (`"valueQuantity"`). A direct hit wins; failing
156/// that, the choice element whose base name prefixes `name` at a type-name
157/// boundary is returned, so `valueQuantity` resolves to `Observation.value[x]`.
158///
159/// ```
160/// use fhir_core::meta;
161///
162/// let table = fhir::r5::meta::elements();
163/// let el = meta::resolve(table, "Observation.valueQuantity", "Observation", "valueQuantity").unwrap();
164/// assert_eq!(el.path, "Observation.value[x]");
165/// ```
166#[must_use]
167pub fn resolve(
168    table: &'static [ElementMeta],
169    path: &str,
170    context: &str,
171    name: &str,
172) -> Option<&'static ElementMeta> {
173    if let Some(e) = find(table, path) {
174        return Some(e);
175    }
176    let prefix = format!("{context}.");
177    table
178        .iter()
179        .filter(|e| e.path.starts_with(&prefix))
180        .find(|e| {
181            e.path.ends_with("[x]") && {
182                let base = &e.path[context.len() + 1..e.path.len() - 3];
183                name.len() > base.len()
184                    && name.starts_with(base)
185                    && name[base.len()..]
186                        .chars()
187                        .next()
188                        .is_some_and(char::is_uppercase)
189            }
190        })
191}
192
193/// The type-name suffix of a choice key, e.g. `"Quantity"` for `valueQuantity`
194/// against the choice element `Observation.value[x]`.
195///
196/// Returns `None` if `name` is not a variant of `choice`.
197#[must_use]
198pub fn choice_suffix<'a>(choice: &ElementMeta, name: &'a str) -> Option<&'a str> {
199    let base = choice.path.rsplit('.').next()?.strip_suffix("[x]")?;
200    let rest = name.strip_prefix(base)?;
201    rest.chars()
202        .next()
203        .is_some_and(char::is_uppercase)
204        .then_some(rest)
205}
206
207/// The JSON shape a FHIR type code takes on the wire.
208///
209/// FHIR's primitives do not all map to JSON strings: `integer` and `decimal`
210/// are JSON numbers, `boolean` is a JSON boolean, and — the case that catches
211/// people — `integer64` is a *string*, so 64-bit values survive parsers whose
212/// numbers are doubles.
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub enum JsonKind {
215    /// A JSON string.
216    String,
217    /// A JSON number.
218    Number,
219    /// A JSON boolean.
220    Boolean,
221    /// A JSON object: a complex datatype or backbone element.
222    Complex,
223}
224
225/// The JSON shape a FHIR type code takes on the wire.
226///
227/// ```
228/// use fhir::meta::{json_kind, JsonKind};
229///
230/// assert_eq!(json_kind("decimal"), JsonKind::Number);
231/// assert_eq!(json_kind("Quantity"), JsonKind::Complex);
232/// // `integer64` is a string in FHIR JSON, deliberately.
233/// assert_eq!(json_kind("integer64"), JsonKind::String);
234/// ```
235#[must_use]
236pub fn json_kind(code: &str) -> JsonKind {
237    match code {
238        "integer" | "decimal" | "positiveInt" | "unsignedInt" => JsonKind::Number,
239        "boolean" => JsonKind::Boolean,
240        _ if is_datatype(code) => JsonKind::Complex,
241        _ => JsonKind::String,
242    }
243}
244
245/// Whether a type code names a complex datatype, as opposed to a primitive
246/// (lowercase) or a backbone element.
247///
248/// Traversal uses this to decide whether a child's metadata lives under the
249/// named datatype (`"HumanName.given"`) or stays on the path
250/// (`"Patient.contact.name"`).
251#[must_use]
252pub fn is_datatype(code: &str) -> bool {
253    !code.is_empty()
254        && code.chars().next().is_some_and(char::is_uppercase)
255        && code != "BackboneElement"
256        && code != "Element"
257}
258
259/// Map every generated struct name to the FHIR path prefix it represents, e.g.
260/// `"AppointmentParticipant"` to `"Appointment.participant"`.
261///
262/// Backbone struct names are the PascalCase concatenation of their path
263/// segments, which is not reversible on its own — `PatientContact` could split
264/// in several places — so the mapping is built from the paths that actually
265/// exist.
266#[must_use]
267pub fn struct_prefixes(table: &'static [ElementMeta]) -> HashMap<String, &'static str> {
268    use ::convert_case::{Case, Casing};
269
270    let mut map = HashMap::new();
271    for e in table {
272        let seg_count = e.path.split('.').count();
273        for take in 1..seg_count {
274            let name: String = e
275                .path
276                .split('.')
277                .take(take)
278                .map(|s| s.to_case(Case::Pascal))
279                .collect();
280            if let Some((end, _)) = e.path.match_indices('.').nth(take - 1) {
281                map.entry(name).or_insert(&e.path[..end]);
282            }
283        }
284    }
285    map
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    static TABLE: &[ElementMeta] = &[
293        ElementMeta {
294            path: "Patient.active",
295            min: 0,
296            max: "1",
297            is_summary: true,
298            binding: None,
299            types: &[TypeRef {
300                code: "boolean",
301                target_profiles: &[],
302            }],
303            content_reference: None,
304        },
305        ElementMeta {
306            path: "Patient.contact",
307            min: 0,
308            max: "*",
309            is_summary: false,
310            binding: None,
311            types: &[TypeRef {
312                code: "BackboneElement",
313                target_profiles: &[],
314            }],
315            content_reference: None,
316        },
317        ElementMeta {
318            path: "Patient.contact.name",
319            min: 0,
320            max: "1",
321            is_summary: false,
322            binding: None,
323            types: &[TypeRef {
324                code: "HumanName",
325                target_profiles: &[],
326            }],
327            content_reference: None,
328        },
329        ElementMeta {
330            path: "Patient.link.other",
331            min: 1,
332            max: "1",
333            is_summary: false,
334            binding: None,
335            types: &[TypeRef {
336                code: "Reference",
337                target_profiles: &["http://hl7.org/fhir/StructureDefinition/Patient"],
338            }],
339            content_reference: None,
340        },
341    ];
342
343    #[test]
344    fn lookup_by_path() {
345        assert_eq!(find(TABLE, "Patient.active").unwrap().max, "1");
346        assert!(find(TABLE, "Patient.nope").is_none());
347    }
348
349    #[test]
350    fn cardinality_helpers() {
351        let active = find(TABLE, "Patient.active").unwrap();
352        assert!(!active.is_required());
353        assert!(!active.is_multiple());
354        let contact = find(TABLE, "Patient.contact").unwrap();
355        assert!(contact.is_multiple());
356        assert!(find(TABLE, "Patient.link.other").unwrap().is_required());
357    }
358
359    #[test]
360    fn target_names_strip_the_profile_url() {
361        let other = find(TABLE, "Patient.link.other").unwrap();
362        let names: Vec<&str> = other.types[0].target_names().collect();
363        assert_eq!(names, ["Patient"]);
364    }
365
366    #[test]
367    fn struct_names_map_back_to_paths() {
368        let prefixes = struct_prefixes(TABLE);
369        assert_eq!(prefixes.get("Patient").copied(), Some("Patient"));
370        assert_eq!(
371            prefixes.get("PatientContact").copied(),
372            Some("Patient.contact")
373        );
374    }
375
376    #[test]
377    fn strength_tokens_parse() {
378        assert_eq!(
379            BindingStrength::from_token("required"),
380            BindingStrength::Required
381        );
382        assert_eq!(
383            BindingStrength::from_token("nonsense"),
384            BindingStrength::Example
385        );
386    }
387}