use crate::manifest::{Manifest, Requires, RouteMode, MANIFEST_VERSION};
use std::collections::BTreeMap;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceScope {
Global,
Project,
User,
}
impl SourceScope {
pub fn as_str(self) -> &'static str {
match self {
SourceScope::Global => "global",
SourceScope::Project => "project",
SourceScope::User => "user",
}
}
}
pub type SourceCatalog = BTreeMap<String, SourceScope>;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ValidationErrorKind {
#[error("unsupported manifest version '{got}' (expected '{}')", MANIFEST_VERSION)]
UnsupportedVersion { got: String },
#[error("source '{name}' not declared in catalog")]
UnknownSource { name: String },
#[error("Mode 1 cannot read from non-'global' source '{name}' (scope='{scope}')")]
Mode1ScopedSource { name: String, scope: &'static str },
#[error("Mode 1 cannot require 'user' (the build can't enumerate users)")]
Mode1RequiresUser,
}
impl ValidationErrorKind {
pub fn label(&self) -> &'static str {
match self {
ValidationErrorKind::UnsupportedVersion { .. } => "unsupported_version",
ValidationErrorKind::UnknownSource { .. } => "unknown_source",
ValidationErrorKind::Mode1ScopedSource { .. } => "mode1_scoped_source",
ValidationErrorKind::Mode1RequiresUser => "mode1_requires_user",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("{path}: {kind}")]
pub struct ValidationError {
pub path: String,
pub kind: ValidationErrorKind,
}
pub fn validate(manifest: &Manifest, catalog: &SourceCatalog) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
if manifest.version != MANIFEST_VERSION {
errors.push(ValidationError {
path: "version".into(),
kind: ValidationErrorKind::UnsupportedVersion {
got: manifest.version.clone(),
},
});
}
for (idx, route) in manifest.routes.iter().enumerate() {
if !matches!(route.mode, RouteMode::Static) {
continue;
}
if let Some(reads) = &route.source_reads {
for name in reads {
let base = format!("routes[{idx}].source_reads");
match catalog.get(name) {
None => errors.push(ValidationError {
path: base,
kind: ValidationErrorKind::UnknownSource { name: name.clone() },
}),
Some(SourceScope::Global) => {}
Some(other) => errors.push(ValidationError {
path: base,
kind: ValidationErrorKind::Mode1ScopedSource {
name: name.clone(),
scope: other.as_str(),
},
}),
}
}
}
if let Some(requires) = &route.requires {
if requires.iter().any(|r| matches!(r, Requires::User)) {
errors.push(ValidationError {
path: format!("routes[{idx}].requires"),
kind: ValidationErrorKind::Mode1RequiresUser,
});
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}