use crate::error::{NomlError, Result};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;
#[cfg(feature = "chrono")]
use chrono::{DateTime, Utc};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Value {
Null,
Bool(bool),
Integer(i64),
Float(f64),
String(String),
Array(Vec<Value>),
Table(BTreeMap<String, Value>),
#[cfg(feature = "chrono")]
#[serde(with = "chrono::serde::ts_seconds")]
DateTime(DateTime<Utc>),
Binary(Vec<u8>),
Size(u64),
Duration(f64),
}
impl Value {
pub fn null() -> Self {
Value::Null
}
pub fn bool(value: bool) -> Self {
Value::Bool(value)
}
pub fn integer(value: i64) -> Self {
Value::Integer(value)
}
pub fn float(value: f64) -> Self {
Value::Float(value)
}
pub fn string(value: impl Into<String>) -> Self {
Value::String(value.into())
}
pub fn array(values: Vec<Value>) -> Self {
Value::Array(values)
}
pub fn table(map: BTreeMap<String, Value>) -> Self {
Value::Table(map)
}
pub fn empty_table() -> Self {
Value::Table(BTreeMap::new())
}
pub fn size(bytes: u64) -> Self {
Value::Size(bytes)
}
pub fn duration(seconds: f64) -> Self {
Value::Duration(seconds)
}
#[inline]
pub fn type_name(&self) -> &'static str {
match self {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Integer(_) => "integer",
Value::Float(_) => "float",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Table(_) => "table",
#[cfg(feature = "chrono")]
Value::DateTime(_) => "datetime",
Value::Binary(_) => "binary",
Value::Size(_) => "size",
Value::Duration(_) => "duration",
}
}
#[inline]
pub fn is_null(&self) -> bool {
matches!(self, Value::Null)
}
pub fn is_bool(&self) -> bool {
matches!(self, Value::Bool(_))
}
pub fn is_number(&self) -> bool {
matches!(self, Value::Integer(_) | Value::Float(_))
}
pub fn is_string(&self) -> bool {
matches!(self, Value::String(_))
}
pub fn is_array(&self) -> bool {
matches!(self, Value::Array(_))
}
#[inline]
pub fn is_table(&self) -> bool {
matches!(self, Value::Table(_))
}
#[inline]
pub fn as_bool(&self) -> Result<bool> {
match self {
Value::Bool(b) => Ok(*b),
Value::String(s) => {
if s.eq_ignore_ascii_case("true")
|| s.eq_ignore_ascii_case("yes")
|| s == "1"
|| s.eq_ignore_ascii_case("on")
{
Ok(true)
} else if s.eq_ignore_ascii_case("false")
|| s.eq_ignore_ascii_case("no")
|| s == "0"
|| s.eq_ignore_ascii_case("off")
{
Ok(false)
} else {
Err(NomlError::type_error(s, "boolean", self.type_name()))
}
}
Value::Integer(i) => Ok(*i != 0),
_ => Err(NomlError::type_error(
format!("<{}>", self.type_name()),
"boolean",
self.type_name(),
)),
}
}
#[inline]
pub fn as_integer(&self) -> Result<i64> {
match self {
Value::Integer(i) => Ok(*i),
Value::Float(f) => {
if f.fract() == 0.0 && *f >= i64::MIN as f64 && *f <= i64::MAX as f64 {
Ok(*f as i64)
} else {
Err(NomlError::type_error(f.to_string(), "integer", "float"))
}
}
Value::String(s) => s
.parse::<i64>()
.map_err(|_| NomlError::type_error(s, "integer", "string")),
Value::Bool(b) => Ok(if *b { 1 } else { 0 }),
_ => Err(NomlError::type_error(
format!("<{}>", self.type_name()),
"integer",
self.type_name(),
)),
}
}
#[inline]
pub fn as_float(&self) -> Result<f64> {
match self {
Value::Float(f) => Ok(*f),
Value::Integer(i) => Ok(*i as f64),
Value::String(s) => s
.parse::<f64>()
.map_err(|_| NomlError::type_error(s, "float", "string")),
_ => Err(NomlError::type_error(
format!("<{}>", self.type_name()),
"float",
self.type_name(),
)),
}
}
#[inline]
pub fn as_string(&self) -> Result<&str> {
match self {
Value::String(s) => Ok(s),
_ => Err(NomlError::type_error(
format!("<{}>", self.type_name()),
"string",
self.type_name(),
)),
}
}
pub fn into_string(self) -> Result<String> {
match self {
Value::String(s) => Ok(s),
_ => Ok(self.to_string()),
}
}
pub fn as_array(&self) -> Result<&Vec<Value>> {
match self {
Value::Array(arr) => Ok(arr),
_ => Err(NomlError::type_error(
format!("<{}>", self.type_name()),
"array",
self.type_name(),
)),
}
}
pub fn as_array_mut(&mut self) -> Result<&mut Vec<Value>> {
match self {
Value::Array(arr) => Ok(arr),
_ => Err(NomlError::type_error(
format!("<{}>", self.type_name()),
"array",
self.type_name(),
)),
}
}
pub fn as_table(&self) -> Result<&BTreeMap<String, Value>> {
match self {
Value::Table(table) => Ok(table),
_ => Err(NomlError::type_error(
format!("<{}>", self.type_name()),
"table",
self.type_name(),
)),
}
}
pub fn as_table_mut(&mut self) -> Result<&mut BTreeMap<String, Value>> {
match self {
Value::Table(table) => Ok(table),
_ => Err(NomlError::type_error(
format!("<{}>", self.type_name()),
"table",
self.type_name(),
)),
}
}
pub fn get(&self, path: &str) -> Option<&Value> {
let mut current = self;
for segment in path.split('.') {
match current {
Value::Table(table) => {
current = table.get(segment)?;
}
Value::Array(array) => {
let index = segment.parse::<usize>().ok()?;
current = array.get(index)?;
}
_ => return None,
}
}
Some(current)
}
pub fn set(&mut self, path: &str, value: Value) -> Result<()> {
let segments: Vec<&str> = path.split('.').collect();
if segments.is_empty() {
return Err(NomlError::validation("Empty path"));
}
if !self.is_table() {
*self = Value::empty_table();
}
let mut current = self;
for segment in &segments[..segments.len() - 1] {
let table = current.as_table_mut()?;
if !table.contains_key(*segment) {
table.insert(segment.to_string(), Value::empty_table());
}
current = table.get_mut(*segment).ok_or_else(|| {
NomlError::validation(format!("Failed to access segment '{segment}'"))
})?;
if !current.is_table() {
return Err(NomlError::validation(format!(
"Cannot set nested key: '{segment}' is not a table"
)));
}
}
let final_key = segments
.last()
.ok_or_else(|| NomlError::validation("Empty segments list"))?;
let table = current.as_table_mut()?;
table.insert(final_key.to_string(), value);
Ok(())
}
pub fn remove(&mut self, path: &str) -> Result<Option<Value>> {
let segments: Vec<&str> = path.split('.').collect();
if segments.is_empty() {
return Err(NomlError::validation("Empty path"));
}
if segments.len() == 1 {
let table = self.as_table_mut()?;
return Ok(table.remove(segments[0]));
}
let mut current = self;
for segment in &segments[..segments.len() - 1] {
current = match current.as_table_mut()?.get_mut(*segment) {
Some(value) => value,
None => return Ok(None),
};
}
let final_key = segments
.last()
.ok_or_else(|| NomlError::validation("Empty segments list"))?;
let table = current.as_table_mut()?;
Ok(table.remove(*final_key))
}
pub fn contains_key(&self, path: &str) -> bool {
self.get(path).is_some()
}
pub fn keys(&self) -> Vec<String> {
match self {
Value::Table(table) => table.keys().cloned().collect(),
_ => Vec::new(),
}
}
pub fn len(&self) -> usize {
match self {
Value::Array(arr) => arr.len(),
Value::Table(table) => table.len(),
Value::String(s) => s.len(),
_ => 0,
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::Null => write!(f, "null"),
Value::Bool(b) => write!(f, "{b}"),
Value::Integer(i) => write!(f, "{i}"),
Value::Float(fl) => write!(f, "{fl}"),
Value::String(s) => write!(f, "\"{s}\""),
Value::Array(arr) => {
write!(f, "[")?;
for (i, item) in arr.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{item}")?;
}
write!(f, "]")
}
Value::Table(table) => {
write!(f, "{{")?;
for (i, (key, value)) in table.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{key}: {value}")?;
}
write!(f, "}}")
}
#[cfg(feature = "chrono")]
Value::DateTime(dt) => write!(f, "{}", dt.format("%Y-%m-%dT%H:%M:%SZ")),
Value::Binary(data) => write!(f, "<{} bytes>", data.len()),
Value::Size(bytes) => write!(f, "{}", format_size(*bytes)),
Value::Duration(seconds) => write!(f, "{}", format_duration(*seconds)),
}
}
}
fn format_size(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB"];
if bytes == 0 {
return "0B".to_string();
}
let mut size = bytes as f64;
let mut unit_index = 0;
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
if unit_index == 0 {
format!("{bytes}B")
} else {
format!("{:.1}{}", size, UNITS[unit_index])
}
}
fn format_duration(seconds: f64) -> String {
if seconds < 1.0 {
format!("{}ms", (seconds * 1000.0) as u64)
} else if seconds < 60.0 {
format!("{seconds:.1}s")
} else if seconds < 3600.0 {
format!("{:.1}m", seconds / 60.0)
} else if seconds < 86400.0 {
format!("{:.1}h", seconds / 3600.0)
} else {
format!("{:.1}d", seconds / 86400.0)
}
}
impl From<bool> for Value {
fn from(b: bool) -> Self {
Value::Bool(b)
}
}
impl From<i32> for Value {
fn from(i: i32) -> Self {
Value::Integer(i as i64)
}
}
impl From<i64> for Value {
fn from(i: i64) -> Self {
Value::Integer(i)
}
}
impl From<f32> for Value {
fn from(f: f32) -> Self {
Value::Float(f as f64)
}
}
impl From<f64> for Value {
fn from(f: f64) -> Self {
Value::Float(f)
}
}
impl From<String> for Value {
fn from(s: String) -> Self {
Value::String(s)
}
}
impl From<&str> for Value {
fn from(s: &str) -> Self {
Value::String(s.to_string())
}
}
impl From<Vec<Value>> for Value {
fn from(arr: Vec<Value>) -> Self {
Value::Array(arr)
}
}
impl From<BTreeMap<String, Value>> for Value {
fn from(table: BTreeMap<String, Value>) -> Self {
Value::Table(table)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn value_creation_and_type_checking() {
let null_val = Value::null();
assert!(null_val.is_null());
assert_eq!(null_val.type_name(), "null");
let bool_val = Value::bool(true);
assert!(bool_val.is_bool());
assert!(bool_val.as_bool().unwrap());
let int_val = Value::integer(42);
assert!(int_val.is_number());
assert_eq!(int_val.as_integer().unwrap(), 42);
let str_val = Value::string("hello");
assert!(str_val.is_string());
assert_eq!(str_val.as_string().unwrap(), "hello");
}
#[test]
fn nested_key_operations() {
let mut value = Value::empty_table();
value
.set("server.host", Value::string("localhost"))
.unwrap();
value.set("server.port", Value::integer(8080)).unwrap();
value
.set("database.url", Value::string("postgres://..."))
.unwrap();
assert_eq!(
value.get("server.host").unwrap().as_string().unwrap(),
"localhost"
);
assert_eq!(
value.get("server.port").unwrap().as_integer().unwrap(),
8080
);
assert!(value.contains_key("server.host"));
assert!(value.contains_key("database.url"));
assert!(!value.contains_key("nonexistent.key"));
assert!(value.remove("server.host").unwrap().is_some());
assert!(!value.contains_key("server.host"));
}
#[test]
#[allow(clippy::approx_constant)]
fn type_conversions() {
let true_val = Value::string("true");
assert!(true_val.as_bool().unwrap());
let false_val = Value::string("false");
assert!(!false_val.as_bool().unwrap());
let float_val = Value::float(42.0);
assert_eq!(float_val.as_integer().unwrap(), 42);
let str_int = Value::string("123");
assert_eq!(str_int.as_integer().unwrap(), 123);
let str_float = Value::string("3.14");
assert_eq!(str_float.as_float().unwrap(), 3.14);
}
#[test]
fn size_and_duration_formatting() {
let size_val = Value::size(1536); assert_eq!(size_val.to_string(), "1.5KB");
let duration_val = Value::duration(90.0); assert_eq!(duration_val.to_string(), "1.5m");
}
#[test]
#[allow(clippy::approx_constant)]
fn from_trait_implementations() {
let _: Value = true.into();
let _: Value = 42i32.into();
let _: Value = 42i64.into();
let _: Value = 3.14f32.into();
let _: Value = 3.14f64.into();
let _: Value = "hello".into();
let _: Value = String::from("world").into();
let _: Value = vec![Value::integer(1), Value::integer(2)].into();
}
}