Skip to main content

aube_codes/
lib.rs

1//! Stable identifiers for every error and warning that aube emits.
2//!
3//! The crate is dependency-free on purpose: every other aube crate may
4//! depend on it. Codes are exposed as `pub const &str` so they can be
5//! used unmodified in `tracing::warn!(code = aube_codes::warnings::X, ...)`,
6//! `#[diagnostic(code = aube_codes::errors::Y)]`, and ndjson-emitting
7//! reporters without needing to call `.as_str()` or do any conversion.
8//!
9//! Naming convention:
10//! - `ERR_AUBE_*` for errors (anything that returns `Err` to the caller
11//!   or aborts with a non-zero exit).
12//! - `WARN_AUBE_*` for warnings (`tracing::warn!`) and non-fatal
13//!   `tracing::error!` sites that don't change exit status.
14//!
15//! aube does not emit `ERR_PNPM_*` codes itself. Where a code maps
16//! cleanly onto a pnpm concept (lockfile, peer-deps, tarball, etc.) we
17//! reuse pnpm's *suffix* under the `ERR_AUBE_` prefix so the code reads
18//! the same to anyone familiar with pnpm — but the published code is
19//! always `ERR_AUBE_*`.
20//!
21//! Codes are stable: once published, a code's identifier and meaning
22//! must not change. Adding new codes is fine; removing or repurposing
23//! one is a breaking change.
24
25#![forbid(unsafe_code)]
26
27pub mod errors;
28pub mod exit;
29pub mod warnings;
30
31/// Metadata for a single error or warning code.
32///
33/// `name` doubles as the emitted string value — every code is
34/// declared as `pub const X: &str = "X"` so this field references the
35/// same constant the call site uses. Keeping the const + the
36/// `CodeMeta` entry pointing at the same identifier lets a rename
37/// flow through both with no drift.
38///
39/// `description` and `category` feed the generated docs page
40/// (`docs/error-codes.data.json`, consumed by `<ErrorCodesTable>`).
41/// `exit_code` is `Some(_)` only for errors that have a bespoke
42/// entry — warnings always set `None` because they don't change
43/// exit status.
44///
45/// `Serialize` is derived so the generator binary can emit each
46/// entry verbatim via `serde_json`. Every consuming crate already
47/// has `serde` in its dep tree; adding it here doesn't grow the
48/// compile graph.
49#[derive(Debug, serde::Serialize)]
50pub struct CodeMeta {
51    pub name: &'static str,
52    pub category: &'static str,
53    pub description: &'static str,
54    pub exit_code: Option<i32>,
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn every_error_const_value_matches_its_name() {
63        // The `pub const ERR_AUBE_X: &str = "ERR_AUBE_X"` shape is
64        // load-bearing — typos between the const name and the value
65        // would silently emit the wrong code. CodeMeta::name is the
66        // const value, so checking the prefix on every entry catches
67        // any rogue addition that didn't follow the convention.
68        for meta in errors::ALL {
69            assert!(
70                meta.name.starts_with("ERR_AUBE_"),
71                "error codes must use the ERR_AUBE_ prefix: {}",
72                meta.name
73            );
74            assert!(
75                !meta.description.is_empty(),
76                "error code {} is missing a description",
77                meta.name
78            );
79            assert!(
80                !meta.category.is_empty(),
81                "error code {} is missing a category",
82                meta.name
83            );
84        }
85    }
86
87    #[test]
88    fn every_warning_const_value_matches_its_name() {
89        for meta in warnings::ALL {
90            assert!(
91                meta.name.starts_with("WARN_AUBE_"),
92                "warning codes must use the WARN_AUBE_ prefix: {}",
93                meta.name
94            );
95            assert!(
96                !meta.description.is_empty(),
97                "warning code {} is missing a description",
98                meta.name
99            );
100            assert!(
101                !meta.category.is_empty(),
102                "warning code {} is missing a category",
103                meta.name
104            );
105            assert!(
106                meta.exit_code.is_none(),
107                "warning {} has an exit_code; warnings don't change exit status",
108                meta.name
109            );
110        }
111    }
112
113    #[test]
114    fn no_duplicate_codes() {
115        use std::collections::HashSet;
116        let all: Vec<&str> = errors::ALL
117            .iter()
118            .chain(warnings::ALL.iter())
119            .map(|m| m.name)
120            .collect();
121        let unique: HashSet<&str> = all.iter().copied().collect();
122        assert_eq!(
123            all.len(),
124            unique.len(),
125            "duplicate code identifier across errors/warnings"
126        );
127    }
128}