Skip to main content

aion_package/codegen/
error.rs

1//! Error taxonomy for the types-first codec generation.
2//!
3//! Every variant carries the offending module, type, field, or path as
4//! structured data so callers can render actionable guidance. Subset failures
5//! always name the types module and the type (and field, where one exists) of
6//! the offending construct.
7
8use std::path::PathBuf;
9
10use crate::PackagingError;
11
12/// Errors produced while generating codecs, schemas, and activity plumbing
13/// from a workflow project's authored types module.
14#[derive(thiserror::Error, Debug)]
15pub enum CodegenError {
16    /// The project descriptor (`workflow.toml`) or a schema it references
17    /// failed to load.
18    #[error(transparent)]
19    Config(#[from] PackagingError),
20
21    /// The project's `gleam.toml` declares a package name that cannot prefix
22    /// a generated Gleam module.
23    #[error("gleam.toml package name `{name}` cannot name the generated module: {reason}")]
24    ProjectName {
25        /// The declared package name.
26        name: String,
27        /// Why the name cannot be used.
28        reason: String,
29    },
30
31    /// The `gleam export package-interface` JSON did not parse into the
32    /// expected shape. The format is owned by the Gleam compiler; an unknown
33    /// shape fails loudly rather than being guessed at.
34    #[error(
35        "the exported package interface did not parse: {source}; the interface JSON is \
36         produced by `gleam export package-interface` (check the installed gleam version)"
37    )]
38    InterfaceParse {
39        /// JSON parsing failure reported by `serde_json`.
40        source: serde_json::Error,
41    },
42
43    /// The exported interface has no types module for the package.
44    #[error(
45        "the package exports no `{module}` module; declare the workflow's boundary types in \
46         `src/{module}.gleam` — it is the authored source `aion generate` reads (ADR-014)"
47    )]
48    TypesModuleMissing {
49        /// The expected types-module name (`<package>_io`).
50        module: String,
51    },
52
53    /// The types module exports functions, constants, or type aliases.
54    #[error(
55        "`src/{module}.gleam` must declare types only, but exports: {offenders:?}; codecs are \
56         generated FROM the types (run `aion generate`), never hand-written beside them"
57    )]
58    TypesModuleNotTypesOnly {
59        /// The types-module name.
60        module: String,
61        /// The offending exports (`fn x`, `const y`, `type alias z`).
62        offenders: Vec<String>,
63    },
64
65    /// The types module declares no public types.
66    #[error("`src/{module}.gleam` declares no public types to generate codecs from")]
67    TypesModuleEmpty {
68        /// The types-module name.
69        module: String,
70    },
71
72    /// A type in the types module falls outside the supported v1 subset.
73    #[error(
74        "unsupported boundary type in `{module}`: `{type_name}`{field_part}: found {found}; {hint}",
75        field_part = .field.as_ref().map(|name| format!(", field `{name}`")).unwrap_or_default()
76    )]
77    UnsupportedType {
78        /// The types-module name.
79        module: String,
80        /// The offending type.
81        type_name: String,
82        /// The offending field, when the construct is field-scoped.
83        field: Option<String>,
84        /// What was found.
85        found: String,
86        /// How to fix it.
87        hint: String,
88    },
89
90    /// A hand-authored (unmarked) `*.json` file sits in the `schemas/`
91    /// directory, which holds generated artifacts only.
92    #[error(
93        "{path} is not a generated schema artifact; schemas are emitted from the types module \
94         (ADR-014) and never authored. To migrate a schema-first project: declare the boundary \
95         types in src/<package>_io.gleam, delete the authored schemas/*.json, and run \
96         `aion generate` — the emitted schemas replace them"
97    )]
98    SchemaStray {
99        /// The stray file.
100        path: PathBuf,
101    },
102
103    /// The `schemas/` directory could not be listed.
104    #[error("failed to list schemas directory {path}: {source}")]
105    SchemasDirRead {
106        /// The `schemas/` directory that could not be listed.
107        path: PathBuf,
108        /// I/O failure reported while listing the directory.
109        source: std::io::Error,
110    },
111
112    /// A schema uses a construct outside the supported v1 subset (the `aion
113    /// input` skeleton walks the emitted schema documents).
114    #[error("unsupported JSON Schema construct in {file} at `{pointer}`: {construct}")]
115    UnsupportedConstruct {
116        /// The schema file containing the construct.
117        file: PathBuf,
118        /// JSON pointer to the offending construct (`` `` is the document root).
119        pointer: String,
120        /// What the construct was and, where helpful, what is supported.
121        construct: String,
122    },
123
124    /// A generated file could not be written.
125    #[error("failed to write generated file {path}: {source}")]
126    Write {
127        /// The path that could not be written.
128        path: PathBuf,
129        /// I/O failure reported while writing.
130        source: std::io::Error,
131    },
132
133    /// `--check` failed: a generated file does not exist on disk.
134    #[error("--check failed: generated file {path} does not exist; run `aion generate`")]
135    CheckMissing {
136        /// The expected generated file path.
137        path: PathBuf,
138    },
139
140    /// `--check` failed: an on-disk file differs from the generated output.
141    #[error(
142        "--check failed: {path} differs from the generated output; \
143         run `aion generate` to regenerate it"
144    )]
145    CheckDrift {
146        /// The drifted generated file path.
147        path: PathBuf,
148    },
149
150    /// An on-disk file could not be read for `--check` comparison.
151    #[error("failed to read {path} for --check: {source}")]
152    CheckRead {
153        /// The path that could not be read.
154        path: PathBuf,
155        /// I/O failure reported while reading.
156        source: std::io::Error,
157    },
158
159    /// The activity manifest JSON emitted by the package's `manifest()` export
160    /// is not a valid declaration array.
161    #[error("activity manifest is not valid declaration JSON: {source}")]
162    ManifestParse {
163        /// JSON parsing failure reported by `serde_json`.
164        source: serde_json::Error,
165    },
166
167    /// A declared activity name cannot name an engine activity or a generated
168    /// artifact (empty, or carrying a path separator, backslash, or the
169    /// deployed-name separator).
170    #[error("activity name `{name}` is invalid: {reason}")]
171    InvalidActivityName {
172        /// The offending activity name.
173        name: String,
174        /// Why the name cannot be used.
175        reason: String,
176    },
177
178    /// Two declarations share an activity name; names must be unique within a
179    /// package so the generated wrappers, registration entries, and
180    /// `workflow.toml` list are unambiguous.
181    #[error("activity `{name}` is declared more than once")]
182    DuplicateActivity {
183        /// The duplicated activity name.
184        name: String,
185    },
186
187    /// A declaration carries a tier outside the supported set.
188    #[error("unknown activity tier `{value}`; expected `in_vm`, `remote_python`, or `remote_rust`")]
189    UnknownTier {
190        /// The unrecognised tier string.
191        value: String,
192    },
193
194    /// A declared activity references a value type not declared in the types
195    /// module, so its codec cannot be generated.
196    #[error(
197        "activity `{activity}` {role} type `{type_name}` is not declared in \
198         src/{module}.gleam (codecs are generated from the types module's public types)"
199    )]
200    ActivityTypeMissing {
201        /// The activity whose type is unresolved.
202        activity: String,
203        /// Which side of the activity the type is on (`input` or `output`).
204        role: &'static str,
205        /// The unresolved value type name.
206        type_name: String,
207        /// The types module the type was expected in (`<package>_io`).
208        module: String,
209    },
210
211    /// While deriving a wire-compat golden, a value type referenced a named
212    /// Gleam type absent from its boundary-type definitions. The interface
213    /// front-end collects every referenced definition into the closure, so
214    /// this signals a generator invariant violation rather than bad author
215    /// input.
216    #[error(
217        "internal: wire-compat golden for type `{root_type}` references undefined \
218         named type `{missing}` in {file}"
219    )]
220    GoldenTypeUnresolved {
221        /// The value type whose golden was being derived.
222        root_type: String,
223        /// The named type that could not be found in the definitions.
224        missing: String,
225        /// The emitted schema artifact whose definitions were searched.
226        file: PathBuf,
227    },
228
229    /// The test-scaffold generator could not read the workflow's entry-module
230    /// source, which it needs to derive the typed entry function and timer count.
231    #[error("failed to read workflow entry source {path}: {source}")]
232    EntrySourceRead {
233        /// The entry-module source path that could not be read.
234        path: PathBuf,
235        /// The underlying I/O error.
236        source: std::io::Error,
237    },
238
239    /// The workflow's entry-module source does not yield the facts the test
240    /// scaffold needs (no `aion/workflow` import, no `define` call, or an
241    /// unidentifiable typed entry function).
242    #[error("cannot derive test-scaffold facts from {path}: {reason}")]
243    ScaffoldFacts {
244        /// The entry-module source path the facts were read from.
245        path: PathBuf,
246        /// Why the facts could not be derived.
247        reason: String,
248    },
249}
250
251#[cfg(test)]
252mod tests {
253    use std::path::PathBuf;
254
255    use super::CodegenError;
256
257    fn assert_send_sync<T: Send + Sync + 'static>() {}
258
259    #[test]
260    fn codegen_error_is_send_sync_and_static() {
261        assert_send_sync::<CodegenError>();
262    }
263
264    #[test]
265    fn display_messages_name_module_type_and_field() {
266        assert_eq!(
267            CodegenError::UnsupportedType {
268                module: "demo_io".to_owned(),
269                type_name: "Order".to_owned(),
270                field: Some("total".to_owned()),
271                found: "a tuple type".to_owned(),
272                hint: "use a record".to_owned(),
273            }
274            .to_string(),
275            "unsupported boundary type in `demo_io`: `Order`, field `total`: \
276             found a tuple type; use a record"
277        );
278        assert_eq!(
279            CodegenError::UnsupportedType {
280                module: "demo_io".to_owned(),
281                type_name: "Order".to_owned(),
282                field: None,
283                found: "an opaque type".to_owned(),
284                hint: "expose the constructors".to_owned(),
285            }
286            .to_string(),
287            "unsupported boundary type in `demo_io`: `Order`: found an opaque type; \
288             expose the constructors"
289        );
290        assert_eq!(
291            CodegenError::CheckDrift {
292                path: PathBuf::from("src/demo_codecs.gleam"),
293            }
294            .to_string(),
295            "--check failed: src/demo_codecs.gleam differs from the generated \
296             output; run `aion generate` to regenerate it"
297        );
298        assert_eq!(
299            CodegenError::CheckMissing {
300                path: PathBuf::from("src/demo_codecs.gleam"),
301            }
302            .to_string(),
303            "--check failed: generated file src/demo_codecs.gleam does not exist; \
304             run `aion generate`"
305        );
306        let missing = CodegenError::TypesModuleMissing {
307            module: "demo_io".to_owned(),
308        }
309        .to_string();
310        assert!(missing.contains("src/demo_io.gleam") && missing.contains("ADR-014"));
311        let activity = CodegenError::ActivityTypeMissing {
312            activity: "charge".to_owned(),
313            role: "output",
314            type_name: "Receipt".to_owned(),
315            module: "demo_io".to_owned(),
316        }
317        .to_string();
318        assert!(activity.contains("charge") && activity.contains("src/demo_io.gleam"));
319    }
320
321    #[test]
322    fn packaging_errors_convert_transparently() {
323        let error = CodegenError::from(crate::PackagingError::ConfigMissing {
324            root: PathBuf::from("/project"),
325        });
326
327        assert_eq!(error.to_string(), "no workflow.toml found in /project");
328        assert!(matches!(error, CodegenError::Config(_)));
329    }
330}