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_yml::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
124impl fmt::Display for ValidationErrorKind {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 match self {
127 Self::InvalidVersion { found } => write!(f, "invalid version (found: {found})"),
128 Self::EmptyDeploymentName => write!(f, "deployment name is empty"),
129 Self::EmptyServiceName => write!(f, "service name is empty"),
130 Self::EmptyImageName => write!(f, "image name is empty"),
131 Self::InvalidPort { port } => {
132 write!(f, "port {port} is out of valid range (1-65535)")
133 }
134 Self::InvalidCpu { cpu } => write!(f, "CPU limit {cpu} is invalid (must be > 0)"),
135 Self::InvalidMemoryFormat { value } => {
136 write!(f, "memory format '{value}' is invalid")
137 }
138 Self::InvalidDuration { value } => write!(f, "duration format '{value}' is invalid"),
139 Self::DuplicateEndpoint { name } => write!(f, "duplicate endpoint '{name}'"),
140 Self::UnknownInitAction { action } => write!(f, "unknown init action '{action}'"),
141 Self::UnknownDependency { service } => {
142 write!(f, "dependency references unknown service '{service}'")
143 }
144 Self::CircularDependency {
145 service,
146 depends_on,
147 } => write!(
148 f,
149 "circular dependency detected: '{service}' depends on '{depends_on}'"
150 ),
151 Self::InvalidScaleRange { min, max } => {
152 write!(f, "invalid scale range: min {min} > max {max}")
153 }
154 Self::EmptyScaleTargets => write!(f, "scale targets are empty in adaptive mode"),
155 Self::InvalidEnvVar { key, reason } => {
156 write!(f, "invalid environment variable '{key}': {reason}")
157 }
158 Self::InvalidCronSchedule { schedule, reason } => {
159 write!(f, "invalid cron schedule '{schedule}': {reason}")
160 }
161 Self::ScheduleOnlyForCron => {
162 write!(f, "schedule field is only valid for rtype: cron")
163 }
164 Self::CronRequiresSchedule => {
165 write!(f, "rtype: cron requires a schedule field")
166 }
167 Self::Generic { message } => write!(f, "{message}"),
168 Self::InsufficientNodes {
169 required,
170 available,
171 message,
172 } => write!(
173 f,
174 "insufficient nodes: need {required} but only {available} available - {message}"
175 ),
176 Self::InvalidTunnelProtocol { protocol } => write!(
177 f,
178 "invalid tunnel protocol '{protocol}' (must be tcp or udp)"
179 ),
180 Self::InvalidTunnelPort { port, field } => {
181 write!(
182 f,
183 "invalid tunnel {field} port: {port} (must be 0 or 1-65535)"
184 )
185 }
186 Self::InvalidTunnelTtl { value, reason } => {
187 write!(f, "invalid tunnel max_ttl '{value}': {reason}")
188 }
189 }
190 }
191}
192
193impl fmt::Display for ValidationError {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 write!(f, "{} at {}", self.kind, self.path)
196 }
197}
198
199impl std::error::Error for ValidationError {}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_validation_error_display() {
207 let err = ValidationError {
208 kind: ValidationErrorKind::InvalidVersion {
209 found: "v2".to_string(),
210 },
211 path: "version".to_string(),
212 };
213 assert!(err.to_string().contains("invalid version"));
214 }
215}