use std::collections::BTreeSet;
use fhir_types::codec::Value;
use fhir_types::r4::code_system::CodeSystemConcept;
use fhir_types::r4::value_set::ValueSetComposeInclude;
use fhir_types::r4::value_set::ValueSetExpansionContains;
use crate::set::PackageId;
use crate::set::PackageSet;
use crate::set::ResolveError;
use crate::validate::ValidateError;
pub(crate) type Code = (Option<String>, String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Enumeration {
Codes(BTreeSet<Code>),
NotEnumerated,
}
impl Enumeration {
pub(crate) fn holds(&self, value: &Value) -> bool {
let Self::Codes(codes) = self else {
return false;
};
match value {
Value::String(code) => codes.iter().any(|(_, held)| held == code),
Value::Object(object) => {
if let Some(codings) = object.get("coding").and_then(Value::as_array) {
return codings.iter().any(|coding| self.holds(coding));
}
let system = object.get("system").and_then(Value::as_str);
let code = object.get("code").and_then(Value::as_str);
match (system, code) {
(Some(system), Some(code)) => codes.iter().any(|(held_system, held)| {
held == code && held_system.as_deref().is_none_or(|held| held == system)
}),
_ => false,
}
}
_ => false,
}
}
}
pub(crate) fn enumerate(
set: &PackageSet,
canonical: &str,
scope: PackageId,
) -> Result<Enumeration, ValidateError> {
let (value_set, origin) = match set.value_set_in(canonical, Some(scope)) {
Ok(found) => found,
Err(ResolveError::NotFound { .. } | ResolveError::Fragment { .. }) => {
return Ok(Enumeration::NotEnumerated);
}
Err(error) => return Err(error.into()),
};
if let Some(compose) = &value_set.compose {
let mut codes = BTreeSet::new();
let mut whole = !compose.include.is_empty();
for include in &compose.include {
match included(set, include, origin)? {
Some(found) => codes.extend(found),
None => whole = false,
}
}
for exclude in &compose.exclude {
match included(set, exclude, origin)? {
Some(found) => codes.retain(|code| !found.contains(code)),
None => whole = false,
}
}
if whole {
return Ok(Enumeration::Codes(codes));
}
}
if let Some(expansion) = &value_set.expansion {
let mut codes = BTreeSet::new();
let entries = flatten_expansion(&expansion.contains, &mut codes);
let total = expansion.total.as_ref().and_then(|total| total.value);
let paged = expansion
.offset
.as_ref()
.and_then(|offset| offset.value)
.is_some_and(|offset| offset != 0);
let complete =
total.is_none_or(|total| usize::try_from(total).is_ok_and(|total| total == entries));
if !paged && complete && !codes.is_empty() {
return Ok(Enumeration::Codes(codes));
}
}
Ok(Enumeration::NotEnumerated)
}
fn included(
set: &PackageSet,
include: &ValueSetComposeInclude,
origin: PackageId,
) -> Result<Option<BTreeSet<Code>>, ValidateError> {
if !include.filter.is_empty() || !include.value_set.is_empty() {
return Ok(None);
}
let Some(system) = include
.system
.as_ref()
.and_then(|system| system.value.clone())
else {
return Ok(None);
};
if include.concept.is_empty() {
return whole_code_system(set, &system, origin);
}
Ok(Some(
include
.concept
.iter()
.filter_map(|concept| concept.code.value.clone())
.map(|code| (Some(system.clone()), code))
.collect(),
))
}
fn whole_code_system(
set: &PackageSet,
system: &str,
origin: PackageId,
) -> Result<Option<BTreeSet<Code>>, ValidateError> {
let code_system = match set.code_system_in(system, Some(origin)) {
Ok((code_system, _)) => code_system,
Err(ResolveError::NotFound { .. } | ResolveError::Fragment { .. }) => return Ok(None),
Err(error) => return Err(error.into()),
};
if code_system.content.value.as_deref() != Some("complete") {
return Ok(None);
}
let mut codes = BTreeSet::new();
flatten_concepts(system, &code_system.concept, &mut codes);
Ok(Some(codes))
}
fn flatten_concepts(system: &str, concepts: &[CodeSystemConcept], codes: &mut BTreeSet<Code>) {
for concept in concepts {
if let Some(code) = &concept.code.value {
codes.insert((Some(system.to_owned()), code.clone()));
}
flatten_concepts(system, &concept.concept, codes);
}
}
fn flatten_expansion(contains: &[ValueSetExpansionContains], codes: &mut BTreeSet<Code>) -> usize {
let mut entries = 0_usize;
for entry in contains {
entries = entries.saturating_add(1);
let abstract_entry = entry
.r#abstract
.as_ref()
.and_then(|flag| flag.value)
.unwrap_or(false);
if let (Some(code), false) = (
entry.code.as_ref().and_then(|code| code.value.clone()),
abstract_entry,
) {
let system = entry
.system
.as_ref()
.and_then(|system| system.value.clone());
codes.insert((system, code));
}
entries = entries.saturating_add(flatten_expansion(&entry.contains, codes));
}
entries
}