Skip to main content

zlayer_spec/
error.rs

1//! Error types for the spec crate
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::path::PathBuf;
6use thiserror::Error;
7
8/// Errors that can occur when parsing or validating a spec
9#[derive(Debug, Error)]
10pub enum SpecError {
11    /// YAML parsing error
12    #[error("YAML parse error: {0}")]
13    YamlError(#[from] serde_yaml::Error),
14
15    /// Validation error
16    #[error("Validation error: {0}")]
17    Validation(#[from] ValidationError),
18
19    /// IO error when reading spec file
20    #[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/// Validation errors for deployment specs
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39pub struct ValidationError {
40    /// The kind of validation error
41    pub kind: ValidationErrorKind,
42
43    /// JSON path to the invalid field
44    pub path: String,
45}
46
47/// The specific kind of validation error
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
49pub enum ValidationErrorKind {
50    /// Version is not "v1"
51    InvalidVersion { found: String },
52
53    /// Deployment name is empty
54    EmptyDeploymentName,
55
56    /// Service name is empty
57    EmptyServiceName,
58
59    /// Image name is empty
60    EmptyImageName,
61
62    /// Port is out of valid range (1-65535)
63    InvalidPort { port: u32 },
64
65    /// CPU limit is invalid (must be > 0)
66    InvalidCpu { cpu: f64 },
67
68    /// Memory format is invalid
69    InvalidMemoryFormat { value: String },
70
71    /// Duration format is invalid
72    InvalidDuration { value: String },
73
74    /// Service has duplicate endpoints
75    DuplicateEndpoint { name: String },
76
77    /// Unknown init action
78    UnknownInitAction { action: String },
79
80    /// Dependency references unknown service
81    UnknownDependency { service: String },
82
83    /// Circular dependency detected
84    CircularDependency { service: String, depends_on: String },
85
86    /// Scale min > max
87    InvalidScaleRange { min: u32, max: u32 },
88
89    /// Scale targets are empty in adaptive mode
90    EmptyScaleTargets,
91
92    /// Invalid environment variable
93    InvalidEnvVar { key: String, reason: String },
94
95    /// Invalid cron schedule expression
96    InvalidCronSchedule { schedule: String, reason: String },
97
98    /// Schedule field is only valid for rtype: cron
99    ScheduleOnlyForCron,
100
101    /// rtype: cron requires a schedule field
102    CronRequiresSchedule,
103
104    /// Generic validation error (from validator crate)
105    Generic { message: String },
106
107    /// Not enough nodes available for dedicated/exclusive placement
108    InsufficientNodes {
109        required: usize,
110        available: usize,
111        message: String,
112    },
113
114    /// Invalid tunnel protocol (must be tcp or udp)
115    InvalidTunnelProtocol { protocol: String },
116
117    /// Invalid tunnel port (must be 0 or 1-65535)
118    InvalidTunnelPort { port: u16, field: String },
119
120    /// Invalid tunnel TTL format
121    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}