#![allow(unused_assignments)]
use std::io;
use miette::{Diagnostic, SourceSpan};
#[derive(Diagnostic, thiserror::Error)]
#[non_exhaustive]
pub enum ConfigError {
#[error("unsupported file format '{0}'")]
#[diagnostic(
code(forgeconf::unsupported_format),
help(
"Forgeconf supports toml, yaml, and json formats. Make sure your file has the correct \
extension."
)
)]
UnsupportedFormat(String),
#[error("configuration file is missing an extension")]
#[diagnostic(
code(forgeconf::missing_extension),
help("Add a file extension like .toml, .yaml, or .json to your configuration file.")
)]
MissingExtension,
#[error("missing required field '{field}'")]
#[diagnostic(
code(forgeconf::missing_field),
help("Add the missing field '{field}' to your configuration file.")
)]
MissingValue {
field: String,
#[label("expected '{field}' to be defined here")]
span: Option<SourceSpan>,
},
#[error("type mismatch for field '{field}'")]
#[diagnostic(
code(forgeconf::type_mismatch),
help(
"Expected a value of type '{expected}', but found '{found}'. Check your configuration \
syntax."
)
)]
TypeMismatch {
field: String,
expected: String,
found: String,
#[label("expected {expected}, but found {found}")]
span: Option<SourceSpan>,
},
#[error("error in nested section '{section}'")]
#[diagnostic(
code(forgeconf::nested_error),
help("Check the '{section}' section of your configuration for errors.")
)]
Nested {
section: String,
#[source]
#[diagnostic_source]
source: Box<dyn Diagnostic + Send + Sync>,
#[label("error in this section")]
span: Option<SourceSpan>,
},
#[error(transparent)]
#[diagnostic(
code(forgeconf::io_error),
help("Check that the file exists and you have permission to read it.")
)]
Io(#[from] io::Error),
#[cfg(feature = "toml")]
#[error("failed to parse TOML")]
#[diagnostic(
code(forgeconf::toml_parse_error),
help(
"Check your TOML syntax. Common issues include unquoted strings, missing commas, or \
invalid escape sequences."
)
)]
Toml {
#[source]
source: toml::de::Error,
#[label("parse error here")]
span: Option<SourceSpan>,
},
#[cfg(feature = "json")]
#[error("failed to parse JSON")]
#[diagnostic(
code(forgeconf::json_parse_error),
help(
"Check your JSON syntax. Common issues include trailing commas, unquoted keys, or \
missing brackets."
)
)]
Json {
#[source]
source: jzon::Error,
#[label("parse error here")]
span: Option<SourceSpan>,
},
#[cfg(feature = "yaml")]
#[error("failed to parse YAML")]
#[diagnostic(
code(forgeconf::yaml_parse_error),
help(
"Check your YAML syntax. Common issues include incorrect indentation, missing colons, \
or invalid characters."
)
)]
Yaml {
#[source]
source: yaml_rust2::ScanError,
#[label("parse error here")]
span: Option<SourceSpan>,
},
}
impl std::fmt::Debug for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
#[cfg(feature = "miette")]
{
use miette::GraphicalReportHandler;
let mut output = String::new();
let handler = GraphicalReportHandler::new();
if handler
.render_report(&mut output, self)
.is_ok()
{
write!(f, "{}", output)
} else {
write!(f, "{}", self)
}
}
#[cfg(not(feature = "miette"))]
{
write!(f, "{}", self)
}
}
}
impl ConfigError {
pub fn mismatch(
field: impl Into<String>,
expected: impl Into<String>,
found: impl Into<String>,
) -> Self {
ConfigError::TypeMismatch {
field: field.into(),
expected: expected.into(),
found: found.into(),
span: None,
}
}
pub fn mismatch_at(
field: impl Into<String>,
expected: impl Into<String>,
found: impl Into<String>,
span: SourceSpan,
) -> Self {
ConfigError::TypeMismatch {
field: field.into(),
expected: expected.into(),
found: found.into(),
span: Some(span),
}
}
pub fn nested(section: impl Into<String>, source: ConfigError) -> Self {
ConfigError::Nested {
section: section.into(),
source: Box::new(source),
span: None,
}
}
pub fn nested_at(section: impl Into<String>, source: ConfigError, span: SourceSpan) -> Self {
ConfigError::Nested {
section: section.into(),
source: Box::new(source),
span: Some(span),
}
}
pub fn missing(key: impl Into<String>) -> Self {
ConfigError::MissingValue { field: key.into(), span: None }
}
pub fn missing_at(key: impl Into<String>, span: SourceSpan) -> Self {
ConfigError::MissingValue { field: key.into(), span: Some(span) }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mismatch_helper_builds_type_mismatch() {
let err = ConfigError::mismatch("port", "u16", "string");
assert!(matches!(
err,
ConfigError::TypeMismatch {
field,
expected,
found,
span: None,
} if field == "port" && expected == "u16" && found == "string"
));
}
#[test]
fn nested_helper_wraps_inner_error() {
let inner = ConfigError::missing("host");
let err = ConfigError::nested("server", inner);
assert!(matches!(
err,
ConfigError::Nested {
section,
span: None,
..
} if section == "server"
));
}
#[test]
fn missing_helper_sets_field_name() {
let err = ConfigError::missing("database");
assert!(matches!(
err,
ConfigError::MissingValue { ref field, span: None }
if field == "database"
));
}
}