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
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}