#[cfg(feature = "toml")]
use std::io;
use std::{
fmt,
path::{Path, PathBuf},
};
#[cfg(feature = "toml")]
use toml_edit::de as toml;
#[cfg(feature = "toml")]
use crate::config::toml::TomlInterpolationError;
use crate::config::{
dotenv::DotenvError,
environment::{EnvironmentContractError, EnvironmentError},
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct Location {
pub(crate) line: usize,
pub(crate) column: usize,
}
impl Location {
pub(crate) fn from_offset(input: &str, offset: usize) -> Self {
let mut line = 1;
let mut column = 1;
for (index, character) in input.char_indices() {
if index >= offset {
break;
}
if character == '\n' {
line += 1;
column = 1;
} else {
column += 1;
}
}
Self { line, column }
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Source {
#[cfg(feature = "toml")]
Toml,
Dotenv,
Environment,
}
impl fmt::Display for Source {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
#[cfg(feature = "toml")]
Self::Toml => formatter.write_str("TOML"),
Self::Dotenv => formatter.write_str("environment file"),
Self::Environment => formatter.write_str("process environment"),
}
}
}
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub struct Error {
kind: Box<ErrorKind>,
}
impl Error {
#[doc(hidden)]
#[must_use]
pub fn __missing_value(field: &'static str) -> Self {
Self { kind: Box::new(ErrorKind::MissingValue { field: String::from(field) }) }
}
#[doc(hidden)]
#[must_use]
pub fn __within(mut self, parent: &'static str) -> Self {
if let ErrorKind::MissingValue { field } = self.kind.as_mut() {
field.insert(0, '.');
field.insert_str(0, parent);
}
self
}
#[cfg(feature = "toml")]
pub(crate) fn read_toml(scope: Source, path: &Path, source: io::Error) -> Self {
Self { kind: Box::new(ErrorKind::ReadToml { scope, path: path.to_path_buf(), source }) }
}
#[cfg(feature = "toml")]
pub(crate) fn interpolate_toml(
scope: Source,
path: &Path,
source: TomlInterpolationError,
) -> Self {
Self {
kind: Box::new(ErrorKind::InterpolateToml { scope, path: path.to_path_buf(), source }),
}
}
#[cfg(feature = "toml")]
pub(crate) fn parse_toml(
scope: Source,
path: &Path,
original: &str,
decoded: &str,
source: toml::Error,
interpolated: bool,
) -> Self {
let location = if original == decoded {
source.span().map(|span| Location::from_offset(original, span.start))
} else {
None
};
let kind = if interpolated {
ErrorKind::ParseInterpolatedToml { scope, path: path.to_path_buf() }
} else {
ErrorKind::ParseToml { scope, path: path.to_path_buf(), source, location }
};
Self { kind: Box::new(kind) }
}
pub(crate) fn dotenv(source: DotenvError) -> Self {
Self { kind: Box::new(ErrorKind::Dotenv { source }) }
}
pub(crate) fn environment(scope: EnvironmentScope, source: EnvironmentError) -> Self {
Self { kind: Box::new(ErrorKind::Environment { scope, source }) }
}
pub(crate) fn environment_contract(source: EnvironmentContractError) -> Self {
Self { kind: Box::new(ErrorKind::EnvironmentContract { source }) }
}
#[must_use]
pub fn configuration_source(&self) -> Option<Source> {
match self.kind.as_ref() {
ErrorKind::MissingValue { .. } | ErrorKind::EnvironmentContract { .. } => None,
#[cfg(feature = "toml")]
ErrorKind::ReadToml { scope, .. }
| ErrorKind::InterpolateToml { scope, .. }
| ErrorKind::ParseToml { scope, .. }
| ErrorKind::ParseInterpolatedToml { scope, .. } => Some(*scope),
ErrorKind::Dotenv { .. } => Some(Source::Dotenv),
ErrorKind::Environment { scope, .. } => Some(scope.source()),
}
}
#[must_use]
pub fn field(&self) -> Option<&str> {
match self.kind.as_ref() {
ErrorKind::MissingValue { field } => Some(field),
ErrorKind::Environment { source, .. } => Some(source.field()),
#[cfg(feature = "toml")]
ErrorKind::ReadToml { .. }
| ErrorKind::InterpolateToml { .. }
| ErrorKind::ParseToml { .. }
| ErrorKind::ParseInterpolatedToml { .. } => None,
ErrorKind::Dotenv { .. } | ErrorKind::EnvironmentContract { .. } => None,
}
}
#[must_use]
pub fn environment_variable(&self) -> Option<&str> {
match self.kind.as_ref() {
#[cfg(feature = "toml")]
ErrorKind::InterpolateToml { source, .. } => source.variable(),
ErrorKind::Dotenv { source } => source.variable(),
ErrorKind::Environment { source, .. } => Some(source.variable()),
ErrorKind::EnvironmentContract { source } => Some(source.variable()),
ErrorKind::MissingValue { .. } => None,
#[cfg(feature = "toml")]
ErrorKind::ReadToml { .. }
| ErrorKind::ParseToml { .. }
| ErrorKind::ParseInterpolatedToml { .. } => None,
}
}
#[must_use]
pub fn path(&self) -> Option<&Path> {
match self.kind.as_ref() {
#[cfg(feature = "toml")]
ErrorKind::ReadToml { path, .. }
| ErrorKind::InterpolateToml { path, .. }
| ErrorKind::ParseToml { path, .. }
| ErrorKind::ParseInterpolatedToml { path, .. } => Some(path),
ErrorKind::Dotenv { source } => source.path(),
ErrorKind::Environment { scope, .. } => scope.path(),
ErrorKind::MissingValue { .. } | ErrorKind::EnvironmentContract { .. } => None,
}
}
#[must_use]
pub fn location(&self) -> Option<(usize, usize)> {
let location = match self.kind.as_ref() {
#[cfg(feature = "toml")]
ErrorKind::InterpolateToml { source, .. } => Some(source.location()),
#[cfg(feature = "toml")]
ErrorKind::ParseToml { location, .. } => *location,
ErrorKind::Dotenv { source } => source.location(),
ErrorKind::MissingValue { .. }
| ErrorKind::Environment { .. }
| ErrorKind::EnvironmentContract { .. } => None,
#[cfg(feature = "toml")]
ErrorKind::ReadToml { .. } | ErrorKind::ParseInterpolatedToml { .. } => None,
}?;
Some((location.line, location.column))
}
}
#[derive(Debug, thiserror::Error)]
enum ErrorKind {
#[error("missing required configuration value `{field}`")]
MissingValue {
field: String,
},
#[error("failed to read {scope} configuration `{}`: {source}", path.display())]
#[cfg(feature = "toml")]
ReadToml {
scope: Source,
path: PathBuf,
source: io::Error,
},
#[error("failed to interpolate {scope} configuration `{}`: {source}", path.display())]
#[cfg(feature = "toml")]
InterpolateToml {
scope: Source,
path: PathBuf,
source: TomlInterpolationError,
},
#[error("failed to parse {scope} configuration `{}`: {source}", path.display())]
#[cfg(feature = "toml")]
ParseToml {
scope: Source,
path: PathBuf,
source: toml::Error,
location: Option<Location>,
},
#[error(
"failed to parse {scope} configuration `{}` after environment interpolation",
path.display(),
)]
#[cfg(feature = "toml")]
ParseInterpolatedToml {
scope: Source,
path: PathBuf,
},
#[error("failed to load dotenv: {source}")]
Dotenv {
source: DotenvError,
},
#[error("failed to parse {scope}: {source}")]
Environment {
scope: EnvironmentScope,
source: EnvironmentError,
},
#[error("invalid environment configuration contract: {source}")]
EnvironmentContract {
source: EnvironmentContractError,
},
}
#[derive(Debug)]
pub(crate) enum EnvironmentScope {
File(PathBuf),
Process,
}
impl EnvironmentScope {
const fn source(&self) -> Source {
match self {
Self::File(_) => Source::Dotenv,
Self::Process => Source::Environment,
}
}
fn path(&self) -> Option<&Path> {
match self {
Self::File(path) => Some(path),
Self::Process => None,
}
}
}
impl fmt::Display for EnvironmentScope {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::File(path) => write!(formatter, "environment file `{}`", path.display()),
Self::Process => formatter.write_str("process environment configuration"),
}
}
}