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