mantra 0.7.2

`mantra` offers a lightweight approach for requirement tracing and coverage.
use std::path::PathBuf;

use mantra_schema::{requirements::ReqId, reviews::ReviewSchema};
use time::PrimitiveDateTime;

use crate::db::{DbError, MantraDb};

#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct Review {
    pub name: String,
    #[serde(with = "super::review_date_format")]
    #[schemars(
        with = "String",
        regex(
            pattern = r"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2}) (?<hour>\d{2}):(?<minute>\d{2})(?<second>:\d{2}(?<subsecond>\.\d{3})?)?"
        )
    )]
    pub date: PrimitiveDateTime,
    pub reviewer: String,
    pub comment: Option<String>,
    pub requirements: Vec<VerifiedRequirement>,
}

#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct VerifiedRequirement {
    pub id: ReqId,
    pub comment: Option<String>,
}

#[derive(Debug, Clone, thiserror::Error)]
pub enum ReviewError {
    #[error("Could not read file '{}'.", .0.display())]
    ReadingFile(PathBuf),
    #[error("File '{}' is not a valid review.", .0.display())]
    Parsing(PathBuf),
    #[error("{}", .0)]
    Db(DbError),
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ReviewConfig {
    #[serde(
        alias = "filepaths",
        alias = "external-files",
        alias = "external-filepaths"
    )]
    pub files: Vec<PathBuf>,
}

pub async fn collect(db: &MantraDb, cfg: ReviewConfig) -> Result<usize, ReviewError> {
    let mut review_cnt = 0;

    for review_file in &cfg.files {
        if !matches!(
            review_file.extension().and_then(|s| s.to_str()),
            Some("toml")
        ) {
            log::warn!(
                "Only TOML format is supported for reviews. Skipped file '{}'.",
                review_file.display()
            );
            continue;
        }

        let file_content = std::fs::read_to_string(review_file)
            .map_err(|_| ReviewError::ReadingFile(review_file.to_path_buf()))?;
        let review: ReviewSchema = toml::from_str(&file_content).map_err(|err| {
            log::error!(
                "Failed parsing review file '{}': {}",
                review_file.display(),
                err
            );
            ReviewError::Parsing(review_file.to_path_buf())
        })?;

        if db.review_exists(&review.name, &review.date).await {
            log::info!(
                "Review '{}' already in the database.",
                review_file.display()
            );
        } else {
            let res = db.add_review(review).await.map_err(ReviewError::Db);

            if let Err(err) = res {
                log::error!("Adding review '{}' failed: {}", review_file.display(), err);
            }

            review_cnt += 1;
        }
    }

    Ok(review_cnt)
}

pub async fn collect_from_schema(db: &MantraDb, review: ReviewSchema) -> Result<(), ReviewError> {
    db.add_review(review).await.map_err(ReviewError::Db)
}