use std::path::Path;
use crate::error::{Error, ErrorKind, Origin};
use crate::source::Format;
use crate::value::Value;
pub(crate) fn parse(text: &str, format: Format) -> Result<Value, Error> {
parse_with(crate::reader::installed(), text, format)
}
pub(crate) fn parse_with(
reader: &'static dyn crate::reader::Reader,
text: &str,
format: Format,
) -> Result<Value, Error> {
let Some(reader) = crate::reader::for_format(reader, format) else {
return Err(crate::reader::unread(format));
};
if text.trim().is_empty() {
return Ok(Value::Table(std::collections::BTreeMap::new()));
}
let parsed = reader.parse(text, format)?;
match parsed {
table @ Value::Table(_) => Ok(table),
_ => Err(Error::new(
ErrorKind::Parse,
"a configuration document is a table of keys; this one is not",
)),
}
}
pub(crate) fn parse_natively(text: &str, format: Format) -> Result<Value, Error> {
#[cfg(not(any(
feature = "json",
feature = "toml",
feature = "yaml",
feature = "ini",
feature = "properties"
)))]
let _ = text;
match format {
#[cfg(feature = "json")]
Format::Json => serde_json::from_str::<Value>(text).map_err(|error| failed("JSON", &error)),
#[cfg(feature = "toml")]
Format::Toml => toml::from_str::<Value>(text).map(dates).map_err(|error| {
let reason = crate::loader::redacted(error.message());
match error.span().map(|span| at(text, span.start)) {
Some((line, column)) => Error::new(
ErrorKind::Parse,
format!("TOML parse error at line {line}, column {column}: {reason}"),
),
None => Error::new(ErrorKind::Parse, format!("TOML parse error: {reason}")),
}
}),
#[cfg(feature = "yaml")]
Format::Yaml => serde_yaml::from_str::<Value>(text).map_err(|error| failed("YAML", &error)),
#[cfg(feature = "ini")]
Format::Ini => crate::loader::ini::parse(text),
#[cfg(feature = "properties")]
Format::Properties => crate::loader::properties::parse(text),
#[allow(unreachable_patterns)]
format => Err(crate::reader::unread(format)),
}
}
pub(crate) fn read(
reader: &'static dyn crate::reader::Reader,
path: &Path,
format: Format,
) -> Result<Option<Value>, Error> {
let text = match std::fs::read_to_string(path) {
Ok(text) => text,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => {
return Err(Error::new(ErrorKind::Io, error.to_string())
.with_origin(Origin::File(path.to_owned())))
}
};
parse_with(reader, &text, format)
.map(Some)
.map_err(|error| error.with_origin(Origin::File(path.to_owned())))
}
#[cfg(any(feature = "json", feature = "yaml"))]
fn failed(format: &str, error: &dyn std::fmt::Display) -> Error {
let rendered = error.to_string();
let first = rendered.lines().next().unwrap_or_default();
Error::new(
ErrorKind::Parse,
format!("{format}: {}", crate::loader::redacted(first)),
)
}
#[cfg(feature = "toml")]
pub(crate) fn dates(value: Value) -> Value {
const DATETIME: &str = "$__toml_private_datetime";
match value {
Value::Table(mut table) => {
if table.len() == 1 {
if let Some(Value::String(written)) = table.remove(DATETIME) {
return Value::String(written);
}
if let Some((key, value)) = table.into_iter().next() {
return Value::Table(std::collections::BTreeMap::from([(key, dates(value))]));
}
return Value::Table(std::collections::BTreeMap::new());
}
Value::Table(
table
.into_iter()
.map(|(key, value)| (key, dates(value)))
.collect(),
)
}
Value::Array(values) => Value::Array(values.into_iter().map(dates).collect()),
other => other,
}
}
#[cfg(feature = "toml")]
fn at(text: &str, offset: usize) -> (usize, usize) {
let before = &text[..offset.min(text.len())];
let line = before.matches('\n').count() + 1;
let column = before
.rsplit_once('\n')
.map_or(before.chars().count(), |(_, last)| last.chars().count())
+ 1;
(line, column)
}