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}