1use std::path::PathBuf;
4use thiserror::Error;
5
6#[derive(Debug, Error)]
8pub enum ComposeError {
9 #[error("payload is not self-describing: missing ucp.capabilities (response) or meta.profile (request)")]
10 NotSelfDescribing,
11
12 #[error("no capabilities declared in ucp.capabilities")]
13 EmptyCapabilities,
14
15 #[error("invalid JSONRPC envelope: {message}")]
16 InvalidEnvelope { message: String },
17
18 #[error("no root capability found (all capabilities have 'extends')")]
19 NoRootCapability,
20
21 #[error("multiple root capabilities found: {}", names.join(", "))]
22 MultipleRootCapabilities { names: Vec<String> },
23
24 #[error("extension '{extension}' references unknown parent '{parent}'")]
25 UnknownParent { extension: String, parent: String },
26
27 #[error("extension '{extension}' does not connect to root '{root}'")]
28 OrphanExtension { extension: String, root: String },
29
30 #[error("extension '{extension}' missing $defs entry for '{expected_key}'")]
31 MissingDefEntry {
32 extension: String,
33 expected_key: String,
34 },
35
36 #[error(
41 "extension '{extension}' does not mirror container capability '{capability}': \
42 its $defs['{capability}'] must contain a nested $defs of operation shapes"
43 )]
44 ContainerExtensionShape {
45 extension: String,
46 capability: String,
47 },
48
49 #[error("failed to fetch schema from {url}: {message}")]
50 SchemaFetch { url: String, message: String },
51
52 #[error("failed to fetch profile from {url}: {message}")]
53 ProfileFetch { url: String, message: String },
54
55 #[error("invalid capability '{name}': {message}")]
56 InvalidCapability { name: String, message: String },
57
58 #[error("invalid URL '{url}': {message}")]
59 InvalidUrl { url: String, message: String },
60
61 #[error("extension '{extension}' requires {target} {range} but found {actual}")]
62 VersionConstraintViolation {
63 extension: String,
64 target: String,
65 range: String,
66 actual: String,
67 },
68}
69
70impl ComposeError {
71 pub fn exit_code(&self) -> i32 {
73 match self {
74 Self::SchemaFetch { .. } | Self::ProfileFetch { .. } => 3, _ => 2, }
77 }
78}
79
80#[derive(Debug, Error)]
82pub enum ResolveError {
83 #[error("file not found: {path}")]
85 FileNotFound { path: PathBuf },
86
87 #[error("cannot read {path}: {source}")]
88 ReadError {
89 path: PathBuf,
90 #[source]
91 source: std::io::Error,
92 },
93
94 #[cfg(feature = "remote")]
95 #[error("failed to fetch {url}: {source}")]
96 NetworkError {
97 url: String,
98 #[source]
99 source: reqwest::Error,
100 },
101
102 #[error("invalid JSON: {source}")]
104 InvalidJson {
105 #[source]
106 source: serde_json::Error,
107 },
108
109 #[error("invalid annotation at {path}: expected string or object, got {actual}")]
111 InvalidAnnotationType { path: String, actual: String },
112
113 #[error("unknown visibility \"{value}\" at {path}: expected omit, required, or optional")]
114 UnknownVisibility { path: String, value: String },
115
116 #[error("invalid schema transition at {path}: {message}")]
117 InvalidSchemaTransition { path: String, message: String },
118
119 #[error(
123 "monotonicity violation at {path}: field \"{field}\" is {base_status} in base schema \
124 but extension sets it to \"{attempted}\""
125 )]
126 MonotonicityViolation {
127 path: String,
128 field: String,
129 base_status: String,
130 attempted: String,
131 },
132
133 #[error(
135 "type conflict at {path}: base declares \"{base_type}\" but extension declares \"{ext_type}\""
136 )]
137 TypeConflict {
138 path: String,
139 base_type: String,
140 ext_type: String,
141 },
142
143 #[error("invalid schema: {message}")]
144 InvalidSchema { message: String },
145
146 #[error(
151 "container schema has no operation shape '{key}' for this (op, direction); \
152 available operation shapes: [{available}]"
153 )]
154 OperationShapeNotFound { key: String, available: String },
155
156 #[error("schema has no $defs entry '{def}'; available: [{available}]")]
161 DefNotFound { def: String, available: String },
162
163 #[error("failed to bundle schema: {message}")]
164 BundleError { message: String },
165}
166
167#[derive(Debug, Error)]
169pub enum ValidateError {
170 #[error(transparent)]
171 Resolve(#[from] ResolveError),
172
173 #[error("validation failed with {} error(s)", errors.len())]
174 Invalid { errors: Vec<SchemaError> },
175}
176
177#[derive(Debug, Clone, serde::Serialize)]
179pub struct SchemaError {
180 pub path: String,
182 pub message: String,
184}
185
186impl std::fmt::Display for SchemaError {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 write!(f, "{}: {}", self.path, self.message)
189 }
190}
191
192impl ResolveError {
193 pub fn exit_code(&self) -> i32 {
195 match self {
196 ResolveError::FileNotFound { .. } | ResolveError::ReadError { .. } => 3,
197 #[cfg(feature = "remote")]
198 ResolveError::NetworkError { .. } => 3,
199 _ => 2,
200 }
201 }
202}
203
204impl ValidateError {
205 pub fn exit_code(&self) -> i32 {
207 match self {
208 ValidateError::Resolve(e) => e.exit_code(),
209 ValidateError::Invalid { .. } => 1,
210 }
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn resolve_error_exit_codes() {
220 let err = ResolveError::FileNotFound {
221 path: PathBuf::from("test.json"),
222 };
223 assert_eq!(err.exit_code(), 3);
224
225 let err = ResolveError::InvalidAnnotationType {
226 path: "/properties/id".into(),
227 actual: "number".into(),
228 };
229 assert_eq!(err.exit_code(), 2);
230
231 let err = ResolveError::UnknownVisibility {
232 path: "/properties/id".into(),
233 value: "readonly".into(),
234 };
235 assert_eq!(err.exit_code(), 2);
236 }
237
238 #[test]
239 fn validate_error_exit_codes() {
240 let err = ValidateError::Invalid {
241 errors: vec![SchemaError {
242 path: "/id".into(),
243 message: "missing required field".into(),
244 }],
245 };
246 assert_eq!(err.exit_code(), 1);
247 }
248
249 #[test]
250 fn schema_error_display() {
251 let err = SchemaError {
252 path: "/buyer/email".into(),
253 message: "expected string, got number".into(),
254 };
255 assert_eq!(err.to_string(), "/buyer/email: expected string, got number");
256 }
257}