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    /// WASM config present on non-WASM service type
124    WasmConfigOnNonWasmType,
125
126    /// WASM `min_instances` > `max_instances`
127    InvalidWasmInstanceRange { min: u32, max: u32 },
128
129    /// WASM capability not available for this service type
130    WasmCapabilityNotAvailable {
131        capability: String,
132        service_type: String,
133    },
134
135    /// `WasmHttp` service missing HTTP endpoint
136    WasmHttpMissingHttpEndpoint,
137
138    /// WASM preopen with empty source or target
139    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}