use ferrobridge_validator::issue::{Issue, Rule, Severity};
use ferrobridge_validator::report::{Constraint, Reason, Report};
use ferrobridge_validator::set::PackageSet;
use ferrobridge_validator::terminology::Terminology;
use ferrobridge_validator::validate::{ValidateError, Validator};
use fhir_types::codec::Value;
use http::StatusCode;
use crate::receive::ReceivedDocument;
use crate::receive::conform::{Finding, FindingKind, Unevaluated, Unread};
impl ReceivedDocument {
pub async fn validate<T: Terminology + ?Sized + Sync>(
&self,
set: &PackageSet,
profile: &str,
terminology: Option<&T>,
) -> Result<Validated, ValidationError> {
let document = Value::Object(self.tree().clone());
let mut report = Validator::new(set)
.validate_against(&document, profile)
.map_err(|source| ValidationError::Validator {
profile: profile.to_owned(),
source: Box::new(source),
})?;
if let Some(terminology) = terminology {
report.resolve_bindings(terminology).await;
}
validated(&report, profile)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Validated {
cautions: Vec<Finding>,
unevaluated: Vec<Unevaluated>,
}
impl Validated {
#[must_use]
pub fn cautions(&self) -> &[Finding] {
&self.cautions
}
#[must_use]
pub fn unevaluated(&self) -> &[Unevaluated] {
&self.unevaluated
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ValidationError {
#[error("the document breaks its profiles in {} places", findings.len())]
NonConformant {
findings: Vec<Finding>,
},
#[error("the terminology server did not check a required binding ({kind})")]
Terminology {
kind: &'static str,
status: Option<StatusCode>,
},
#[error("the package set cannot validate a document against {profile}")]
Validator {
profile: String,
#[source]
source: Box<ValidateError>,
},
}
fn validated(report: &Report, profile: &str) -> Result<Validated, ValidationError> {
let (refusals, cautions): (Vec<&Issue>, Vec<&Issue>) = report
.issues()
.iter()
.partition(|issue| matches!(issue.severity, Severity::Error | Severity::Fatal));
if !refusals.is_empty() {
return Err(ValidationError::NonConformant {
findings: refusals
.into_iter()
.map(|issue| finding(issue, profile))
.collect(),
});
}
if let Some((kind, status)) = report.unevaluated().iter().find_map(|unevaluated| {
let required = match &unevaluated.constraint {
Constraint::Binding { strength, .. } => strength == "required",
Constraint::MaxValueSet { .. } => true,
_ => false,
};
match unevaluated.reason {
Reason::TerminologyFailed { kind, status } if required => Some((kind, status)),
_ => None,
}
}) {
return Err(ValidationError::Terminology { kind, status });
}
Ok(Validated {
cautions: cautions
.into_iter()
.map(|issue| finding(issue, profile))
.collect(),
unevaluated: report
.unevaluated()
.iter()
.map(|unevaluated| Unevaluated {
profile: unevaluated.structure.clone(),
element: unevaluated.element.clone(),
reason: Unread::Constraint {
constraint: described(&unevaluated.constraint),
reason: unevaluated.reason.to_string(),
},
})
.collect(),
})
}
fn finding(issue: &Issue, profile: &str) -> Finding {
let (profile, element) = match &issue.rule {
Rule::Constraint { key, source } => (source.clone(), key.clone()),
Rule::Message(id) => (profile.to_owned(), id.code().to_owned()),
};
Finding {
profile,
element,
location: issue.expression.clone(),
kind: FindingKind::Issue {
severity: issue.severity.code(),
code: issue.code.code(),
message: issue.message.clone(),
},
}
}
fn described(constraint: &Constraint) -> String {
match 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,
} => value_set.as_ref().map_or_else(
|| format!("the {strength} binding"),
|value_set| format!("the {strength} binding to {value_set}"),
),
Constraint::MaxValueSet { value_set } => format!("the maximum value set {value_set}"),
Constraint::TypeProfile { profiles } => {
format!("the type profiles {}", profiles.join(", "))
}
Constraint::TargetProfile { profiles } => {
format!("the target profiles {}", profiles.join(", "))
}
Constraint::Aggregation { modes } => format!("the aggregation {}", modes.join(", ")),
Constraint::Versioning { rule } => format!("the versioning {rule}"),
Constraint::ClaimedProfile { url } => format!("the claimed profile {url}"),
Constraint::ExtensionContext => "the extension context".to_owned(),
Constraint::ContextInvariant { expression } => {
format!("the context invariant {expression}")
}
}
}