use std::collections::BTreeMap;
use std::fmt;
use std::path::PathBuf;
use schemars::JsonSchema;
use serde::Serialize;
use serde_json::{Map, Value};
use super::ConfigError;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
#[serde(tag = "layer", rename_all = "kebab-case")]
pub enum Origin {
Default,
File {
path: PathBuf,
},
Environment {
variable: String,
},
Flag {
flag: String,
},
}
impl fmt::Display for Origin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Default => f.write_str("default"),
Self::File { path } => write!(f, "file {}", path.display()),
Self::Environment { variable } => write!(f, "environment {variable}"),
Self::Flag { flag } => write!(f, "flag {flag}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, JsonSchema)]
#[serde(into = "String")]
#[schemars(with = "String")]
pub struct SettingPath(Vec<String>);
impl SettingPath {
pub fn new(segments: Vec<String>, source: &str) -> Result<Self, ConfigError> {
if segments.is_empty() || segments.iter().any(String::is_empty) {
return Err(ConfigError::setting(
source,
"that is not a setting path; a path is one or more dot-separated names, \
none of them empty",
"write it as a dotted path, for example `page_size` or \
`sources.work.config.root`.",
));
}
Ok(Self(segments))
}
pub fn parse(dotted: &str) -> Result<Self, ConfigError> {
Self::new(dotted.split('.').map(str::to_owned).collect(), dotted)
}
#[must_use]
pub fn segments(&self) -> &[String] {
&self.0
}
fn overlaps(&self, other: &Self) -> bool {
let shared = self.0.len().min(other.0.len());
self.0[..shared] == other.0[..shared]
}
}
impl fmt::Display for SettingPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0.join("."))
}
}
impl From<SettingPath> for String {
fn from(value: SettingPath) -> Self {
value.to_string()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
pub struct Setting {
pub key: SettingPath,
pub value: Value,
pub origin: Origin,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Layer {
settings: Vec<Setting>,
}
impl Layer {
#[must_use]
pub fn new(settings: Vec<Setting>) -> Self {
Self { settings }
}
#[must_use]
pub fn settings(&self) -> &[Setting] {
&self.settings
}
pub fn from_document(path: PathBuf, document: &Value) -> Result<Self, ConfigError> {
let origin = Origin::File { path };
let fields = match document {
Value::Null => return Ok(Self::default()),
Value::Object(fields) => fields,
other => {
return Err(ConfigError::setting(
"the document's root",
format!(
"a configuration document must be a mapping of settings, but {origin} \
holds {}",
kind_of(other)
),
"write the document as `key: value` pairs — see `onetaskgraph config show \
--help` for the settings it may hold.",
));
}
};
let mut settings = Vec::new();
flatten(&mut Vec::new(), fields, &origin, &mut settings);
Ok(Self { settings })
}
}
fn kind_of(value: &Value) -> &'static str {
match value {
Value::Bool(_) => "a boolean",
Value::Number(_) => "a number",
Value::String(_) => "a string",
_ => "a list",
}
}
fn flatten(
prefix: &mut Vec<String>,
fields: &Map<String, Value>,
origin: &Origin,
out: &mut Vec<Setting>,
) {
for (name, value) in fields {
prefix.push(name.clone());
match value {
Value::Object(nested) if !nested.is_empty() => flatten(prefix, nested, origin, out),
leaf => out.push(Setting {
key: SettingPath(prefix.clone()),
value: leaf.clone(),
origin: origin.clone(),
}),
}
prefix.pop();
}
}
#[must_use]
pub fn merge(layers: &[Layer]) -> Merged {
let mut merged: BTreeMap<SettingPath, Setting> = BTreeMap::new();
for layer in layers {
for setting in &layer.settings {
merged.retain(|key, _| !key.overlaps(&setting.key));
merged.insert(setting.key.clone(), setting.clone());
}
}
Merged(merged)
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Merged(BTreeMap<SettingPath, Setting>);
impl std::ops::Deref for Merged {
type Target = BTreeMap<SettingPath, Setting>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[must_use]
pub fn unflatten(settings: &Merged) -> Value {
let mut root = Map::new();
for setting in settings.values() {
let segments = setting.key.segments();
let mut cursor = &mut root;
for segment in &segments[..segments.len() - 1] {
cursor = cursor
.entry(segment.clone())
.or_insert_with(|| Value::Object(Map::new()))
.as_object_mut()
.expect("`Merged` holds no setting that is an ancestor of another");
}
cursor.insert(segments[segments.len() - 1].clone(), setting.value.clone());
}
Value::Object(root)
}
#[must_use]
pub fn value_from_text(raw: &str) -> Value {
if raw.contains(',') {
Value::Array(raw.split(',').map(|part| scalar(part.trim())).collect())
} else {
scalar(raw)
}
}
fn scalar(raw: &str) -> Value {
if let Ok(integer) = raw.parse::<i64>() {
return Value::from(integer);
}
if let Ok(unsigned) = raw.parse::<u64>() {
return Value::from(unsigned);
}
if let Ok(number) = raw.parse::<f64>()
&& let Some(value) = serde_json::Number::from_f64(number)
{
return Value::Number(value);
}
match raw {
"true" => Value::Bool(true),
"false" => Value::Bool(false),
other => Value::String(other.to_owned()),
}
}