use crate::de::{set_list_context, take_list_context};
use crate::error::{Error, ErrorKind, Result};
use crate::ser::{escape_str_into, write_f64};
use indexmap::IndexMap;
use serde::de::{self, Deserialize, Deserializer, IntoDeserializer, MapAccess, SeqAccess, Visitor};
use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer};
use std::fmt;
use std::ops::Index;
use std::path::Path;
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Bool(bool),
Integer(i32),
Integer64(i64),
Float(f64),
String(String),
Array(Vec<Value>),
List(Vec<Value>),
Group(Map),
}
pub type Map = IndexMap<String, Value>;
#[derive(Debug, Clone, PartialEq)]
enum PathSegment<'a> {
Key(&'a str),
Index(usize),
}
fn parse_path(path: &str) -> Option<Vec<PathSegment<'_>>> {
if path.is_empty() {
return None;
}
let mut segments = Vec::new();
for part in path.split('.') {
if part.is_empty() {
return None;
}
if part.starts_with('[') && part.ends_with(']') {
let inner = &part[1..part.len() - 1];
let index: usize = inner.parse().ok()?;
segments.push(PathSegment::Index(index));
} else if let Ok(index) = part.parse::<usize>() {
segments.push(PathSegment::Index(index));
} else {
segments.push(PathSegment::Key(part));
}
}
if segments.is_empty() {
None
} else {
Some(segments)
}
}
impl Value {
pub fn is_bool(&self) -> bool {
matches!(self, Value::Bool(_))
}
pub fn is_integer(&self) -> bool {
matches!(self, Value::Integer(_) | Value::Integer64(_))
}
pub fn is_i32(&self) -> bool {
matches!(self, Value::Integer(_))
}
pub fn is_i64(&self) -> bool {
matches!(self, Value::Integer64(_))
}
pub fn is_float(&self) -> bool {
matches!(self, Value::Float(_))
}
pub fn is_number(&self) -> bool {
matches!(
self,
Value::Integer(_) | Value::Integer64(_) | Value::Float(_)
)
}
pub fn is_string(&self) -> bool {
matches!(self, Value::String(_))
}
pub fn is_array(&self) -> bool {
matches!(self, Value::Array(_))
}
pub fn is_list(&self) -> bool {
matches!(self, Value::List(_))
}
pub fn is_group(&self) -> bool {
matches!(self, Value::Group(_))
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Value::Bool(b) => Some(*b),
_ => None,
}
}
pub fn as_i32(&self) -> Option<i32> {
match self {
Value::Integer(i) => Some(*i),
Value::Integer64(i) if *i >= i32::MIN as i64 && *i <= i32::MAX as i64 => {
Some(*i as i32)
}
_ => None,
}
}
pub fn as_i64(&self) -> Option<i64> {
match self {
Value::Integer(i) => Some(*i as i64),
Value::Integer64(i) => Some(*i),
_ => None,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
Value::Float(f) => Some(*f),
Value::Integer(i) => Some(*i as f64),
Value::Integer64(i) => Some(*i as f64),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
Value::String(s) => Some(s),
_ => None,
}
}
pub fn as_array(&self) -> Option<&Vec<Value>> {
match self {
Value::Array(arr) | Value::List(arr) => Some(arr),
_ => None,
}
}
pub fn as_array_mut(&mut self) -> Option<&mut Vec<Value>> {
match self {
Value::Array(arr) | Value::List(arr) => Some(arr),
_ => None,
}
}
pub fn as_group(&self) -> Option<&Map> {
match self {
Value::Group(map) => Some(map),
_ => None,
}
}
pub fn as_group_mut(&mut self) -> Option<&mut Map> {
match self {
Value::Group(map) => Some(map),
_ => None,
}
}
pub fn get<S: AsRef<str>>(&self, key: S) -> Option<&Value> {
match self {
Value::Group(map) => map.get(key.as_ref()),
_ => None,
}
}
pub fn get_mut<S: AsRef<str>>(&mut self, key: S) -> Option<&mut Value> {
match self {
Value::Group(map) => map.get_mut(key.as_ref()),
_ => None,
}
}
pub fn get_index(&self, index: usize) -> Option<&Value> {
match self {
Value::Array(arr) | Value::List(arr) => arr.get(index),
_ => None,
}
}
pub fn get_index_mut(&mut self, index: usize) -> Option<&mut Value> {
match self {
Value::Array(arr) | Value::List(arr) => arr.get_mut(index),
_ => None,
}
}
pub fn lookup(&self, path: &str) -> Option<&Value> {
let segments = parse_path(path)?;
let mut current = self;
for segment in &segments {
match segment {
PathSegment::Key(key) => current = current.get(*key)?,
PathSegment::Index(index) => current = current.get_index(*index)?,
}
}
Some(current)
}
pub fn lookup_mut(&mut self, path: &str) -> Option<&mut Value> {
let segments = parse_path(path)?;
let mut current = self;
for segment in &segments {
match segment {
PathSegment::Key(key) => match current {
Value::Group(map) => current = map.get_mut(*key)?,
_ => return None,
},
PathSegment::Index(index) => match current {
Value::Array(arr) | Value::List(arr) => current = arr.get_mut(*index)?,
_ => return None,
},
}
}
Some(current)
}
pub fn set(&mut self, path: &str, value: impl Into<Value>) -> Result<()> {
let segments = parse_path(path)
.ok_or_else(|| Error::from(ErrorKind::InvalidPath(path.to_string())))?;
for segment in &segments {
if let PathSegment::Key(key) = segment {
if !crate::ser::is_valid_name(key) {
return Err(ErrorKind::InvalidSettingName(key.to_string()).into());
}
}
}
let mut current = self;
for segment in &segments[..segments.len() - 1] {
match segment {
PathSegment::Key(key) => match current {
Value::Group(map) => {
current = map
.entry(key.to_string())
.or_insert_with(|| Value::Group(IndexMap::new()));
}
_ => {
return Err(ErrorKind::PathTraversal(format!(
"'{}' in '{}' is not a group",
key, path
))
.into());
}
},
PathSegment::Index(index) => match current {
Value::Array(arr) | Value::List(arr) => {
let len = arr.len();
current = arr.get_mut(*index).ok_or_else(|| {
Error::from(ErrorKind::PathTraversal(format!(
"index {} out of bounds (length {}) in '{}'",
index, len, path
)))
})?;
}
_ => {
return Err(ErrorKind::PathTraversal(format!(
"cannot index into a non-array value in '{}'",
path
))
.into());
}
},
}
}
match segments.last().unwrap() {
PathSegment::Key(key) => match current {
Value::Group(map) => {
map.insert(key.to_string(), value.into());
Ok(())
}
_ => Err(ErrorKind::PathTraversal(format!(
"cannot set '{}': parent is not a group",
path
))
.into()),
},
PathSegment::Index(index) => match current {
Value::Array(arr) | Value::List(arr) => {
if *index < arr.len() {
arr[*index] = value.into();
Ok(())
} else {
Err(ErrorKind::PathTraversal(format!(
"index {} out of bounds (length {}) in '{}'",
index,
arr.len(),
path
))
.into())
}
}
_ => Err(ErrorKind::PathTraversal(format!(
"cannot set '{}': parent is not an array or list",
path
))
.into()),
},
}
}
pub fn remove(&mut self, path: &str) -> Option<Value> {
let segments = parse_path(path)?;
if segments.is_empty() {
return None;
}
let mut current = self;
for segment in &segments[..segments.len() - 1] {
match segment {
PathSegment::Key(key) => match current {
Value::Group(map) => current = map.get_mut(*key)?,
_ => return None,
},
PathSegment::Index(index) => match current {
Value::Array(arr) | Value::List(arr) => current = arr.get_mut(*index)?,
_ => return None,
},
}
}
match segments.last().unwrap() {
PathSegment::Key(key) => match current {
Value::Group(map) => map.shift_remove(*key),
_ => None,
},
PathSegment::Index(index) => match current {
Value::Array(arr) | Value::List(arr) => {
if *index < arr.len() {
Some(arr.remove(*index))
} else {
None
}
}
_ => None,
},
}
}
#[allow(clippy::should_implement_trait)] pub fn from_str(s: &str) -> Result<Value> {
crate::from_str(s)
}
}
impl std::str::FromStr for Value {
type Err = Error;
fn from_str(s: &str) -> Result<Value> {
crate::from_str(s)
}
}
impl From<bool> for Value {
fn from(v: bool) -> Value {
Value::Bool(v)
}
}
impl From<i8> for Value {
fn from(v: i8) -> Value {
Value::Integer(i32::from(v))
}
}
impl From<i16> for Value {
fn from(v: i16) -> Value {
Value::Integer(i32::from(v))
}
}
impl From<i32> for Value {
fn from(v: i32) -> Value {
Value::Integer(v)
}
}
impl From<i64> for Value {
fn from(v: i64) -> Value {
Value::Integer64(v)
}
}
impl From<u8> for Value {
fn from(v: u8) -> Value {
Value::Integer(i32::from(v))
}
}
impl From<u16> for Value {
fn from(v: u16) -> Value {
Value::Integer(i32::from(v))
}
}
impl From<u32> for Value {
fn from(v: u32) -> Value {
if v > i32::MAX as u32 {
Value::Integer64(i64::from(v))
} else {
Value::Integer(v as i32)
}
}
}
impl From<f32> for Value {
fn from(v: f32) -> Value {
Value::Float(f64::from(v))
}
}
impl From<f64> for Value {
fn from(v: f64) -> Value {
Value::Float(v)
}
}
impl From<&str> for Value {
fn from(v: &str) -> Value {
Value::String(v.to_string())
}
}
impl From<String> for Value {
fn from(v: String) -> Value {
Value::String(v)
}
}
impl From<Map> for Value {
fn from(v: Map) -> Value {
Value::Group(v)
}
}
impl Index<&str> for Value {
type Output = Value;
fn index(&self, key: &str) -> &Self::Output {
self.lookup(key)
.unwrap_or_else(|| panic!("path '{}' not found", key))
}
}
impl Index<usize> for Value {
type Output = Value;
fn index(&self, index: usize) -> &Self::Output {
self.get_index(index)
.unwrap_or_else(|| panic!("index {} out of bounds", index))
}
}
impl PartialEq<bool> for Value {
fn eq(&self, other: &bool) -> bool {
self.as_bool() == Some(*other)
}
}
impl PartialEq<i32> for Value {
fn eq(&self, other: &i32) -> bool {
self.as_i32() == Some(*other)
}
}
impl PartialEq<i64> for Value {
fn eq(&self, other: &i64) -> bool {
self.as_i64() == Some(*other)
}
}
impl PartialEq<f64> for Value {
fn eq(&self, other: &f64) -> bool {
self.as_f64() == Some(*other)
}
}
impl PartialEq<str> for Value {
fn eq(&self, other: &str) -> bool {
self.as_str() == Some(other)
}
}
impl PartialEq<&str> for Value {
fn eq(&self, other: &&str) -> bool {
self.as_str() == Some(*other)
}
}
impl PartialEq<String> for Value {
fn eq(&self, other: &String) -> bool {
self.as_str() == Some(other.as_str())
}
}
impl PartialEq<Value> for bool {
fn eq(&self, other: &Value) -> bool {
other == self
}
}
impl PartialEq<Value> for i32 {
fn eq(&self, other: &Value) -> bool {
other == self
}
}
impl PartialEq<Value> for i64 {
fn eq(&self, other: &Value) -> bool {
other == self
}
}
impl PartialEq<Value> for f64 {
fn eq(&self, other: &Value) -> bool {
other == self
}
}
impl PartialEq<Value> for str {
fn eq(&self, other: &Value) -> bool {
other == self
}
}
impl PartialEq<Value> for &str {
fn eq(&self, other: &Value) -> bool {
other == *self
}
}
impl PartialEq<Value> for String {
fn eq(&self, other: &Value) -> bool {
other == self
}
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Value::Bool(b) => write!(f, "{}", b),
Value::Integer(i) => write!(f, "{}", i),
Value::Integer64(i) => write!(f, "{}L", i),
Value::Float(fl) => {
let mut s = String::new();
match write_f64(&mut s, *fl, &crate::WriteOptions::default()) {
Ok(()) => f.write_str(&s),
Err(_) => write!(f, "{}", fl),
}
}
Value::String(s) => {
let mut escaped = String::with_capacity(s.len() + 2);
escape_str_into(&mut escaped, s);
f.write_str(&escaped)
}
Value::Array(arr) => {
write!(f, "[")?;
for (i, v) in arr.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", v)?;
}
write!(f, "]")
}
Value::List(arr) => {
write!(f, "(")?;
for (i, v) in arr.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", v)?;
}
write!(f, ")")
}
Value::Group(map) => {
write!(f, "{{")?;
for (k, v) in map.iter() {
write!(f, " {} = {};", k, v)?;
}
write!(f, " }}")
}
}
}
}
impl Serialize for Value {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Value::Bool(b) => serializer.serialize_bool(*b),
Value::Integer(i) => serializer.serialize_i32(*i),
Value::Integer64(i) => serializer.serialize_i64(*i),
Value::Float(f) => serializer.serialize_f64(*f),
Value::String(s) => serializer.serialize_str(s),
Value::Array(arr) => {
let mut seq = serializer.serialize_seq(Some(arr.len()))?;
for element in arr {
seq.serialize_element(element)?;
}
seq.end()
}
Value::List(arr) => {
use serde::ser::SerializeTuple;
let mut tup = serializer.serialize_tuple(arr.len())?;
for element in arr {
tup.serialize_element(element)?;
}
tup.end()
}
Value::Group(map) => {
let mut map_ser = serializer.serialize_map(Some(map.len()))?;
for (k, v) in map {
map_ser.serialize_entry(k, v)?;
}
map_ser.end()
}
}
}
}
impl<'de> Deserialize<'de> for Value {
fn deserialize<D>(deserializer: D) -> std::result::Result<Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(ValueVisitor)
}
}
pub(crate) fn scalar_family(v: &Value) -> Option<u8> {
match v {
Value::Bool(_) => Some(0),
Value::Integer(_) | Value::Integer64(_) => Some(1),
Value::Float(_) => Some(2),
Value::String(_) => Some(3),
_ => None,
}
}
pub(crate) fn is_homogeneous_scalar_seq(values: &[Value]) -> bool {
let mut family = None;
for v in values {
let Some(f) = scalar_family(v) else {
return false;
};
match family {
None => family = Some(f),
Some(prev) if prev != f => return false,
_ => {}
}
}
true
}
struct ValueVisitor;
impl<'de> Visitor<'de> for ValueVisitor {
type Value = Value;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("any valid libconfig value")
}
fn visit_bool<E>(self, value: bool) -> std::result::Result<Value, E>
where
E: de::Error,
{
Ok(Value::Bool(value))
}
fn visit_i8<E>(self, value: i8) -> std::result::Result<Value, E>
where
E: de::Error,
{
Ok(Value::Integer(value as i32))
}
fn visit_i16<E>(self, value: i16) -> std::result::Result<Value, E>
where
E: de::Error,
{
Ok(Value::Integer(value as i32))
}
fn visit_i32<E>(self, value: i32) -> std::result::Result<Value, E>
where
E: de::Error,
{
Ok(Value::Integer(value))
}
fn visit_i64<E>(self, value: i64) -> std::result::Result<Value, E>
where
E: de::Error,
{
Ok(Value::Integer64(value))
}
fn visit_u8<E>(self, value: u8) -> std::result::Result<Value, E>
where
E: de::Error,
{
Ok(Value::Integer(value as i32))
}
fn visit_u16<E>(self, value: u16) -> std::result::Result<Value, E>
where
E: de::Error,
{
Ok(Value::Integer(value as i32))
}
fn visit_u32<E>(self, value: u32) -> std::result::Result<Value, E>
where
E: de::Error,
{
if value <= i32::MAX as u32 {
Ok(Value::Integer(value as i32))
} else {
Ok(Value::Integer64(value as i64))
}
}
fn visit_u64<E>(self, value: u64) -> std::result::Result<Value, E>
where
E: de::Error,
{
if value <= i32::MAX as u64 {
Ok(Value::Integer(value as i32))
} else {
Ok(Value::Integer64(value as i64))
}
}
fn visit_f32<E>(self, value: f32) -> std::result::Result<Value, E>
where
E: de::Error,
{
Ok(Value::Float(value as f64))
}
fn visit_f64<E>(self, value: f64) -> std::result::Result<Value, E>
where
E: de::Error,
{
Ok(Value::Float(value))
}
fn visit_str<E>(self, value: &str) -> std::result::Result<Value, E>
where
E: de::Error,
{
Ok(Value::String(value.to_string()))
}
fn visit_borrowed_str<E>(self, value: &'de str) -> std::result::Result<Value, E>
where
E: de::Error,
{
Ok(Value::String(value.to_string()))
}
fn visit_string<E>(self, value: String) -> std::result::Result<Value, E>
where
E: de::Error,
{
Ok(Value::String(value))
}
fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Value, A::Error>
where
A: SeqAccess<'de>,
{
let context = take_list_context();
let mut values = Vec::new();
while let Some(value) = seq.next_element()? {
values.push(value);
}
match context {
Some(true) => Ok(Value::List(values)),
Some(false) => {
if !is_homogeneous_scalar_seq(&values) {
return Err(de::Error::custom(
"array elements must all be scalar values of the same type",
));
}
Ok(Value::Array(values))
}
None => {
if is_homogeneous_scalar_seq(&values) {
Ok(Value::Array(values))
} else {
Ok(Value::List(values))
}
}
}
}
fn visit_map<A>(self, mut map: A) -> std::result::Result<Value, A::Error>
where
A: MapAccess<'de>,
{
let mut values = Map::new();
while let Some((key, value)) = map.next_entry::<String, Value>()? {
match values.entry(key) {
indexmap::map::Entry::Occupied(entry) => {
return Err(de::Error::custom(format!(
"duplicate setting name '{}'",
entry.key()
)));
}
indexmap::map::Entry::Vacant(entry) => {
entry.insert(value);
}
}
}
Ok(Value::Group(values))
}
}
pub fn to_value<T>(value: &T) -> Result<Value>
where
T: ?Sized + Serialize,
{
value.serialize(ValueSerializer)
}
struct ValueSerializer;
impl Serializer for ValueSerializer {
type Ok = Value;
type Error = Error;
type SerializeSeq = SerializeValueSeq;
type SerializeTuple = SerializeValueList;
type SerializeTupleStruct = SerializeValueList;
type SerializeTupleVariant = SerializeValueTupleVariant;
type SerializeMap = SerializeValueMap;
type SerializeStruct = SerializeValueMap;
type SerializeStructVariant = SerializeValueStructVariant;
fn serialize_bool(self, v: bool) -> Result<Value> {
Ok(Value::Bool(v))
}
fn serialize_i8(self, v: i8) -> Result<Value> {
Ok(Value::Integer(i32::from(v)))
}
fn serialize_i16(self, v: i16) -> Result<Value> {
Ok(Value::Integer(i32::from(v)))
}
fn serialize_i32(self, v: i32) -> Result<Value> {
Ok(Value::Integer(v))
}
fn serialize_i64(self, v: i64) -> Result<Value> {
Ok(Value::Integer64(v))
}
fn serialize_u8(self, v: u8) -> Result<Value> {
Ok(Value::Integer(i32::from(v)))
}
fn serialize_u16(self, v: u16) -> Result<Value> {
Ok(Value::Integer(i32::from(v)))
}
fn serialize_u32(self, v: u32) -> Result<Value> {
Ok(Value::from(v))
}
fn serialize_u64(self, v: u64) -> Result<Value> {
i64::try_from(v)
.map(Value::Integer64)
.map_err(|_| ErrorKind::IntegerOutOfRange.into())
}
fn serialize_f32(self, v: f32) -> Result<Value> {
Ok(Value::Float(f64::from(v)))
}
fn serialize_f64(self, v: f64) -> Result<Value> {
Ok(Value::Float(v))
}
fn serialize_char(self, v: char) -> Result<Value> {
Ok(Value::String(v.to_string()))
}
fn serialize_str(self, v: &str) -> Result<Value> {
Ok(Value::String(v.to_string()))
}
fn serialize_bytes(self, v: &[u8]) -> Result<Value> {
Ok(Value::Array(
v.iter().map(|b| Value::Integer(i32::from(*b))).collect(),
))
}
fn serialize_none(self) -> Result<Value> {
Err(ErrorKind::TypeNotSupported("Option::None".to_string()).into())
}
fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<Value> {
value.serialize(self)
}
fn serialize_unit(self) -> Result<Value> {
Err(ErrorKind::UnitNotSupported.into())
}
fn serialize_unit_struct(self, _name: &'static str) -> Result<Value> {
self.serialize_unit()
}
fn serialize_unit_variant(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
) -> Result<Value> {
Ok(Value::String(variant.to_string()))
}
fn serialize_newtype_struct<T: ?Sized + Serialize>(
self,
_name: &'static str,
value: &T,
) -> Result<Value> {
value.serialize(self)
}
fn serialize_newtype_variant<T: ?Sized + Serialize>(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
value: &T,
) -> Result<Value> {
validate_variant_name(variant)?;
let mut map = Map::new();
map.insert(variant.to_string(), value.serialize(ValueSerializer)?);
Ok(Value::Group(map))
}
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq> {
Ok(SerializeValueSeq {
items: Vec::with_capacity(len.unwrap_or(0)),
})
}
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple> {
Ok(SerializeValueList {
items: Vec::with_capacity(len),
})
}
fn serialize_tuple_struct(
self,
_name: &'static str,
len: usize,
) -> Result<Self::SerializeTupleStruct> {
self.serialize_tuple(len)
}
fn serialize_tuple_variant(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
len: usize,
) -> Result<Self::SerializeTupleVariant> {
validate_variant_name(variant)?;
Ok(SerializeValueTupleVariant {
variant,
items: Vec::with_capacity(len),
})
}
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
Ok(SerializeValueMap {
map: Map::new(),
pending_key: None,
})
}
fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
Ok(SerializeValueMap {
map: Map::new(),
pending_key: None,
})
}
fn serialize_struct_variant(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
_len: usize,
) -> Result<Self::SerializeStructVariant> {
validate_variant_name(variant)?;
Ok(SerializeValueStructVariant {
variant,
map: Map::new(),
})
}
}
fn validate_variant_name(name: &str) -> Result<()> {
if crate::ser::is_valid_name(name) {
Ok(())
} else {
Err(ErrorKind::InvalidSettingName(name.to_string()).into())
}
}
struct SerializeValueSeq {
items: Vec<Value>,
}
impl serde::ser::SerializeSeq for SerializeValueSeq {
type Ok = Value;
type Error = Error;
fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
self.items.push(value.serialize(ValueSerializer)?);
Ok(())
}
fn end(self) -> Result<Value> {
if is_homogeneous_scalar_seq(&self.items) {
Ok(Value::Array(self.items))
} else {
Ok(Value::List(self.items))
}
}
}
struct SerializeValueList {
items: Vec<Value>,
}
impl serde::ser::SerializeTuple for SerializeValueList {
type Ok = Value;
type Error = Error;
fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
self.items.push(value.serialize(ValueSerializer)?);
Ok(())
}
fn end(self) -> Result<Value> {
Ok(Value::List(self.items))
}
}
impl serde::ser::SerializeTupleStruct for SerializeValueList {
type Ok = Value;
type Error = Error;
fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
self.items.push(value.serialize(ValueSerializer)?);
Ok(())
}
fn end(self) -> Result<Value> {
Ok(Value::List(self.items))
}
}
struct SerializeValueTupleVariant {
variant: &'static str,
items: Vec<Value>,
}
impl serde::ser::SerializeTupleVariant for SerializeValueTupleVariant {
type Ok = Value;
type Error = Error;
fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
self.items.push(value.serialize(ValueSerializer)?);
Ok(())
}
fn end(self) -> Result<Value> {
let mut map = Map::new();
map.insert(self.variant.to_string(), Value::List(self.items));
Ok(Value::Group(map))
}
}
struct SerializeValueMap {
map: Map,
pending_key: Option<String>,
}
impl serde::ser::SerializeMap for SerializeValueMap {
type Ok = Value;
type Error = Error;
fn serialize_key<T: ?Sized + Serialize>(&mut self, key: &T) -> Result<()> {
let mut key_capture = crate::ser::KeyCapture { key: String::new() };
key.serialize(&mut key_capture)?;
self.pending_key = Some(key_capture.key);
Ok(())
}
fn serialize_value<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
let key = self.pending_key.take().ok_or_else(|| {
Error::from(ErrorKind::Message(
"serialize_value called without serialize_key".to_string(),
))
})?;
self.map.insert(key, value.serialize(ValueSerializer)?);
Ok(())
}
fn end(self) -> Result<Value> {
Ok(Value::Group(self.map))
}
}
impl serde::ser::SerializeStruct for SerializeValueMap {
type Ok = Value;
type Error = Error;
fn serialize_field<T: ?Sized + Serialize>(
&mut self,
key: &'static str,
value: &T,
) -> Result<()> {
validate_variant_name(key)?;
self.map.insert(key.to_string(), value.serialize(ValueSerializer)?);
Ok(())
}
fn end(self) -> Result<Value> {
Ok(Value::Group(self.map))
}
}
struct SerializeValueStructVariant {
variant: &'static str,
map: Map,
}
impl serde::ser::SerializeStructVariant for SerializeValueStructVariant {
type Ok = Value;
type Error = Error;
fn serialize_field<T: ?Sized + Serialize>(
&mut self,
key: &'static str,
value: &T,
) -> Result<()> {
validate_variant_name(key)?;
self.map.insert(key.to_string(), value.serialize(ValueSerializer)?);
Ok(())
}
fn end(self) -> Result<Value> {
let mut inner = Map::new();
inner.insert(self.variant.to_string(), Value::Group(self.map));
Ok(Value::Group(inner))
}
}
pub fn from_value<T>(value: Value) -> Result<T>
where
T: de::DeserializeOwned,
{
let result = T::deserialize(value);
crate::de::clear_list_context();
result
}
impl<'de> Deserializer<'de> for Value {
type Error = Error;
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
match self {
Value::Bool(b) => visitor.visit_bool(b),
Value::Integer(i) => visitor.visit_i32(i),
Value::Integer64(i) => visitor.visit_i64(i),
Value::Float(f) => visitor.visit_f64(f),
Value::String(s) => visitor.visit_string(s),
Value::Array(arr) => {
set_list_context(false);
visitor.visit_seq(SeqDeserializer::new(arr.into_iter()))
}
Value::List(arr) => {
set_list_context(true);
visitor.visit_seq(SeqDeserializer::new(arr.into_iter()))
}
Value::Group(map) => visitor.visit_map(MapDeserializer::new(map.into_iter())),
}
}
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
visitor.visit_some(self)
}
fn deserialize_enum<V>(
self,
_name: &'static str,
_variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value>
where
V: Visitor<'de>,
{
match self {
Value::String(s) => visitor.visit_enum(s.into_deserializer()),
Value::Group(map) => {
if map.len() != 1 {
return Err(ErrorKind::Message(
"expected a group with a single setting for an enum variant".to_string(),
).into());
}
let (variant, value) = map.into_iter().next().unwrap();
visitor.visit_enum(ValueEnumAccess { variant, value })
}
_ => Err(ErrorKind::Message(
"expected a string or group for an enum".to_string(),
).into()),
}
}
serde::forward_to_deserialize_any! {
bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string
bytes byte_buf unit unit_struct newtype_struct seq tuple
tuple_struct map struct identifier ignored_any
}
}
struct ValueEnumAccess {
variant: String,
value: Value,
}
impl<'de> de::EnumAccess<'de> for ValueEnumAccess {
type Error = Error;
type Variant = ValueVariantAccess;
fn variant_seed<V>(self, seed: V) -> Result<(V::Value, Self::Variant)>
where
V: de::DeserializeSeed<'de>,
{
let variant_de: de::value::StringDeserializer<Error> = self.variant.into_deserializer();
let variant = seed.deserialize(variant_de)?;
Ok((variant, ValueVariantAccess { value: self.value }))
}
}
struct ValueVariantAccess {
value: Value,
}
impl<'de> de::VariantAccess<'de> for ValueVariantAccess {
type Error = Error;
fn unit_variant(self) -> Result<()> {
Err(ErrorKind::Message(
"unit enum variants are represented as strings, not groups".to_string(),
).into())
}
fn newtype_variant_seed<T>(self, seed: T) -> Result<T::Value>
where
T: de::DeserializeSeed<'de>,
{
seed.deserialize(self.value)
}
fn tuple_variant<V>(self, _len: usize, visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
match self.value {
Value::Array(arr) | Value::List(arr) => {
visitor.visit_seq(SeqDeserializer::new(arr.into_iter()))
}
_ => Err(ErrorKind::Message(
"expected a list for a tuple enum variant".to_string(),
).into()),
}
}
fn struct_variant<V>(self, _fields: &'static [&'static str], visitor: V) -> Result<V::Value>
where
V: Visitor<'de>,
{
match self.value {
Value::Group(map) => visitor.visit_map(MapDeserializer::new(map.into_iter())),
_ => Err(ErrorKind::Message(
"expected a group for a struct enum variant".to_string(),
).into()),
}
}
}
struct SeqDeserializer {
iter: std::vec::IntoIter<Value>,
}
impl SeqDeserializer {
fn new(iter: std::vec::IntoIter<Value>) -> Self {
SeqDeserializer { iter }
}
}
impl<'de> SeqAccess<'de> for SeqDeserializer {
type Error = Error;
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>>
where
T: de::DeserializeSeed<'de>,
{
match self.iter.next() {
Some(value) => seed.deserialize(value).map(Some),
None => Ok(None),
}
}
}
struct MapDeserializer {
iter: indexmap::map::IntoIter<String, Value>,
value: Option<Value>,
}
impl MapDeserializer {
fn new(iter: indexmap::map::IntoIter<String, Value>) -> Self {
MapDeserializer { iter, value: None }
}
}
impl<'de> MapAccess<'de> for MapDeserializer {
type Error = Error;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>>
where
K: de::DeserializeSeed<'de>,
{
match self.iter.next() {
Some((key, value)) => {
self.value = Some(value);
seed.deserialize(Value::String(key)).map(Some)
}
None => Ok(None),
}
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value>
where
V: de::DeserializeSeed<'de>,
{
match self.value.take() {
Some(value) => seed.deserialize(value),
None => Err(ErrorKind::Message("value is missing".to_string()).into()),
}
}
}
const MAX_INCLUDE_DEPTH: usize = 10;
type IncludeResolver<'a> = dyn Fn(&str) -> Result<Vec<std::path::PathBuf>> + 'a;
fn default_include_resolver(
include_dir: Option<&Path>,
) -> impl Fn(&str) -> Result<Vec<std::path::PathBuf>> + '_ {
move |path: &str| {
let mut resolved = std::path::PathBuf::from(path);
if resolved.is_relative() {
if let Some(dir) = include_dir {
resolved = dir.join(resolved);
}
}
Ok(vec![resolved])
}
}
fn resolve_includes(input: &str, resolver: &IncludeResolver, depth: usize) -> Result<String> {
if depth > MAX_INCLUDE_DEPTH {
return Err(ErrorKind::Include(format!(
"include files nested too deeply (maximum {} levels)",
MAX_INCLUDE_DEPTH
))
.into());
}
let mut out = String::with_capacity(input.len());
let mut in_block_comment = false;
for line in input.lines() {
let trimmed = line.trim_start();
if !in_block_comment && trimmed.starts_with("@include") {
let path_str = parse_include_path(&trimmed["@include".len()..])?;
for path in resolver(&path_str)? {
let contents = std::fs::read_to_string(&path).map_err(|e| {
ErrorKind::Include(format!(
"failed to read include file '{}': {}",
path.display(),
e
))
})?;
out.push_str(&resolve_includes(&contents, resolver, depth + 1)?);
out.push('\n');
}
} else {
update_block_comment_state(line, &mut in_block_comment);
out.push_str(line);
out.push('\n');
}
}
Ok(out)
}
fn parse_include_path(rest: &str) -> Result<String> {
let rest = rest.trim_start();
let mut chars = rest.chars();
if chars.next() != Some('"') {
return Err(ErrorKind::Include(
"expected a quoted path after @include".to_string(),
).into());
}
let mut path = String::new();
let mut escaped = false;
let mut closed = false;
for ch in chars.by_ref() {
if escaped {
match ch {
'\\' => path.push('\\'),
'"' => path.push('"'),
_ => {
return Err(ErrorKind::Include(format!(
"invalid escape '\\{}' in @include path",
ch
)).into());
}
}
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == '"' {
closed = true;
break;
} else {
path.push(ch);
}
}
if !closed {
return Err(ErrorKind::Include(
"unterminated path in @include directive".to_string(),
).into());
}
let remainder = chars.as_str().trim_start();
if !(remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with("//")) {
return Err(ErrorKind::Include(
"unexpected content after @include directive".to_string(),
).into());
}
Ok(path)
}
fn update_block_comment_state(line: &str, in_block_comment: &mut bool) {
let bytes = line.as_bytes();
let mut i = 0;
let mut in_string = false;
while i < bytes.len() {
if *in_block_comment {
if bytes[i] == b'*' && bytes.get(i + 1) == Some(&b'/') {
*in_block_comment = false;
i += 2;
} else {
i += 1;
}
} else if in_string {
match bytes[i] {
b'\\' => i += 2,
b'"' => {
in_string = false;
i += 1;
}
_ => i += 1,
}
} else {
match bytes[i] {
b'"' => {
in_string = true;
i += 1;
}
b'#' => break,
b'/' if bytes.get(i + 1) == Some(&b'/') => break,
b'/' if bytes.get(i + 1) == Some(&b'*') => {
*in_block_comment = true;
i += 2;
}
_ => i += 1,
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
value: Value,
}
impl Config {
pub fn new() -> Self {
Config {
value: Value::Group(Map::new()),
}
}
pub fn into_value(self) -> Value {
self.value
}
#[allow(clippy::should_implement_trait)] pub fn from_str(s: &str) -> Result<Config> {
Config::parse(s, &default_include_resolver(None))
}
pub fn from_str_with_include_dir<P: AsRef<Path>>(s: &str, include_dir: P) -> Result<Config> {
Config::parse(s, &default_include_resolver(Some(include_dir.as_ref())))
}
pub fn from_str_with_include_fn<F>(s: &str, resolver: F) -> Result<Config>
where
F: Fn(&str) -> Result<Vec<std::path::PathBuf>>,
{
Config::parse(s, &resolver)
}
fn parse(s: &str, resolver: &IncludeResolver) -> Result<Config> {
let processed;
let text: &str = if s.contains("@include") {
processed = resolve_includes(s, resolver, 0)?;
&processed
} else {
s
};
let value: Value = crate::from_str(text)?;
if !value.is_group() {
return Err(ErrorKind::ExpectedGroup.into());
}
Ok(Config { value })
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Config> {
let contents = std::fs::read_to_string(path)
.map_err(|e| ErrorKind::Message(format!("failed to read file: {}", e)))?;
Config::from_str(&contents)
}
pub fn from_file_with_include_dir<P: AsRef<Path>, Q: AsRef<Path>>(
path: P,
include_dir: Q,
) -> Result<Config> {
let contents = std::fs::read_to_string(path)
.map_err(|e| ErrorKind::Message(format!("failed to read file: {}", e)))?;
Config::from_str_with_include_dir(&contents, include_dir)
}
pub fn from_file_with_include_fn<P: AsRef<Path>, F>(path: P, resolver: F) -> Result<Config>
where
F: Fn(&str) -> Result<Vec<std::path::PathBuf>>,
{
let contents = std::fs::read_to_string(path)
.map_err(|e| ErrorKind::Message(format!("failed to read file: {}", e)))?;
Config::parse(&contents, &resolver)
}
pub fn to_string(&self) -> Result<String> {
self.to_string_with_options(&crate::WriteOptions::default())
}
pub fn to_string_with_options(&self, options: &crate::WriteOptions) -> Result<String> {
if !self.value.is_group() {
return Err(ErrorKind::ExpectedGroup.into());
}
crate::ser::to_string_with_options(&self.value, options)
}
pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
self.to_file_with_options(path, &crate::WriteOptions::default())
}
pub fn to_file_with_options<P: AsRef<Path>>(
&self,
path: P,
options: &crate::WriteOptions,
) -> Result<()> {
let mut contents = self.to_string_with_options(options)?;
if !contents.is_empty() {
contents.push('\n');
}
std::fs::write(path, contents).map_err(|e| ErrorKind::Io(e.to_string()))?;
Ok(())
}
}
impl Default for Config {
fn default() -> Self {
Config::new()
}
}
impl std::str::FromStr for Config {
type Err = Error;
fn from_str(s: &str) -> Result<Config> {
Config::from_str(s)
}
}
impl TryFrom<Value> for Config {
type Error = Error;
fn try_from(value: Value) -> Result<Config> {
if value.is_group() {
Ok(Config { value })
} else {
Err(ErrorKind::ExpectedGroup.into())
}
}
}
impl<'de> Deserialize<'de> for Config {
fn deserialize<D>(deserializer: D) -> std::result::Result<Config, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
if value.is_group() {
Ok(Config { value })
} else {
Err(de::Error::custom(
"configuration root must be a group of settings",
))
}
}
}
impl std::ops::Deref for Config {
type Target = Value;
fn deref(&self) -> &Value {
&self.value
}
}
impl std::ops::DerefMut for Config {
fn deref_mut(&mut self) -> &mut Value {
&mut self.value
}
}
impl fmt::Display for Config {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.to_string() {
Ok(s) => write!(f, "{}", s),
Err(e) => write!(f, "<error: {}>", e),
}
}
}
impl Serialize for Config {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
self.value.serialize(serializer)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::from_str;
#[test]
fn test_value_types() {
let v = Value::Bool(true);
assert!(v.is_bool());
assert_eq!(v.as_bool(), Some(true));
let v = Value::Integer(42);
assert!(v.is_i32());
assert_eq!(v.as_i32(), Some(42));
let v = Value::Float(2.25);
assert!(v.is_float());
assert_eq!(v.as_f64(), Some(2.25));
let v = Value::String("hello".to_string());
assert!(v.is_string());
assert_eq!(v.as_str(), Some("hello"));
}
#[test]
fn test_value_indexing() {
let config = r#"
name = "Test";
count = 42;
items = [1, 2, 3];
"#;
let v: Value = from_str(config).unwrap();
assert_eq!(v["name"], "Test");
assert_eq!(v["count"], 42);
assert_eq!(v["items"][0], 1);
assert_eq!(v["items"][1], 2);
}
#[test]
fn test_value_lookup() {
let config = r#"
application = {
window = {
title = "My App";
size = { w = 640; h = 480; };
};
};
"#;
let v: Value = from_str(config).unwrap();
assert_eq!(
v.lookup("application.window.title")
.and_then(|v| v.as_str()),
Some("My App")
);
assert_eq!(
v.lookup("application.window.size")
.and_then(|v| v.get("w"))
.and_then(|v| v.as_i32()),
Some(640)
);
}
#[test]
fn test_value_lookup_with_array_index() {
let config = r#"
name = "Test";
items = [10, 20, 30];
nested = {
ports = [80, 443];
};
"#;
let v: Value = from_str(config).unwrap();
assert_eq!(v.lookup("items.0").and_then(|v| v.as_i32()), Some(10));
assert_eq!(v.lookup("items.1").and_then(|v| v.as_i32()), Some(20));
assert_eq!(v.lookup("items.2").and_then(|v| v.as_i32()), Some(30));
assert_eq!(
v.lookup("nested.ports.0").and_then(|v| v.as_i32()),
Some(80)
);
assert_eq!(
v.lookup("nested.ports.1").and_then(|v| v.as_i32()),
Some(443)
);
assert!(v.lookup("items.5").is_none());
assert!(v.lookup("name.0").is_none());
assert_eq!(v["items.0"], 10);
assert_eq!(v["nested.ports.1"], 443);
}
#[test]
fn test_value_comparison() {
let v = Value::Bool(true);
assert!(v == true);
let v = Value::Integer(42);
assert!(v == 42);
let v = Value::String("hello".to_string());
assert!(v == "hello");
}
#[test]
fn test_value_round_trip() {
let config = r#"
name = "Test";
version = 1;
enabled = true;
ratio = 2.25;
items = [1, 2, 3];
"#;
let v: Value = from_str(config).unwrap();
let serialized = crate::to_string(&v).unwrap();
let v2: Value = from_str(&serialized).unwrap();
assert_eq!(v["name"], v2["name"]);
assert_eq!(v["version"], v2["version"]);
assert_eq!(v["enabled"], v2["enabled"]);
}
#[test]
fn test_parse_path_dotted_keys() {
let segs = parse_path("a.b.c").unwrap();
assert_eq!(
segs,
vec![
PathSegment::Key("a"),
PathSegment::Key("b"),
PathSegment::Key("c"),
]
);
}
#[test]
fn test_parse_path_bracket_index() {
let segs = parse_path("items.[0].title").unwrap();
assert_eq!(
segs,
vec![
PathSegment::Key("items"),
PathSegment::Index(0),
PathSegment::Key("title"),
]
);
}
#[test]
fn test_parse_path_bare_numeric() {
let segs = parse_path("items.0").unwrap();
assert_eq!(segs, vec![PathSegment::Key("items"), PathSegment::Index(0)]);
}
#[test]
fn test_parse_path_single_segment() {
assert_eq!(
parse_path("version").unwrap(),
vec![PathSegment::Key("version")]
);
}
#[test]
fn test_parse_path_invalid() {
assert!(parse_path("").is_none());
assert!(parse_path("a..b").is_none());
assert!(parse_path(".a").is_none());
assert!(parse_path("a.").is_none());
assert!(parse_path("items.[abc]").is_none());
assert!(parse_path("items.[-1]").is_none());
assert!(parse_path("items.[]").is_none());
}
#[test]
fn test_lookup_bracket_notation() {
let config = r#"
books = (
{ title = "Treasure Island"; price = 29; },
{ title = "Snow Crash"; price = 9; }
);
"#;
let v: Value = from_str(config).unwrap();
assert_eq!(
v.lookup("books.[0].title").and_then(|v| v.as_str()),
Some("Treasure Island")
);
assert_eq!(
v.lookup("books.[1].price").and_then(|v| v.as_i32()),
Some(9)
);
assert_eq!(
v.lookup("books.0.title").and_then(|v| v.as_str()),
Some("Treasure Island")
);
}
#[test]
fn test_lookup_mut_bracket_notation() {
let config = r#"items = [10, 20, 30];"#;
let mut v: Value = from_str(config).unwrap();
*v.lookup_mut("items.[1]").unwrap() = Value::Integer(99);
assert_eq!(v.lookup("items.[1]").unwrap().as_i32(), Some(99));
assert_eq!(v.lookup("items.1").unwrap().as_i32(), Some(99));
}
#[test]
fn test_set_existing_key() {
let mut v: Value = from_str(r#"name = "old";"#).unwrap();
assert!(v.set("name", Value::String("new".to_string())).is_ok());
assert_eq!(v.lookup("name").unwrap().as_str(), Some("new"));
}
#[test]
fn test_set_new_key() {
let mut v: Value = from_str(r#"a = 1;"#).unwrap();
assert!(v.set("b", Value::Integer(2)).is_ok());
assert_eq!(v.lookup("b").unwrap().as_i32(), Some(2));
}
#[test]
fn test_set_creates_intermediate_groups() {
let mut v = Value::Group(IndexMap::new());
assert!(v.set("a.b.c", Value::Integer(42)).is_ok());
assert_eq!(v.lookup("a.b.c").unwrap().as_i32(), Some(42));
assert!(v.lookup("a").unwrap().is_group());
assert!(v.lookup("a.b").unwrap().is_group());
}
#[test]
fn test_set_array_element() {
let mut v: Value = from_str(r#"items = [1, 2, 3];"#).unwrap();
assert!(v.set("items.[0]", Value::Integer(99)).is_ok());
assert_eq!(v.lookup("items.[0]").unwrap().as_i32(), Some(99));
}
#[test]
fn test_set_array_out_of_bounds() {
let mut v: Value = from_str(r#"items = [1, 2];"#).unwrap();
assert!(v.set("items.[5]", Value::Integer(99)).is_err());
}
#[test]
fn test_set_on_scalar_fails() {
let mut v: Value = from_str(r#"name = "hello";"#).unwrap();
assert!(v.set("name.child", Value::Integer(1)).is_err());
}
#[test]
fn test_set_deeply_nested() {
let mut v = Value::Group(IndexMap::new());
assert!(v.set("level1.level2.level3.level4", Value::Bool(true)).is_ok());
assert_eq!(
v.lookup("level1.level2.level3.level4").unwrap().as_bool(),
Some(true)
);
}
#[test]
fn test_set_into_list_element_group() {
let config = r#"
books = (
{ title = "Old Title"; }
);
"#;
let mut v: Value = from_str(config).unwrap();
assert!(v.set("books.[0].title", Value::String("New Title".to_string())).is_ok());
assert_eq!(
v.lookup("books.[0].title").unwrap().as_str(),
Some("New Title")
);
}
#[test]
fn test_remove_group_key() {
let mut v: Value = from_str(r#"a = 1; b = 2; c = 3;"#).unwrap();
let removed = v.remove("b");
assert_eq!(removed, Some(Value::Integer(2)));
assert!(v.lookup("b").is_none());
assert_eq!(v.lookup("a").unwrap().as_i32(), Some(1));
assert_eq!(v.lookup("c").unwrap().as_i32(), Some(3));
}
#[test]
fn test_remove_array_element() {
let mut v: Value = from_str(r#"items = [10, 20, 30];"#).unwrap();
let removed = v.remove("items.[1]");
assert_eq!(removed, Some(Value::Integer(20)));
assert_eq!(v.lookup("items.[0]").unwrap().as_i32(), Some(10));
assert_eq!(v.lookup("items.[1]").unwrap().as_i32(), Some(30));
assert!(v.lookup("items.[2]").is_none());
}
#[test]
fn test_remove_nested() {
let config = r#"
database = {
host = "localhost";
port = 5432;
};
"#;
let mut v: Value = from_str(config).unwrap();
let removed = v.remove("database.host");
assert_eq!(removed, Some(Value::String("localhost".to_string())));
assert!(v.lookup("database.host").is_none());
assert_eq!(v.lookup("database.port").unwrap().as_i32(), Some(5432));
}
#[test]
fn test_remove_nonexistent() {
let mut v: Value = from_str(r#"a = 1;"#).unwrap();
assert_eq!(v.remove("z"), None);
assert_eq!(v.remove("a.b"), None);
}
#[test]
fn test_remove_index_out_of_bounds() {
let mut v: Value = from_str(r#"items = [1, 2];"#).unwrap();
assert_eq!(v.remove("items.[5]"), None);
}
#[test]
fn test_remove_entire_subtree() {
let config = r#"
app = {
window = { title = "hi"; };
debug = false;
};
"#;
let mut v: Value = from_str(config).unwrap();
let removed = v.remove("app.window");
assert!(removed.unwrap().is_group());
assert!(v.lookup("app.window").is_none());
assert_eq!(v.lookup("app.debug").unwrap().as_bool(), Some(false));
}
#[test]
fn test_remove_bare_numeric_index() {
let mut v: Value = from_str(r#"items = [10, 20, 30];"#).unwrap();
let removed = v.remove("items.0");
assert_eq!(removed, Some(Value::Integer(10)));
assert_eq!(v.lookup("items.0").unwrap().as_i32(), Some(20));
}
#[test]
fn test_ordering_preserved_in_round_trip() {
let input = r#"
first = 1;
second = 2;
third = 3;
fourth = 4;
"#;
let v: Value = from_str(input).unwrap();
let output = Config::try_from(v.clone()).unwrap().to_string().unwrap();
let v_output: Value = from_str(&output).unwrap();
if let Value::Group(map) = v {
if let Value::Group(map_output) = v_output {
let keys1: Vec<&String> = map.keys().collect();
let keys2: Vec<&String> = map_output.keys().collect();
assert_eq!(keys1, keys2);
assert_eq!(keys1, vec!["first", "second", "third", "fourth"]);
}
}
}
#[test]
fn test_config_implicit_no_outer_braces() {
let input = "a = 1;\nb = 2;\nc = 3;";
let v: Value = from_str(input).unwrap();
let output = Config::try_from(v).unwrap().to_string().unwrap();
assert!(!output.trim_start().starts_with('{'));
assert!(!output.trim_end().ends_with('}'));
assert!(output.contains("a = 1"));
assert!(output.contains("b = 2"));
assert!(output.contains("c = 3"));
}
#[test]
fn test_nested_groups_have_braces() {
let input = r#"
outer = 1;
nested = {
inner = 2;
};
"#;
let v: Value = from_str(input).unwrap();
let output = Config::try_from(v).unwrap().to_string().unwrap();
assert!(!output.trim_start().starts_with('{'));
assert!(output.contains("nested = {"));
let v2: Value = from_str(&output).unwrap();
assert_eq!(v2["outer"], 1);
assert_eq!(v2["nested.inner"], 2);
}
#[test]
fn test_ordering_with_mixed_types() {
let input = r#"
string_first = "hello";
int_second = 42;
bool_third = true;
float_fourth = 2.25;
array_fifth = [1, 2, 3];
"#;
let v: Value = from_str(input).unwrap();
if let Value::Group(map) = &v {
let keys: Vec<&String> = map.keys().collect();
assert_eq!(
keys,
vec![
"string_first",
"int_second",
"bool_third",
"float_fourth",
"array_fifth"
]
);
}
let output = Config::try_from(v).unwrap().to_string().unwrap();
let v2: Value = from_str(&output).unwrap();
if let Value::Group(map) = &v2 {
let keys: Vec<&String> = map.keys().collect();
assert_eq!(
keys,
vec![
"string_first",
"int_second",
"bool_third",
"float_fourth",
"array_fifth"
]
);
}
}
#[test]
fn test_adding_new_keys_appends_to_end() {
let input = "a = 1;\nb = 2;";
let mut v: Value = from_str(input).unwrap();
v.set("c", Value::Integer(3)).unwrap();
if let Value::Group(map) = &v {
let keys: Vec<&String> = map.keys().collect();
assert_eq!(keys, vec!["a", "b", "c"]);
}
let output = Config::try_from(v).unwrap().to_string().unwrap();
let a_pos = output.find("a = 1").unwrap();
let b_pos = output.find("b = 2").unwrap();
let c_pos = output.find("c = 3").unwrap();
assert!(a_pos < b_pos);
assert!(b_pos < c_pos);
}
#[test]
fn test_implicit_group_round_trip_exact() {
let input = "name = \"test\";\nversion = 42;";
let v: Value = from_str(input).unwrap();
let output = Config::try_from(v).unwrap().to_string().unwrap();
assert_eq!(output, input);
let v2: Value = from_str(&output).unwrap();
let output2 = Config::try_from(v2).unwrap().to_string().unwrap();
assert_eq!(output, output2);
}
#[test]
fn test_nested_indentation() {
let input = r#"
outer = 1;
nested = {
inner = 2;
deep = {
value = 3;
};
};
another = 4;
"#;
let v: Value = from_str(input).unwrap();
let output = Config::try_from(v).unwrap().to_string().unwrap();
assert!(output.contains("outer = 1"));
assert!(output.contains("nested = {"));
assert!(output.contains(" inner = 2"));
assert!(output.contains(" deep = {"));
assert!(output.contains(" value = 3"));
let v2: Value = from_str(&output).unwrap();
assert_eq!(v2["outer"], 1);
assert_eq!(v2["nested.inner"], 2);
assert_eq!(v2["nested.deep.value"], 3);
assert_eq!(v2["another"], 4);
}
#[test]
fn test_implicit_group_with_nested_explicit_groups() {
let input = r#"top_level = 1;
nested = {
inner = 2;
};
also_top = 3;"#;
let v: Value = from_str(input).unwrap();
let output = Config::try_from(v).unwrap().to_string().unwrap();
assert!(!output.trim().starts_with('{'));
assert!(output.contains("nested = {"));
assert!(output.contains(" inner = 2"));
let v2: Value = from_str(&output).unwrap();
assert_eq!(v2["top_level"], 1);
assert_eq!(v2["nested.inner"], 2);
assert_eq!(v2["also_top"], 3);
}
#[test]
fn test_deeply_nested_groups() {
let input = r#"
level1 = {
level2 = {
level3 = {
value = 42;
};
};
};
"#;
let v: Value = from_str(input).unwrap();
let output = Config::try_from(v).unwrap().to_string().unwrap();
assert!(output.contains("level1 = {"));
assert!(output.contains(" level2 = {"));
assert!(output.contains(" level3 = {"));
assert!(output.contains(" value = 42"));
}
#[test]
fn test_config_implicit_round_trip() {
let input = "name = \"test\";\nversion = 42;";
let config = Config::from_str(input).unwrap();
assert_eq!(config["name"], "test");
assert_eq!(config["version"], 42);
let output = config.to_string().unwrap();
assert_eq!(output, input);
}
#[test]
fn test_config_deref_lookup() {
let config = Config::from_str("a = 1; b = { c = 2; };").unwrap();
assert_eq!(config.lookup("b.c").unwrap().as_i32(), Some(2));
assert!(config.is_group());
}
#[test]
fn test_config_deref_mut() {
let mut config = Config::from_str("a = 1; b = 2;").unwrap();
config.set("c", Value::Integer(3)).unwrap();
assert_eq!(config["c"], 3);
let output = config.to_string().unwrap();
assert!(output.contains("c = 3"));
}
#[test]
fn test_config_display() {
let config = Config::from_str("x = 10; y = 20;").unwrap();
let displayed = format!("{}", config);
assert!(displayed.contains("x = 10"));
assert!(displayed.contains("y = 20"));
assert!(!displayed.starts_with('{'));
}
#[test]
fn test_config_new_empty() {
let mut config = Config::new();
assert!(config.is_group());
config.set("key", Value::String("val".to_string())).unwrap();
let output = config.to_string().unwrap();
assert_eq!(output, "key = \"val\";");
}
#[test]
fn test_config_from_value() {
let mut map = Map::new();
map.insert("key".to_string(), Value::String("val".to_string()));
let config = Config::try_from(Value::Group(map)).unwrap();
let output = config.to_string().unwrap();
assert_eq!(output, "key = \"val\";");
}
#[test]
fn test_config_default() {
let config = Config::default();
assert!(config.is_group());
assert_eq!(config.as_group().unwrap().len(), 0);
}
#[test]
fn test_config_serialize() {
let config = Config::from_str("a = 1; b = 2;").unwrap();
let serde_output = crate::to_string(&config).unwrap();
assert_eq!(serde_output, "a = 1;\nb = 2;");
let format_output = config.to_string().unwrap();
assert!(!format_output.starts_with('{'));
}
#[test]
fn test_serde_root_is_implicit_nested_is_braced() {
use serde::Serialize;
#[derive(Serialize)]
struct Inner {
x: i32,
}
#[derive(Serialize)]
struct Outer {
a: i32,
inner: Inner,
}
let out = crate::to_string(&Outer {
a: 1,
inner: Inner { x: 2 },
})
.unwrap();
assert_eq!(out, "a = 1;\ninner = {\n x = 2;\n};");
let v: Value = from_str(&out).unwrap();
assert_eq!(v["a"], 1);
assert_eq!(v["inner.x"], 2);
#[derive(Serialize)]
struct Empty {}
assert_eq!(crate::to_string(&Empty {}).unwrap(), "");
}
#[test]
fn test_top_level_braces_rejected() {
assert!(matches!(
from_str::<Value>("{ a = 1; }"),
Err(e) if *e.kind() == ErrorKind::TopLevelBraces
));
assert!(matches!(
Config::from_str("{ a = 1; }"),
Err(e) if *e.kind() == ErrorKind::TopLevelBraces
));
assert!(Config::from_str("# header\n{ a = 1; }").is_err());
assert!(Config::from_str("/* block */ { a = 1; }").is_err());
let v: Value = from_str("g = { a = 1; };").unwrap();
assert_eq!(v["g.a"], 1);
}
#[test]
fn test_config_requires_group_root() {
assert!(matches!(Config::from_str("42"), Err(e) if *e.kind() == ErrorKind::ExpectedGroup));
assert!(matches!(Config::from_str("true"), Err(e) if *e.kind() == ErrorKind::ExpectedGroup));
assert_eq!(from_str::<i32>("42").unwrap(), 42);
}
#[test]
fn test_from_value_preserves_array_vs_list() {
let _warmup: Value = from_str("(1, 2)").unwrap();
let arr = Value::Array(vec![Value::Integer(1), Value::Integer(2)]);
let round: Value = from_value(arr.clone()).unwrap();
assert_eq!(round, arr);
assert!(round.is_array());
let _warmup: Value = from_str("[1, 2]").unwrap();
let list = Value::List(vec![Value::Integer(1), Value::String("a".into())]);
let round: Value = from_value(list.clone()).unwrap();
assert_eq!(round, list);
assert!(round.is_list());
}
#[test]
fn test_from_value_enum() {
use serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq)]
enum E {
Unit,
Newtype(i32),
Struct { x: i32 },
}
assert_eq!(
from_value::<E>(Value::String("Unit".to_string())).unwrap(),
E::Unit
);
let v: Value = from_str("Newtype = 5;").unwrap();
assert_eq!(from_value::<E>(v).unwrap(), E::Newtype(5));
let v: Value = from_str("Struct = { x = 9; };").unwrap();
assert_eq!(from_value::<E>(v).unwrap(), E::Struct { x: 9 });
}
#[test]
fn test_display_is_valid_libconfig() {
let v: Value = from_str(r#"s = "a\"b"; g = { n = 1; }; items = [1, 2];"#).unwrap();
let displayed = format!("{}", v);
let reparsed: Value = from_str(&format!("wrapped = {};", displayed)).unwrap();
assert_eq!(reparsed["wrapped"], v);
}
#[test]
fn test_display_scalars() {
assert_eq!(format!("{}", Value::Integer64(5)), "5L");
assert_eq!(format!("{}", Value::Float(1.0)), "1.0");
assert_eq!(
format!("{}", Value::String("say \"hi\"".to_string())),
r#""say \"hi\"""#
);
}
#[test]
fn test_non_finite_float_errors_in_config_to_string() {
let mut config = Config::new();
config.set("bad", Value::Float(f64::NAN)).unwrap();
assert!(matches!(config.to_string(), Err(e) if *e.kind() == ErrorKind::NonFiniteFloat));
}
fn temp_dir(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"libconfigasaurus-test-{}-{}",
tag,
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn test_include_directive() {
let dir = temp_dir("basic");
std::fs::write(dir.join("inner.cfg"), "b = 2;\n").unwrap();
std::fs::write(
dir.join("outer.cfg"),
"a = 1;\n@include \"inner.cfg\"\nc = 3;\n",
)
.unwrap();
let config = Config::from_file_with_include_dir(dir.join("outer.cfg"), &dir).unwrap();
assert_eq!(config["a"], 1);
assert_eq!(config["b"], 2);
assert_eq!(config["c"], 3);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_include_nested_inside_group() {
let dir = temp_dir("nested");
std::fs::write(dir.join("quote.cfg"), "quote = \"Criticism\";\n").unwrap();
std::fs::write(
dir.join("test.cfg"),
"info: {\n name = \"WC\";\n @include \"quote.cfg\"\n country = \"UK\";\n};\n",
)
.unwrap();
let config = Config::from_file_with_include_dir(dir.join("test.cfg"), &dir).unwrap();
assert_eq!(config["info.name"], "WC");
assert_eq!(config["info.quote"], "Criticism");
assert_eq!(config["info.country"], "UK");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_include_depth_limit() {
let dir = temp_dir("depth");
std::fs::write(dir.join("loop.cfg"), "@include \"loop.cfg\"\n").unwrap();
let result = Config::from_file_with_include_dir(dir.join("loop.cfg"), &dir);
assert!(matches!(result, Err(e) if matches!(e.kind(), ErrorKind::Include(_))));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_include_missing_file_errors() {
let result = Config::from_str("@include \"does-not-exist-anywhere.cfg\"\n");
assert!(matches!(result, Err(e) if matches!(e.kind(), ErrorKind::Include(_))));
}
#[test]
fn test_set_validates_names_and_paths() {
let mut config = Config::new();
assert!(matches!(
config.set("my key", 1).unwrap_err().kind(),
ErrorKind::InvalidSettingName(_)
));
assert!(matches!(
config.set("2bad", 1).unwrap_err().kind(),
ErrorKind::InvalidPath(_) | ErrorKind::InvalidSettingName(_)
));
assert!(matches!(
config.set("", 1).unwrap_err().kind(),
ErrorKind::InvalidPath(_)
));
config.set("scalar", 5).unwrap();
assert!(matches!(
config.set("scalar.child", 1).unwrap_err().kind(),
ErrorKind::PathTraversal(_)
));
config.set("items", Value::Array(vec![Value::Integer(1)])).unwrap();
assert!(matches!(
config.set("items.[5]", 9).unwrap_err().kind(),
ErrorKind::PathTraversal(_)
));
}
#[test]
fn test_set_accepts_into_value() {
let mut config = Config::new();
config.set("count", 42).unwrap();
config.set("big", 5_000_000_000i64).unwrap();
config.set("ratio", 0.5).unwrap();
config.set("on", true).unwrap();
config.set("name", "svc").unwrap();
config.set("label", String::from("owned")).unwrap();
assert_eq!(config["count"], 42);
assert_eq!(config["big"], 5_000_000_000i64);
assert_eq!(config["ratio"], 0.5);
assert_eq!(config["on"], true);
assert_eq!(config["name"], "svc");
assert_eq!(config["label"], "owned");
}
#[test]
fn test_value_from_conversions() {
assert_eq!(Value::from(true), Value::Bool(true));
assert_eq!(Value::from(7i32), Value::Integer(7));
assert_eq!(Value::from(7u16), Value::Integer(7));
assert_eq!(Value::from(7i64), Value::Integer64(7));
assert_eq!(Value::from(u32::MAX), Value::Integer64(4294967295));
assert_eq!(Value::from(2.5f64), Value::Float(2.5));
assert_eq!(Value::from("x"), Value::String("x".to_string()));
assert_eq!(Value::from(Map::new()), Value::Group(Map::new()));
}
#[test]
fn test_reverse_comparisons() {
let v: Value = from_str("a = 42; s = \"hi\"; b = true; f = 2.5;").unwrap();
assert!(42 == v["a"]);
assert!("hi" == v["s"]);
assert!(true == v["b"]);
assert!(2.5 == v["f"]);
let owned = String::from("hi");
assert!(owned == v["s"]);
}
#[test]
fn test_mutable_accessors() {
let mut v: Value = from_str("g = { a = 1; }; items = [1, 2];").unwrap();
v.get_mut("g")
.unwrap()
.as_group_mut()
.unwrap()
.insert("b".to_string(), Value::Integer(2));
assert_eq!(v["g.b"], 2);
v.get_mut("items")
.unwrap()
.as_array_mut()
.unwrap()
.push(Value::Integer(3));
assert_eq!(v["items.[2]"], 3);
*v.get_mut("items").unwrap().get_index_mut(0).unwrap() = Value::Integer(9);
assert_eq!(v["items.[0]"], 9);
}
#[test]
fn test_to_value() {
use serde::Serialize;
#[derive(Serialize)]
struct Inner {
x: i32,
}
#[derive(Serialize)]
struct Outer {
a: i32,
names: Vec<String>,
inner: Inner,
things: Vec<Inner>,
}
let v = to_value(&Outer {
a: 1,
names: vec!["p".into(), "q".into()],
inner: Inner { x: 2 },
things: vec![Inner { x: 3 }],
})
.unwrap();
assert_eq!(v["a"], 1);
assert!(v["names"].is_array());
assert_eq!(v["inner.x"], 2);
assert!(v["things"].is_list()); assert_eq!(v["things.[0].x"], 3);
assert_eq!(
crate::to_string(&v).unwrap(),
crate::to_string(&Outer {
a: 1,
names: vec!["p".into(), "q".into()],
inner: Inner { x: 2 },
things: vec![Inner { x: 3 }],
})
.unwrap()
);
assert!(matches!(
to_value(&u64::MAX).unwrap_err().kind(),
ErrorKind::IntegerOutOfRange
));
assert!(to_value(&()).is_err());
}
#[test]
fn test_to_value_enums() {
use serde::Serialize;
#[derive(Serialize)]
enum E {
Unit,
Newtype(i32),
Tuple(i32, bool),
Struct { x: i32 },
}
assert_eq!(to_value(&E::Unit).unwrap(), Value::String("Unit".into()));
let v = to_value(&E::Newtype(5)).unwrap();
assert_eq!(v["Newtype"], 5);
let v = to_value(&E::Tuple(7, true)).unwrap();
assert!(v["Tuple"].is_list());
let v = to_value(&E::Struct { x: 9 }).unwrap();
assert_eq!(v["Struct.x"], 9);
}
#[test]
fn test_option_none_fields_are_omitted() {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct S {
a: i32,
b: Option<i32>,
c: Option<String>,
}
let none = S {
a: 1,
b: None,
c: Some("x".into()),
};
let text = crate::to_string(&none).unwrap();
assert_eq!(text, "a = 1;\nc = \"x\";");
let back: S = from_str(&text).unwrap();
assert_eq!(back, none);
let mut map: IndexMap<String, Option<i32>> = IndexMap::new();
map.insert("kept".into(), Some(1));
map.insert("dropped".into(), None);
assert_eq!(crate::to_string(&map).unwrap(), "kept = 1;");
assert!(crate::to_string(&None::<i32>).is_err());
assert!(crate::to_string(&(1, None::<i32>)).is_err());
}
#[test]
fn test_config_try_from_and_into_value() {
let group: Value = from_str("a = 1;").unwrap();
let config = Config::try_from(group.clone()).unwrap();
assert_eq!(config.into_value(), group);
assert!(matches!(
Config::try_from(Value::Integer(42)).unwrap_err().kind(),
ErrorKind::ExpectedGroup
));
}
#[test]
fn test_config_root_invariant_checked_at_write() {
let mut config = Config::new();
*config = Value::Integer(42);
assert!(matches!(
config.to_string().unwrap_err().kind(),
ErrorKind::ExpectedGroup
));
}
#[test]
fn test_config_deserialize() {
let config: Config = from_str("a = 1; g = { b = 2; };").unwrap();
assert_eq!(config["a"], 1);
assert_eq!(config["g.b"], 2);
assert!(from_str::<Config>("42").is_err());
}
#[test]
fn test_config_to_file_round_trip() {
let dir = temp_dir("tofile");
let path = dir.join("out.cfg");
let mut config = Config::new();
config.set("name", "svc").unwrap();
config.set("db.port", 5432).unwrap();
config.to_file(&path).unwrap();
let raw = std::fs::read_to_string(&path).unwrap();
assert!(raw.ends_with('\n'));
let back = Config::from_file(&path).unwrap();
assert_eq!(back, config);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_from_reader_to_writer() {
let mut buf = Vec::new();
let v: Value = from_str("a = 1; b = \"x\";").unwrap();
crate::to_writer(&mut buf, &v).unwrap();
let back: Value = crate::from_reader(buf.as_slice()).unwrap();
assert_eq!(back, v);
}
#[test]
fn test_value_via_foreign_format_is_deterministic() {
let _warm: Value = from_str("(1, 2)").unwrap();
let v: Value = serde_json::from_str("[1, 2]").unwrap();
assert!(v.is_array(), "homogeneous scalars from JSON become an array");
let v: Value = serde_json::from_str("[1, \"mixed\"]").unwrap();
assert!(v.is_list(), "heterogeneous JSON sequences become a list");
let v: Value = serde_json::from_str(r#"{"a": 1}"#).unwrap();
assert!(v.is_group());
}
#[test]
fn test_include_custom_resolver() {
let dir = temp_dir("resolver");
std::fs::write(dir.join("one.cfg"), "a = 1;\n").unwrap();
std::fs::write(dir.join("two.cfg"), "b = 2;\n").unwrap();
let d = dir.clone();
let config = Config::from_str_with_include_fn("@include \"*.cfg\"\nc = 3;\n", |path| {
assert_eq!(path, "*.cfg");
Ok(vec![d.join("one.cfg"), d.join("two.cfg")])
})
.unwrap();
assert_eq!(config["a"], 1);
assert_eq!(config["b"], 2);
assert_eq!(config["c"], 3);
let keys: Vec<&String> = config.as_group().unwrap().keys().collect();
assert_eq!(keys, ["a", "b", "c"]);
let result = Config::from_str_with_include_fn("@include \"nope\"\n", |path| {
Err(ErrorKind::Include(format!("cannot resolve '{path}'")).into())
});
assert!(matches!(
result.unwrap_err().kind(),
ErrorKind::Include(msg) if msg.contains("cannot resolve")
));
std::fs::write(dir.join("outer.cfg"), "@include \"inner\"\nz = 9;\n").unwrap();
std::fs::write(dir.join("inner.cfg"), "y = 8;\n").unwrap();
let d = dir.clone();
let config =
Config::from_file_with_include_fn(dir.join("outer.cfg"), move |path| {
Ok(vec![d.join(format!("{path}.cfg"))])
})
.unwrap();
assert_eq!(config["y"], 8);
assert_eq!(config["z"], 9);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_include_ignored_in_comments_and_strings() {
let config = Config::from_str("/*\n@include \"nope.cfg\"\n*/\na = 1;").unwrap();
assert_eq!(config["a"], 1);
let config = Config::from_str("s = \"see @include docs\";").unwrap();
assert_eq!(config["s"], "see @include docs");
}
}