Add the library-wide error enum
d3j needs to distinguish unparsable input and unknown languages from
merge conflicts at the CLI level, since those map to a different exit
code than a clean merge with unresolved conflicts. `Error` covers the
three v1 cases (UnknownLanguage, Parse, Io) with miette diagnostics
for actionable CLI output.

Assisted-by: Claude Sonnet 5 via Claude Code
change xqtxwqrkmnlwyuvuzrppxstkqoqxqllp
commit 8f80d92740d9684b896917de2ece3b0223644ec9
author Alpha Chen <alpha@kejadlen.dev>
date
parent kwmovmmy
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..d61b59d
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,88 @@
+use std::path::PathBuf;
+
+use miette::Diagnostic;
+
+/// The library-wide error space for d3j.
+///
+/// Unparsable input and an unknown language are distinguished from merge
+/// conflicts at this level so the CLI can map them to a different exit
+/// code than a clean merge with unresolved conflicts.
+#[derive(Debug, thiserror::Error, Diagnostic)]
+pub enum Error {
+    #[error("cannot detect language for {}", path.display())]
+    #[diagnostic(
+        code(d3j::unknown_language),
+        help("pass --lang; supported: rust, java, json")
+    )]
+    UnknownLanguage { path: PathBuf },
+
+    #[error("{} does not parse as {lang}", path.display())]
+    #[diagnostic(
+        code(d3j::parse),
+        help("structural merge requires syntactically valid inputs")
+    )]
+    Parse { path: PathBuf, lang: String },
+
+    #[error("io error on {}: {source}", path.display())]
+    #[diagnostic(code(d3j::io))]
+    Io {
+        path: PathBuf,
+        #[source]
+        source: std::io::Error,
+    },
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn code_of(err: &Error) -> Option<String> {
+        err.code().map(|c| c.to_string())
+    }
+
+    fn help_of(err: &Error) -> Option<String> {
+        err.help().map(|h| h.to_string())
+    }
+
+    #[test]
+    fn errors_render_with_diagnostic_codes() {
+        let err = Error::UnknownLanguage {
+            path: "x.zig".into(),
+        };
+        assert!(err.to_string().contains("x.zig"));
+        assert_eq!(code_of(&err).as_deref(), Some("d3j::unknown_language"));
+        assert_eq!(
+            help_of(&err).as_deref(),
+            Some("pass --lang; supported: rust, java, json")
+        );
+    }
+
+    #[test]
+    fn parse_error_renders_path_and_language() {
+        let err = Error::Parse {
+            path: "x.rs".into(),
+            lang: "rust".into(),
+        };
+        assert!(err.to_string().contains("x.rs"));
+        assert!(err.to_string().contains("rust"));
+        assert_eq!(code_of(&err).as_deref(), Some("d3j::parse"));
+        assert_eq!(
+            help_of(&err).as_deref(),
+            Some("structural merge requires syntactically valid inputs")
+        );
+    }
+
+    #[test]
+    fn io_error_renders_path_and_source() {
+        let source = std::io::Error::other("permission denied");
+        let err = Error::Io {
+            path: "x.json".into(),
+            source,
+        };
+        let rendered = err.to_string();
+        assert!(rendered.contains("x.json"));
+        assert!(rendered.contains("permission denied"));
+        assert_eq!(code_of(&err).as_deref(), Some("d3j::io"));
+        assert!(std::error::Error::source(&err).is_some());
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 8b13789..dd4aeed 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1 +1,3 @@
+pub mod error;
 
+pub use error::Error;