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}