use std::collections::BTreeMap;
use std::fmt;
pub(crate) fn one_line(text: &str) -> String {
text.replace('\n', "\\n").replace('\r', "\\r")
}
pub(crate) fn shown(value: &Value) -> String {
match value {
Value::List(items) if items.is_empty() => "[]".to_string(),
Value::Map(entries) if entries.is_empty() => "{}".to_string(),
Value::String(text) if text.is_empty() => "\"\"".to_string(),
Value::List(items) if items.iter().any(|item| item.display().is_empty()) => {
let items: Vec<String> = items.iter().map(shown).collect();
format!("[{}]", items.join(","))
}
other => one_line(&other.display()),
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Bool(bool),
Int(i64),
Float(f64),
String(String),
List(Vec<Value>),
Map(BTreeMap<String, Value>),
}
impl Value {
pub fn type_name(&self) -> &'static str {
match self {
Self::Bool(_) => "a boolean",
Self::Int(_) => "an integer",
Self::Float(_) => "a number",
Self::String(_) => "a string",
Self::List(_) => "a list",
Self::Map(_) => "a table",
}
}
pub fn display(&self) -> String {
match self {
Self::Bool(b) => b.to_string(),
Self::Int(i) => i.to_string(),
Self::Float(f) => {
let text = f.to_string();
match f.is_finite() && !text.contains(['.', 'e', 'E']) {
true => format!("{text}.0"),
false => text,
}
}
Self::String(s) => s.clone(),
Self::List(items) => items
.iter()
.map(Self::display)
.collect::<Vec<_>>()
.join(","),
Self::Map(entries) => entries
.iter()
.map(|(key, value)| format!("{key}={}", value.display()))
.collect::<Vec<_>>()
.join(","),
}
}
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.display())
}
}
impl From<bool> for Value {
fn from(value: bool) -> Self {
Self::Bool(value)
}
}
impl From<i64> for Value {
fn from(value: i64) -> Self {
Self::Int(value)
}
}
impl From<f64> for Value {
fn from(value: f64) -> Self {
Self::Float(value)
}
}
impl From<&str> for Value {
fn from(value: &str) -> Self {
Self::String(value.to_string())
}
}
impl From<String> for Value {
fn from(value: String) -> Self {
Self::String(value)
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Const {
Bool(bool),
Int(i64),
Float(f64),
Str(&'static str),
List(&'static [Const]),
Map(&'static [(&'static str, Const)]),
}
impl Const {
pub fn matches(self, value: &Value) -> bool {
match (self, value) {
(Self::Bool(a), Value::Bool(b)) => a == *b,
(Self::Int(a), Value::Int(b)) => a == *b,
(Self::Float(a), Value::Float(b)) => a == *b,
(Self::Str(a), Value::String(b)) => a == b,
(Self::List(_) | Self::Map(_), _) | (_, Value::List(_) | Value::Map(_)) => false,
(choice, value) => choice.to_value().display() == value.display(),
}
}
pub fn to_value(self) -> Value {
match self {
Self::Bool(b) => Value::Bool(b),
Self::Int(i) => Value::Int(i),
Self::Float(f) => Value::Float(f),
Self::Str(s) => Value::String(s.to_string()),
Self::List(items) => Value::List(items.iter().map(|item| item.to_value()).collect()),
Self::Map(entries) => Value::Map(
entries
.iter()
.map(|(key, value)| ((*key).to_string(), value.to_value()))
.collect(),
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_declared_default_becomes_the_value_it_names() {
const NESTED: &[Const] = &[Const::Int(80), Const::Int(443)];
const PAIRS: &[(&str, Const)] = &[("a", Const::Bool(true))];
assert_eq!(Const::Bool(true).to_value(), Value::Bool(true));
assert_eq!(Const::Str("x").to_value(), Value::String("x".into()));
assert_eq!(
Const::List(NESTED).to_value(),
Value::List(vec![Value::Int(80), Value::Int(443)])
);
assert_eq!(
Const::Map(PAIRS).to_value(),
Value::Map([("a".to_string(), Value::Bool(true))].into_iter().collect())
);
}
#[test]
fn an_empty_value_is_told_apart_from_a_value_that_writes_as_nothing() {
assert_eq!(shown(&Value::List(Vec::new())), "[]");
assert_eq!(shown(&Value::List(vec![Value::from("")])), "[\"\"]");
assert_eq!(
shown(&Value::List(vec![Value::from(""), Value::from("")])),
"[\"\",\"\"]"
);
assert_eq!(shown(&Value::from("")), "\"\"");
assert_eq!(shown(&Value::Map(BTreeMap::new())), "{}");
assert_eq!(shown(&Value::from("git")), "git");
assert_eq!(shown(&Value::Int(0)), "0");
assert_eq!(shown(&Value::Bool(false)), "false");
assert_eq!(
shown(&Value::List(vec![Value::from("a"), Value::from("b")])),
"a,b"
);
assert_eq!(
shown(&Value::List(vec![Value::from("a"), Value::from("")])),
"[a,\"\"]"
);
assert_eq!(
shown(&Value::Map(
[("k".to_string(), Value::from(""))].into_iter().collect()
)),
"k="
);
}
#[test]
fn a_choice_and_a_value_written_the_same_way_are_the_same_choice() {
assert!(Const::Int(4).matches(&Value::from("4")));
assert!(Const::Str("4").matches(&Value::Int(4)));
assert!(Const::Bool(true).matches(&Value::from("true")));
assert!(!Const::Int(1).matches(&Value::Float(1.0)));
assert_eq!(Value::Float(1.0).display(), "1.0");
assert_eq!(Value::Float(0.5).display(), "0.5");
assert_eq!(Value::Int(1).display(), "1");
assert!(Const::Str("git").matches(&Value::from("git")));
assert!(!Const::Str("git").matches(&Value::from("svn")));
assert!(!Const::Int(4).matches(&Value::Int(5)));
const ITEMS: &[Const] = &[Const::Str("a"), Const::Str("b")];
let list = Value::List(vec![Value::from("a"), Value::from("b")]);
assert!(!Const::Str("a,b").matches(&list));
assert!(!Const::List(ITEMS).matches(&Value::from("a,b")));
}
#[test]
fn a_value_can_be_written_the_way_it_was_typed() {
assert_eq!(Value::Bool(false).display(), "false");
assert_eq!(
Value::List(vec![Value::String("a".into()), Value::Int(2)]).display(),
"a,2"
);
assert_eq!(
Value::Map([("k".to_string(), Value::Int(1))].into_iter().collect()).display(),
"k=1"
);
}
}