use crate::error::{Error, Result};
use indexmap::IndexMap;
use std::str::FromStr;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub(crate) type CclMap = IndexMap<String, Vec<CclObject>>;
pub(crate) type CclMapIter<'a> = indexmap::map::Iter<'a, String, Vec<CclObject>>;
#[derive(Debug, Clone, Copy, Default)]
pub struct BoolOptions {
pub lenient: bool,
}
impl BoolOptions {
pub fn new() -> Self {
Self::default()
}
pub fn lenient() -> Self {
Self { lenient: true }
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ListOptions {
pub coerce: bool,
}
impl ListOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_coerce() -> Self {
Self { coerce: true }
}
}
fn is_scalar_literal(s: &str) -> bool {
if s.parse::<i64>().is_ok() {
return true;
}
if s.parse::<f64>().is_ok() {
return true;
}
matches!(s, "true" | "false" | "yes" | "no")
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Entry {
pub key: String,
pub value: String,
}
impl Entry {
pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
Self {
key: key.into(),
value: value.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CclObject(CclMap);
impl CclObject {
pub fn new() -> Self {
CclObject(IndexMap::new())
}
pub(crate) fn from_map(map: CclMap) -> Self {
CclObject(map)
}
pub fn get(&self, key: &str) -> Result<&CclObject> {
self.0
.get(key)
.and_then(|vec| vec.first())
.ok_or_else(|| Error::MissingKey(key.to_string()))
}
pub fn get_all(&self, key: &str) -> Result<&[CclObject]> {
self.0
.get(key)
.map(|vec| vec.as_slice())
.ok_or_else(|| Error::MissingKey(key.to_string()))
}
pub fn keys(&self) -> impl Iterator<Item = &String> {
self.0.keys()
}
pub fn values(&self) -> impl Iterator<Item = &CclObject> {
self.0.values().filter_map(|vec| vec.first())
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &CclObject)> {
self.0
.iter()
.filter_map(|(k, vec)| vec.first().map(|v| (k, v)))
}
pub fn iter_all(&self) -> impl Iterator<Item = (&String, &CclObject)> {
self.0
.iter()
.flat_map(|(k, vec)| vec.iter().map(move |v| (k, v)))
}
pub(crate) fn iter_map(&self) -> indexmap::map::Iter<'_, String, Vec<CclObject>> {
self.0.iter()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn inner_mut(&mut self) -> &mut CclMap {
&mut self.0
}
pub fn empty() -> Self {
CclObject(IndexMap::new())
}
pub fn from_list(items: Vec<impl Into<String>>) -> Self {
let mut map = IndexMap::new();
let values: Vec<CclObject> = items
.into_iter()
.map(|item| CclObject::from_string(item))
.collect();
map.insert("".to_string(), values);
CclObject(map)
}
pub fn add_comment(&mut self, text: impl Into<String>) {
let comment_key = format!("/= {}", text.into());
self.0.insert(comment_key, vec![CclObject::empty()]);
}
pub fn add_blank_line(&mut self) {
self.0.insert("".to_string(), vec![CclObject::empty()]);
}
pub fn compose(&self, other: &CclObject) -> CclObject {
let mut result: CclMap = CclMap::new();
for (key, self_values) in &self.0 {
if let Some(other_values) = other.0.get(key) {
let composed_values = Self::compose_value_lists(self_values, other_values);
result.insert(key.clone(), composed_values);
} else {
result.insert(key.clone(), self_values.clone());
}
}
for (key, other_values) in &other.0 {
if !self.0.contains_key(key) {
result.insert(key.clone(), other_values.clone());
}
}
CclObject(result)
}
fn compose_value_lists(a: &[CclObject], b: &[CclObject]) -> Vec<CclObject> {
let mut composed = CclObject::new();
for obj in a {
composed = composed.compose(obj);
}
for obj in b {
composed = composed.compose(obj);
}
vec![composed]
}
pub fn compose_associative(a: &CclObject, b: &CclObject, c: &CclObject) -> bool {
let left = a.compose(b).compose(c);
let right = a.compose(&b.compose(c));
left == right
}
pub fn identity_left(x: &CclObject) -> bool {
let empty = CclObject::new();
empty.compose(x) == *x
}
pub fn identity_right(x: &CclObject) -> bool {
let empty = CclObject::new();
x.compose(&empty) == *x
}
pub(crate) fn as_string(&self) -> Result<&str> {
if self.0.len() == 1 {
let (key, vec) = self.0.iter().next().unwrap();
if vec.len() == 1 && vec[0].0.is_empty() {
return Ok(key.as_str());
}
}
Err(Error::ValueError(
"expected single string value (map with one key and single empty value)".to_string(),
))
}
pub fn get_string(&self, key: &str) -> Result<&str> {
self.get(key)?.as_string()
}
pub(crate) fn as_bool(&self) -> Result<bool> {
self.as_bool_with_options(BoolOptions::new())
}
pub(crate) fn as_bool_with_options(&self, options: BoolOptions) -> Result<bool> {
let s = self.as_string()?;
if options.lenient {
match s {
"true" | "yes" => Ok(true),
"false" | "no" => Ok(false),
_ => Err(Error::ValueError(format!(
"failed to parse '{}' as bool",
s
))),
}
} else {
s.parse::<bool>()
.map_err(|_| Error::ValueError(format!("failed to parse '{}' as bool", s)))
}
}
pub fn get_bool(&self, key: &str) -> Result<bool> {
self.get(key)?.as_bool()
}
pub fn get_bool_with_options(&self, key: &str, options: BoolOptions) -> Result<bool> {
self.get(key)?.as_bool_with_options(options)
}
pub fn get_bool_lenient(&self, key: &str) -> Result<bool> {
self.get_bool_with_options(key, BoolOptions::lenient())
}
pub(crate) fn as_int(&self) -> Result<i64> {
let s = self.as_string()?;
s.parse::<i64>()
.map_err(|_| Error::ValueError(format!("failed to parse '{}' as integer", s)))
}
pub fn get_int(&self, key: &str) -> Result<i64> {
self.get(key)?.as_int()
}
pub(crate) fn as_float(&self) -> Result<f64> {
let s = self.as_string()?;
s.parse::<f64>()
.map_err(|_| Error::ValueError(format!("failed to parse '{}' as float", s)))
}
pub fn get_float(&self, key: &str) -> Result<f64> {
self.get(key)?.as_float()
}
pub(crate) fn as_list_with_options(&self, options: ListOptions) -> Vec<String> {
if options.coerce {
self.keys()
.filter(|k| !is_scalar_literal(k))
.cloned()
.collect()
} else {
let non_comment_keys: Vec<&String> =
self.keys().filter(|k| !k.starts_with('/')).collect();
if non_comment_keys.len() == 1 && non_comment_keys[0].is_empty() {
if let Ok(children) = self.get_all("") {
return children
.iter()
.flat_map(|child| child.keys().filter(|k| !k.starts_with('/')).cloned())
.collect();
}
}
if non_comment_keys.len() <= 1 {
return Vec::new();
}
Vec::new()
}
}
pub fn get_list(&self, key: &str) -> Result<Vec<String>> {
Ok(self.get(key)?.as_list_with_options(ListOptions::new()))
}
pub fn get_list_coerced(&self, key: &str) -> Result<Vec<String>> {
let all_values = self.get_all(key)?;
let result: Vec<String> = all_values
.iter()
.flat_map(|obj| obj.keys().filter(|k| !is_scalar_literal(k)).cloned())
.collect();
Ok(result)
}
pub fn get_list_typed<T>(&self, key: &str) -> Result<Vec<T>>
where
T: FromStr,
T::Err: std::fmt::Display,
{
let model = self.get(key)?;
if model.len() >= 2 {
model
.keys()
.map(|k| {
k.parse::<T>().map_err(|e| {
Error::ValueError(format!(
"Failed to parse '{}' as {}: {}",
k,
std::any::type_name::<T>(),
e
))
})
})
.collect()
} else {
Ok(Vec::new())
}
}
pub fn from_string(s: impl Into<String>) -> Self {
let mut map = IndexMap::new();
map.insert(s.into(), vec![CclObject::new()]);
CclObject(map)
}
#[cfg(feature = "serde-serialize")]
pub(crate) fn insert_string(&mut self, key: &str, value: String) {
let mut inner = IndexMap::new();
inner.insert(value, vec![CclObject::new()]);
self.0.insert(key.to_string(), vec![CclObject(inner)]);
}
#[cfg(feature = "serde-serialize")]
pub(crate) fn insert_list(&mut self, key: &str, values: Vec<String>) {
let mut inner = IndexMap::new();
for value in values {
inner.insert(value, vec![CclObject::new()]);
}
self.0.insert(key.to_string(), vec![CclObject(inner)]);
}
#[cfg(feature = "serde-serialize")]
pub(crate) fn insert_object(&mut self, key: &str, obj: CclObject) {
self.0.insert(key.to_string(), vec![obj]);
}
}
impl Default for CclObject {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bool_options_default() {
let opts = BoolOptions::new();
assert!(!opts.lenient);
}
#[test]
fn test_bool_options_lenient() {
let opts = BoolOptions::lenient();
assert!(opts.lenient);
}
#[test]
fn test_bool_options_default_trait() {
let opts = BoolOptions::default();
assert!(!opts.lenient);
}
#[test]
fn test_list_options_default() {
let opts = ListOptions::new();
assert!(!opts.coerce);
}
#[test]
fn test_list_options_with_coerce() {
let opts = ListOptions::with_coerce();
assert!(opts.coerce);
}
#[test]
fn test_list_options_default_trait() {
let opts = ListOptions::default();
assert!(!opts.coerce);
}
#[test]
fn test_is_scalar_literal_integers() {
assert!(is_scalar_literal("42"));
assert!(is_scalar_literal("-17"));
assert!(is_scalar_literal("0"));
assert!(is_scalar_literal("999999"));
}
#[test]
fn test_is_scalar_literal_floats() {
assert!(is_scalar_literal("3.14"));
assert!(is_scalar_literal("-2.5"));
assert!(is_scalar_literal("0.0"));
assert!(is_scalar_literal("1e10"));
}
#[test]
fn test_is_scalar_literal_booleans() {
assert!(is_scalar_literal("true"));
assert!(is_scalar_literal("false"));
assert!(is_scalar_literal("yes"));
assert!(is_scalar_literal("no"));
}
#[test]
fn test_is_scalar_literal_not_scalars() {
assert!(!is_scalar_literal("hello"));
assert!(!is_scalar_literal("web1"));
assert!(!is_scalar_literal(""));
assert!(!is_scalar_literal("True")); assert!(!is_scalar_literal("YES"));
}
#[test]
fn test_empty_model() {
let model = CclObject::new();
assert!(model.is_empty());
}
#[test]
fn test_map_navigation() {
let mut inner = IndexMap::new();
inner.insert("name".to_string(), vec![CclObject::new()]);
inner.insert("version".to_string(), vec![CclObject::new()]);
let model = CclObject(inner);
assert!(model.get("name").is_ok());
assert!(model.get("version").is_ok());
assert!(model.get("nonexistent").is_err());
}
#[test]
fn test_compose_disjoint_keys() {
let a = CclObject::from_string("hello");
let b = CclObject::from_string("world");
let mut obj_a = CclObject::new();
obj_a.inner_mut().insert("a".to_string(), vec![a]);
let mut obj_b = CclObject::new();
obj_b.inner_mut().insert("b".to_string(), vec![b]);
let composed = obj_a.compose(&obj_b);
assert!(composed.get("a").is_ok());
assert!(composed.get("b").is_ok());
}
#[test]
fn test_compose_overlapping_keys() {
let mut obj_a = CclObject::new();
obj_a.inner_mut().insert(
"config".to_string(),
vec![{
let mut inner = CclObject::new();
inner.inner_mut().insert(
"host".to_string(),
vec![CclObject::from_string("localhost")],
);
inner
}],
);
let mut obj_b = CclObject::new();
obj_b.inner_mut().insert(
"config".to_string(),
vec![{
let mut inner = CclObject::new();
inner
.inner_mut()
.insert("port".to_string(), vec![CclObject::from_string("8080")]);
inner
}],
);
let composed = obj_a.compose(&obj_b);
let config = composed.get("config").unwrap();
assert!(config.get("host").is_ok());
assert!(config.get("port").is_ok());
}
#[test]
fn test_compose_left_identity() {
let mut obj = CclObject::new();
obj.inner_mut()
.insert("key".to_string(), vec![CclObject::from_string("value")]);
assert!(CclObject::identity_left(&obj));
}
#[test]
fn test_compose_right_identity() {
let mut obj = CclObject::new();
obj.inner_mut()
.insert("key".to_string(), vec![CclObject::from_string("value")]);
assert!(CclObject::identity_right(&obj));
}
#[test]
fn test_compose_associativity() {
let mut a = CclObject::new();
a.inner_mut()
.insert("a".to_string(), vec![CclObject::from_string("1")]);
let mut b = CclObject::new();
b.inner_mut()
.insert("b".to_string(), vec![CclObject::from_string("2")]);
let mut c = CclObject::new();
c.inner_mut()
.insert("c".to_string(), vec![CclObject::from_string("3")]);
assert!(CclObject::compose_associative(&a, &b, &c));
}
#[test]
fn test_compose_nested_associativity() {
let mut a = CclObject::new();
a.inner_mut().insert(
"config".to_string(),
vec![{
let mut inner = CclObject::new();
inner.inner_mut().insert(
"host".to_string(),
vec![CclObject::from_string("localhost")],
);
inner
}],
);
let mut b = CclObject::new();
b.inner_mut().insert(
"config".to_string(),
vec![{
let mut inner = CclObject::new();
inner
.inner_mut()
.insert("port".to_string(), vec![CclObject::from_string("8080")]);
inner
}],
);
let mut c = CclObject::new();
c.inner_mut().insert(
"db".to_string(),
vec![{
let mut inner = CclObject::new();
inner
.inner_mut()
.insert("name".to_string(), vec![CclObject::from_string("test")]);
inner
}],
);
assert!(CclObject::compose_associative(&a, &b, &c));
}
}