use crate::{Model, Value};
use serde_json::Map;
use std::{collections::BTreeMap, fmt, fs, io, path::Path};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ValueMode {
#[default]
JsonOrString,
String,
Json,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SettingsError {
Io(io::ErrorKind),
Syntax,
RootNotObject,
InvalidKey,
ConflictingKeys,
InvalidUnicode,
NotRegularFile,
Validation {
count: usize,
},
}
impl fmt::Display for SettingsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(kind) => write!(f, "settings source I/O failed ({kind:?})"),
Self::Syntax => f.write_str("invalid settings source syntax"),
Self::RootNotObject => f.write_str("settings source must be a JSON object"),
Self::InvalidKey => f.write_str("invalid settings source key"),
Self::ConflictingKeys => f.write_str("conflicting settings keys in one source"),
Self::InvalidUnicode => f.write_str("settings source is not valid Unicode"),
Self::NotRegularFile => {
f.write_str("settings source must be a regular file, not a symlink")
}
Self::Validation { count } => write!(
f,
"settings validation failed ({count} errors; details redacted)"
),
}
}
}
impl std::error::Error for SettingsError {}
#[derive(Clone, Default)]
pub struct Settings {
values: Map<String, Value>,
}
impl fmt::Debug for Settings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Settings { values: [REDACTED] }")
}
}
impl Settings {
pub fn new() -> Self {
Self::default()
}
pub fn json(mut self, value: Value) -> Result<Self, SettingsError> {
let Value::Object(object) = value else {
return Err(SettingsError::RootNotObject);
};
merge(&mut self.values, object);
Ok(self)
}
pub fn json_file(self, path: impl AsRef<Path>) -> Result<Self, SettingsError> {
let text = read_file(path.as_ref())?;
self.json(serde_json::from_str(&text).map_err(|_| SettingsError::Syntax)?)
}
pub fn env(self, prefix: &str, mode: ValueMode) -> Result<Self, SettingsError> {
check_prefix(prefix)?;
let mut selected = Vec::new();
for (key, value) in std::env::vars_os() {
if key.as_encoded_bytes().starts_with(prefix.as_bytes()) {
let key = key.to_str().ok_or(SettingsError::InvalidUnicode)?;
selected.push((
key.to_owned(),
value
.into_string()
.map_err(|_| SettingsError::InvalidUnicode)?,
));
}
}
self.pairs(selected, prefix, mode)
}
pub fn pairs<I, K, V>(
self,
pairs: I,
prefix: &str,
mode: ValueMode,
) -> Result<Self, SettingsError>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
check_prefix(prefix)?;
let mut entries = BTreeMap::new();
for (key, value) in pairs {
if let Some(suffix) = key.as_ref().strip_prefix(prefix) {
let path = key_path(suffix)?;
if entries
.insert(path, parse_value(value.as_ref(), mode)?)
.is_some()
{
return Err(SettingsError::ConflictingKeys);
}
}
}
self.json(Value::Object(nest(entries)?))
}
pub fn dotenv_file(
self,
path: impl AsRef<Path>,
prefix: &str,
mode: ValueMode,
) -> Result<Self, SettingsError> {
check_prefix(prefix)?;
let text = read_file(path.as_ref())?;
if text.split("\r\n").any(|part| part.contains('\r')) {
return Err(SettingsError::Syntax);
}
let mut pairs = Vec::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (key, value) = line.split_once('=').ok_or(SettingsError::Syntax)?;
let key = key.trim();
if !identifier(key) {
return Err(SettingsError::InvalidKey);
}
let value = value.trim();
let value = if value.starts_with('"') {
serde_json::from_str::<String>(value).map_err(|_| SettingsError::Syntax)?
} else if let Some(inner) = value.strip_prefix('\'') {
let inner = inner.strip_suffix('\'').ok_or(SettingsError::Syntax)?;
if inner.contains('\'') {
return Err(SettingsError::Syntax);
}
inner.to_owned()
} else {
value.to_owned()
};
pairs.push((key.to_owned(), value));
}
self.pairs(pairs, prefix, mode)
}
pub fn secrets_dir(
self,
path: impl AsRef<Path>,
mode: ValueMode,
) -> Result<Self, SettingsError> {
let mut entries = BTreeMap::new();
for entry in fs::read_dir(path).map_err(io_error)? {
let entry = entry.map_err(io_error)?;
let name = entry
.file_name()
.into_string()
.map_err(|_| SettingsError::InvalidUnicode)?;
let key = key_path(&name)?;
let text = read_file(&entry.path())?;
let text = text
.strip_suffix("\r\n")
.or_else(|| text.strip_suffix('\n'))
.unwrap_or(&text);
if entries.insert(key, parse_value(text, mode)?).is_some() {
return Err(SettingsError::ConflictingKeys);
}
}
self.json(Value::Object(nest(entries)?))
}
pub fn load<T: Model>(self) -> Result<T, SettingsError> {
T::parse_value(Value::Object(self.values)).map_err(|errors| SettingsError::Validation {
count: errors.0.len(),
})
}
}
fn io_error(error: io::Error) -> SettingsError {
SettingsError::Io(error.kind())
}
fn read_file(path: &Path) -> Result<String, SettingsError> {
if !fs::symlink_metadata(path)
.map_err(io_error)?
.file_type()
.is_file()
{
return Err(SettingsError::NotRegularFile);
}
fs::read_to_string(path).map_err(|error| {
if error.kind() == io::ErrorKind::InvalidData {
SettingsError::InvalidUnicode
} else {
io_error(error)
}
})
}
fn identifier(value: &str) -> bool {
let mut bytes = value.bytes();
matches!(bytes.next(), Some(b'a'..=b'z' | b'A'..=b'Z' | b'_'))
&& bytes.all(|b| b.is_ascii_alphanumeric() || b == b'_')
}
fn check_prefix(prefix: &str) -> Result<(), SettingsError> {
if prefix.is_empty()
|| !prefix
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_')
{
Err(SettingsError::InvalidKey)
} else {
Ok(())
}
}
fn key_path(key: &str) -> Result<Vec<String>, SettingsError> {
if key.split("__").count() > 64 {
return Err(SettingsError::InvalidKey);
}
key.split("__")
.map(|part| {
if identifier(part) {
Ok(part.to_ascii_lowercase())
} else {
Err(SettingsError::InvalidKey)
}
})
.collect()
}
fn parse_value(text: &str, mode: ValueMode) -> Result<Value, SettingsError> {
match mode {
ValueMode::String => Ok(Value::String(text.to_owned())),
ValueMode::Json => serde_json::from_str(text).map_err(|_| SettingsError::Syntax),
ValueMode::JsonOrString => {
Ok(serde_json::from_str(text).unwrap_or_else(|_| Value::String(text.to_owned())))
}
}
}
fn nest(entries: BTreeMap<Vec<String>, Value>) -> Result<Map<String, Value>, SettingsError> {
let mut previous: Option<Vec<String>> = None;
let mut object = Map::new();
for (path, value) in entries {
if previous
.as_ref()
.is_some_and(|parent| path.starts_with(parent))
{
return Err(SettingsError::ConflictingKeys);
}
insert(&mut object, &path, value);
previous = Some(path);
}
Ok(object)
}
fn insert(object: &mut Map<String, Value>, path: &[String], value: Value) {
if path.len() == 1 {
object.insert(path[0].clone(), value);
} else {
let child = object
.entry(path[0].clone())
.or_insert_with(|| Value::Object(Map::new()));
insert(
child.as_object_mut().expect("validated settings path"),
&path[1..],
value,
);
}
}
fn merge(target: &mut Map<String, Value>, source: Map<String, Value>) {
for (key, value) in source {
match (target.get_mut(&key), value) {
(Some(Value::Object(old)), Value::Object(new)) => merge(old, new),
(_, value) => {
target.insert(key, value);
}
}
}
}