aion_toolchain/error.rs
1//! Error taxonomy for the Gleam authoring toolchain.
2//!
3//! Every variant carries the offending path or the verbatim compiler
4//! diagnostics as structured data, so callers (the `aion-server` authoring
5//! endpoint) can map a type error onto an inline 400 distinctly from a spawn
6//! failure or a packaging fault.
7
8use std::path::PathBuf;
9
10/// Failures produced while compiling, type-checking, and packaging Gleam
11/// workflow source through the external `gleam` binary.
12#[derive(thiserror::Error, Debug)]
13pub enum ToolchainError {
14 /// The configured `gleam` binary could not be spawned (not found on the
15 /// configured path, not executable, or the OS refused the process).
16 ///
17 /// This is an operator-configuration fault, never a caller-correctable
18 /// source error: it names the path that was invoked.
19 #[error("failed to spawn the gleam binary at `{gleam_path}`: {source}")]
20 GleamSpawn {
21 /// The `gleam` binary path that could not be spawned.
22 gleam_path: PathBuf,
23 /// The underlying spawn failure reported by the OS.
24 source: std::io::Error,
25 },
26
27 /// `gleam build` exited non-zero: the submitted source did not compile or
28 /// type-check. The captured compiler output travels back verbatim so the
29 /// author sees the real type error inline.
30 ///
31 /// No `.aion` is produced and no partial package is returned on this path.
32 #[error("gleam compilation failed:\n{diagnostics}")]
33 TypeCheck {
34 /// The verbatim `gleam build` diagnostics (stderr, with any stdout
35 /// appended) — the inline type error.
36 diagnostics: String,
37 },
38
39 /// `gleam` exited non-zero from inside its **own dependency layer** — a
40 /// fetch, a Hex API call, unpacking a downloaded tarball, or a version
41 /// solve that could not reach the registry.
42 ///
43 /// 🔴 The submitted source was **never compiled**, so nothing has been
44 /// established about it. This variant exists because every such failure
45 /// used to be reported as [`Self::TypeCheck`], which is a structured claim
46 /// that type-checking *happened and produced diagnostics* — telling an
47 /// author their code failed to type-check when a registry was unreachable.
48 /// The wording was not the defect; the variant was (#125).
49 ///
50 /// Callers must not present this as a source error. It is transient, it is
51 /// the operator's or the network's to fix, and it is worth retrying.
52 #[error(
53 "gleam could not reach its package registry, so the submitted source was never compiled \
54 — this is an infrastructure failure, not a defect in the source:\n{diagnostics}"
55 )]
56 DependencyLayer {
57 /// The verbatim `gleam` output (stderr, with any stdout appended)
58 /// naming the registry failure.
59 diagnostics: String,
60 },
61
62 /// The submitted source compiled and type-checked, but assembling the
63 /// `.aion` archive from the built project failed.
64 #[error(transparent)]
65 Packaging(#[from] aion_package::PackagingError),
66
67 /// A filesystem operation against the project root failed (reading the
68 /// descriptor, writing the submitted source, or resolving a path).
69 #[error("filesystem operation on `{path}` failed: {source}")]
70 Io {
71 /// The path the failing operation targeted.
72 path: PathBuf,
73 /// The underlying I/O failure.
74 source: std::io::Error,
75 },
76
77 /// The project root is not a usable Gleam workflow project, or a request
78 /// field was malformed before any build ran (no `gleam.toml`, no
79 /// `workflow.toml`, an entry module that escapes the project `src/`
80 /// directory, or an entry module name that is not a safe logical name).
81 #[error("invalid authoring project: {message}")]
82 InvalidProject {
83 /// Human-readable description of why the project or request was
84 /// rejected, naming the offending field or file.
85 message: String,
86 },
87}
88
89#[cfg(test)]
90mod tests {
91 use std::path::PathBuf;
92
93 use super::ToolchainError;
94
95 fn assert_send_sync<T: Send + Sync + 'static>() {}
96
97 #[test]
98 fn toolchain_error_is_send_sync_and_static() {
99 assert_send_sync::<ToolchainError>();
100 }
101
102 #[test]
103 fn type_check_message_carries_the_diagnostics_verbatim() {
104 let error = ToolchainError::TypeCheck {
105 diagnostics: "error: Type mismatch\n expected Int, got String".to_owned(),
106 };
107 let rendered = error.to_string();
108 assert!(rendered.contains("Type mismatch"));
109 assert!(rendered.contains("expected Int, got String"));
110 }
111
112 #[test]
113 fn spawn_message_names_the_binary_path() {
114 let error = ToolchainError::GleamSpawn {
115 gleam_path: PathBuf::from("/usr/local/bin/gleam"),
116 source: std::io::Error::from(std::io::ErrorKind::NotFound),
117 };
118 assert!(error.to_string().contains("/usr/local/bin/gleam"));
119 assert!(std::error::Error::source(&error).is_some());
120 }
121
122 #[test]
123 fn io_message_names_the_path() {
124 let error = ToolchainError::Io {
125 path: PathBuf::from("/work/src/demo.gleam"),
126 source: std::io::Error::from(std::io::ErrorKind::PermissionDenied),
127 };
128 assert!(error.to_string().contains("/work/src/demo.gleam"));
129 assert!(std::error::Error::source(&error).is_some());
130 }
131
132 #[test]
133 fn packaging_error_converts_transparently() {
134 let error = ToolchainError::from(aion_package::PackagingError::ConfigMissing {
135 root: PathBuf::from("/work"),
136 });
137 assert_eq!(error.to_string(), "no workflow.toml found in /work");
138 assert!(matches!(error, ToolchainError::Packaging(_)));
139 }
140}