use sbol_ontology::Ontology;
use crate::validation::blocker::Blocker;
use crate::validation::context::{ExternalValidationMode, ValidationContext};
use crate::validation::options::ValidationOptions;
use crate::validation::report::{
AppliedOptions, CoverageKind, NotApplied, NotAppliedReason, PartialApplication, RuleCoverage,
ValidationIssue, ValidationReport,
};
use crate::validation::resolver::OwnershipIndex;
use crate::validation::spec::{RuleStatus, ValidationRuleStatus, validation_rule_statuses};
use crate::{Document, Object};
pub(crate) struct Validator<'a> {
pub(crate) document: &'a Document,
pub(crate) ownership: OwnershipIndex,
pub(crate) context: ValidationContext<'a>,
pub(crate) issues: Vec<ValidationIssue>,
}
impl<'a> Validator<'a> {
pub(crate) fn new(document: &'a Document, options: ValidationOptions) -> Self {
Self::new_with_context(document, ValidationContext::with_options(options))
}
pub(crate) fn new_with_context(document: &'a Document, context: ValidationContext<'a>) -> Self {
Self {
document,
ownership: OwnershipIndex::new(document),
context,
issues: Vec::new(),
}
}
pub(crate) fn ontology(&self) -> &Ontology {
self.context.ontology()
}
pub(crate) fn validate(&mut self) {
for object in self.document.objects().values() {
self.validate_sbol_namespace(object);
self.validate_sbol_types(object);
self.validate_table_rules(object);
self.validate_display_id(object);
self.validate_top_level(object);
self.validate_sequence(object);
self.validate_feature_vocabularies(object);
self.validate_class_specific_rules(object);
self.validate_workflow_rules(object);
}
self.validate_derived_from_cycles();
self.validate_was_generated_by_cycles();
self.validate_component_instance_cycles();
self.validate_variant_derivation_cycles();
self.validate_top_level_url_prefixes();
self.validate_child_url_patterns();
self.validate_location_sequence_membership();
}
pub(crate) fn finish(self) -> ValidationReport {
let coverage = compute_coverage(self.context.external_mode());
let options = self.context.options();
let options_summary = AppliedOptions {
topology_completeness: options.topology_completeness,
external_mode: self.context.external_mode(),
document_resolvers: self.context.document_resolvers().len()
+ self.context.documents().len(),
content_resolvers: self.context.content_resolvers().len(),
overridden_rules: options.overrides().collect(),
severity_floor: options.severity_floor(),
severity_ceiling: options.severity_ceiling(),
};
ValidationReport {
issues: self.issues,
coverage,
options_summary,
}
}
pub(crate) fn error(
&mut self,
rule: &'static str,
object: &Object,
property: Option<&'static str>,
message: impl Into<String>,
) {
self.emit(rule, object, property, message, crate::Severity::Error);
}
pub(crate) fn warning(
&mut self,
rule: &'static str,
object: &Object,
property: Option<&'static str>,
message: impl Into<String>,
) {
self.emit(rule, object, property, message, crate::Severity::Warning);
}
fn emit(
&mut self,
rule: &'static str,
object: &Object,
property: Option<&'static str>,
message: impl Into<String>,
catalog_default: crate::Severity,
) {
let Some(severity) = self
.context
.options()
.resolved_severity(rule, catalog_default)
else {
return;
};
let issue = match severity {
crate::Severity::Error => {
ValidationIssue::error(rule, object.identity().clone(), property, message)
}
crate::Severity::Warning => {
ValidationIssue::warning(rule, object.identity().clone(), property, message)
}
};
self.issues.push(issue);
}
pub(crate) fn extend_with_overrides(
&mut self,
issues: impl IntoIterator<Item = ValidationIssue>,
) {
let options = self.context.options();
for mut issue in issues {
let Some(severity) = options.resolved_severity(issue.rule, issue.severity) else {
continue;
};
issue.severity = severity;
self.issues.push(issue);
}
}
}
fn compute_coverage(external_mode: ExternalValidationMode) -> RuleCoverage {
let mut coverage = RuleCoverage::default();
for status in validation_rule_statuses() {
match coverage_for_rule(status, external_mode) {
RuleCoverageOutcome::Fully => coverage.fully_applied.push(status.rule),
RuleCoverageOutcome::Partial(application) => {
coverage.partially_applied.push(application);
}
RuleCoverageOutcome::NotApplied(record) => coverage.not_applied.push(record),
}
}
coverage
}
enum RuleCoverageOutcome {
Fully,
Partial(PartialApplication),
NotApplied(NotApplied),
}
fn coverage_for_rule(
status: &ValidationRuleStatus,
external_mode: ExternalValidationMode,
) -> RuleCoverageOutcome {
let resolver_satisfied = matches!(
external_mode,
ExternalValidationMode::ProvidedOnly | ExternalValidationMode::ExternalAllowed
);
match status.status {
RuleStatus::Error | RuleStatus::Warning => RuleCoverageOutcome::Fully,
RuleStatus::Configurable => {
let blocker = status
.blocker
.expect("Configurable rule must declare a blocker (build invariant)");
match blocker {
Blocker::Resolver if resolver_satisfied => RuleCoverageOutcome::Fully,
Blocker::Resolver => RuleCoverageOutcome::Partial(PartialApplication {
rule: status.rule,
blocker,
coverage_kind: CoverageKind::LocalReferencesOnly,
}),
Blocker::External => RuleCoverageOutcome::Partial(PartialApplication {
rule: status.rule,
blocker,
coverage_kind: CoverageKind::LocalReferencesOnly,
}),
_ => RuleCoverageOutcome::Fully,
}
}
RuleStatus::MachineUncheckable => RuleCoverageOutcome::NotApplied(NotApplied {
rule: status.rule,
reason: NotAppliedReason::MachineUncheckable,
}),
RuleStatus::Unimplemented => {
let blocker = status
.blocker
.expect("Unimplemented rule must declare a blocker (build invariant)");
RuleCoverageOutcome::NotApplied(NotApplied {
rule: status.rule,
reason: NotAppliedReason::Deferred(blocker),
})
}
}
}
#[allow(dead_code)] fn coverage_kind_for(blocker: Blocker) -> CoverageKind {
match blocker {
Blocker::Ontology => CoverageKind::OntologyKnownTermsOnly,
Blocker::Resolver | Blocker::External => CoverageKind::LocalReferencesOnly,
Blocker::Policy => CoverageKind::PolicyDefaultUndecided,
Blocker::StrictDatatype => CoverageKind::LexicalShapeOnly,
}
}