1use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::path::PathBuf;
6use thiserror::Error;
7
8#[derive(Debug, Error)]
10pub enum SpecError {
11 #[error("YAML parse error: {0}")]
13 YamlError(#[from] serde_yaml::Error),
14
15 #[error("Validation error: {0}")]
17 Validation(#[from] ValidationError),
18
19 #[error("IO error reading {path}: {source}")]
21 Io {
22 path: PathBuf,
23 #[source]
24 source: std::io::Error,
25 },
26}
27
28impl From<std::io::Error> for SpecError {
29 fn from(err: std::io::Error) -> Self {
30 SpecError::Io {
31 path: PathBuf::from("<unknown>"),
32 source: err,
33 }
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39pub struct ValidationError {
40 pub kind: ValidationErrorKind,
42
43 pub path: String,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
49pub enum ValidationErrorKind {
50 InvalidVersion { found: String },
52
53 EmptyDeploymentName,
55
56 EmptyServiceName,
58
59 EmptyImageName,
61
62 InvalidPort { port: u32 },
64
65 InvalidCpu { cpu: f64 },
67
68 InvalidMemoryFormat { value: String },
70
71 InvalidDuration { value: String },
73
74 DuplicateEndpoint { name: String },
76
77 UnknownInitAction { action: String },
79
80 UnknownDependency { service: String },
82
83 CircularDependency { service: String, depends_on: String },
85
86 InvalidScaleRange { min: u32, max: u32 },
88
89 EmptyScaleTargets,
91
92 InvalidEnvVar { key: String, reason: String },
94
95 InvalidCronSchedule { schedule: String, reason: String },
97
98 ScheduleOnlyForCron,
100
101 CronRequiresSchedule,
103
104 Generic { message: String },
106
107 InsufficientNodes {
109 required: usize,
110 available: usize,
111 message: String,
112 },
113
114 InvalidTunnelProtocol { protocol: String },
116
117 InvalidTunnelPort { port: u16, field: String },
119
120 InvalidTunnelTtl { value: String, reason: String },
122
123 WasmConfigOnNonWasmType,
125
126 InvalidWasmInstanceRange { min: u32, max: u32 },
128
129 WasmCapabilityNotAvailable {
131 capability: String,
132 service_type: String,
133 },
134
135 WasmHttpMissingHttpEndpoint,
137
138 WasmPreopenEmpty { index: usize, field: String },
140}
141
142impl fmt::Display for ValidationErrorKind {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 match self {
145 Self::InvalidVersion { found } => write!(f, "invalid version (found: {found})"),
146 Self::EmptyDeploymentName => write!(f, "deployment name is empty"),
147 Self::EmptyServiceName => write!(f, "service name is empty"),
148 Self::EmptyImageName => write!(f, "image name is empty"),
149 Self::InvalidPort { port } => {
150 write!(f, "port {port} is out of valid range (1-65535)")
151 }
152 Self::InvalidCpu { cpu } => write!(f, "CPU limit {cpu} is invalid (must be > 0)"),
153 Self::InvalidMemoryFormat { value } => {
154 write!(f, "memory format '{value}' is invalid")
155 }
156 Self::InvalidDuration { value } => write!(f, "duration format '{value}' is invalid"),
157 Self::DuplicateEndpoint { name } => write!(f, "duplicate endpoint '{name}'"),
158 Self::UnknownInitAction { action } => write!(f, "unknown init action '{action}'"),
159 Self::UnknownDependency { service } => {
160 write!(f, "dependency references unknown service '{service}'")
161 }
162 Self::CircularDependency {
163 service,
164 depends_on,
165 } => write!(
166 f,
167 "circular dependency detected: '{service}' depends on '{depends_on}'"
168 ),
169 Self::InvalidScaleRange { min, max } => {
170 write!(f, "invalid scale range: min {min} > max {max}")
171 }
172 Self::EmptyScaleTargets => write!(f, "scale targets are empty in adaptive mode"),
173 Self::InvalidEnvVar { key, reason } => {
174 write!(f, "invalid environment variable '{key}': {reason}")
175 }
176 Self::InvalidCronSchedule { schedule, reason } => {
177 write!(f, "invalid cron schedule '{schedule}': {reason}")
178 }
179 Self::ScheduleOnlyForCron => {
180 write!(f, "schedule field is only valid for rtype: cron")
181 }
182 Self::CronRequiresSchedule => {
183 write!(f, "rtype: cron requires a schedule field")
184 }
185 Self::Generic { message } => write!(f, "{message}"),
186 Self::InsufficientNodes {
187 required,
188 available,
189 message,
190 } => write!(
191 f,
192 "insufficient nodes: need {required} but only {available} available - {message}"
193 ),
194 Self::InvalidTunnelProtocol { protocol } => write!(
195 f,
196 "invalid tunnel protocol '{protocol}' (must be tcp or udp)"
197 ),
198 Self::InvalidTunnelPort { port, field } => {
199 write!(
200 f,
201 "invalid tunnel {field} port: {port} (must be 0 or 1-65535)"
202 )
203 }
204 Self::InvalidTunnelTtl { value, reason } => {
205 write!(f, "invalid tunnel max_ttl '{value}': {reason}")
206 }
207 Self::WasmConfigOnNonWasmType => {
208 write!(f, "wasm config provided but service_type is not a WASM type")
209 }
210 Self::InvalidWasmInstanceRange { min, max } => {
211 write!(
212 f,
213 "wasm min_instances ({min}) > max_instances ({max})"
214 )
215 }
216 Self::WasmCapabilityNotAvailable {
217 capability,
218 service_type,
219 } => write!(
220 f,
221 "capability '{capability}' is not available for WASM service type '{service_type}' (world does not import it)"
222 ),
223 Self::WasmHttpMissingHttpEndpoint => {
224 write!(
225 f,
226 "wasm_http service type should have at least one HTTP endpoint"
227 )
228 }
229 Self::WasmPreopenEmpty { index, field } => {
230 write!(f, "wasm preopen[{index}].{field} cannot be empty")
231 }
232 }
233 }
234}
235
236impl fmt::Display for ValidationError {
237 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238 write!(f, "{} at {}", self.kind, self.path)
239 }
240}
241
242impl std::error::Error for ValidationError {}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn test_validation_error_display() {
250 let err = ValidationError {
251 kind: ValidationErrorKind::InvalidVersion {
252 found: "v2".to_string(),
253 },
254 path: "version".to_string(),
255 };
256 assert!(err.to_string().contains("invalid version"));
257 }
258}