use std::collections::BTreeMap;
use std::collections::BTreeSet;
use fhir_types::codec::EncodeError;
use fhir_types::codec::Value;
use fhir_types::definition::CodeSystemRead;
use fhir_types::definition::ConceptRead;
const PARENT: &str = "http://hl7.org/fhir/concept-properties#parent";
const CHILD: &str = "http://hl7.org/fhir/concept-properties#child";
#[derive(Debug, Clone, Default)]
pub(crate) struct Concept {
pub(crate) code: String,
pub(crate) display: Option<String>,
pub(crate) designations: Vec<String>,
pub(crate) properties: BTreeMap<String, Vec<String>>,
parents: BTreeSet<String>,
}
#[derive(Debug)]
pub(crate) struct CodeIndex {
pub(crate) content: Option<String>,
pub(crate) is_a: bool,
pub(crate) declared: BTreeSet<String>,
case_insensitive: bool,
concepts: BTreeMap<String, Concept>,
}
impl CodeIndex {
pub(crate) fn new<S: CodeSystemRead>(system: &S) -> Result<Self, EncodeError> {
let parent_codes: BTreeSet<String> = property_codes(system, PARENT, "parent");
let child_codes: BTreeSet<String> = property_codes(system, CHILD, "child");
let mut concepts = BTreeMap::new();
index_concepts(system.concepts(), None, &mut concepts)?;
let mut edges = Vec::new();
for concept in concepts.values() {
for (property, values) in &concept.properties {
for value in values {
if parent_codes.contains(property) {
edges.push((concept.code.clone(), value.clone()));
}
if child_codes.contains(property) {
edges.push((value.clone(), concept.code.clone()));
}
}
}
}
for (child, parent) in edges {
if let Some(concept) = concepts.get_mut(&child) {
concept.parents.insert(parent);
}
}
Ok(Self {
content: system.content().map(str::to_owned),
is_a: system.hierarchy_meaning() == Some("is-a"),
declared: system
.properties()
.into_iter()
.filter_map(|property| property.code.map(str::to_owned))
.collect(),
case_insensitive: system.case_sensitive() == Some(false),
concepts,
})
}
pub(crate) fn complete(&self) -> bool {
self.content.as_deref() == Some("complete")
}
pub(crate) fn concept(&self, code: &str) -> Option<&Concept> {
if let Some(found) = self.concepts.get(code) {
return Some(found);
}
if self.case_insensitive {
return self
.concepts
.values()
.find(|concept| concept.code.to_lowercase() == code.to_lowercase());
}
None
}
pub(crate) fn descends(&self, code: &str, ancestor: &str) -> bool {
let mut seen = BTreeSet::new();
let mut pending: Vec<&str> = self
.concept(code)
.map(|concept| concept.parents.iter().map(String::as_str).collect())
.unwrap_or_default();
while let Some(next) = pending.pop() {
if !seen.insert(next) {
continue;
}
let Some(concept) = self.concept(next) else {
continue;
};
if concept.code == ancestor {
return true;
}
pending.extend(concept.parents.iter().map(String::as_str));
}
false
}
}
fn property_codes<S: CodeSystemRead>(system: &S, uri: &str, code: &str) -> BTreeSet<String> {
let mut codes: BTreeSet<String> = system
.properties()
.into_iter()
.filter(|property| property.uri == Some(uri))
.filter_map(|property| property.code.map(str::to_owned))
.collect();
codes.insert(code.to_owned());
codes
}
fn index_concepts<C: ConceptRead>(
concepts: &[C],
parent: Option<&str>,
index: &mut BTreeMap<String, Concept>,
) -> Result<(), EncodeError> {
for concept in concepts {
let Some(code) = concept.code().map(str::to_owned) else {
continue;
};
let entry = index.entry(code.clone()).or_insert_with(|| Concept {
code: code.clone(),
..Concept::default()
});
if entry.display.is_none() {
entry.display = concept.display().map(str::to_owned);
}
entry
.designations
.extend(concept.designations().into_iter().map(str::to_owned));
for property in concept.properties()? {
let value = property.value.and_then(|value| value.value);
if let (Some(name), Some(value)) = (property.code, value.as_ref().and_then(lexical)) {
entry
.properties
.entry(name.to_owned())
.or_default()
.push(value);
}
}
if let Some(parent) = parent {
entry.parents.insert(parent.to_owned());
}
index_concepts(concept.concepts(), Some(&code), index)?;
}
Ok(())
}
fn lexical(value: &Value) -> Option<String> {
match value {
Value::String(text) => Some(text.clone()),
Value::Number(number) => Some(number.as_str().to_owned()),
Value::Bool(flag) => Some(flag.to_string()),
Value::Object(object) => object
.get("code")
.and_then(Value::as_str)
.map(str::to_owned),
_ => None,
}
}