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
163#[cfg(test)]
164mod tests {
165    use std::path::PathBuf;
166
167    use super::CodegenError;
168
169    fn assert_send_sync<T: Send + Sync + 'static>() {}
170
171    #[test]
172    fn codegen_error_is_send_sync_and_static() {
173        assert_send_sync::<CodegenError>();
174    }
175
176    #[test]
177    fn display_messages_name_file_and_pointer() {
178        assert_eq!(
179            CodegenError::UnsupportedConstruct {
180                file: PathBuf::from("schemas/input.json"),
181                pointer: "/properties/tag/oneOf".to_owned(),
182                construct: "unrecognised keyword `oneOf`".to_owned(),
183            }
184            .to_string(),
185            "unsupported JSON Schema construct in schemas/input.json at \
186             `/properties/tag/oneOf`: unrecognised keyword `oneOf`"
187        );
188        assert_eq!(
189            CodegenError::CheckDrift {
190                path: PathBuf::from("src/demo_io.gleam"),
191            }
192            .to_string(),
193            "--check failed: src/demo_io.gleam differs from the schema-generated \
194             output; run `aion codegen` to regenerate it"
195        );
196        assert_eq!(
197            CodegenError::CheckMissing {
198                path: PathBuf::from("src/demo_io.gleam"),
199            }
200            .to_string(),
201            "--check failed: generated module src/demo_io.gleam does not exist; \
202             run `aion codegen`"
203        );
204        assert_eq!(
205            CodegenError::SchemaOutsideSchemasDir {
206                field: "workflow[0].input_schema".to_owned(),
207                path: PathBuf::from("/project/io/input.json"),
208            }
209            .to_string(),
210            "invalid workflow.toml: workflow[0].input_schema: schema \
211             /project/io/input.json is outside the schemas/ directory; \
212             `aion codegen` only generates from schemas/*.json"
213        );
214    }
215
216    #[test]
217    fn packaging_errors_convert_transparently() {
218        let error = CodegenError::from(crate::PackagingError::ConfigMissing {
219            root: PathBuf::from("/project"),
220        });
221
222        assert_eq!(error.to_string(), "no workflow.toml found in /project");
223        assert!(matches!(error, CodegenError::Config(_)));
224    }
225}