mesofact_core/
validate.rs1use crate::manifest::{Manifest, Requires, RouteMode, MANIFEST_VERSION};
18use std::collections::BTreeMap;
19use thiserror::Error;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum SourceScope {
23 Global,
24 Project,
25 User,
26}
27
28impl SourceScope {
29 pub fn as_str(self) -> &'static str {
30 match self {
31 SourceScope::Global => "global",
32 SourceScope::Project => "project",
33 SourceScope::User => "user",
34 }
35 }
36}
37
38pub type SourceCatalog = BTreeMap<String, SourceScope>;
39
40#[derive(Debug, Clone, PartialEq, Eq, Error)]
41pub enum ValidationErrorKind {
42 #[error("unsupported manifest version '{got}' (expected '{}')", MANIFEST_VERSION)]
43 UnsupportedVersion { got: String },
44 #[error("source '{name}' not declared in catalog")]
45 UnknownSource { name: String },
46 #[error("Mode 1 cannot read from non-'global' source '{name}' (scope='{scope}')")]
47 Mode1ScopedSource { name: String, scope: &'static str },
48 #[error("Mode 1 cannot require 'user' (the build can't enumerate users)")]
49 Mode1RequiresUser,
50}
51
52impl ValidationErrorKind {
53 pub fn label(&self) -> &'static str {
56 match self {
57 ValidationErrorKind::UnsupportedVersion { .. } => "unsupported_version",
58 ValidationErrorKind::UnknownSource { .. } => "unknown_source",
59 ValidationErrorKind::Mode1ScopedSource { .. } => "mode1_scoped_source",
60 ValidationErrorKind::Mode1RequiresUser => "mode1_requires_user",
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Error)]
66#[error("{path}: {kind}")]
67pub struct ValidationError {
68 pub path: String,
69 pub kind: ValidationErrorKind,
70}
71
72pub fn validate(manifest: &Manifest, catalog: &SourceCatalog) -> Result<(), Vec<ValidationError>> {
73 let mut errors = Vec::new();
74
75 if manifest.version != MANIFEST_VERSION {
76 errors.push(ValidationError {
77 path: "version".into(),
78 kind: ValidationErrorKind::UnsupportedVersion {
79 got: manifest.version.clone(),
80 },
81 });
82 }
83
84 for (idx, route) in manifest.routes.iter().enumerate() {
85 if !matches!(route.mode, RouteMode::Static) {
86 continue;
87 }
88
89 if let Some(reads) = &route.source_reads {
90 for name in reads {
91 let base = format!("routes[{idx}].source_reads");
92 match catalog.get(name) {
93 None => errors.push(ValidationError {
94 path: base,
95 kind: ValidationErrorKind::UnknownSource { name: name.clone() },
96 }),
97 Some(SourceScope::Global) => {}
98 Some(other) => errors.push(ValidationError {
99 path: base,
100 kind: ValidationErrorKind::Mode1ScopedSource {
101 name: name.clone(),
102 scope: other.as_str(),
103 },
104 }),
105 }
106 }
107 }
108
109 if let Some(requires) = &route.requires {
110 if requires.iter().any(|r| matches!(r, Requires::User)) {
111 errors.push(ValidationError {
112 path: format!("routes[{idx}].requires"),
113 kind: ValidationErrorKind::Mode1RequiresUser,
114 });
115 }
116 }
117 }
118
119 if errors.is_empty() {
120 Ok(())
121 } else {
122 Err(errors)
123 }
124}