#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{cmp::Ordering, fmt};
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum ConfigSourceKind {
Default,
File,
Environment,
Runtime,
Override,
SecretReference,
Custom(String),
}
impl fmt::Display for ConfigSourceKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Default => formatter.write_str("default"),
Self::File => formatter.write_str("file"),
Self::Environment => formatter.write_str("environment"),
Self::Runtime => formatter.write_str("runtime"),
Self::Override => formatter.write_str("override"),
Self::SecretReference => formatter.write_str("secret-reference"),
Self::Custom(value) => formatter.write_str(value),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct ConfigSource {
pub kind: ConfigSourceKind,
pub name: Option<String>,
pub priority: i32,
}
impl ConfigSource {
#[must_use]
pub fn new(kind: ConfigSourceKind, name: Option<String>, priority: i32) -> Self {
Self {
kind,
name: normalize_name(name),
priority,
}
}
#[must_use]
pub fn unnamed(kind: ConfigSourceKind, priority: i32) -> Self {
Self::new(kind, None, priority)
}
#[must_use]
pub fn named(kind: ConfigSourceKind, name: impl Into<String>, priority: i32) -> Self {
Self::new(kind, Some(name.into()), priority)
}
#[must_use]
pub const fn kind(&self) -> &ConfigSourceKind {
&self.kind
}
#[must_use]
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
#[must_use]
pub const fn priority(&self) -> i32 {
self.priority
}
}
impl fmt::Display for ConfigSource {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(name) = &self.name {
write!(formatter, "{}:{}@{}", self.kind, name, self.priority)
} else {
write!(formatter, "{}@{}", self.kind, self.priority)
}
}
}
impl Ord for ConfigSource {
fn cmp(&self, other: &Self) -> Ordering {
self.priority
.cmp(&other.priority)
.then_with(|| self.kind.cmp(&other.kind))
.then_with(|| self.name.cmp(&other.name))
}
}
impl PartialOrd for ConfigSource {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
fn normalize_name(name: Option<String>) -> Option<String> {
name.and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_owned())
}
})
}
#[cfg(test)]
mod tests {
use super::{ConfigSource, ConfigSourceKind};
#[test]
fn source_creation() {
let source = ConfigSource::named(ConfigSourceKind::File, " app.toml ", 10);
assert_eq!(source.kind(), &ConfigSourceKind::File);
assert_eq!(source.name(), Some("app.toml"));
assert_eq!(source.priority(), 10);
}
#[test]
fn source_display() {
let named = ConfigSource::named(ConfigSourceKind::Override, "cli", 20);
let unnamed = ConfigSource::unnamed(ConfigSourceKind::Default, 0);
assert_eq!(named.to_string(), "override:cli@20");
assert_eq!(unnamed.to_string(), "default@0");
}
#[test]
fn priority_ordering() {
let low = ConfigSource::unnamed(ConfigSourceKind::Default, 0);
let high = ConfigSource::unnamed(ConfigSourceKind::Override, 10);
let mut sources = vec![high.clone(), low.clone()];
sources.sort();
assert_eq!(sources, vec![low, high]);
}
#[test]
fn custom_source_kind() {
let source = ConfigSource::named(ConfigSourceKind::Custom("fixture".to_owned()), "base", 5);
assert_eq!(source.kind().to_string(), "fixture");
assert_eq!(source.to_string(), "fixture:base@5");
}
}