Skip to main content

hocon/
error.rs

1use std::fmt;
2
3/// Error returned when HOCON input contains a syntax error.
4///
5/// Includes the line and column where the error was detected.
6#[non_exhaustive]
7#[derive(Debug, Clone)]
8pub struct ParseError {
9    /// Human-readable description of the error.
10    pub message: String,
11    /// 1-based line number.
12    pub line: usize,
13    /// 1-based column number.
14    pub col: usize,
15}
16
17impl fmt::Display for ParseError {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        write!(
20            f,
21            "ParseError at {}:{}: {}",
22            self.line, self.col, self.message
23        )
24    }
25}
26
27impl std::error::Error for ParseError {}
28
29/// Error returned when substitution resolution fails (e.g., missing
30/// required substitution, cyclic reference).
31#[non_exhaustive]
32#[derive(Debug, Clone)]
33pub struct ResolveError {
34    /// Human-readable description of the error.
35    pub message: String,
36    /// The substitution path that failed (e.g., `"db.host"`).
37    pub path: String,
38    /// 1-based line number where the substitution appeared.
39    pub line: usize,
40    /// 1-based column number where the substitution appeared.
41    pub col: usize,
42}
43
44impl fmt::Display for ResolveError {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(
47            f,
48            "ResolveError at {}:{}: {} (path: {})",
49            self.line, self.col, self.message, self.path
50        )
51    }
52}
53
54impl ResolveError {
55    /// Construct a type-mismatch error for value-concat (S10.4/S10.13/S10.19).
56    ///
57    /// `line` and `col` are the position of the concat expression in the source
58    /// (from `ConcatPlaceholder`). `path` is the field path being resolved.
59    pub(crate) fn concat_type_mismatch(
60        left_type: &str,
61        right_type: &str,
62        line: usize,
63        col: usize,
64    ) -> Self {
65        ResolveError {
66            message: format!(
67                "value concatenation requires same-kind operands per HOCON S10; \
68                 got {} + {}",
69                left_type, right_type
70            ),
71            path: String::new(),
72            line,
73            col,
74        }
75    }
76}
77
78impl std::error::Error for ResolveError {}
79
80/// Error returned by [`Config`](crate::Config) getters when a key is missing
81/// or the value has the wrong type.
82#[non_exhaustive]
83#[derive(Debug, Clone)]
84pub struct ConfigError {
85    /// Human-readable description of the error.
86    pub message: String,
87    /// The dot-separated path that was looked up. Empty for file-root errors
88    /// (e.g. the S3.5 array-at-file-root rejection), where no access path
89    /// exists.
90    pub path: String,
91}
92
93impl fmt::Display for ConfigError {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        if self.path.is_empty() {
96            // File-root errors (S3.5 array-at-root) have no access path.
97            return write!(f, "ConfigError: {}", self.message);
98        }
99        write!(f, "ConfigError: {} (path: {})", self.message, self.path)
100    }
101}
102
103impl std::error::Error for ConfigError {}
104
105impl ConfigError {
106    /// Returns `true` if this error was produced because the value at the path
107    /// contains an unresolved substitution placeholder.
108    ///
109    /// Use this instead of message-string matching to detect the "not resolved"
110    /// condition when the caller holds a `ConfigError` (i.e., from a typed getter
111    /// such as `get_string`, `get_i64`, etc.).
112    ///
113    /// # Example
114    /// ```rust,ignore
115    /// let err = config.get_string("my.key").unwrap_err();
116    /// if err.is_not_resolved() {
117    ///     // Call config.resolve(ResolveOptions::defaults()) first
118    /// }
119    /// ```
120    pub fn is_not_resolved(&self) -> bool {
121        self.message.starts_with("value is not resolved")
122    }
123}
124
125/// Error returned when a getter is called on a [`Config`](crate::Config) path
126/// whose value (or any transitive parent) contains an unresolved substitution
127/// placeholder.
128///
129/// Detect via `downcast_ref::<NotResolvedError>()` or by matching
130/// `HoconError::NotResolved(e)`.  Per E12 decision 12.
131#[derive(Debug, Clone)]
132pub struct NotResolvedError {
133    /// The dot-separated path that was accessed.
134    pub path: String,
135}
136
137impl fmt::Display for NotResolvedError {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        write!(
140            f,
141            "value at path {:?} is not resolved (call resolve() before accessing values)",
142            self.path
143        )
144    }
145}
146
147impl std::error::Error for NotResolvedError {}
148
149/// Unified error type returned by top-level parse functions.
150///
151/// Wraps the possible failure modes: syntax errors ([`ParseError`]),
152/// substitution resolution failures ([`ResolveError`]), file I/O errors
153/// ([`std::io::Error`]), unresolved-getter errors ([`NotResolvedError`]),
154/// and Config-boundary type errors ([`ConfigError`] — e.g. the S3.5
155/// array-at-file-root rejection).
156#[non_exhaustive]
157#[derive(Debug)]
158pub enum HoconError {
159    /// Syntax error during lexing or parsing.
160    Parse(ParseError),
161    /// Substitution resolution failure (missing key, cycle, etc.).
162    Resolve(ResolveError),
163    /// File I/O error when reading the top-level config file.
164    Io(std::io::Error),
165    /// A getter was called on a path whose value contains an unresolved
166    /// substitution placeholder. Per E12 decision 12.
167    NotResolved(NotResolvedError),
168    /// Type-mismatch error at the Config boundary (the Lightbend
169    /// `ConfigException.WrongType` analog). Returned by the parse functions
170    /// for an array-root document (S3.5, HOCON.md L989-991): the document is
171    /// valid syntax, but the object-rooted Config API requires an object at
172    /// file root. `ConfigError.path` is empty for file-root errors.
173    Config(ConfigError),
174}
175
176impl fmt::Display for HoconError {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        match self {
179            HoconError::Parse(e) => write!(f, "{}", e),
180            HoconError::Resolve(e) => write!(f, "{}", e),
181            HoconError::Io(e) => write!(f, "I/O error: {}", e),
182            HoconError::NotResolved(e) => write!(f, "{}", e),
183            HoconError::Config(e) => write!(f, "{}", e),
184        }
185    }
186}
187
188impl std::error::Error for HoconError {
189    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
190        match self {
191            HoconError::Parse(e) => Some(e),
192            HoconError::Resolve(e) => Some(e),
193            HoconError::Io(e) => Some(e),
194            HoconError::NotResolved(e) => Some(e),
195            HoconError::Config(e) => Some(e),
196        }
197    }
198}
199
200impl From<ParseError> for HoconError {
201    fn from(e: ParseError) -> Self {
202        HoconError::Parse(e)
203    }
204}
205
206impl From<ResolveError> for HoconError {
207    fn from(e: ResolveError) -> Self {
208        HoconError::Resolve(e)
209    }
210}
211
212impl From<std::io::Error> for HoconError {
213    fn from(e: std::io::Error) -> Self {
214        HoconError::Io(e)
215    }
216}