use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, HashMap},
fmt,
str::FromStr,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Format {
Yaml,
Toml,
Json,
Unsupported,
}
impl Default for Format {
fn default() -> Self {
Format::Json
}
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let format_str = match self {
Format::Yaml => "YAML",
Format::Toml => "TOML",
Format::Json => "JSON",
Format::Unsupported => "Unsupported",
};
write!(f, "{}", format_str)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Value {
Null,
String(String),
Number(f64),
Boolean(bool),
Array(Vec<Value>),
Object(Box<Frontmatter>),
Tagged(String, Box<Value>),
}
impl Value {
pub fn as_str(&self) -> Option<&str> {
if let Value::String(s) = self {
Some(s)
} else {
None
}
}
pub const fn as_f64(&self) -> Option<f64> {
if let Value::Number(n) = self {
Some(*n)
} else {
None
}
}
pub const fn as_bool(&self) -> Option<bool> {
if let Value::Boolean(b) = self {
Some(*b)
} else {
None
}
}
pub const fn as_array(&self) -> Option<&Vec<Value>> {
if let Value::Array(arr) = self {
Some(arr)
} else {
None
}
}
pub fn as_object(&self) -> Option<&Frontmatter> {
if let Value::Object(obj) = self {
Some(obj)
} else {
None
}
}
pub fn as_tagged(&self) -> Option<(&str, &Value)> {
if let Value::Tagged(tag, val) = self {
Some((tag, val))
} else {
None
}
}
pub const fn is_null(&self) -> bool {
matches!(self, Value::Null)
}
pub const fn is_string(&self) -> bool {
matches!(self, Value::String(_))
}
pub const fn is_number(&self) -> bool {
matches!(self, Value::Number(_))
}
pub const fn is_boolean(&self) -> bool {
matches!(self, Value::Boolean(_))
}
pub const fn is_array(&self) -> bool {
matches!(self, Value::Array(_))
}
pub const fn is_object(&self) -> bool {
matches!(self, Value::Object(_))
}
pub const fn is_tagged(&self) -> bool {
matches!(self, Value::Tagged(_, _))
}
pub fn array_len(&self) -> Option<usize> {
if let Value::Array(arr) = self {
Some(arr.len())
} else {
None
}
}
pub fn to_object(self) -> Result<Frontmatter, String> {
if let Value::Object(obj) = self {
Ok(*obj)
} else {
Err("Value is not an object".into())
}
}
pub fn to_string_representation(&self) -> String {
format!("{}", self)
}
pub fn into_string(self) -> Result<String, String> {
if let Value::String(s) = self {
Ok(s)
} else {
Err("Value is not a string".into())
}
}
pub fn into_f64(self) -> Result<f64, String> {
if let Value::Number(n) = self {
Ok(n)
} else {
Err("Value is not a number".into())
}
}
pub fn into_bool(self) -> Result<bool, String> {
if let Value::Boolean(b) = self {
Ok(b)
} else {
Err("Value is not a boolean".into())
}
}
pub fn get_mut_array(&mut self) -> Option<&mut Vec<Value>> {
if let Value::Array(arr) = self {
Some(arr)
} else {
None
}
}
}
impl Default for Value {
fn default() -> Self {
Value::Null
}
}
impl From<&str> for Value {
fn from(s: &str) -> Self {
Value::String(s.to_string())
}
}
impl From<String> for Value {
fn from(s: String) -> Self {
Value::String(s)
}
}
impl From<f64> for Value {
fn from(n: f64) -> Self {
Value::Number(n)
}
}
impl From<bool> for Value {
fn from(b: bool) -> Self {
Value::Boolean(b)
}
}
impl FromIterator<Value> for Value {
fn from_iter<I: IntoIterator<Item = Value>>(iter: I) -> Self {
Value::Array(iter.into_iter().collect())
}
}
impl FromStr for Value {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("null") {
Ok(Value::Null)
} else if s.eq_ignore_ascii_case("true") {
Ok(Value::Boolean(true))
} else if s.eq_ignore_ascii_case("false") {
Ok(Value::Boolean(false))
} else if let Ok(n) = s.parse::<f64>() {
Ok(Value::Number(n))
} else {
Ok(Value::String(s.to_string()))
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Frontmatter(pub HashMap<String, Value>);
impl Frontmatter {
#[must_use]
pub fn new() -> Self {
Frontmatter(HashMap::new())
}
pub fn insert(
&mut self,
key: String,
value: Value,
) -> Option<Value> {
self.0.insert(key, value)
}
pub fn get(&self, key: &str) -> Option<&Value> {
self.0.get(key)
}
pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
self.0.get_mut(key)
}
pub fn remove(&mut self, key: &str) -> Option<Value> {
self.0.remove(key)
}
pub fn contains_key(&self, key: &str) -> bool {
self.0.contains_key(key)
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn iter(
&self,
) -> std::collections::hash_map::Iter<String, Value> {
self.0.iter()
}
pub fn iter_mut(
&mut self,
) -> std::collections::hash_map::IterMut<String, Value> {
self.0.iter_mut()
}
pub fn merge(&mut self, other: Frontmatter) {
self.0.extend(other.0);
}
pub fn is_null(&self, key: &str) -> bool {
matches!(self.get(key), Some(Value::Null))
}
pub fn clear(&mut self) {
self.0.clear();
}
pub fn capacity(&self) -> usize {
self.0.capacity()
}
pub fn reserve(&mut self, additional: usize) {
self.0.reserve(additional);
}
}
impl Default for Frontmatter {
fn default() -> Self {
Self(HashMap::with_capacity(8))
}
}
impl IntoIterator for Frontmatter {
type Item = (String, Value);
type IntoIter = std::collections::hash_map::IntoIter<String, Value>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl FromIterator<(String, Value)> for Frontmatter {
fn from_iter<I: IntoIterator<Item = (String, Value)>>(
iter: I,
) -> Self {
let mut fm = Frontmatter::new();
for (key, value) in iter {
let _ = fm.insert(key, value);
}
fm
}
}
impl fmt::Display for Frontmatter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{{")?;
let mut sorted_map = BTreeMap::new();
for (key, value) in &self.0 {
let _ = sorted_map.insert(key, value);
}
for (i, (key, value)) in sorted_map.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "\"{}\": {}", escape_str(key), value)?;
}
write!(f, "}}")
}
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::Null => write!(f, "null"),
Value::String(s) => write!(f, "\"{}\"", escape_str(s)),
Value::Number(n) => {
if n.fract() == 0.0 {
write!(f, "{:.0}", n)
} else {
write!(f, "{}", n)
}
}
Value::Boolean(b) => write!(f, "{}", b),
Value::Array(arr) => {
write!(f, "[")?;
for (i, v) in arr.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", v)?;
}
write!(f, "]")
}
Value::Object(obj) => write!(f, "{}", obj),
Value::Tagged(tag, val) => {
write!(f, "\"{}\": {}", escape_str(tag), val)
}
}
}
}
pub fn escape_str(s: &str) -> String {
let mut escaped = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => escaped.push_str("\\\""),
'\\' => escaped.push_str("\\\\"),
_ => escaped.push(c),
}
}
escaped
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
mod format_tests {
use super::*;
#[test]
fn test_format_default() {
assert_eq!(Format::default(), Format::Json);
}
}
mod value_tests {
use super::*;
#[test]
fn test_value_default() {
assert_eq!(Value::default(), Value::Null);
}
#[test]
fn test_value_as_str() {
let value = Value::String("Hello".to_string());
assert_eq!(value.as_str(), Some("Hello"));
let value = Value::Number(42.0);
assert_eq!(value.as_str(), None);
}
#[test]
fn test_value_as_f64() {
let value = Value::Number(42.0);
assert_eq!(value.as_f64(), Some(42.0));
let value = Value::String("Not a number".to_string());
assert_eq!(value.as_f64(), None);
}
#[test]
fn test_value_as_bool() {
let value = Value::Boolean(true);
assert_eq!(value.as_bool(), Some(true));
let value = Value::String("Not a boolean".to_string());
assert_eq!(value.as_bool(), None);
}
#[test]
fn test_value_is_null() {
assert!(Value::Null.is_null());
assert!(!Value::String("Not null".to_string()).is_null());
}
#[test]
fn test_value_is_string() {
assert!(Value::String("test".to_string()).is_string());
assert!(!Value::Number(42.0).is_string());
}
#[test]
fn test_value_is_number() {
assert!(Value::Number(42.0).is_number());
assert!(!Value::String("42".to_string()).is_number());
}
#[test]
fn test_value_is_boolean() {
assert!(Value::Boolean(true).is_boolean());
assert!(!Value::String("true".to_string()).is_boolean());
}
#[test]
fn test_value_as_array() {
let value =
Value::Array(vec![Value::Null, Value::Boolean(false)]);
assert!(value.as_array().is_some());
assert_eq!(value.as_array().unwrap().len(), 2);
assert!(Value::String("Not an array".to_string())
.as_array()
.is_none());
}
#[test]
fn test_value_as_object() {
let mut fm = Frontmatter::new();
let _ = fm.insert(
"key".to_string(),
Value::String("value".to_string()),
);
let value = Value::Object(Box::new(fm.clone()));
assert_eq!(value.as_object().unwrap(), &fm);
assert!(Value::String("Not an object".to_string())
.as_object()
.is_none());
}
#[test]
fn test_value_to_object() {
let fm = Frontmatter::new();
let obj = Value::Object(Box::new(fm.clone()));
assert_eq!(obj.to_object().unwrap(), fm);
assert!(Value::String("Not an object".to_string())
.to_object()
.is_err());
}
#[test]
fn test_value_to_string_representation() {
assert_eq!(
Value::String("test".to_string())
.to_string_representation(),
"\"test\""
);
assert_eq!(
Value::Number(42.0).to_string_representation(),
"42"
);
assert_eq!(
Value::Boolean(true).to_string_representation(),
"true"
);
}
#[test]
fn test_value_display() {
assert_eq!(format!("{}", Value::Null), "null");
assert_eq!(
format!("{}", Value::String("test".to_string())),
"\"test\""
);
assert_eq!(
format!("{}", Value::Number(PI)),
format!("{}", PI)
);
assert_eq!(format!("{}", Value::Boolean(true)), "true");
}
}
mod frontmatter_tests {
use super::*;
#[test]
fn test_frontmatter_new() {
let fm = Frontmatter::new();
assert!(fm.is_empty());
assert_eq!(fm.len(), 0);
}
#[test]
fn test_frontmatter_insert_and_get() {
let mut fm = Frontmatter::new();
let _ = fm.insert(
"title".to_string(),
Value::String("Hello World".to_string()),
);
assert_eq!(
fm.get("title"),
Some(&Value::String("Hello World".to_string()))
);
}
#[test]
fn test_frontmatter_len_and_is_empty() {
let mut fm = Frontmatter::new();
assert!(fm.is_empty());
let _ = fm.insert("key1".to_string(), Value::Null);
assert_eq!(fm.len(), 1);
assert!(!fm.is_empty());
}
#[test]
fn test_frontmatter_merge() {
let mut fm1 = Frontmatter::new();
let _ = fm1.insert(
"key1".to_string(),
Value::String("value1".to_string()),
);
let mut fm2 = Frontmatter::new();
let _ = fm2.insert("key2".to_string(), Value::Number(42.0));
fm1.merge(fm2);
assert_eq!(fm1.len(), 2);
assert_eq!(fm1.get("key2"), Some(&Value::Number(42.0)));
}
#[test]
fn test_frontmatter_display() {
let mut fm = Frontmatter::new();
let _ = fm.insert(
"key1".to_string(),
Value::String("value1".to_string()),
);
let _ = fm.insert("key2".to_string(), Value::Number(42.0));
let display = format!("{}", fm);
assert!(display.contains("\"key1\": \"value1\""));
assert!(display.contains("\"key2\": 42"));
}
#[test]
fn test_frontmatter_is_null() {
let mut fm = Frontmatter::new();
let _ = fm.insert("key".to_string(), Value::Null);
assert!(fm.is_null("key"));
assert!(!fm.is_null("nonexistent_key"));
}
}
mod utility_tests {
use super::*;
#[test]
fn test_escape_str() {
assert_eq!(
escape_str(r#"Hello "World""#),
r#"Hello \"World\""#
);
assert_eq!(
escape_str(r"C:\path\to\file"),
r"C:\\path\\to\\file"
);
}
#[test]
fn test_escape_str_empty() {
assert_eq!(escape_str(""), "");
}
}
mod additional_tests {
use super::*;
#[test]
fn test_frontmatter_clear() {
let mut fm = Frontmatter::new();
let _ = fm.insert(
"key1".to_string(),
Value::String("value1".to_string()),
);
let _ = fm.insert("key2".to_string(), Value::Number(42.0));
fm.clear();
assert!(fm.is_empty());
assert_eq!(fm.len(), 0);
}
#[test]
fn test_frontmatter_capacity_and_reserve() {
let mut fm = Frontmatter::new();
let initial_capacity = fm.capacity();
fm.reserve(10);
assert!(fm.capacity() >= initial_capacity + 10);
}
#[test]
fn test_value_tagged() {
let tagged_value = Value::Tagged(
"tag".to_string(),
Box::new(Value::Number(42.0)),
);
if let Value::Tagged(tag, value) = tagged_value {
assert_eq!(tag, "tag");
assert_eq!(*value, Value::Number(42.0));
} else {
panic!("Expected Value::Tagged");
}
}
#[test]
fn test_value_array_mutation() {
let mut value = Value::Array(vec![
Value::Number(1.0),
Value::Number(2.0),
]);
if let Some(array) = value.get_mut_array() {
array.push(Value::Number(3.0));
}
assert_eq!(value.array_len(), Some(3));
assert!(value
.as_array()
.unwrap()
.contains(&Value::Number(3.0)));
}
#[test]
fn test_value_conversion_errors() {
let value = Value::Boolean(true);
assert!(value.clone().into_f64().is_err());
assert!(value.into_string().is_err());
let value = Value::Number(42.0);
assert!(value.into_bool().is_err());
}
#[test]
fn test_value_from_str_error_handling() {
assert_eq!("null".parse::<Value>().unwrap(), Value::Null);
assert_eq!(
"true".parse::<Value>().unwrap(),
Value::Boolean(true)
);
assert_eq!(
"false".parse::<Value>().unwrap(),
Value::Boolean(false)
);
let invalid_number = "abc123".parse::<Value>();
assert!(invalid_number.is_ok()); assert_eq!(
invalid_number.unwrap(),
Value::String("abc123".to_string())
);
}
#[test]
fn test_frontmatter_empty_iterator() {
let fm = Frontmatter::new();
let mut iter = fm.iter();
assert!(iter.next().is_none());
}
#[test]
fn test_frontmatter_duplicate_merge() {
let mut fm1 = Frontmatter::new();
let _ = fm1.insert(
"key1".to_string(),
Value::String("value1".to_string()),
);
let mut fm2 = Frontmatter::new();
let _ = fm2.insert(
"key1".to_string(),
Value::String("new_value".to_string()),
);
fm1.merge(fm2);
assert_eq!(
fm1.get("key1"),
Some(&Value::String("new_value".to_string()))
);
}
#[test]
fn test_display_for_empty_frontmatter() {
let fm = Frontmatter::new();
let display = format!("{}", fm);
assert_eq!(display, "{}");
}
#[test]
fn test_value_from_iterator_empty() {
let vec: Vec<Value> = vec![];
let array_value: Value = vec.into_iter().collect();
assert_eq!(array_value, Value::Array(vec![]));
}
#[test]
fn test_escape_str_edge_cases() {
let special_chars = r"Special \chars\n\t";
assert_eq!(
escape_str(special_chars),
r"Special \\chars\\n\\t"
);
}
}
}