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}