Skip to main content

fhir_diff/
model.rs

1//! The effective element tree of a profile: its FHIR Schema merged over every schema it is
2//! based on, down to the core type. Two versions of a profile are compared on these trees, so
3//! a change inherited from a base profile counts as much as one the profile makes itself.
4
5use std::collections::{BTreeMap, BTreeSet, HashMap};
6
7use octofhir_fhirschema::types::{FhirSchemaConstraint, FhirSchemaSliceMatch, FhirSchemaSlicing};
8use octofhir_fhirschema::{FhirSchema, FhirSchemaElement, FhirVersion, StructureDefinition};
9use serde::Serialize;
10use serde_json::Value;
11
12use crate::package::{Definition, Loaded};
13
14/// Strength of a terminology binding, weakest first.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
16#[serde(rename_all = "lowercase")]
17pub enum Strength {
18    Example,
19    Preferred,
20    Extensible,
21    Required,
22}
23
24impl Strength {
25    fn parse(text: &str) -> Self {
26        match text {
27            "required" => Self::Required,
28            "extensible" => Self::Extensible,
29            "preferred" => Self::Preferred,
30            _ => Self::Example,
31        }
32    }
33
34    pub fn as_str(self) -> &'static str {
35        match self {
36            Self::Example => "example",
37            Self::Preferred => "preferred",
38            Self::Extensible => "extensible",
39            Self::Required => "required",
40        }
41    }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
45pub struct Binding {
46    pub strength: Strength,
47    pub value_set: Option<String>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
51pub struct Constraint {
52    pub severity: String,
53    pub expression: String,
54    pub human: String,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
58pub struct Slicing {
59    pub rules: String,
60    pub ordered: bool,
61    pub discriminators: Vec<String>,
62}
63
64/// One element of the effective tree.
65#[derive(Debug, Clone, PartialEq, Serialize)]
66pub struct Node {
67    pub min: u32,
68    /// `None` is `*`.
69    pub max: Option<u32>,
70    /// The element's type, or for a choice element the types it may take.
71    pub types: BTreeSet<String>,
72    /// Profiles a Reference (or canonical) may point to.
73    pub targets: BTreeSet<String>,
74    pub must_support: bool,
75    pub is_modifier: bool,
76    pub binding: Option<Binding>,
77    pub constraints: BTreeMap<String, Constraint>,
78    /// `fixed[x]` / `pattern[x]` value.
79    pub pattern: Option<Value>,
80    pub slicing: Option<Slicing>,
81    pub slices: BTreeMap<String, Node>,
82    pub children: BTreeMap<String, Node>,
83    /// Canonical of the extension an extension slice holds.
84    pub extension_url: Option<String>,
85}
86
87impl Node {
88    /// An element nothing constrains: what a missing slice or child is compared with.
89    pub fn unconstrained() -> Self {
90        Self {
91            min: 0,
92            max: None,
93            types: BTreeSet::new(),
94            targets: BTreeSet::new(),
95            must_support: false,
96            is_modifier: false,
97            binding: None,
98            constraints: BTreeMap::new(),
99            pattern: None,
100            slicing: None,
101            slices: BTreeMap::new(),
102            children: BTreeMap::new(),
103            extension_url: None,
104        }
105    }
106}
107
108/// A profile (or extension definition) with its effective tree.
109#[derive(Debug, Clone, Serialize)]
110pub struct Profile {
111    pub url: String,
112    pub version: Option<String>,
113    pub name: String,
114    pub title: Option<String>,
115    /// The resource or data type it constrains (`Patient`, `Extension`).
116    pub type_name: String,
117    pub base: Option<String>,
118    pub kind: String,
119    /// File in the package.
120    pub file: String,
121    #[serde(skip)]
122    pub tree: Node,
123}
124
125/// Every schema a package's profiles may be based on: the package's own, its dependencies'
126/// and the core release's.
127pub struct Pool {
128    by_url: HashMap<String, FhirSchema>,
129    core: &'static HashMap<String, FhirSchema>,
130    /// The package's own constraint definitions, in file order.
131    own: Vec<(String, FhirSchema, Definition)>,
132    /// Definitions that did not convert, with the reason.
133    pub skipped: Vec<(String, String)>,
134    /// Bases that were not found (compared over the core type instead).
135    pub missing: std::cell::RefCell<BTreeSet<String>>,
136    /// Bases found only among the other package's dependencies (see [`Pool::borrow_bases`]).
137    pub borrowed: BTreeSet<String>,
138}
139
140const CORE_PREFIX: &str = "http://hl7.org/fhir/StructureDefinition/";
141
142/// The core release of a package's `fhirVersions`.
143pub fn release(fhir_versions: &[String]) -> FhirVersion {
144    let version = fhir_versions.first().map(String::as_str).unwrap_or("4.0.1");
145    if version.starts_with("4.3") {
146        FhirVersion::R4B
147    } else if version.starts_with("5.") {
148        FhirVersion::R5
149    } else if version.starts_with("6.") {
150        FhirVersion::R6
151    } else {
152        FhirVersion::R4
153    }
154}
155
156impl Pool {
157    pub fn new(loaded: &Loaded) -> Self {
158        let core = octofhir_fhirschema::get_schemas(release(&loaded.package.fhir_versions));
159        let mut pool = Self {
160            by_url: HashMap::new(),
161            core,
162            own: Vec::new(),
163            skipped: Vec::new(),
164            missing: Default::default(),
165            borrowed: BTreeSet::new(),
166        };
167        for package in &loaded.dependencies {
168            for definition in &package.definitions {
169                if let Ok(schema) = convert(&definition.json) {
170                    pool.by_url.insert(schema.url.clone(), schema);
171                }
172            }
173        }
174        for definition in &loaded.package.definitions {
175            match convert(&definition.json) {
176                Ok(schema) => {
177                    pool.by_url.insert(schema.url.clone(), schema.clone());
178                    if definition.json["derivation"] == "constraint" {
179                        pool.own
180                            .push((schema.url.clone(), schema, definition.clone()));
181                    }
182                }
183                Err(error) => pool.skipped.push((definition.file.clone(), error)),
184            }
185        }
186        pool
187    }
188
189    /// The pools of two packages compared with each other, each lent the bases it lacks by the
190    /// other ([`Pool::borrow_bases`]).
191    pub fn pair(old: &Loaded, new: &Loaded) -> (Self, Self) {
192        let (mut old_pool, mut new_pool) = (Self::new(old), Self::new(new));
193        old_pool.borrow_bases(&new_pool);
194        new_pool.borrow_bases(&old_pool);
195        (old_pool, new_pool)
196    }
197
198    /// Take the bases this package's profiles need but its manifest does not provide from
199    /// `other`, the package compared with. US Core 6.1.0 bases a profile on SDC without listing
200    /// SDC; comparing that profile over the core type would report SDC's own constraints as
201    /// changes. A base with the same canonical URL from the other side's dependencies isolates
202    /// the package's own changes.
203    pub fn borrow_bases(&mut self, other: &Pool) {
204        let mut pending: Vec<String> = self
205            .own
206            .iter()
207            .filter_map(|(_, schema, _)| schema.base.clone())
208            .collect();
209        let mut seen = BTreeSet::new();
210        while let Some(base) = pending.pop() {
211            let plain = base.split('|').next().unwrap_or(&base).to_owned();
212            if !seen.insert(plain.clone()) {
213                continue;
214            }
215            let schema = match self.schema(&plain) {
216                Some(schema) => schema.clone(),
217                None => match other.by_url.get(&plain) {
218                    Some(schema) => {
219                        self.by_url.insert(plain.clone(), schema.clone());
220                        self.borrowed.insert(plain);
221                        schema.clone()
222                    }
223                    None => continue,
224                },
225            };
226            pending.extend(schema.base);
227        }
228    }
229
230    fn schema(&self, url: &str) -> Option<&FhirSchema> {
231        let plain = url.split('|').next().unwrap_or(url);
232        self.by_url.get(plain).or_else(|| {
233            plain
234                .strip_prefix(CORE_PREFIX)
235                .and_then(|name| self.core.get(name))
236        })
237    }
238
239    /// The package's profiles and extension definitions with their effective trees.
240    pub fn profiles(&self) -> Vec<Profile> {
241        self.own
242            .iter()
243            .map(|(url, schema, definition)| Profile {
244                url: url.clone(),
245                version: schema.version.clone(),
246                name: schema.name.clone(),
247                title: definition.json["title"].as_str().map(str::to_owned),
248                type_name: schema.type_name.clone(),
249                base: schema.base.clone(),
250                kind: schema.kind.clone(),
251                file: definition.file.clone(),
252                tree: self.effective(schema),
253            })
254            .collect()
255    }
256
257    /// The type a profile constrains (`Patient` for US Core Patient), or the type itself for a
258    /// core URL.
259    pub fn type_of(&self, url: &str) -> Option<String> {
260        let plain = url.split('|').next().unwrap_or(url);
261        match self.by_url.get(plain) {
262            Some(schema) => Some(schema.type_name.clone()),
263            None => plain.strip_prefix(CORE_PREFIX).map(str::to_owned),
264        }
265    }
266
267    /// The core definition of element `child` of type `type_name`: what a child that only one
268    /// version constrains is compared with.
269    pub fn base_node(&self, type_name: &str, child: &str) -> Option<Node> {
270        let schema = self.core.get(type_name)?;
271        let parent = level(schema);
272        let element = parent.elements.as_ref()?.get(child)?;
273        Some(node(self.core, element, Some(&parent), child))
274    }
275
276    /// `schema` merged over its base chain, as one tree rooted at the constrained type.
277    pub fn effective(&self, schema: &FhirSchema) -> Node {
278        let mut chain = vec![level(schema)];
279        let mut seen = BTreeSet::from([schema.url.clone()]);
280        let mut current = schema.base.clone();
281        while let Some(base) = current {
282            if !seen.insert(base.clone()) {
283                break;
284            }
285            let Some(base_schema) = self.schema(&base) else {
286                // A base from a package the manifest does not list: carry on from the core
287                // type, so the comparison still sees the core definitions.
288                if let Some(core) = self.core.get(&schema.type_name)
289                    && seen.insert(core.url.clone())
290                {
291                    self.missing.borrow_mut().insert(base.clone());
292                    chain.push(level(core));
293                }
294                break;
295            };
296            chain.push(level(base_schema));
297            current = base_schema.base.clone();
298        }
299        let merged = chain
300            .into_iter()
301            .rev()
302            .reduce(|base, overlay| merge(&base, &overlay))
303            .unwrap_or_default();
304        let mut root = node(self.core, &merged, None, "");
305        root.min = 0;
306        root.max = None;
307        root.types = BTreeSet::from([schema.type_name.clone()]);
308        root
309    }
310}
311
312fn convert(json: &Value) -> Result<FhirSchema, String> {
313    let definition: StructureDefinition =
314        serde_json::from_value(json.clone()).map_err(|e| e.to_string())?;
315    octofhir_fhirschema::translate(definition, None).map_err(|e| e.to_string())
316}
317
318/// A schema's top level as an element, so every level merges the same way.
319fn level(schema: &FhirSchema) -> FhirSchemaElement {
320    FhirSchemaElement {
321        type_name: Some(schema.type_name.clone()),
322        elements: schema.elements.clone(),
323        required: schema.required.clone(),
324        excluded: schema.excluded.clone(),
325        constraint: schema.constraint.clone(),
326        extensions: schema.extensions.clone(),
327        ..Default::default()
328    }
329}
330
331/// `overlay` over `base`: the overlay's constraints win, collections are united.
332fn merge(base: &FhirSchemaElement, overlay: &FhirSchemaElement) -> FhirSchemaElement {
333    let mut result = base.clone();
334    macro_rules! take {
335        ($($field:ident),*) => {$(
336            if overlay.$field.is_some() {
337                result.$field = overlay.$field.clone();
338            }
339        )*};
340    }
341    take!(
342        type_name,
343        array,
344        min,
345        max,
346        refers,
347        binding,
348        pattern,
349        choices,
350        choice_of,
351        url,
352        must_support,
353        is_modifier,
354        short
355    );
356    if let Some(constraints) = &overlay.constraint {
357        let merged = result.constraint.get_or_insert_with(HashMap::new);
358        for (key, constraint) in constraints {
359            merged.insert(key.clone(), constraint.clone());
360        }
361    }
362    for (target, source) in [
363        (&mut result.required, &overlay.required),
364        (&mut result.excluded, &overlay.excluded),
365    ] {
366        if let Some(names) = source {
367            let list = target.get_or_insert_with(Vec::new);
368            for name in names {
369                if !list.contains(name) {
370                    list.push(name.clone());
371                }
372            }
373        }
374    }
375    if let Some(elements) = &overlay.elements {
376        let merged = result.elements.get_or_insert_with(HashMap::new);
377        for (name, element) in elements {
378            let next = match merged.get(name) {
379                Some(existing) => merge(existing, element),
380                None => element.clone(),
381            };
382            merged.insert(name.clone(), next);
383        }
384    }
385    if let Some(slicing) = &overlay.slicing {
386        result.slicing = Some(merge_slicing(result.slicing.as_ref(), slicing));
387    }
388    result
389}
390
391fn merge_slicing(
392    base: Option<&FhirSchemaSlicing>,
393    overlay: &FhirSchemaSlicing,
394) -> FhirSchemaSlicing {
395    let Some(base) = base else {
396        return overlay.clone();
397    };
398    let mut result = base.clone();
399    if overlay.discriminator.is_some() {
400        result.discriminator = overlay.discriminator.clone();
401    }
402    if overlay.rules.is_some() {
403        result.rules = overlay.rules.clone();
404    }
405    if overlay.ordered.is_some() {
406        result.ordered = overlay.ordered;
407    }
408    if let Some(slices) = &overlay.slices {
409        let merged = result.slices.get_or_insert_with(HashMap::new);
410        for (name, slice) in slices {
411            let next = match merged.get(name) {
412                Some(existing) => FhirSchemaSliceMatch {
413                    match_value: slice.match_value.clone().or(existing.match_value.clone()),
414                    schema: match (&existing.schema, &slice.schema) {
415                        (Some(a), Some(b)) => Some(merge(a, b)),
416                        (a, b) => b.clone().or(a.clone()),
417                    },
418                    min: slice.min.or(existing.min),
419                    max: slice.max.or(existing.max),
420                },
421                None => slice.clone(),
422            };
423            merged.insert(name.clone(), next);
424        }
425    }
426    result
427}
428
429/// The node of `element` (named `name` in a parent whose `required` / `excluded` lists
430/// apply to it).
431fn node(
432    core: &HashMap<String, FhirSchema>,
433    element: &FhirSchemaElement,
434    parent: Option<&FhirSchemaElement>,
435    name: &str,
436) -> Node {
437    let listed =
438        |list: Option<&Vec<String>>| list.is_some_and(|names| names.iter().any(|n| n == name));
439    let required = parent.is_some_and(|p| listed(p.required.as_ref()));
440    let excluded = parent.is_some_and(|p| listed(p.excluded.as_ref()));
441    let min = element.min.unwrap_or(0).max(0) as u32;
442    let min = if required { min.max(1) } else { min };
443    let max = if excluded {
444        Some(0)
445    } else if element.array == Some(true) {
446        element.max.map(|max| max.max(0) as u32)
447    } else {
448        Some(element.max.map_or(1, |max| max.max(0) as u32))
449    };
450    let types = match &element.choices {
451        Some(choices) => choices.iter().cloned().collect(),
452        None => element.type_name.iter().cloned().collect(),
453    };
454    // What the element's type itself requires or forbids of its children (Narrative.status).
455    let type_schema = element.type_name.as_deref().and_then(|t| core.get(t));
456    let mut scope = element.clone();
457    if let Some(type_schema) = type_schema {
458        for (target, source) in [
459            (&mut scope.required, &type_schema.required),
460            (&mut scope.excluded, &type_schema.excluded),
461        ] {
462            for name in source.iter().flatten() {
463                let list = target.get_or_insert_with(Vec::new);
464                if !list.contains(name) {
465                    list.push(name.clone());
466                }
467            }
468        }
469    }
470    let mut children: BTreeMap<String, Node> = element
471        .elements
472        .iter()
473        .flatten()
474        .map(|(child, schema)| {
475            // A nested constraint (`type.coding` min 1) says nothing of what the child
476            // inherits from its type (`CodeableConcept.coding` repeats): merge it over that.
477            let inherited = element
478                .type_name
479                .as_deref()
480                .and_then(|type_name| core.get(type_name))
481                .and_then(|type_schema| type_schema.elements.as_ref()?.get(child));
482            let schema = match inherited {
483                Some(base) => merge(base, schema),
484                None => schema.clone(),
485            };
486            (child.clone(), node(core, &schema, Some(&scope), child))
487        })
488        .collect();
489    normalize_choices(&mut children);
490    let (slicing, slices) = match &element.slicing {
491        Some(slicing) => (
492            Some(Slicing {
493                rules: slicing.rules.clone().unwrap_or_else(|| "open".to_owned()),
494                ordered: slicing.ordered.unwrap_or(false),
495                discriminators: slicing
496                    .discriminator
497                    .iter()
498                    .flatten()
499                    .map(|d| format!("{}:{}", d.type_name, d.path))
500                    .collect(),
501            }),
502            slicing
503                .slices
504                .iter()
505                .flatten()
506                .map(|(slice_name, slice)| {
507                    // An extension slice's cardinality is in the parent's `extensions`.
508                    let extension = matches!(name, "extension" | "modifierExtension")
509                        .then(|| parent.and_then(|p| p.extensions.as_ref()?.get(slice_name)))
510                        .flatten();
511                    (
512                        slice_name.clone(),
513                        slice_node(core, element, slice, extension),
514                    )
515                })
516                .collect(),
517        ),
518        None => (None, BTreeMap::new()),
519    };
520    Node {
521        min,
522        max,
523        types,
524        targets: element.refers.iter().flatten().cloned().collect(),
525        must_support: element.must_support.unwrap_or(false),
526        is_modifier: element.is_modifier.unwrap_or(false),
527        binding: element.binding.as_ref().map(|binding| Binding {
528            strength: Strength::parse(&binding.strength),
529            value_set: binding.value_set.clone(),
530        }),
531        constraints: element
532            .constraint
533            .iter()
534            .flatten()
535            .map(|(key, constraint)| (key.clone(), constraint_of(constraint)))
536            .collect(),
537        pattern: element
538            .pattern
539            .as_ref()
540            .map(|pattern| pattern.value.clone()),
541        slicing,
542        slices,
543        children,
544        extension_url: element.url.clone(),
545    }
546}
547
548/// A profile may narrow a choice through one of its variants (`Observation.effectiveDateTime`
549/// with `min` 1 restricts `effective[x]` to dateTime and requires it) rather than through the
550/// choice element's own types: carry that onto the choice element, so both ways of saying it
551/// compare equal.
552fn normalize_choices(children: &mut BTreeMap<String, Node>) {
553    let choices: Vec<(String, Vec<String>)> = children
554        .iter()
555        .filter(|(_, node)| {
556            node.types.len() > 1 || node.types.iter().any(|t| looks_like_variant(t))
557        })
558        .map(|(name, node)| (name.clone(), node.types.iter().cloned().collect()))
559        .collect();
560    for (name, variants) in choices {
561        let present: Vec<String> = variants
562            .iter()
563            .filter(|variant| children.contains_key(*variant))
564            .cloned()
565            .collect();
566        if present.is_empty() {
567            continue;
568        }
569        let required: Vec<&String> = present
570            .iter()
571            .filter(|variant| children[*variant].min >= 1)
572            .collect();
573        let excluded: BTreeSet<String> = present
574            .iter()
575            .filter(|variant| children[*variant].max == Some(0))
576            .cloned()
577            .collect();
578        let only = (required.len() == 1).then(|| required[0].clone());
579        // A variant named by a type slice keeps its own mustSupport; one named by the
580        // renamed path (`effectiveDateTime`) is the choice element itself.
581        let sliced = !children[&name].slices.is_empty();
582        let variant_must_support = !sliced
583            && only
584                .as_ref()
585                .is_some_and(|variant| children[variant].must_support);
586        if let Some(choice) = children.get_mut(&name) {
587            if let Some(only) = only {
588                choice.min = choice.min.max(1);
589                choice.types = BTreeSet::from([only]);
590                choice.must_support |= variant_must_support;
591            }
592            choice.types.retain(|variant| !excluded.contains(variant));
593        }
594    }
595}
596
597/// Whether a type name is a choice variant (`valueQuantity`): a lowercase prefix then an
598/// uppercase type name.
599fn looks_like_variant(name: &str) -> bool {
600    let mut chars = name.chars();
601    chars.next().is_some_and(char::is_lowercase) && name.chars().any(char::is_uppercase)
602}
603
604/// Whether `name` is one of the variants a sibling choice element lists.
605pub fn is_variant(siblings: &BTreeMap<String, Node>, name: &str) -> bool {
606    siblings
607        .values()
608        .any(|node| node.types.contains(name) && node.types.iter().all(|t| looks_like_variant(t)))
609}
610
611/// A slice: its schema over the sliced element (a slice of `Coverage.class` keeps what
612/// `class` requires of its children), with the slice's own cardinality.
613fn slice_node(
614    core: &HashMap<String, FhirSchema>,
615    sliced: &FhirSchemaElement,
616    slice: &FhirSchemaSliceMatch,
617    extension: Option<&Value>,
618) -> Node {
619    let mut base = sliced.clone();
620    base.slicing = None;
621    base.min = None;
622    base.max = None;
623    let schema = match &slice.schema {
624        Some(schema) => merge(&base, schema),
625        None => base,
626    };
627    let mut slice_node = node(core, &schema, None, "");
628    let number = |value: Option<&Value>| match value {
629        Some(Value::Number(n)) => n.as_u64().map(|n| n as u32),
630        Some(Value::String(s)) => s.parse().ok(),
631        _ => None,
632    };
633    let unbounded = |value: Option<&Value>| matches!(value, Some(Value::String(s)) if s == "*");
634    slice_node.min = slice
635        .min
636        .map(|min| min.max(0) as u32)
637        .or_else(|| number(extension.and_then(|e| e.get("min"))))
638        .unwrap_or(0);
639    slice_node.max = match slice.max {
640        Some(max) => Some(max.max(0) as u32),
641        None => match extension.and_then(|e| e.get("max")) {
642            max if unbounded(max) => None,
643            Some(max) => number(Some(max)),
644            None => slice
645                .schema
646                .as_ref()
647                .and_then(|s| s.max)
648                .map(|max| max.max(0) as u32),
649        },
650    };
651    slice_node
652}
653
654fn constraint_of(constraint: &FhirSchemaConstraint) -> Constraint {
655    Constraint {
656        severity: constraint.severity.clone(),
657        expression: constraint.expression.clone(),
658        human: constraint.human.clone(),
659    }
660}