use std::fmt;
use std::str::FromStr;
use crate::{ConfigFormat, ConfigObject, PathError, RefPath, Seg};
use serde_json::{Map, Value};
#[derive(Debug, Clone, PartialEq)]
pub struct PathLeaf<V> {
path: RefPath,
leaf: V,
}
impl<V> PathLeaf<V> {
pub fn new(path: Vec<String>, leaf: V) -> Result<Self, PathError> {
Ok(Self {
path: RefPath::from_keys(path)?,
leaf,
})
}
pub fn path(&self) -> &[Seg] {
self.path.segs()
}
pub fn leaf(&self) -> &V {
&self.leaf
}
pub fn map_leaf<T>(self, f: impl FnOnce(V) -> T) -> PathLeaf<T> {
PathLeaf {
path: self.path,
leaf: f(self.leaf),
}
}
pub fn try_map_leaf<T, E>(self, f: impl FnOnce(V) -> Result<T, E>) -> Result<PathLeaf<T>, E> {
Ok(PathLeaf {
path: self.path,
leaf: f(self.leaf)?,
})
}
fn try_into_nested(self, nest: impl Fn(String, V) -> V) -> Result<V, PathError> {
let keys = self.path.try_into_keys()?;
Ok(keys
.into_iter()
.rev()
.fold(self.leaf, |acc, key| nest(key, acc)))
}
}
impl PathLeaf<String> {
pub fn validate_keys(&self) -> Result<(), PathError> {
self.path.clone().try_into_keys().map(|_| ())
}
pub fn into_layer<V: ConfigFormat>(self) -> Result<V, PathError> {
let keys = self.path.try_into_keys()?;
let leaf = V::parse_inline(self.leaf);
Ok(keys.into_iter().rev().fold(leaf, |value, key| {
let mut object = V::Object::new();
object.insert(key, value);
V::object(object)
}))
}
}
pub fn toml_or_string(text: String) -> toml::Value {
text.trim_matches([' ', '\t', '\r', '\n'])
.parse()
.unwrap_or_else(|_| toml::Value::String(text))
}
impl FromStr for PathLeaf<String> {
type Err = PathError;
fn from_str(expr: &str) -> Result<Self, Self::Err> {
let Some((lhs, rhs)) = expr.split_once('=') else {
return Err(PathError::MissingEquals);
};
Ok(Self {
path: lhs.parse()?,
leaf: rhs.to_string(),
})
}
}
impl fmt::Display for PathLeaf<String> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}={}", self.path, self.leaf)
}
}
impl FromStr for PathLeaf<Value> {
type Err = PathError;
fn from_str(expr: &str) -> Result<Self, Self::Err> {
Ok(PathLeaf::<String>::from_str(expr)?.into())
}
}
pub fn json_or_string(text: String) -> Value {
serde_json::from_str(&text).unwrap_or_else(|_| Value::String(text))
}
impl From<PathLeaf<String>> for PathLeaf<Value> {
fn from(path_leaf: PathLeaf<String>) -> Self {
path_leaf.map_leaf(json_or_string)
}
}
impl fmt::Display for PathLeaf<Value> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let rhs = serde_json::to_string(&self.leaf).expect("a Value always serializes");
write!(f, "{}={rhs}", self.path)
}
}
impl TryFrom<PathLeaf<Value>> for Value {
type Error = PathError;
fn try_from(path_leaf: PathLeaf<Value>) -> Result<Self, Self::Error> {
path_leaf.try_into_nested(|key, acc| {
let mut obj = Map::new();
obj.insert(key, acc);
Value::Object(obj)
})
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn toml_inline_literals_ignore_surrounding_whitespace() {
for literal in [
"8080",
"true",
"1.0",
"1979-05-27",
"inf",
"[1, 2]",
"{a=1}",
"' name '",
] {
for padding in [" ", "\t", "\n", "\r\n", " \t\r\n"] {
let expected = toml_or_string(literal.into());
for text in [
format!("{padding}{literal}"),
format!("{literal}{padding}"),
format!("{padding}{literal}{padding}"),
] {
assert_eq!(toml_or_string(text.clone()), expected, "{text:?}");
}
}
}
for text in [
" \tnull\n",
" 9223372036854775808\n",
" [a,b] ",
" text\n",
" \t\r\n",
"\u{a0}8080\u{a0}",
] {
assert_eq!(
toml_or_string(text.into()),
toml::Value::String(text.into())
);
}
}
#[test]
fn rejects_malformed_expressions() {
for (bad, want) in [
("noequals", PathError::MissingEquals),
("=1", PathError::EmptyPath),
(
"a..b=1",
PathError::EmptySegment {
path: "a..b".into(),
},
),
(".a=1", PathError::EmptySegment { path: ".a".into() }),
("a.=1", PathError::EmptySegment { path: "a.".into() }),
("a[]=1", PathError::BadIndex { path: "a[]".into() }),
] {
assert_eq!(bad.parse::<PathLeaf<String>>().unwrap_err(), want, "{bad}");
}
}
#[test]
fn new_rejects_empty_path_and_empty_segments() {
assert_eq!(
PathLeaf::<String>::new(vec![], "1".into()).unwrap_err(),
PathError::EmptyPath
);
assert_eq!(
PathLeaf::<String>::new(vec!["".into()], "1".into()).unwrap_err(),
PathError::EmptySegment { path: "".into() }
);
assert_eq!(
PathLeaf::<String>::new(vec!["a".into(), "".into()], "1".into()).unwrap_err(),
PathError::EmptySegment { path: "a.".into() }
);
}
#[test]
fn raw_fromstr_keeps_the_rhs_unparsed() {
let path_leaf: PathLeaf<String> = "port=8080".parse().unwrap();
assert_eq!(path_leaf.path(), [Seg::Key("port".into())]);
assert_eq!(path_leaf.leaf(), "8080");
assert_eq!(path_leaf.to_string(), "port=8080");
}
#[test]
fn path_leaf_accepts_brackets_its_writers_reject() {
let parsed: PathLeaf<String> = "a[0]=1".parse().unwrap();
assert_eq!(parsed.path(), [Seg::Key("a".into()), Seg::Index(0)]);
assert_eq!(parsed.to_string(), "a[0]=1");
}
#[test]
fn map_leaf_preserves_the_path() {
let path_leaf = PathLeaf::new(vec!["a".into()], "xy".to_string())
.unwrap()
.map_leaf(|s| s.len());
assert_eq!(path_leaf.path(), [Seg::Key("a".into())]);
assert_eq!(*path_leaf.leaf(), 2);
}
fn parse(expr: &str) -> PathLeaf<Value> {
expr.parse().expect("valid PathLeaf")
}
fn nested(expr: &str) -> Value {
Value::try_from(parse(expr)).expect("all-key path")
}
#[test]
fn value_typing() {
assert_eq!(nested("port=8080"), json!({"port": 8080}));
assert_eq!(nested("debug=true"), json!({"debug": true}));
assert_eq!(nested("name=foo"), json!({"name": "foo"}));
assert_eq!(nested("proxy=null"), json!({"proxy": null}));
assert_eq!(nested(r#"tags=["a","b"]"#), json!({"tags": ["a", "b"]}));
assert_eq!(nested("tags=[a,b]"), json!({"tags": "[a,b]"}));
}
#[test]
fn numeric_looking_strings() {
assert_eq!(nested("version=1.0"), json!({"version": 1.0}));
assert_eq!(nested(r#"version="1.0""#), json!({"version": "1.0"}));
}
#[test]
fn dotted_paths_nest() {
assert_eq!(
nested("server.port=8080"),
json!({"server": {"port": 8080}})
);
assert_eq!(nested("a.b.c=1"), json!({"a": {"b": {"c": 1}}}));
}
#[test]
fn splits_on_the_first_equals_only() {
assert_eq!(nested("q=a=b"), json!({"q": "a=b"}));
assert_eq!(nested("q="), json!({"q": ""}));
}
#[test]
fn display_is_canonical() {
assert_eq!(parse("name=foo").to_string(), r#"name="foo""#);
assert_eq!(parse("port=8080").to_string(), "port=8080");
assert_eq!(parse("q=").to_string(), r#"q="""#);
assert_eq!(parse("q=a=b").to_string(), r#"q="a=b""#);
assert_eq!(parse("server.port=8080").to_string(), "server.port=8080");
}
#[test]
fn fromstr_display_preserves_path_and_leaf() {
for expr in [
"port=8080",
"name=foo",
r#"name="foo""#,
"debug=true",
"proxy=null",
r#"tags=["a","b"]"#,
"q=",
"q=a=b",
"server.port=8080",
] {
let parsed = parse(expr);
let round = parsed.to_string().parse::<PathLeaf<Value>>().unwrap();
assert_eq!(round.path(), parsed.path(), "{expr}");
assert_eq!(round.leaf(), parsed.leaf(), "{expr}");
}
}
#[test]
fn from_raw_path_leaf_parses_the_rhs() {
let raw: PathLeaf<String> = "server.port=8080".parse().unwrap();
let typed = PathLeaf::<Value>::from(raw);
assert_eq!(
Value::try_from(typed).unwrap(),
json!({"server": {"port": 8080}})
);
}
#[test]
fn bracketed_paths_parse_but_cannot_write() {
let err = Value::try_from(parse("servers[0].host=x")).unwrap_err();
assert_eq!(
err,
PathError::IndexInKeyPath {
path: "servers[0].host".into()
}
);
assert_eq!(
err.to_string(),
"`servers[0].host` contains an array index; merge paths take keys only"
);
}
}