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