use std::collections::BTreeMap;
use std::collections::BTreeSet;
use fhir_types::codec::Json;
use fhir_types::codec::Object;
use fhir_types::codec::Value;
use fhir_types::r4::operation_outcome::OperationOutcome;
use fhir_types::r4::resource::Resource;
use crate::category::Category;
use crate::terminology::ehn;
use crate::terminology::walk::{self, Coded};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Provided,
Received,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Pin {
pub context: Option<String>,
pub template: String,
pub profile: Option<String>,
pub files: Vec<String>,
pub package: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Tally {
count: usize,
paths: BTreeMap<String, usize>,
}
impl Tally {
fn add(&mut self, path: Option<&str>) {
self.count = self.count.saturating_add(1);
if let Some(path) = path {
let held = self.paths.entry(path.to_owned()).or_default();
*held = held.saturating_add(1);
}
}
fn extend(&mut self, other: &Self) {
self.count = self.count.saturating_add(other.count);
for (path, count) in &other.paths {
let held = self.paths.entry(path.clone()).or_default();
*held = held.saturating_add(*count);
}
}
#[must_use]
pub const fn count(&self) -> usize {
self.count
}
#[must_use]
pub const fn paths(&self) -> &BTreeMap<String, usize> {
&self.paths
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Report {
direction: Direction,
category: Category,
pin: Pin,
terminology: BTreeSet<String>,
transcoded: Tally,
untranscoded: Tally,
dropped: Tally,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ReportError {
#[error("a transformed resource does not encode as FHIR JSON")]
Encode {
#[source]
source: fhir_types::codec::EncodeError,
},
#[error("{location} is an element the R4 element table does not describe")]
Unwalkable {
location: String,
},
}
impl Report {
#[must_use]
pub fn new(direction: Direction, category: Category, pin: Pin) -> Self {
Self {
direction,
category,
pin,
terminology: ehn::RELEASES
.iter()
.map(|(name, version)| format!("{name}|{version}"))
.collect(),
transcoded: Tally::default(),
untranscoded: Tally::default(),
dropped: Tally::default(),
}
}
pub fn count_coded(
&mut self,
resource: &Resource,
preferred: impl Fn(&str) -> bool,
) -> Result<(), ReportError> {
let object = Json::to_json(resource).map_err(|source| ReportError::Encode { source })?;
let kind = object
.get("resourceType")
.and_then(Value::as_str)
.unwrap_or("Resource")
.to_owned();
let found =
walk::coded(&object, &kind, false).map_err(|unwalkable| ReportError::Unwalkable {
location: unwalkable.location,
})?;
let mut concepts: Vec<String> = Vec::new();
for occurrence in &found {
if occurrence.element.starts_with("Meta.") {
continue;
}
let codings: Vec<&Object> = match &occurrence.value {
Coded::Code(_) => continue,
Coded::Concept(concept) => {
concepts.push(format!("{}.coding", occurrence.location));
concept
.get("coding")
.and_then(Value::as_array)
.map(|codings| codings.iter().filter_map(Value::as_object).collect())
.unwrap_or_default()
}
Coded::Coding(coding) => {
if concepts
.iter()
.any(|concept| occurrence.location.starts_with(concept.as_str()))
{
continue;
}
vec![*coding]
}
};
let mut carried = false;
for coding in codings {
let system = coding.get("system").and_then(Value::as_str);
carried |= system.is_some_and(&preferred);
if let (Some(system), Some(version)) =
(system, coding.get("version").and_then(Value::as_str))
{
self.terminology.insert(format!("{system}|{version}"));
}
}
let tally = if carried {
&mut self.transcoded
} else {
&mut self.untranscoded
};
tally.add(Some(occurrence.element));
}
Ok(())
}
pub fn count_losses(&mut self, outcome: &OperationOutcome) {
for issue in &outcome.issue {
if issue.severity.value.as_deref() != Some("warning") {
continue;
}
let path = issue
.expression
.first()
.and_then(|expression| expression.value.as_deref())
.map(element_path);
self.dropped.add(path.as_deref());
}
}
pub fn count_uncovered(&mut self) {
self.dropped.add(Some("/"));
}
pub fn absorb(&mut self, other: &Self) {
self.terminology.extend(other.terminology.iter().cloned());
self.transcoded.extend(&other.transcoded);
self.untranscoded.extend(&other.untranscoded);
self.dropped.extend(&other.dropped);
}
#[must_use]
pub const fn direction(&self) -> Direction {
self.direction
}
#[must_use]
pub const fn category(&self) -> Category {
self.category
}
#[must_use]
pub const fn pin(&self) -> &Pin {
&self.pin
}
#[must_use]
pub const fn terminology(&self) -> &BTreeSet<String> {
&self.terminology
}
#[must_use]
pub const fn transcoded(&self) -> &Tally {
&self.transcoded
}
#[must_use]
pub const fn untranscoded(&self) -> &Tally {
&self.untranscoded
}
#[must_use]
pub const fn dropped(&self) -> &Tally {
&self.dropped
}
}
fn element_path(expression: &str) -> String {
let mut path = String::with_capacity(expression.len());
let mut indexed = false;
for character in expression.chars() {
match character {
'[' => indexed = true,
']' => indexed = false,
_ if !indexed => path.push(character),
_ => {}
}
}
path
}