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 /// The submitted source compiled and type-checked, but assembling the
40 /// `.aion` archive from the built project failed.
41 #[error(transparent)]
42 Packaging(#[from] aion_package::PackagingError),
43
44 /// A filesystem operation against the project root failed (reading the
45 /// descriptor, writing the submitted source, or resolving a path).
46 #[error("filesystem operation on `{path}` failed: {source}")]
47 Io {
48 /// The path the failing operation targeted.
49 path: PathBuf,
50 /// The underlying I/O failure.
51 source: std::io::Error,
52 },
53
54 /// The project root is not a usable Gleam workflow project, or a request
55 /// field was malformed before any build ran (no `gleam.toml`, no
56 /// `workflow.toml`, an entry module that escapes the project `src/`
57 /// directory, or an entry module name that is not a safe logical name).
58 #[error("invalid authoring project: {message}")]
59 InvalidProject {
60 /// Human-readable description of why the project or request was
61 /// rejected, naming the offending field or file.
62 message: String,
63 },
64}
65
66#[cfg(test)]
67mod tests {
68 use std::path::PathBuf;
69
70 use super::ToolchainError;
71
72 fn assert_send_sync<T: Send + Sync + 'static>() {}
73
74 #[test]
75 fn toolchain_error_is_send_sync_and_static() {
76 assert_send_sync::<ToolchainError>();
77 }
78
79 #[test]
80 fn type_check_message_carries_the_diagnostics_verbatim() {
81 let error = ToolchainError::TypeCheck {
82 diagnostics: "error: Type mismatch\n expected Int, got String".to_owned(),
83 };
84 let rendered = error.to_string();
85 assert!(rendered.contains("Type mismatch"));
86 assert!(rendered.contains("expected Int, got String"));
87 }
88
89 #[test]
90 fn spawn_message_names_the_binary_path() {
91 let error = ToolchainError::GleamSpawn {
92 gleam_path: PathBuf::from("/usr/local/bin/gleam"),
93 source: std::io::Error::from(std::io::ErrorKind::NotFound),
94 };
95 assert!(error.to_string().contains("/usr/local/bin/gleam"));
96 assert!(std::error::Error::source(&error).is_some());
97 }
98
99 #[test]
100 fn io_message_names_the_path() {
101 let error = ToolchainError::Io {
102 path: PathBuf::from("/work/src/demo.gleam"),
103 source: std::io::Error::from(std::io::ErrorKind::PermissionDenied),
104 };
105 assert!(error.to_string().contains("/work/src/demo.gleam"));
106 assert!(std::error::Error::source(&error).is_some());
107 }
108
109 #[test]
110 fn packaging_error_converts_transparently() {
111 let error = ToolchainError::from(aion_package::PackagingError::ConfigMissing {
112 root: PathBuf::from("/work"),
113 });
114 assert_eq!(error.to_string(), "no workflow.toml found in /work");
115 assert!(matches!(error, ToolchainError::Packaging(_)));
116 }
117}