Skip to main content

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}