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    /// `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}