mod target;
mod workflow;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use miette::{Diagnostic, JSONReportHandler, Severity, SourceSpan};
use crate::code::CodeReference;
use crate::description::DescriptionOwner;
use crate::graph::WorkflowGraph;
use crate::link::Link;
use crate::model::{ByteRange, Element, MAX_MAGNITUDE, Model, ViewAngle};
use crate::path::ElementPath;
use crate::problem::{
DuplicateElement, DuplicateOverride, DuplicateWorkflow, ElementSite, ErrorSeverity,
ImpossibleHeight, MalformedCodeReference, MalformedWorkflowId, MismatchedStyleValue,
MissingAncestor, NoWorkflow, Problem, StyleOwner, Suggestion, UnknownStyleKey, UnresolvedLink,
UnshownElement,
};
use crate::properties::ComponentProperties;
use crate::style::AnyStyleKey;
use crate::workflow::{Workflow, WorkflowId};
use self::target::{ComponentScope, TargetScope};
use self::workflow::WorkflowCheck;
#[derive(Debug)]
pub struct UncheckedModel {
pub(crate) model: Model,
pub(crate) duplicate_declarations: Vec<(ElementPath, Option<SourceSpan>)>,
pub(crate) duplicate_overrides: Vec<(WorkflowId, ElementPath, Option<SourceSpan>)>,
}
#[derive(Debug)]
pub struct ModelAdmission {
pub model: Option<Model>,
pub report: ModelReport,
}
#[derive(Debug, Default)]
pub struct ModelReport {
problems: Vec<Problem>,
}
struct SiteValue<'a, T> {
site: &'a ElementSite,
value: Option<T>,
span: Option<SourceSpan>,
}
impl Default for UncheckedModel {
fn default() -> Self {
Self::from_parts(BTreeMap::new(), Vec::new())
}
}
impl UncheckedModel {
pub fn from_parts(elements: BTreeMap<ElementPath, Element>, workflows: Vec<Workflow>) -> Self {
let model = Model {
view: None,
elements,
workflows,
};
Self {
model,
duplicate_declarations: Vec::new(),
duplicate_overrides: Vec::new(),
}
}
pub fn with_view(mut self, view: ViewAngle) -> Self {
self.model.view = Some(view);
self
}
pub fn admit(self) -> ModelAdmission {
let report = self.validate();
let has_errors = report.has_errors();
if has_errors {
return ModelAdmission {
model: None,
report,
};
}
let admitted_model = Some(self.model);
ModelAdmission {
model: admitted_model,
report,
}
}
fn validate(&self) -> ModelReport {
let model = &self.model;
let graphs: Vec<WorkflowGraph> = model
.workflows()
.iter()
.map(|workflow| WorkflowGraph::of(model, workflow))
.collect();
let duplicate_elements = self.find_duplicate_elements();
let missing_ancestors = model.find_missing_ancestors();
let element_property_problems = model.find_element_property_problems();
let workflow_set_problems = model.find_workflow_set_problems();
let duplicate_overrides = self.find_duplicate_overrides();
let unshown_elements = model.find_unshown_elements(&graphs);
let component_description_problems = model.find_component_description_problems(&graphs);
let workflow_problems =
model
.workflows()
.iter()
.zip(&graphs)
.flat_map(|(workflow, graph)| {
let workflow_check = WorkflowCheck::new(model, workflow, graph);
workflow_check.problems()
});
let problems = duplicate_elements
.into_iter()
.chain(missing_ancestors)
.chain(element_property_problems)
.chain(workflow_set_problems)
.chain(duplicate_overrides)
.chain(unshown_elements)
.chain(component_description_problems)
.chain(workflow_problems)
.collect();
ModelReport { problems }
}
fn find_duplicate_elements(&self) -> Vec<Problem> {
let duplicate_declarations = self.duplicate_declarations.iter();
let problems = duplicate_declarations.map(|(path, span)| {
let duplicate_element = DuplicateElement {
element: path.clone(),
span: *span,
};
Problem::DuplicateElement(duplicate_element)
});
problems.collect()
}
fn find_duplicate_overrides(&self) -> Vec<Problem> {
let duplicate_overrides = self.duplicate_overrides.iter();
let problems = duplicate_overrides.map(|(workflow, path, span)| {
let duplicate_override = DuplicateOverride {
workflow: workflow.clone(),
element: path.clone(),
span: *span,
};
Problem::DuplicateOverride(duplicate_override)
});
problems.collect()
}
}
impl Model {
fn find_missing_ancestors(&self) -> Vec<Problem> {
let elements = self.elements();
let mut reported_ancestors = BTreeSet::new();
let mut problems = Vec::new();
for (path, element) in elements {
for ancestor in path.ancestors() {
let is_present = elements.contains_key(&ancestor);
if is_present {
continue;
}
let is_already_reported = !reported_ancestors.insert(ancestor.clone());
if is_already_reported {
continue;
}
let suggestion = self.suggest_closest_element(&ancestor);
let span = element.source_span();
let missing_ancestor = MissingAncestor {
element: path.clone(),
ancestor,
suggestion,
span,
};
let problem = Problem::MissingAncestor(missing_ancestor);
problems.push(problem);
}
}
problems
}
fn find_element_property_problems(&self) -> Vec<Problem> {
let mut problems = Vec::new();
for (path, element) in self.elements() {
let site = ElementSite::Declaration(path.clone());
let site_span = element.source_span();
let elements = self.elements();
let property_problems =
site.check_properties(element.properties(), site_span, elements);
problems.extend(property_problems);
}
problems
}
fn find_workflow_set_problems(&self) -> Vec<Problem> {
let workflows = self.workflows();
let has_no_workflow = workflows.is_empty();
if has_no_workflow {
return vec![Problem::NoWorkflow(NoWorkflow)];
}
let mut seen_ids = BTreeSet::new();
let mut problems = Vec::new();
for workflow in workflows {
let id = workflow.id();
let span = workflow.source_span();
let is_malformed = !id.is_well_formed();
if is_malformed {
let malformed_id = MalformedWorkflowId {
workflow: id.clone(),
span,
};
problems.push(Problem::MalformedWorkflowId(malformed_id));
}
let is_first_with_id = seen_ids.insert(id);
if is_first_with_id {
continue;
}
let duplicate_workflow = DuplicateWorkflow {
workflow: id.clone(),
span,
};
problems.push(Problem::DuplicateWorkflow(duplicate_workflow));
}
problems
}
fn find_unshown_elements(&self, graphs: &[WorkflowGraph]) -> Vec<Problem> {
let has_no_workflow = self.workflows().is_empty();
if has_no_workflow {
return Vec::new();
}
let elements = self.elements().iter();
let unshown_elements = elements.filter(|(path, _element)| {
let mut graphs = graphs.iter();
graphs.all(|graph| graph.element(path).is_none())
});
let problems = unshown_elements.map(|(path, element)| {
let span = element.source_span();
let unshown_element = UnshownElement {
element: path.clone(),
span,
};
Problem::UnshownElement(unshown_element)
});
problems.collect()
}
fn find_component_description_problems(&self, graphs: &[WorkflowGraph]) -> Vec<Problem> {
let mut problems = Vec::new();
for (path, element) in self.elements() {
let Some(description) = element.description() else {
continue;
};
let scope = ComponentScope::new(self, graphs, path);
let owner = DescriptionOwner::Element(path.clone());
let span = element.source_span();
let description_problems = scope.check_description(&owner, description, span);
problems.extend(description_problems);
}
problems
}
fn suggest_closest_element(&self, unknown: &ElementPath) -> Option<Suggestion<ElementPath>> {
let unknown_text = unknown.to_string();
let known_paths = self.elements().keys().cloned();
Suggestion::closest_to(&unknown_text, known_paths)
}
}
impl ElementSite {
pub(crate) fn check_properties(
&self,
properties: &ComponentProperties,
site_span: Option<SourceSpan>,
elements: &BTreeMap<ElementPath, Element>,
) -> Vec<Problem> {
let owner = StyleOwner::Element(self.clone());
let style_entries = properties.style().iter();
let style_problems =
style_entries.filter_map(|(key, value)| owner.check_style_entry(key, value, site_span));
let mut problems: Vec<Problem> = style_problems.collect();
let height = SiteValue::new(self, properties.height(), None, site_span);
let code_span = properties.code_span();
let code = SiteValue::new(self, properties.code(), code_span, site_span);
let link_span = properties.link_span();
let link = SiteValue::new(self, properties.link(), link_span, site_span);
problems.extend(height.check_height_is_bounded());
problems.extend(code.check_code_is_well_formed());
problems.extend(link.check_link_resolves(elements));
problems
}
}
impl<'a, T> SiteValue<'a, T> {
fn new(
site: &'a ElementSite,
value: Option<T>,
value_span: Option<ByteRange>,
site_span: Option<SourceSpan>,
) -> Self {
let value_source_span = value_span.map(SourceSpan::from);
let span = value_source_span.or(site_span);
Self { site, value, span }
}
}
impl SiteValue<'_, f64> {
fn check_height_is_bounded(&self) -> Option<Problem> {
let height = self.value?;
let is_positive = height > 0.0;
let is_within_magnitude = height <= MAX_MAGNITUDE;
let is_bounded = is_positive && is_within_magnitude;
if is_bounded {
return None;
}
let impossible_height = ImpossibleHeight {
site: self.site.clone(),
height,
span: self.span,
};
Some(Problem::ImpossibleHeight(impossible_height))
}
}
impl SiteValue<'_, &str> {
fn check_code_is_well_formed(&self) -> Option<Problem> {
let raw_code = self.value?;
let parsed_code = CodeReference::parse(raw_code);
let Err(reason) = parsed_code else {
return None;
};
let malformed_code_reference = MalformedCodeReference {
site: self.site.clone(),
code: raw_code.to_owned(),
reason,
span: self.span,
};
Some(Problem::MalformedCodeReference(malformed_code_reference))
}
fn check_link_resolves(&self, elements: &BTreeMap<ElementPath, Element>) -> Option<Problem> {
let raw_link = self.value?;
let link = Link::resolve(raw_link, elements);
let target = link.path_shaped_url()?;
let target_text = target.to_string();
let known_paths = elements.keys().cloned();
let suggestion = Suggestion::closest_to(&target_text, known_paths);
let unresolved_link = UnresolvedLink {
site: self.site.clone(),
link: raw_link.to_owned(),
suggestion,
span: self.span,
};
Some(Problem::UnresolvedLink(unresolved_link))
}
}
impl StyleOwner {
fn check_style_entry(
&self,
key: &str,
value: &str,
span: Option<SourceSpan>,
) -> Option<Problem> {
let Some(style_key) = AnyStyleKey::find(key) else {
let known_names = AnyStyleKey::ALL.iter().map(AnyStyleKey::name);
let suggestion = Suggestion::closest_to(key, known_names);
let unknown_style_key = UnknownStyleKey {
owner: self.clone(),
key: key.to_owned(),
suggestion,
span,
};
return Some(Problem::UnknownStyleKey(unknown_style_key));
};
let expected = style_key.value_kind();
let is_fitting_value = expected.accepts(value);
if is_fitting_value {
return None;
}
let mismatched_style_value = MismatchedStyleValue {
owner: self.clone(),
key: style_key,
value: value.to_owned(),
expected,
span,
};
Some(Problem::MismatchedStyleValue(mismatched_style_value))
}
}
impl ModelReport {
pub fn has_errors(&self) -> bool {
self.problems.iter().any(Problem::is_error)
}
pub fn to_json(&self) -> String {
let handler = JSONReportHandler::new();
let mut rendered = String::new();
handler
.render_report(&mut rendered, self)
.expect("a report renders into a string: every message is plain text");
rendered
}
pub fn retain(&mut self, keep: impl FnMut(&Problem) -> bool) {
self.problems.retain(keep);
}
pub fn problems(&self) -> &[Problem] {
&self.problems
}
}
impl fmt::Display for ModelReport {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let error_count = self
.problems
.iter()
.filter(|problem| problem.is_error())
.count();
let warning_count = self.problems.len() - error_count;
write!(
formatter,
"the model has {error_count} error(s) and {warning_count} warning(s)"
)
}
}
impl std::error::Error for ModelReport {}
impl Diagnostic for ModelReport {
fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
let code: Box<dyn fmt::Display + 'a> = Box::new("svugy::model_report");
Some(code)
}
fn severity(&self) -> Option<Severity> {
let has_errors = self.has_errors();
if has_errors {
return Some(Severity::Error);
}
let is_empty = self.problems.is_empty();
if is_empty {
return Some(Severity::Advice);
}
Some(Severity::Warning)
}
fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
let problems = self
.problems
.iter()
.map(|problem| problem as &dyn Diagnostic);
let related: Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a> = Box::new(problems);
Some(related)
}
}