Skip to main content

lindera_binding_core/
error.rs

1//! Shared, FFI-independent error type for the language bindings.
2//!
3//! Each binding historically defined its own error plumbing (a `PyException`
4//! wrapper, a `napi::Error` helper, a magnus `Error`, a `PhpException`, a
5//! `JsValue`), all of them message-only. This module provides a single
6//! [`CoreError`] carrying an [`ErrorKind`] category plus a message, so each
7//! binding can keep just one `From<CoreError>` conversion into its native
8//! exception type.
9
10use std::fmt;
11
12use lindera::error::{LinderaError, LinderaErrorKind};
13
14/// Category of a binding-facing error.
15///
16/// Lets each binding map a [`CoreError`] onto the most appropriate native
17/// exception (for example, an [`ErrorKind::InvalidArgument`] can become a
18/// `ValueError` rather than a generic exception).
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ErrorKind {
21    /// The caller supplied an invalid argument or value.
22    InvalidArgument,
23    /// A dictionary could not be found, loaded, or resolved.
24    Dictionary,
25    /// A tokenizer or other component failed to build.
26    Build,
27    /// An input/output operation failed.
28    Io,
29    /// Data could not be parsed, decoded, or (de)serialized.
30    Parse,
31    /// A schema or record failed validation.
32    Validation,
33    /// A runtime failure that does not fit a more specific category.
34    Runtime,
35}
36
37impl ErrorKind {
38    /// Returns a stable, lowercase identifier for the kind, suitable for
39    /// logging or exposing to the host language.
40    pub fn as_str(&self) -> &'static str {
41        match self {
42            ErrorKind::InvalidArgument => "invalid_argument",
43            ErrorKind::Dictionary => "dictionary",
44            ErrorKind::Build => "build",
45            ErrorKind::Io => "io",
46            ErrorKind::Parse => "parse",
47            ErrorKind::Validation => "validation",
48            ErrorKind::Runtime => "runtime",
49        }
50    }
51}
52
53impl fmt::Display for ErrorKind {
54    /// Writes the lowercase identifier returned by [`ErrorKind::as_str`].
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        f.write_str(self.as_str())
57    }
58}
59
60/// An FFI-independent error shared by all Lindera language bindings.
61///
62/// Carries an [`ErrorKind`] category plus a human-readable message. Each
63/// binding implements a single `From<CoreError>` to map it onto its native
64/// exception type.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct CoreError {
67    /// The error category.
68    kind: ErrorKind,
69    /// The human-readable error message.
70    message: String,
71}
72
73impl CoreError {
74    /// Creates a new error with the given kind and message.
75    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
76        Self {
77            kind,
78            message: message.into(),
79        }
80    }
81
82    /// Creates an [`ErrorKind::InvalidArgument`] error.
83    pub fn invalid_argument(message: impl Into<String>) -> Self {
84        Self::new(ErrorKind::InvalidArgument, message)
85    }
86
87    /// Creates an [`ErrorKind::Dictionary`] error.
88    pub fn dictionary(message: impl Into<String>) -> Self {
89        Self::new(ErrorKind::Dictionary, message)
90    }
91
92    /// Creates an [`ErrorKind::Build`] error.
93    pub fn build(message: impl Into<String>) -> Self {
94        Self::new(ErrorKind::Build, message)
95    }
96
97    /// Creates an [`ErrorKind::Validation`] error.
98    pub fn validation(message: impl Into<String>) -> Self {
99        Self::new(ErrorKind::Validation, message)
100    }
101
102    /// Creates an [`ErrorKind::Runtime`] error.
103    pub fn runtime(message: impl Into<String>) -> Self {
104        Self::new(ErrorKind::Runtime, message)
105    }
106
107    /// Returns the error category.
108    pub fn kind(&self) -> ErrorKind {
109        self.kind
110    }
111
112    /// Returns the human-readable error message.
113    pub fn message(&self) -> &str {
114        &self.message
115    }
116}
117
118impl fmt::Display for CoreError {
119    /// Writes the error message (the kind is available via [`CoreError::kind`]).
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        f.write_str(&self.message)
122    }
123}
124
125impl std::error::Error for CoreError {}
126
127impl From<LinderaError> for CoreError {
128    /// Maps a [`lindera::error::LinderaError`] onto a [`CoreError`], translating
129    /// the [`LinderaErrorKind`] into the binding-facing [`ErrorKind`].
130    fn from(err: LinderaError) -> Self {
131        let kind = match err.kind() {
132            LinderaErrorKind::Args | LinderaErrorKind::Mode => ErrorKind::InvalidArgument,
133            LinderaErrorKind::Dictionary | LinderaErrorKind::NotFound => ErrorKind::Dictionary,
134            LinderaErrorKind::Build => ErrorKind::Build,
135            LinderaErrorKind::Io => ErrorKind::Io,
136            LinderaErrorKind::Content
137            | LinderaErrorKind::Decode
138            | LinderaErrorKind::Deserialize
139            | LinderaErrorKind::Serialize
140            | LinderaErrorKind::Parse => ErrorKind::Parse,
141            LinderaErrorKind::FeatureDisabled => ErrorKind::Runtime,
142        };
143        CoreError::new(kind, err.to_string())
144    }
145}
146
147/// A convenient `Result` alias for binding-core operations.
148pub type CoreResult<T> = Result<T, CoreError>;
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn new_exposes_kind_and_message() {
156        let err = CoreError::new(ErrorKind::Build, "boom");
157        assert_eq!(err.kind(), ErrorKind::Build);
158        assert_eq!(err.message(), "boom");
159        assert_eq!(err.to_string(), "boom");
160    }
161
162    #[test]
163    fn convenience_constructors_set_kind() {
164        assert_eq!(
165            CoreError::invalid_argument("x").kind(),
166            ErrorKind::InvalidArgument
167        );
168        assert_eq!(CoreError::dictionary("x").kind(), ErrorKind::Dictionary);
169        assert_eq!(CoreError::validation("x").kind(), ErrorKind::Validation);
170        assert_eq!(CoreError::runtime("x").kind(), ErrorKind::Runtime);
171    }
172
173    #[test]
174    fn kind_as_str_is_stable() {
175        assert_eq!(ErrorKind::InvalidArgument.as_str(), "invalid_argument");
176        assert_eq!(ErrorKind::Dictionary.to_string(), "dictionary");
177    }
178
179    #[test]
180    fn from_lindera_error_maps_kind() {
181        let parse = LinderaErrorKind::Parse.with_error(std::io::Error::other("bad"));
182        assert_eq!(CoreError::from(parse).kind(), ErrorKind::Parse);
183
184        let args = LinderaErrorKind::Args.with_error(std::io::Error::other("bad arg"));
185        assert_eq!(CoreError::from(args).kind(), ErrorKind::InvalidArgument);
186
187        let notfound = LinderaErrorKind::NotFound.with_error(std::io::Error::other("missing"));
188        assert_eq!(CoreError::from(notfound).kind(), ErrorKind::Dictionary);
189    }
190}