Skip to main content

sbol_db_postgres/repo/
validation.rs

1use sbol::v3::ValidationReport;
2use sbol_db_core::{DomainError, GraphId, Severity, ValidationRunId, ValidationStatus};
3use sqlx::Row;
4use uuid::Uuid;
5
6use crate::repo::db_err;
7use crate::PgPool;
8
9#[derive(Clone)]
10pub struct ValidationRepository {
11    _pool: PgPool,
12}
13
14#[derive(Debug, Clone)]
15pub struct RecordedValidation {
16    pub run_id: ValidationRunId,
17    pub status: ValidationStatus,
18    pub issue_count: usize,
19}
20
21impl ValidationRepository {
22    pub fn new(pool: PgPool) -> Self {
23        Self { _pool: pool }
24    }
25
26    /// Record a validation run plus all of its findings. Run inside the
27    /// caller's transaction.
28    #[allow(clippy::too_many_arguments)]
29    pub async fn record_run(
30        &self,
31        conn: &mut sqlx::PgConnection,
32        target_iri: &str,
33        graph_id: Option<GraphId>,
34        validator_name: &str,
35        validator_version: Option<&str>,
36        ruleset: &str,
37        report: &ValidationReport,
38    ) -> Result<RecordedValidation, DomainError> {
39        let status = classify(report);
40        let run_row = sqlx::query(
41            r#"
42            INSERT INTO sbol_validation_runs (
43                target_iri, graph_id, validator_name, validator_version,
44                ruleset, status, finished_at, summary
45            ) VALUES ($1, $2, $3, $4, $5, $6, now(), $7)
46            RETURNING id
47            "#,
48        )
49        .bind(target_iri)
50        .bind(graph_id.map(|d| d.0))
51        .bind(validator_name)
52        .bind(validator_version)
53        .bind(ruleset)
54        .bind(status.as_db_str())
55        .bind(serde_json::json!({
56            "issue_count": report.issues().len(),
57            "error_count": report.errors().count(),
58            "warning_count": report.warnings().count(),
59        }))
60        .fetch_one(&mut *conn)
61        .await
62        .map_err(db_err)?;
63        let run_id: Uuid = run_row.try_get("id").map_err(db_err)?;
64
65        for issue in report.issues() {
66            let severity = match issue.severity {
67                sbol::v3::Severity::Warning => Severity::Warning,
68                sbol::v3::Severity::Error => Severity::Error,
69                _ => Severity::Warning,
70            };
71            let subject_iri = issue.subject.as_iri().map(|i| i.as_str().to_owned());
72            sqlx::query(
73                r#"
74                INSERT INTO sbol_validation_findings (
75                    validation_run_id, severity, rule_id, message, subject_iri, path
76                ) VALUES ($1, $2, $3, $4, $5, $6)
77                "#,
78            )
79            .bind(run_id)
80            .bind(severity.as_db_str())
81            .bind(issue.rule)
82            .bind(&issue.message)
83            .bind(subject_iri)
84            .bind(issue.property)
85            .execute(&mut *conn)
86            .await
87            .map_err(db_err)?;
88        }
89
90        Ok(RecordedValidation {
91            run_id: ValidationRunId(run_id),
92            status,
93            issue_count: report.issues().len(),
94        })
95    }
96}
97
98fn classify(report: &ValidationReport) -> ValidationStatus {
99    if report.has_errors() {
100        ValidationStatus::Failed
101    } else if report.warnings().next().is_some() {
102        ValidationStatus::Warning
103    } else {
104        ValidationStatus::Passed
105    }
106}