use std::string::String;
use std::sync::Arc;
use std::vec::Vec;
use camino::Utf8PathBuf;
use facet::Facet;
#[derive(Debug, Clone, Facet)]
pub struct ConfigFile {
pub path: Utf8PathBuf,
pub contents: String,
}
impl ConfigFile {
pub fn new(path: impl Into<Utf8PathBuf>, contents: impl Into<String>) -> Self {
Self {
path: path.into(),
contents: contents.into(),
}
}
}
#[repr(u8)]
#[derive(Debug, Clone, Default, facet::Facet)]
pub enum Provenance {
Cli {
arg: String,
value: String,
},
Env {
var: String,
value: String,
},
File {
file: Arc<ConfigFile>,
key_path: String,
offset: usize,
len: usize,
},
#[default]
Default,
}
impl Provenance {
pub fn cli(arg: impl Into<String>, value: impl Into<String>) -> Self {
Self::Cli {
arg: arg.into(),
value: value.into(),
}
}
pub fn env(var: impl Into<String>, value: impl Into<String>) -> Self {
Self::Env {
var: var.into(),
value: value.into(),
}
}
pub fn file(
file: Arc<ConfigFile>,
key_path: impl Into<String>,
offset: usize,
len: usize,
) -> Self {
Self::File {
file,
key_path: key_path.into(),
offset,
len,
}
}
pub fn is_cli(&self) -> bool {
matches!(self, Self::Cli { .. })
}
pub fn is_env(&self) -> bool {
matches!(self, Self::Env { .. })
}
pub fn is_file(&self) -> bool {
matches!(self, Self::File { .. })
}
pub fn is_default(&self) -> bool {
matches!(self, Self::Default)
}
pub fn priority(&self) -> u8 {
match self {
Self::Cli { .. } => 3,
Self::Env { .. } => 2,
Self::File { .. } => 1,
Self::Default => 0,
}
}
pub fn source_description(&self) -> String {
match self {
Self::Cli { arg, .. } => format!("CLI: {arg}"),
Self::Env { var, .. } => format!("env: {var}"),
Self::File { file, key_path, .. } => format!("{}: {key_path}", file.path),
Self::Default => "default".into(),
}
}
}
impl core::fmt::Display for Provenance {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Cli { arg, .. } => write!(f, "from CLI argument {arg}"),
Self::Env { var, .. } => write!(f, "from environment variable {var}"),
Self::File { file, key_path, .. } => {
write!(f, "from {}: {key_path}", file.path)
}
Self::Default => write!(f, "from default"),
}
}
}
#[derive(Debug, Clone)]
pub struct Override {
pub path: String,
pub winner: Provenance,
pub loser: Provenance,
}
impl Override {
pub fn new(path: impl Into<String>, winner: Provenance, loser: Provenance) -> Self {
Self {
path: path.into(),
winner,
loser,
}
}
}
impl core::fmt::Display for Override {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}: {} overrides {}",
self.path,
self.winner.source_description(),
self.loser.source_description()
)
}
}
#[derive(Facet, Debug, Clone)]
#[repr(u8)]
pub enum FilePathStatus {
Picked,
NotTried,
Absent,
}
#[derive(Facet, Debug, Clone)]
pub struct FilePathResolution {
pub path: Utf8PathBuf,
pub status: FilePathStatus,
pub explicit: bool,
}
#[derive(Facet, Debug, Clone, Default)]
pub struct FileResolution {
pub paths: Vec<FilePathResolution>,
pub had_explicit: bool,
}
impl FileResolution {
pub fn new() -> Self {
Self::default()
}
pub fn add_explicit(&mut self, path: Utf8PathBuf, exists: bool) {
self.had_explicit = true;
self.paths.push(FilePathResolution {
path,
status: if exists {
FilePathStatus::Picked
} else {
FilePathStatus::Absent
},
explicit: true,
});
}
pub fn add_default(&mut self, path: Utf8PathBuf, status: FilePathStatus) {
self.paths.push(FilePathResolution {
path,
status,
explicit: false,
});
}
pub fn mark_defaults_not_tried(&mut self, default_paths: &[Utf8PathBuf]) {
for path in default_paths {
self.paths.push(FilePathResolution {
path: path.clone(),
status: FilePathStatus::NotTried,
explicit: false,
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_provenance_priority() {
assert!(
Provenance::cli("--port", "8080").priority()
> Provenance::env("PORT", "9000").priority()
);
assert!(Provenance::env("PORT", "9000").priority() > Provenance::Default.priority());
let file = Arc::new(ConfigFile::new("config.json", "{}"));
let file_prov = Provenance::file(file, "port", 0, 4);
assert!(Provenance::env("PORT", "9000").priority() > file_prov.priority());
assert!(file_prov.priority() > Provenance::Default.priority());
}
#[test]
fn test_provenance_display() {
let cli = Provenance::cli("--config.port", "8080");
assert!(cli.to_string().contains("--config.port"));
let env = Provenance::env("REEF__PORT", "9000");
assert!(env.to_string().contains("REEF__PORT"));
let file = Arc::new(ConfigFile::new("config.json", "{}"));
let file_prov = Provenance::file(file, "port", 0, 4);
assert!(file_prov.to_string().contains("config.json"));
assert!(file_prov.to_string().contains("port"));
let default = Provenance::Default;
assert!(default.to_string().contains("default"));
}
#[test]
fn test_provenance_is_checks() {
assert!(Provenance::cli("--port", "8080").is_cli());
assert!(!Provenance::cli("--port", "8080").is_env());
assert!(Provenance::env("PORT", "9000").is_env());
assert!(!Provenance::env("PORT", "9000").is_cli());
let file = Arc::new(ConfigFile::new("config.json", "{}"));
assert!(Provenance::file(file, "port", 0, 4).is_file());
assert!(Provenance::Default.is_default());
}
#[test]
fn test_config_file() {
let file = ConfigFile::new("config.json", r#"{"port": 8080}"#);
assert_eq!(file.path, "config.json");
assert!(file.contents.contains("port"));
}
#[test]
fn test_override_display() {
let ovr = Override::new(
"config.port",
Provenance::cli("--config.port", "8080"),
Provenance::env("REEF__PORT", "9000"),
);
let display = ovr.to_string();
assert!(display.contains("config.port"));
assert!(display.contains("CLI"));
assert!(display.contains("env"));
}
}