Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326#[cfg(feature = "cbor")]mod cbor;#[cfg(feature = "dotenv")]mod dotenv;#[cfg(feature = "env")]mod env;#[cfg(feature = "ini")]mod ini;#[cfg(feature = "json")]mod json;#[cfg(feature = "keyring")]mod keyring;#[cfg(feature = "msgpack")]mod msgpack;mod serialized;#[cfg(feature = "toml")]mod toml;#[cfg(feature = "xml")]mod xml;#[cfg(feature = "yaml")]mod yaml;
#[cfg(any(feature = "dotenv", feature = "env", feature = "keyring"))]use std::collections::BTreeMap;
#[cfg(feature = "cbor")]pub use cbor::Cbor;#[cfg(feature = "dotenv")]pub use dotenv::Dotenv;#[cfg(feature = "env")]pub use env::Env;#[cfg(feature = "ini")]pub use ini::Ini;#[cfg(feature = "json")]pub use json::Json;#[cfg(feature = "keyring")]pub use keyring::Keyring;#[cfg(feature = "msgpack")]pub use msgpack::MsgPack;pub use serialized::Serialized;#[cfg(feature = "toml")]pub use toml::Toml;#[cfg(feature = "xml")]pub use xml::Xml;#[cfg(feature = "yaml")]pub use yaml::Yaml;
use crate::{Result, Value};
/// A source of configuration.////// Implement this to teach [`Compote`](crate::Compote) to read from somewhere it does not already/// know about.////// ```/// use compote::{Provider, Result, Value};////// struct Fixed;////// impl Provider for Fixed {/// fn data(&self) -> Result<Value> {/// Ok(Value::String("hello".to_owned()))/// }/// }/// ```pub trait Provider { /// Reads this source and returns what it holds. /// /// Called once for every [`merge`](crate::Compote::merge) or [`join`](crate::Compote::join), and /// may be called more than once for the same source. fn data(&self) -> Result<Value>;}
/// Names the file a parser failed on, and reads a document holding nothing as an empty table.////// A source that says only `null` has nothing to contribute to a merge, which is the same thing an/// absent key says, so it lays over the layer beneath without taking anything from it.#[cfg(any( feature = "cbor", feature = "dotenv", feature = "ini", feature = "json", feature = "msgpack", feature = "toml", feature = "xml", feature = "yaml"))]fn finish<E>(path: &std::path::Path, parsed: std::result::Result<Value, E>) -> Result<Value>where E: std::error::Error + Send + Sync + 'static,{ let value = parsed.map_err(|source| crate::Error::Parse { path: path.to_path_buf(), source: Box::new(source), })?;
Ok(match value { Value::Null => Value::table(), other => other, })}
/// Reads a text file and hands its contents to `parse`.#[cfg(any( feature = "dotenv", feature = "ini", feature = "json", feature = "toml", feature = "xml", feature = "yaml"))]fn load<E>( path: &std::path::Path, required: bool, parse: impl FnOnce(&str) -> std::result::Result<Value, E>,) -> Result<Value>where E: std::error::Error + Send + Sync + 'static,{ let Some(source) = read(path, required, std::fs::read_to_string(path))? else { return Ok(Value::table()); };
if source.trim().is_empty() { return Ok(Value::table()); }
finish(path, parse(&source))}
/// Reads a binary file and hands its bytes to `parse`.#[cfg(any(feature = "cbor", feature = "msgpack"))]fn load_bytes<E>( path: &std::path::Path, required: bool, parse: impl FnOnce(&[u8]) -> std::result::Result<Value, E>,) -> Result<Value>where E: std::error::Error + Send + Sync + 'static,{ let Some(source) = read(path, required, std::fs::read(path))? else { return Ok(Value::table()); };
if source.is_empty() { return Ok(Value::table()); }
finish(path, parse(&source))}
/// Names the file a read failed on, and reads a file that is not there as nothing unless `required`.////// A file that is not there has nothing to contribute to a merge, the same as an empty one, so a/// layer for the machine that has not been set up yet costs nothing. Not being there is the only/// failure forgiven. A file that is there and cannot be read, a directory or one without permission,/// is still an error, since it says something is wrong rather than that nothing was written yet.#[cfg(any( feature = "cbor", feature = "dotenv", feature = "ini", feature = "json", feature = "msgpack", feature = "toml", feature = "xml", feature = "yaml"))]fn read<T>(path: &std::path::Path, required: bool, contents: std::io::Result<T>) -> Result<Option<T>> { match contents { Ok(contents) => Ok(Some(contents)), Err(source) if source.kind() == std::io::ErrorKind::NotFound && !required => Ok(None), Err(source) => Err(crate::Error::Read { path: path.to_path_buf(), source, }), }}
/// Turns text pairs into a table, which is the shape the environment and a `.env` file share.////// Only names carrying `prefix` are kept, and the prefix comes off what is left. Names are/// lowercased, since both sources spell theirs in upper case by convention and neither means the/// case to carry meaning, so one `.env` file lands the same whether it is read here or sourced into/// the environment first. A name in `ignore` is dropped. Nothing nests unless `separator` says/// where, and a nested key beats a scalar already sitting at the same path.#[cfg(any(feature = "dotenv", feature = "env"))]fn overlay( pairs: impl IntoIterator<Item = (String, String)>, prefix: &str, ignore: &[String], separator: &str,) -> Value { // Sorted so a name and the path beneath it arrive in a fixed order, which is what lets the // deeper one win. The sort is stable, so a name said twice still ends on its last value. let mut sorted: Vec<(String, String)> = pairs.into_iter().collect(); sorted.sort();
let mut table = BTreeMap::new();
for (key, value) in sorted { let Some(name) = key.strip_prefix(prefix).map(str::to_ascii_lowercase) else { continue; };
if name.is_empty() || ignore.contains(&name) { continue; }
insert(&mut table, &name, separator, value); }
Value::Table(table)}
/// Puts `value` at the path `key` names, making each table along the way that is missing.#[cfg(any(feature = "dotenv", feature = "env", feature = "keyring"))]fn insert(table: &mut BTreeMap<String, Value>, key: &str, separator: &str, value: String) { let segments: Vec<&str> = if separator.is_empty() { vec![key] } else { key.split(separator).filter(|segment| !segment.is_empty()).collect() };
let Some((leaf, parents)) = segments.split_last() else { return; };
let mut current = table;
for segment in parents { let entry = current.entry((*segment).to_owned()).or_insert_with(Value::table);
if !matches!(entry, Value::Table(_)) { *entry = Value::table(); }
let Value::Table(nested) = entry else { unreachable!("the entry was just replaced with a table") };
current = nested; }
current.insert((*leaf).to_owned(), Value::String(value));}
#[cfg(all(test, any(feature = "dotenv", feature = "env")))]mod tests { use super::*;
fn string(value: &str) -> Value { Value::String(value.to_owned()) }
fn table(entries: Vec<(&str, Value)>) -> Value { Value::Table( entries .into_iter() .map(|(key, value)| (key.to_owned(), value)) .collect(), ) }
fn pairs(entries: &[(&str, &str)]) -> Vec<(String, String)> { entries .iter() .map(|(key, value)| ((*key).to_owned(), (*value).to_owned())) .collect() }
mod overlay { use pretty_assertions::assert_eq;
use super::*;
#[test] fn it_keeps_the_last_value_a_name_is_given() { assert_eq!( overlay(pairs(&[("HOST", "first"), ("HOST", "last")]), "", &[], ""), table(vec![("host", string("last"))]), "one name holds one value, which is all an environment variable can be" ); }
#[test] fn it_lets_a_nested_key_win_over_a_scalar_at_the_same_path() { assert_eq!( overlay( pairs(&[("SERVER", "off"), ("SERVER__HOST", "localhost")]), "", &[], "__" ), table(vec![("server", table(vec![("host", string("localhost"))]))]) ); } }
mod insert { use pretty_assertions::assert_eq;
use super::*;
#[test] fn it_drops_empty_segments() { let mut entries = BTreeMap::new();
insert(&mut entries, "server____host", "__", "localhost".to_owned());
assert_eq!( Value::Table(entries), table(vec![("server", table(vec![("host", string("localhost"))]))]) ); }
#[test] fn it_ignores_a_key_that_is_nothing_but_separators() { let mut entries = BTreeMap::new();
insert(&mut entries, "__", "__", "localhost".to_owned());
assert_eq!(Value::Table(entries), Value::table()); } }}