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