1use std::error::Error;
4use std::fmt;
5
6use boxology_contract::{BoxId, CapabilityId, Detail, ExposureLevel};
7
8#[non_exhaustive]
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum AssemblyError {
12 DuplicateBox {
14 box_id: BoxId,
16 },
17 UnknownImportConsumer {
19 consumer: BoxId,
21 },
22 UnknownImportSlot {
24 consumer: BoxId,
26 slot: BoxId,
28 },
29 DuplicateImportResolution {
31 consumer: BoxId,
33 slot: BoxId,
35 },
36 UnknownImportTarget {
38 consumer: BoxId,
40 slot: BoxId,
42 target: BoxId,
44 },
45 MissingImportResolution {
47 consumer: BoxId,
49 slot: BoxId,
51 },
52 MissingImportedCapability {
54 consumer: BoxId,
56 slot: BoxId,
58 capability: CapabilityId,
60 },
61 UnknownExposureProvider {
63 provider: BoxId,
65 },
66 UnknownExposedCapability {
68 provider: BoxId,
70 capability: CapabilityId,
72 },
73 ExposureExceedsMaximum {
75 capability: CapabilityId,
77 requested: ExposureLevel,
79 maximum: ExposureLevel,
81 },
82 TransportConformanceFailed {
84 capability: CapabilityId,
86 detail: Detail,
88 },
89 TransportPrepareFailed {
91 detail: Detail,
93 },
94 TransportStartFailed {
96 detail: Detail,
98 },
99}
100
101impl fmt::Display for AssemblyError {
102 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103 match self {
104 Self::DuplicateBox { box_id } => {
105 write!(formatter, "duplicate box registration: {box_id}")
106 }
107 Self::UnknownImportConsumer { consumer } => {
108 write!(formatter, "unknown import consumer: {consumer}")
109 }
110 Self::UnknownImportSlot { consumer, slot } => {
111 write!(
112 formatter,
113 "unknown import slot {slot} for consumer {consumer}"
114 )
115 }
116 Self::DuplicateImportResolution { consumer, slot } => write!(
117 formatter,
118 "duplicate import resolution for consumer {consumer}, slot {slot}"
119 ),
120 Self::UnknownImportTarget {
121 consumer,
122 slot,
123 target,
124 } => write!(
125 formatter,
126 "unknown import target {target} for consumer {consumer}, slot {slot}"
127 ),
128 Self::MissingImportResolution { consumer, slot } => write!(
129 formatter,
130 "missing import resolution for consumer {consumer}, slot {slot}"
131 ),
132 Self::MissingImportedCapability {
133 consumer,
134 slot,
135 capability,
136 } => write!(
137 formatter,
138 "import target for consumer {consumer}, slot {slot} is missing capability {capability}"
139 ),
140 Self::UnknownExposureProvider { provider } => {
141 write!(formatter, "unknown exposure provider: {provider}")
142 }
143 Self::UnknownExposedCapability {
144 provider,
145 capability,
146 } => write!(
147 formatter,
148 "unknown exposed capability {capability} for provider {provider}"
149 ),
150 Self::ExposureExceedsMaximum {
151 capability,
152 requested,
153 maximum,
154 } => write!(
155 formatter,
156 "exposure {} exceeds maximum {} for capability {capability}",
157 level_name(*requested),
158 level_name(*maximum)
159 ),
160 Self::TransportConformanceFailed { capability, detail } => write!(
161 formatter,
162 "transport conformance failed for capability {capability}: {detail}"
163 ),
164 Self::TransportPrepareFailed { detail } => {
165 write!(formatter, "transport prepare failed: {detail}")
166 }
167 Self::TransportStartFailed { detail } => {
168 write!(formatter, "transport start failed: {detail}")
169 }
170 }
171 }
172}
173
174fn level_name(level: ExposureLevel) -> &'static str {
175 match level {
176 ExposureLevel::CodeOnly => "code-only",
177 ExposureLevel::Internal => "internal",
178 ExposureLevel::External => "external",
179 }
180}
181
182impl Error for AssemblyError {}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct AssemblyErrors(Box<[AssemblyError]>);
187
188impl AssemblyErrors {
189 pub fn errors(&self) -> &[AssemblyError] {
191 &self.0
192 }
193
194 pub(crate) fn from_errors(errors: Vec<AssemblyError>) -> Option<Self> {
195 (!errors.is_empty()).then(|| Self(errors.into_boxed_slice()))
196 }
197}
198
199impl fmt::Display for AssemblyErrors {
200 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
201 for (index, error) in self.errors().iter().enumerate() {
202 if index != 0 {
203 formatter.write_str("\n")?;
204 }
205 write!(formatter, "{error}")?;
206 }
207 Ok(())
208 }
209}
210
211impl Error for AssemblyErrors {}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use boxology_contract::CapabilityName;
217
218 fn box_id(value: &str) -> BoxId {
219 BoxId::new(value).unwrap()
220 }
221
222 fn capability(package: &str, name: &str) -> CapabilityId {
223 CapabilityId::new(box_id(package), CapabilityName::new(name).unwrap())
224 }
225
226 fn failure_cases() -> [(AssemblyError, &'static str); 13] {
227 [
228 (
229 AssemblyError::DuplicateBox {
230 box_id: box_id("duplicate"),
231 },
232 "duplicate box registration: duplicate",
233 ),
234 (
235 AssemblyError::UnknownImportConsumer {
236 consumer: box_id("unknown-consumer"),
237 },
238 "unknown import consumer: unknown-consumer",
239 ),
240 (
241 AssemblyError::UnknownImportSlot {
242 consumer: box_id("consumer"),
243 slot: box_id("unknown-slot"),
244 },
245 "unknown import slot unknown-slot for consumer consumer",
246 ),
247 (
248 AssemblyError::DuplicateImportResolution {
249 consumer: box_id("consumer"),
250 slot: box_id("duplicate-slot"),
251 },
252 "duplicate import resolution for consumer consumer, slot duplicate-slot",
253 ),
254 (
255 AssemblyError::UnknownImportTarget {
256 consumer: box_id("consumer"),
257 slot: box_id("slot"),
258 target: box_id("unknown-target"),
259 },
260 "unknown import target unknown-target for consumer consumer, slot slot",
261 ),
262 (
263 AssemblyError::MissingImportResolution {
264 consumer: box_id("consumer"),
265 slot: box_id("missing-slot"),
266 },
267 "missing import resolution for consumer consumer, slot missing-slot",
268 ),
269 (
270 AssemblyError::MissingImportedCapability {
271 consumer: box_id("consumer"),
272 slot: box_id("slot"),
273 capability: capability("target", "needed"),
274 },
275 "import target for consumer consumer, slot slot is missing capability target.needed",
276 ),
277 (
278 AssemblyError::UnknownExposureProvider {
279 provider: box_id("unknown-provider"),
280 },
281 "unknown exposure provider: unknown-provider",
282 ),
283 (
284 AssemblyError::UnknownExposedCapability {
285 provider: box_id("provider"),
286 capability: capability("provider", "unknown"),
287 },
288 "unknown exposed capability provider.unknown for provider provider",
289 ),
290 (
291 AssemblyError::ExposureExceedsMaximum {
292 capability: capability("provider", "limited"),
293 requested: ExposureLevel::External,
294 maximum: ExposureLevel::Internal,
295 },
296 "exposure external exceeds maximum internal for capability provider.limited",
297 ),
298 (
299 AssemblyError::TransportConformanceFailed {
300 capability: capability("provider", "rejected"),
301 detail: Detail::new("test_conformance"),
302 },
303 "transport conformance failed for capability provider.rejected: test_conformance",
304 ),
305 (
306 AssemblyError::TransportPrepareFailed {
307 detail: Detail::new("test_prepare"),
308 },
309 "transport prepare failed: test_prepare",
310 ),
311 (
312 AssemblyError::TransportStartFailed {
313 detail: Detail::new("test_start"),
314 },
315 "transport start failed: test_start",
316 ),
317 ]
318 }
319
320 #[test]
321 fn every_failure_is_cloneable_equal_and_has_exact_display() {
322 for (error, expected) in failure_cases() {
323 assert_eq!(error, error.clone());
324 assert_eq!(error.to_string(), expected);
325 }
326 assert_eq!(level_name(ExposureLevel::CodeOnly), "code-only");
327 }
328
329 #[test]
330 fn diagnostics_satisfy_error_and_thread_safety_bounds() {
331 fn assert_error<T: Error>() {}
332 fn assert_bounds<T: Send + Sync + 'static>() {}
333
334 assert_error::<AssemblyError>();
335 assert_error::<AssemblyErrors>();
336 assert_bounds::<AssemblyError>();
337 assert_bounds::<AssemblyErrors>();
338 }
339
340 #[test]
341 fn empty_failure_collection_is_absent() {
342 assert_eq!(AssemblyErrors::from_errors(Vec::new()), None);
343 }
344
345 #[test]
346 fn aggregate_preserves_deliberately_shuffled_order_exactly() {
347 let cases = failure_cases();
348 let shuffled = [12, 6, 0, 4, 5, 3, 1, 2, 9, 7, 11, 8, 10];
349 let ordered = shuffled.map(|index| cases[index].0.clone()).to_vec();
350 let aggregate = AssemblyErrors::from_errors(ordered.clone()).unwrap();
351
352 assert_eq!(aggregate.errors(), ordered);
353 assert_eq!(aggregate, aggregate.clone());
354 let expected = shuffled.map(|index| cases[index].1).join("\n");
355 let rendered = aggregate.to_string();
356 assert_eq!(rendered, expected);
357 assert!(!rendered.ends_with('\n'));
358 }
359}