1use 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#[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#[derive(Debug, Clone, PartialEq, Serialize)]
66pub struct Node {
67 pub min: u32,
68 pub max: Option<u32>,
70 pub types: BTreeSet<String>,
72 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 pub pattern: Option<Value>,
80 pub slicing: Option<Slicing>,
81 pub slices: BTreeMap<String, Node>,
82 pub children: BTreeMap<String, Node>,
83 pub extension_url: Option<String>,
85}
86
87impl Node {
88 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#[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 pub type_name: String,
117 pub base: Option<String>,
118 pub kind: String,
119 pub file: String,
121 #[serde(skip)]
122 pub tree: Node,
123}
124
125pub struct Pool {
128 by_url: HashMap<String, FhirSchema>,
129 core: &'static HashMap<String, FhirSchema>,
130 own: Vec<(String, FhirSchema, Definition)>,
132 pub skipped: Vec<(String, String)>,
134 pub missing: std::cell::RefCell<BTreeSet<String>>,
136 pub borrowed: BTreeSet<String>,
138}
139
140const CORE_PREFIX: &str = "http://hl7.org/fhir/StructureDefinition/";
141
142pub 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 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 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 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 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 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 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 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
318fn 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
331fn 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
429fn 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 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 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 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
548fn 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 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
597fn 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
604pub 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
611fn 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}