Skip to main content

aion_package/codegen/
error.rs

1//! Error taxonomy for Gleam codec generation.
2
3use std::path::PathBuf;
4
5use crate::PackagingError;
6
7/// Errors produced while generating Gleam types and codecs from a workflow
8/// project's JSON Schemas.
9///
10/// Every variant carries the offending file, JSON pointer, or path as
11/// structured data so callers can render actionable guidance. Schema-shape
12/// failures always name the schema file and the JSON pointer of the
13/// offending construct.
14#[derive(thiserror::Error, Debug)]
15pub enum CodegenError {
16    /// The project descriptor (`workflow.toml`) or a schema it references
17    /// failed to load: missing descriptor, invalid TOML, a referenced schema
18    /// file that does not exist, or invalid JSON in a referenced schema.
19    #[error(transparent)]
20    Config(#[from] PackagingError),
21
22    /// The project's `gleam.toml` declares a package name that cannot prefix
23    /// a generated Gleam module.
24    #[error("gleam.toml package name `{name}` cannot name the generated module: {reason}")]
25    ProjectName {
26        /// The declared package name.
27        name: String,
28        /// Why the name cannot be used.
29        reason: String,
30    },
31
32    /// A `workflow.toml` entry references a schema outside the project's
33    /// `schemas/` directory, where codegen would never see it; schema/codec
34    /// drift protection requires every referenced schema to live there.
35    #[error(
36        "invalid workflow.toml: {field}: schema {path} is outside the schemas/ directory; \
37         `aion codegen` only generates from schemas/*.json"
38    )]
39    SchemaOutsideSchemasDir {
40        /// Descriptor field that declared the path, e.g. `workflow[0].input_schema`.
41        field: String,
42        /// The resolved schema path.
43        path: PathBuf,
44    },
45
46    /// The project has no `schemas/` directory to generate from.
47    #[error("schemas directory {path} does not exist")]
48    SchemasDirMissing {
49        /// The expected `schemas/` directory.
50        path: PathBuf,
51    },
52
53    /// The `schemas/` directory exists but contains no `*.json` files.
54    #[error("no *.json schema files found in {path}")]
55    SchemasDirEmpty {
56        /// The searched `schemas/` directory.
57        path: PathBuf,
58    },
59
60    /// The `schemas/` directory could not be listed.
61    #[error("failed to list schemas directory {path}: {source}")]
62    SchemasDirRead {
63        /// The `schemas/` directory that could not be listed.
64        path: PathBuf,
65        /// I/O failure reported while listing the directory.
66        source: std::io::Error,
67    },
68
69    /// A schema file could not be read.
70    #[error("failed to read schema {path}: {source}")]
71    SchemaRead {
72        /// The schema file that could not be read.
73        path: PathBuf,
74        /// I/O failure reported while reading the file.
75        source: std::io::Error,
76    },
77
78    /// A schema file is not valid JSON (including duplicate object keys,
79    /// which would make property order ambiguous).
80    #[error("schema {path} is not valid JSON: {source}")]
81    SchemaParse {
82        /// The schema file that failed to parse.
83        path: PathBuf,
84        /// JSON parsing failure reported by `serde_json`.
85        source: serde_json::Error,
86    },
87
88    /// A schema file name cannot derive a Gleam type name.
89    #[error("schema file name {path} cannot name a Gleam type: {reason}")]
90    SchemaFileName {
91        /// The offending schema file.
92        path: PathBuf,
93        /// Why the file name cannot derive a type name.
94        reason: String,
95    },
96
97    /// A schema uses a construct outside the supported v1 subset.
98    #[error("unsupported JSON Schema construct in {file} at `{pointer}`: {construct}")]
99    UnsupportedConstruct {
100        /// The schema file containing the construct.
101        file: PathBuf,
102        /// JSON pointer to the offending construct (`` `` is the document root).
103        pointer: String,
104        /// What the construct was and, where helpful, what is supported.
105        construct: String,
106    },
107
108    /// Two schema locations derive the same generated Gleam name.
109    #[error(
110        "generated Gleam name `{name}` collides: derived from {first_file} at \
111         `{first_pointer}` and from {second_file} at `{second_pointer}`; \
112         rename one of the schema properties or files"
113    )]
114    NameCollision {
115        /// The colliding generated type or constructor name.
116        name: String,
117        /// Schema file of the first derivation.
118        first_file: PathBuf,
119        /// JSON pointer of the first derivation.
120        first_pointer: String,
121        /// Schema file of the second derivation.
122        second_file: PathBuf,
123        /// JSON pointer of the second derivation.
124        second_pointer: String,
125    },
126
127    /// The generated module could not be written.
128    #[error("failed to write generated module {path}: {source}")]
129    Write {
130        /// The module path that could not be written.
131        path: PathBuf,
132        /// I/O failure reported while writing.
133        source: std::io::Error,
134    },
135
136    /// `--check` failed: the generated module does not exist on disk.
137    #[error("--check failed: generated module {path} does not exist; run `aion codegen`")]
138    CheckMissing {
139        /// The expected generated module path.
140        path: PathBuf,
141    },
142
143    /// `--check` failed: the on-disk module differs from the generated output.
144    #[error(
145        "--check failed: {path} differs from the schema-generated output; \
146         run `aion codegen` to regenerate it"
147    )]
148    CheckDrift {
149        /// The drifted generated module path.
150        path: PathBuf,
151    },
152
153    /// The on-disk module could not be read for `--check` comparison.
154    #[error("failed to read {path} for --check: {source}")]
155    CheckRead {
156        /// The module path that could not be read.
157        path: PathBuf,
158        /// I/O failure reported while reading.
159        source: std::io::Error,
160    },
161
162    /// The activity manifest JSON emitted by the package's `manifest()` export
163    /// is not a valid declaration array.
164    #[error("activity manifest is not valid declaration JSON: {source}")]
165    ManifestParse {
166        /// JSON parsing failure reported by `serde_json`.
167        source: serde_json::Error,
168    },
169
170    /// A declared activity name cannot name an engine activity or a generated
171    /// artifact (empty, or carrying a path separator, backslash, or the
172    /// deployed-name separator).
173    #[error("activity name `{name}` is invalid: {reason}")]
174    InvalidActivityName {
175        /// The offending activity name.
176        name: String,
177        /// Why the name cannot be used.
178        reason: String,
179    },
180
181    /// Two declarations share an activity name; names must be unique within a
182    /// package so the generated wrappers, registration entries, and
183    /// `workflow.toml` list are unambiguous.
184    #[error("activity `{name}` is declared more than once")]
185    DuplicateActivity {
186        /// The duplicated activity name.
187        name: String,
188    },
189
190    /// A declaration carries a tier outside the supported set.
191    #[error("unknown activity tier `{value}`; expected `in_vm`, `remote_python`, or `remote_rust`")]
192    UnknownTier {
193        /// The unrecognised tier string.
194        value: String,
195    },
196
197    /// A declared activity references a value type with no `schemas/<type>.json`
198    /// document, so its codec cannot be generated.
199    #[error(
200        "activity `{activity}` {role} type `{type_name}` has no schema: expected {path} \
201         (codecs are generated from schemas/*.json)"
202    )]
203    ActivitySchemaMissing {
204        /// The activity whose type is unresolved.
205        activity: String,
206        /// Which side of the activity the type is on (`input` or `output`).
207        role: &'static str,
208        /// The unresolved value type name.
209        type_name: String,
210        /// The schema path that was expected to exist.
211        path: PathBuf,
212    },
213
214    /// While deriving a wire-compat golden, a value type referenced a named
215    /// Gleam type absent from its schema artifact's definitions. The schema
216    /// walker emits every nested record and enum into the artifact, so this
217    /// signals a generator invariant violation rather than bad author input.
218    #[error(
219        "internal: wire-compat golden for type `{root_type}` references undefined \
220         named type `{missing}` in {file}"
221    )]
222    GoldenTypeUnresolved {
223        /// The value type whose golden was being derived.
224        root_type: String,
225        /// The named type that could not be found in the artifact definitions.
226        missing: String,
227        /// The schema artifact whose definitions were searched.
228        file: PathBuf,
229    },
230
231    /// The test-scaffold generator could not read the workflow's entry-module
232    /// source, which it needs to derive the typed entry function and timer count.
233    #[error("failed to read workflow entry source {path}: {source}")]
234    EntrySourceRead {
235        /// The entry-module source path that could not be read.
236        path: PathBuf,
237        /// The underlying I/O error.
238        source: std::io::Error,
239    },
240
241    /// The workflow's entry-module source does not yield the facts the test
242    /// scaffold needs (no `aion/workflow` import, no `define` call, or an
243    /// unidentifiable typed entry function).
244    #[error("cannot derive test-scaffold facts from {path}: {reason}")]
245    ScaffoldFacts {
246        /// The entry-module source path the facts were read from.
247        path: PathBuf,
248        /// Why the facts could not be derived.
249        reason: String,
250    },
251}
252
253#[cfg(test)]
254mod tests {
255    use std::path::PathBuf;
256
257    use super::CodegenError;
258
259    fn assert_send_sync<T: Send + Sync + 'static>() {}
260
261    #[test]
262    fn codegen_error_is_send_sync_and_static() {
263        assert_send_sync::<CodegenError>();
264    }
265
266    #[test]
267    fn display_messages_name_file_and_pointer() {
268        assert_eq!(
269            CodegenError::UnsupportedConstruct {
270                file: PathBuf::from("schemas/input.json"),
271                pointer: "/properties/tag/oneOf".to_owned(),
272                construct: "unrecognised keyword `oneOf`".to_owned(),
273            }
274            .to_string(),
275            "unsupported JSON Schema construct in schemas/input.json at \
276             `/properties/tag/oneOf`: unrecognised keyword `oneOf`"
277        );
278        assert_eq!(
279            CodegenError::CheckDrift {
280                path: PathBuf::from("src/demo_io.gleam"),
281            }
282            .to_string(),
283            "--check failed: src/demo_io.gleam differs from the schema-generated \
284             output; run `aion codegen` to regenerate it"
285        );
286        assert_eq!(
287            CodegenError::CheckMissing {
288                path: PathBuf::from("src/demo_io.gleam"),
289            }
290            .to_string(),
291            "--check failed: generated module src/demo_io.gleam does not exist; \
292             run `aion codegen`"
293        );
294        assert_eq!(
295            CodegenError::SchemaOutsideSchemasDir {
296                field: "workflow[0].input_schema".to_owned(),
297                path: PathBuf::from("/project/io/input.json"),
298            }
299            .to_string(),
300            "invalid workflow.toml: workflow[0].input_schema: schema \
301             /project/io/input.json is outside the schemas/ directory; \
302             `aion codegen` only generates from schemas/*.json"
303        );
304    }
305
306    #[test]
307    fn packaging_errors_convert_transparently() {
308        let error = CodegenError::from(crate::PackagingError::ConfigMissing {
309            root: PathBuf::from("/project"),
310        });
311
312        assert_eq!(error.to_string(), "no workflow.toml found in /project");
313        assert!(matches!(error, CodegenError::Config(_)));
314    }
315}