use std::collections::BTreeMap;
use std::fmt;
use fhir_types::r4::codeable_concept::CodeableConcept;
use fhir_types::r4::coding::Coding;
use fhir_types::r4::operation_outcome::OperationOutcome;
use fhir_types::r4::operation_outcome::OperationOutcomeIssue;
use fhir_types::r4::primitives;
use crate::issue::Issue;
use crate::issue::IssueType;
use crate::issue::MessageId;
use crate::issue::Rule;
use crate::issue::Severity;
pub const MESSAGE_SYSTEM: &str = "https://ferrobridge.eu/fhir/CodeSystem/validator-message";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Constraint {
Slicing,
Discriminator {
kind: String,
path: String,
},
DiscriminatorValueSet {
path: String,
value_set: String,
},
Invariant {
key: String,
severity: String,
},
Binding {
strength: String,
value_set: Option<String>,
},
TypeProfile {
profiles: Vec<String>,
},
TargetProfile {
profiles: Vec<String>,
},
ClaimedProfile {
url: String,
},
ExtensionContext,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Reason {
DiscriminatorPath,
DiscriminatorType,
NoDiscriminatorValue,
ValueSetNotEnumerated,
UnresolvedReference,
ReferenceOutOfScope,
ProfileNotFound,
DefaultSliceNotClosed,
Invariants,
Bindings,
Profiles,
ExtensionContext,
}
impl fmt::Display for Reason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::DiscriminatorPath => {
"the discriminator path is outside the FHIRPath subset discriminators use"
}
Self::DiscriminatorType => "the discriminator type is none R4 defines",
Self::NoDiscriminatorValue => {
"the slice sets nothing at the discriminator path to tell it apart by"
}
Self::ValueSetNotEnumerated => {
"the value set cannot be enumerated from the package set"
}
Self::UnresolvedReference => "a reference the discriminator resolves does not resolve",
Self::ReferenceOutOfScope => {
"a reference the discriminator resolves points outside the resources searched"
}
Self::ProfileNotFound => "the profile is not in the package set",
Self::DefaultSliceNotClosed => {
"the @default slice is not used, because the slicing rules are not closed"
}
Self::Invariants => "invariants are not evaluated",
Self::Bindings => "terminology bindings are not checked",
Self::Profiles => "target profiles and claimed profiles are not validated",
Self::ExtensionContext => "extension contexts are not checked",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Unevaluated {
pub constraint: Constraint,
pub structure: String,
pub element: String,
pub location: String,
pub reason: Reason,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Report {
issues: Vec<Issue>,
unevaluated: Vec<Unevaluated>,
}
impl Report {
#[must_use]
pub fn issues(&self) -> &[Issue] {
&self.issues
}
#[must_use]
pub fn unevaluated(&self) -> &[Unevaluated] {
&self.unevaluated
}
#[must_use]
pub fn is_valid(&self) -> bool {
!self
.issues
.iter()
.any(|issue| matches!(issue.severity, Severity::Fatal | Severity::Error))
}
pub fn with_message(&self, id: MessageId) -> impl Iterator<Item = &Issue> {
self.issues
.iter()
.filter(move |issue| issue.rule == Rule::Message(id))
}
}
#[derive(Debug, Default)]
pub(crate) struct Collector {
issues: Vec<Issue>,
unevaluated: BTreeMap<(String, String, Constraint), (String, Reason)>,
}
impl Collector {
pub(crate) fn issue(
&mut self,
severity: Severity,
code: IssueType,
id: MessageId,
expression: &str,
message: String,
) {
self.issues.push(Issue {
severity,
code,
expression: expression.to_owned(),
rule: Rule::Message(id),
message,
});
}
pub(crate) fn unevaluated(
&mut self,
structure: &str,
element: &str,
constraint: Constraint,
location: &str,
reason: Reason,
) {
self.unevaluated
.entry((structure.to_owned(), element.to_owned(), constraint))
.or_insert_with(|| (location.to_owned(), reason));
}
pub(crate) fn is_valid(&self) -> bool {
self.first_error().is_none()
}
pub(crate) fn first_error(&self) -> Option<&Issue> {
self.issues
.iter()
.find(|issue| matches!(issue.severity, Severity::Fatal | Severity::Error))
}
pub(crate) fn absorb(&mut self, other: Self) {
self.issues.extend(other.issues);
for (key, value) in other.unevaluated {
self.unevaluated.entry(key).or_insert(value);
}
}
pub(crate) fn finish(self) -> Report {
Report {
issues: self.issues,
unevaluated: self
.unevaluated
.into_iter()
.map(
|((structure, element, constraint), (location, reason))| Unevaluated {
constraint,
structure,
element,
location,
reason,
},
)
.collect(),
}
}
}
impl From<&Report> for OperationOutcome {
fn from(report: &Report) -> Self {
let mut issues: Vec<OperationOutcomeIssue> =
report.issues.iter().map(outcome_issue).collect();
issues.extend(report.unevaluated.iter().map(|unevaluated| {
outcome_issue(&Issue {
severity: Severity::Warning,
code: IssueType::NotSupported,
expression: unevaluated.location.clone(),
rule: Rule::Message(MessageId::ConstraintUnevaluated),
message: describe(unevaluated),
})
}));
if issues.is_empty() {
issues.push(outcome_issue(&Issue {
severity: Severity::Information,
code: IssueType::Informational,
expression: String::new(),
rule: Rule::Message(MessageId::AllOk),
message: "no issues, and every constraint met was evaluated".to_owned(),
}));
}
Self {
issue: issues,
..Self::default()
}
}
}
fn describe(unevaluated: &Unevaluated) -> String {
let what = match &unevaluated.constraint {
Constraint::Slicing => "the slicing".to_owned(),
Constraint::Discriminator { kind, path } => {
format!("the {kind} discriminator {path}")
}
Constraint::DiscriminatorValueSet { path, value_set } => {
format!("the discriminator {path} over the value set {value_set}")
}
Constraint::Invariant { key, .. } => format!("the invariant {key}"),
Constraint::Binding {
strength,
value_set,
} => match value_set {
Some(value_set) => format!("the {strength} binding to {value_set}"),
None => format!("the {strength} binding"),
},
Constraint::TypeProfile { profiles } => {
format!("the type profiles {}", profiles.join(", "))
}
Constraint::TargetProfile { profiles } => {
format!("the target profiles {}", profiles.join(", "))
}
Constraint::ClaimedProfile { url } => format!("the claimed profile {url}"),
Constraint::ExtensionContext => "the extension context".to_owned(),
};
format!(
"{what} on {} of {} was not evaluated: {}",
unevaluated.element, unevaluated.structure, unevaluated.reason
)
}
fn outcome_issue(issue: &Issue) -> OperationOutcomeIssue {
let coding = match &issue.rule {
Rule::Message(id) => Coding {
system: Some(primitives::Uri::from(MESSAGE_SYSTEM.to_owned())),
code: Some(primitives::Code::from(id.code().to_owned())),
..Coding::default()
},
Rule::Constraint { key, source } => Coding {
system: Some(primitives::Uri::from(source.clone())),
code: Some(primitives::Code::from(key.clone())),
..Coding::default()
},
};
OperationOutcomeIssue {
severity: primitives::Code::from(issue.severity.code().to_owned()),
code: primitives::Code::from(issue.code.code().to_owned()),
details: Some(CodeableConcept {
coding: vec![coding],
text: Some(primitives::String::from(issue.message.clone())),
..CodeableConcept::default()
}),
expression: if issue.expression.is_empty() {
Vec::new()
} else {
vec![primitives::String::from(issue.expression.clone())]
},
..OperationOutcomeIssue::default()
}
}