use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::future::Future;
use ferrobridge_term::client::Client;
use ferrobridge_term::error::Error;
use ferrobridge_term::outcome::Coded;
use ferrobridge_term::outcome::Request;
use ferrobridge_term::outcome::ValidateOutcome;
use ferrobridge_term::outcome::Validation;
use crate::issue::Issue;
use crate::issue::IssueType;
use crate::issue::MessageId;
use crate::issue::Rule;
use crate::issue::Severity;
use crate::report::Key;
use crate::report::Reason;
use crate::report::Report;
pub trait Terminology {
fn validate_all(
&self,
validations: &[Validation],
) -> impl Future<Output = Result<Vec<Result<ValidateOutcome, Error>>, Error>> + Send;
fn supports(&self, _validation: &Validation) -> Result<(), Error> {
Ok(())
}
}
impl Terminology for Client {
fn validate_all(
&self,
validations: &[Validation],
) -> impl Future<Output = Result<Vec<Result<ValidateOutcome, Error>>, Error>> + Send {
Self::validate_all(self, validations)
}
fn supports(&self, validation: &Validation) -> Result<(), Error> {
self.check(&Request::ValidateCode(validation.clone()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Strength {
Required,
Extensible,
Preferred,
Maximum,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingBinding {
pub(crate) key: Key,
pub(crate) location: String,
pub(crate) strength: Strength,
pub(crate) value_set: String,
pub(crate) validation: Validation,
pub(crate) display: Option<String>,
}
impl PendingBinding {
#[must_use]
pub fn location(&self) -> &str {
&self.location
}
#[must_use]
pub fn value_set(&self) -> &str {
&self.value_set
}
#[must_use]
pub const fn validation(&self) -> &Validation {
&self.validation
}
}
impl Report {
pub async fn resolve_bindings<T: Terminology + ?Sized>(&mut self, terminology: &T) {
if self.pending.is_empty() {
return;
}
let pending = std::mem::take(&mut self.pending);
let mut questions: Vec<Validation> = Vec::new();
let mut positions: BTreeMap<&Validation, usize> = BTreeMap::new();
let mut asked = Vec::with_capacity(pending.len());
for check in &pending {
let position = *positions.entry(&check.validation).or_insert_with(|| {
questions.push(check.validation.clone());
questions.len().saturating_sub(1)
});
asked.push(position);
}
let answers = ask(terminology, &questions).await;
let mut failed: BTreeMap<Key, (String, Reason)> = BTreeMap::new();
for (check, position) in pending.iter().zip(asked) {
let reason = match answers.get(position) {
Some(Ok(ValidateOutcome::Valid { display, .. })) => {
if let (Some(held), Some(answered)) = (&check.display, display)
&& held != answered
{
self.issues.push(display_issue(
&check.location,
check.validation_system(),
Some(answered),
));
}
continue;
}
Some(Ok(ValidateOutcome::Invalid { .. })) => {
self.issues.push(binding_issue(
check.strength,
&check.key.1,
&check.value_set,
&check.location,
));
continue;
}
Some(Ok(_)) => Reason::TerminologyFailed {
kind: "unknown-outcome",
status: None,
},
Some(Err(reason)) => reason.clone(),
None => batch_arity(),
};
failed
.entry(check.key.clone())
.or_insert_with(|| (check.location.clone(), reason));
}
let keys: BTreeSet<&Key> = pending.iter().map(|check| &check.key).collect();
let held = &self.held;
self.unevaluated.retain_mut(|unevaluated| {
let key = (
unevaluated.structure.clone(),
unevaluated.element.clone(),
unevaluated.constraint.clone(),
);
if !keys.contains(&key) || held.contains(&key) {
return true;
}
match failed.get(&key) {
Some((location, reason)) => {
unevaluated.location.clone_from(location);
unevaluated.reason = reason.clone();
true
}
None => false,
}
});
}
}
impl PendingBinding {
fn validation_system(&self) -> &str {
match &self.validation.coded {
Coded::Coding(concept) => concept.system.as_deref().unwrap_or_default(),
_ => "",
}
}
}
async fn ask<T: Terminology + ?Sized>(
terminology: &T,
questions: &[Validation],
) -> Vec<Result<ValidateOutcome, Reason>> {
let mut answers: Vec<Option<Result<ValidateOutcome, Reason>>> = Vec::new();
let mut sent = Vec::new();
for question in questions {
match terminology.supports(question) {
Ok(()) => {
sent.push(question.clone());
answers.push(None);
}
Err(error) => answers.push(Some(Err(failure(&error)))),
}
}
let mut replies: Vec<Result<ValidateOutcome, Reason>> =
match terminology.validate_all(&sent).await {
Ok(replies) if replies.len() == sent.len() => replies
.into_iter()
.map(|reply| reply.map_err(|error| failure(&error)))
.collect(),
Ok(_) => sent.iter().map(|_| Err(batch_arity())).collect(),
Err(error) => sent.iter().map(|_| Err(failure(&error))).collect(),
};
replies.reverse();
answers
.into_iter()
.map(|answer| {
answer
.or_else(|| replies.pop())
.unwrap_or_else(|| Err(batch_arity()))
})
.collect()
}
fn failure(error: &Error) -> Reason {
Reason::TerminologyFailed {
kind: error.kind(),
status: error.upstream_status(),
}
}
const fn batch_arity() -> Reason {
Reason::TerminologyFailed {
kind: "batch-arity",
status: None,
}
}
pub(crate) fn binding_issue(
strength: Strength,
element: &str,
value_set: &str,
location: &str,
) -> Issue {
let (severity, id, what) = match strength {
Strength::Required => (Severity::Error, MessageId::NotInValueSet, "required"),
Strength::Maximum => (
Severity::Error,
MessageId::NotInValueSet,
"maximum value set",
),
Strength::Extensible => (
Severity::Warning,
MessageId::NotInExtensibleValueSet,
"extensible",
),
Strength::Preferred => (
Severity::Information,
MessageId::NotInPreferredValueSet,
"preferred",
),
};
Issue {
severity,
code: IssueType::CodeInvalid,
expression: location.to_owned(),
rule: Rule::Message(id),
message: format!(
"{element} is bound ({what}) to {value_set}, and the value holds no code of it"
),
}
}
pub(crate) fn display_issue(location: &str, system: &str, expected: Option<&str>) -> Issue {
let expected = expected.map_or_else(
|| "the code system gives the code no display".to_owned(),
|display| format!("the code system displays the code as \"{display}\""),
);
Issue {
severity: Severity::Warning,
code: IssueType::Value,
expression: location.to_owned(),
rule: Rule::Message(MessageId::DisplayMismatch),
message: format!("the display is none {system} defines for the code; {expected}"),
}
}