Skip to main content

adam_rs/template/
error.rs

1//! Construction and binding failures without language source dependencies.
2
3use std::any::TypeId;
4use std::fmt;
5
6use super::{CellSlot, RelationshipSlot, TemplateId};
7use crate::{CellId, Error};
8
9/// Identifies an authored construction boundary before any live relationship exists.
10///
11/// Method indices precede source specialization; policy indices count registrations
12/// on the same local cell. Handles retain definition identity without retaining recipes.
13///
14/// # Examples
15/// ```
16/// use adam_rs::{ConstructionSite, Sheet, TemplateBuilder, TemplateError};
17/// let mut builder = TemplateBuilder::new();
18/// let slot = builder.cell::<i32, _>("value",
19///     || Err(std::io::Error::other("initialization failed").into()))?;
20/// let template = builder.finish()?;
21/// let error = template.instantiate(&mut Sheet::new(), &[]).unwrap_err();
22/// assert!(matches!(error, TemplateError::Construction {
23///     site: ConstructionSite::Cell(ref failed), ..
24/// } if failed == &slot));
25/// # Ok::<(), TemplateError>(())
26/// ```
27#[derive(Debug)]
28pub enum ConstructionSite {
29    /// Constructs one private cell.
30    Cell(CellSlot),
31    /// Attaches one cell's filter or requirement, in registration order.
32    Policy {
33        /// Identifies the governed cell.
34        cell: CellSlot,
35        /// Identifies the cell-local policy registration.
36        index: usize,
37    },
38    /// Constructs one authored method before specialization.
39    Method {
40        /// Identifies the local relationship.
41        relationship: RelationshipSlot,
42        /// Identifies the original authored method.
43        index: usize,
44    },
45    /// Registers the combined relationship after its factories succeed.
46    Relationship(RelationshipSlot),
47}
48
49/// Identifies a malformed recipe, binding, factory, or native graph construction.
50///
51/// Binding and declared-structure errors precede destination mutation. Factory-result
52/// validation, allocation and native construction failures may leave partial structure;
53/// the sheet remains safe to discard. See [`super::SheetTemplate::instantiate`].
54///
55/// # Examples
56/// See [`ConstructionSite`]'s failed-initializer example.
57#[derive(Debug)]
58pub enum TemplateError {
59    /// A binding names no formal parameter.
60    UnknownParameter {
61        /// The unrecognized binding name.
62        name: String,
63    },
64    /// A formal parameter has no binding.
65    MissingParameter {
66        /// The unsupplied formal name.
67        name: String,
68    },
69    /// A formal name is declared or bound more than once.
70    DuplicateParameter {
71        /// The repeated declaration or binding name.
72        name: String,
73    },
74    /// Two formals bind the same actual cell.
75    AliasedArgument {
76        /// The first formal bound to the actual.
77        first: String,
78        /// The second formal bound to the same actual.
79        second: String,
80    },
81    /// A local handle belongs to another recipe.
82    ForeignHandle {
83        /// The local handle from a different owner.
84        slot: CellSlot,
85    },
86    /// An actual handle is absent from the destination.
87    InvalidActual {
88        /// The formal receiving the invalid actual.
89        name: String,
90        /// The absent live handle.
91        actual: CellId,
92    },
93    /// A binding's concrete type differs from its formal declaration.
94    TypeMismatch {
95        /// The formal receiving the wrongly typed argument.
96        name: String,
97        /// The formal's declared concrete type.
98        expected: TypeId,
99        /// The actual's registered concrete type.
100        found: TypeId,
101    },
102    /// An actual is a caller-owned out cell, unsupported even as an input.
103    OutArgument {
104        /// The formal receiving the out cell.
105        name: String,
106        /// The unsupported caller-owned out cell.
107        actual: CellId,
108    },
109    /// A relationship has no methods.
110    EmptyRelationship,
111    /// A component containing private structure is used in a relationship-only site.
112    ConditionalComponent,
113    /// A declaration or factory result violates the recipe's graph contract.
114    InvalidDefinition {
115        /// The violated recipe invariant.
116        detail: &'static str,
117        /// The implicated local output, when a declaration has one.
118        slot: Option<CellSlot>,
119    },
120    /// The expanded graph exceeds representable or reservable capacity.
121    Capacity,
122    /// A repeatable initializer or method factory failed.
123    Factory {
124        /// The failed initializer or method factory.
125        source: anyhow::Error,
126        /// The private cell or first declared method output.
127        slot: CellSlot,
128    },
129    /// A conditional's repeatable match-expression or key factory failed.
130    ConditionalFactory {
131        /// The underlying factory failure.
132        source: anyhow::Error,
133    },
134    /// Retains a construction boundary and only its actual root-to-child inclusion chain.
135    ///
136    /// The boxed original failure preserves concrete error downcasting.
137    /// See [`super::SheetTemplate::instantiate`].
138    Construction {
139        /// Identifies the failed definition.
140        template: TemplateId,
141        /// Identifies the declaration or registration that failed.
142        site: ConstructionSite,
143        /// Retains active inclusion sites, ordered root to child.
144        inclusions: Vec<RelationshipSlot>,
145        /// Retains the original failure without requiring Send or Sync.
146        source: Box<TemplateError>,
147    },
148    /// Retains a failed conditional's definition, local ordinal and active invocation sites.
149    ///
150    /// Context owns identities, not recipes or live state. The original error and
151    /// its typed cause remain available through [`std::error::Error::source`].
152    /// See [`super::SheetTemplate::instantiate`].
153    ConditionalConstruction {
154        /// Identifies the definition containing the failed conditional.
155        template: TemplateId,
156        /// Identifies the zero-based conditional registration ordinal in that definition.
157        conditional: usize,
158        /// Retains only active inclusion sites, ordered from the root to the failed definition.
159        inclusions: Vec<RelationshipSlot>,
160        /// Retains the original concrete construction failure and its cause.
161        source: Box<TemplateError>,
162    },
163    /// An existing native graph mutator rejected construction.
164    Runtime(Error),
165}
166
167impl fmt::Display for TemplateError {
168    /// Describes the error with its local name, handle, or underlying failure.
169    ///
170    /// - Complexity: O(L + D) for L formatted characters and D nested construction
171    ///   wrappers, plus underlying factory/native error formatting.
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        match self {
174            Self::UnknownParameter { name } => write!(f, "unknown parameter {name:?}"),
175            Self::MissingParameter { name } => write!(f, "missing parameter {name:?}"),
176            Self::DuplicateParameter { name } => write!(f, "duplicate parameter {name:?}"),
177            Self::AliasedArgument { first, second } => {
178                write!(f, "parameters {first:?} and {second:?} alias one actual")
179            }
180            Self::ForeignHandle { slot } => write!(f, "foreign local cell {slot:?}"),
181            Self::InvalidActual { name, actual } => {
182                write!(f, "invalid actual {actual:?} for {name:?}")
183            }
184            Self::TypeMismatch {
185                name,
186                expected,
187                found,
188            } => {
189                write!(
190                    f,
191                    "type mismatch for {name:?}: expected {expected:?}, found {found:?}"
192                )
193            }
194            Self::OutArgument { name, actual } => write!(f, "out argument {actual:?} for {name:?}"),
195            Self::EmptyRelationship => f.write_str("relationship requires a method"),
196            Self::ConditionalComponent => f.write_str("component is not relationship-only"),
197            Self::InvalidDefinition { detail, slot } => write!(f, "{detail} at {slot:?}"),
198            Self::Capacity => f.write_str("template expansion exceeds capacity"),
199            Self::Factory { source, slot } => {
200                write!(f, "template factory failed at {slot:?}: {source}")
201            }
202            Self::ConditionalFactory { source } => {
203                write!(f, "template conditional factory failed: {source}")
204            }
205            Self::Construction { source, .. } => source.fmt(f),
206            Self::ConditionalConstruction {
207                conditional,
208                source,
209                ..
210            } => write!(
211                f,
212                "template conditional {conditional} construction failed: {source}"
213            ),
214            Self::Runtime(source) => write!(f, "native template construction failed: {source}"),
215        }
216    }
217}
218
219impl std::error::Error for TemplateError {
220    /// Returns the underlying factory or native construction failure.
221    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
222        match self {
223            Self::Factory { source, .. } => Some(source.as_ref()),
224            Self::ConditionalFactory { source } => Some(source.as_ref()),
225            Self::ConditionalConstruction { source, .. } => Some(source.as_ref()),
226            Self::Construction { source, .. } => Some(source.as_ref()),
227            Self::Runtime(source) => Some(source),
228            _ => None,
229        }
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use std::error::Error as _;
237
238    /// Describes every leaf error and exposes causes only for wrapped failures.
239    #[test]
240    fn leaf_errors_preserve_display_and_sources() {
241        let mut builder = super::super::TemplateBuilder::new();
242        let slot = builder.parameter::<i32>("value").unwrap();
243        let cases = [
244            (
245                TemplateError::UnknownParameter {
246                    name: "value".into(),
247                },
248                "unknown parameter",
249            ),
250            (
251                TemplateError::MissingParameter {
252                    name: "value".into(),
253                },
254                "missing parameter",
255            ),
256            (
257                TemplateError::DuplicateParameter {
258                    name: "value".into(),
259                },
260                "duplicate parameter",
261            ),
262            (
263                TemplateError::AliasedArgument {
264                    first: "a".into(),
265                    second: "b".into(),
266                },
267                "alias",
268            ),
269            (
270                TemplateError::ForeignHandle { slot: slot.clone() },
271                "foreign local cell",
272            ),
273            (
274                TemplateError::InvalidActual {
275                    name: "value".into(),
276                    actual: CellId::default(),
277                },
278                "invalid actual",
279            ),
280            (
281                TemplateError::TypeMismatch {
282                    name: "value".into(),
283                    expected: TypeId::of::<i32>(),
284                    found: TypeId::of::<bool>(),
285                },
286                "type mismatch",
287            ),
288            (
289                TemplateError::OutArgument {
290                    name: "value".into(),
291                    actual: CellId::default(),
292                },
293                "out argument",
294            ),
295            (TemplateError::EmptyRelationship, "requires a method"),
296            (TemplateError::ConditionalComponent, "not relationship-only"),
297            (
298                TemplateError::InvalidDefinition {
299                    detail: "invalid edges",
300                    slot: Some(slot.clone()),
301                },
302                "invalid edges",
303            ),
304            (TemplateError::Capacity, "capacity"),
305        ];
306        for (error, expected) in cases {
307            assert!(error.to_string().contains(expected), "{error}");
308            assert!(error.source().is_none());
309        }
310        let factory = TemplateError::Factory {
311            source: std::io::Error::other("typed cause").into(),
312            slot,
313        };
314        assert!(factory.to_string().contains("typed cause"));
315        assert!(
316            factory
317                .source()
318                .unwrap()
319                .downcast_ref::<std::io::Error>()
320                .is_some()
321        );
322        let conditional = TemplateError::ConditionalFactory {
323            source: std::io::Error::other("key cause").into(),
324        };
325        assert!(conditional.to_string().contains("key cause"));
326        assert!(
327            conditional
328                .source()
329                .unwrap()
330                .downcast_ref::<std::io::Error>()
331                .is_some()
332        );
333        let runtime = TemplateError::Runtime(Error::InvalidId);
334        assert!(runtime.to_string().contains(&Error::InvalidId.to_string()));
335        assert!(runtime.source().unwrap().downcast_ref::<Error>().is_some());
336    }
337
338    /// Retains construction context and concrete nested causes without losing display details.
339    #[test]
340    fn construction_wrappers_preserve_context_and_typed_cause() {
341        let mut builder = super::super::TemplateBuilder::new();
342        let slot = builder.parameter::<i32>("value").unwrap();
343        let template = builder.finish().unwrap();
344        let error = TemplateError::Construction {
345            template: template.id(),
346            site: ConstructionSite::Cell(slot.clone()),
347            inclusions: vec![],
348            source: Box::new(TemplateError::Factory {
349                slot: slot.clone(),
350                source: std::io::Error::other("root cause").into(),
351            }),
352        };
353        assert!(error.to_string().contains("root cause"));
354        assert!(
355            error
356                .source()
357                .unwrap()
358                .source()
359                .unwrap()
360                .downcast_ref::<std::io::Error>()
361                .is_some()
362        );
363        assert!(matches!(&error, TemplateError::Construction {
364                    template: owner, site: ConstructionSite::Cell(cell), inclusions, ..
365                } if *owner == template.id() && *cell == slot && inclusions.is_empty()));
366        let error = TemplateError::ConditionalConstruction {
367            template: template.id(),
368            conditional: 2,
369            inclusions: vec![],
370            source: Box::new(error),
371        };
372        assert!(error.to_string().contains("conditional 2"));
373        assert!(error.to_string().contains("root cause"));
374        assert!(
375            error
376                .source()
377                .unwrap()
378                .downcast_ref::<TemplateError>()
379                .is_some()
380        );
381    }
382}