use indexmap::IndexMap;
use crate::error::Result;
#[derive(Debug, Clone, PartialEq, Default)]
pub enum Value {
#[default]
Null,
Bool(bool),
Int(i64),
Float(f64),
String(String),
Sequence(Vec<Value>),
Mapping(Mapping),
}
pub type Mapping = IndexMap<String, Value>;
impl Value {
pub fn as_str(&self) -> Option<&str> {
match self {
Value::String(s) => Some(s),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Value::Bool(b) => Some(*b),
_ => None,
}
}
pub fn as_sequence(&self) -> Option<&[Value]> {
match self {
Value::Sequence(v) => Some(v),
_ => None,
}
}
pub fn as_mapping(&self) -> Option<&Mapping> {
match self {
Value::Mapping(m) => Some(m),
_ => None,
}
}
pub fn is_null(&self) -> bool {
matches!(self, Value::Null)
}
pub fn get(&self, key: &str) -> Option<&Value> {
self.as_mapping().and_then(|m| m.get(key))
}
pub fn link_strings(&self) -> Vec<String> {
match self {
Value::String(s) => vec![s.clone()],
Value::Sequence(seq) => seq
.iter()
.filter_map(|v| v.as_str().map(str::to_owned))
.collect(),
_ => Vec::new(),
}
}
}
pub fn parse_value(s: &str, format: fig::Format) -> Result<Value> {
let doc = fig::Document::parse(s.as_bytes(), format)?;
Ok(Value::from(doc.to_value()?))
}
pub fn parse_mapping(s: &str, format: fig::Format) -> Result<Mapping> {
match parse_value(s, format)? {
Value::Mapping(m) => Ok(m),
Value::Null => Ok(Mapping::new()),
_ => Err(crate::error::Error::Structure(
"frontmatter must be a mapping".into(),
)),
}
}
pub fn serialize_mapping(map: &Mapping, format: fig::Format) -> Result<String> {
let value = fig::Value::from(&Value::Mapping(map.clone()));
Ok(value.serialize_with(format, fig::SerializeOptions::default().width(1))?)
}
pub fn serialize_value(value: &Value, format: fig::Format) -> Result<String> {
Ok(
fig::Value::from(value)
.serialize_with(format, fig::SerializeOptions::default().width(1))?,
)
}
impl From<&Value> for fig::Value {
fn from(value: &Value) -> Self {
match value {
Value::Null => fig::Value::Null,
Value::Bool(b) => fig::Value::Bool(*b),
Value::Int(i) => fig::Value::Int(*i),
Value::Float(f) => fig::Value::Float(*f),
Value::String(s) => fig::Value::Str(s.clone()),
Value::Sequence(seq) => fig::Value::Seq(seq.iter().map(fig::Value::from).collect()),
Value::Mapping(map) => fig::Value::Map(
map.iter()
.map(|(k, v)| (fig::Value::Str(k.clone()), fig::Value::from(v)))
.collect(),
),
}
}
}
impl From<fig::Value> for Value {
fn from(value: fig::Value) -> Self {
match value {
fig::Value::Null => Value::Null,
fig::Value::Bool(b) => Value::Bool(b),
fig::Value::Int(i) => Value::Int(i),
fig::Value::Uint(u) => {
if u <= i64::MAX as u64 {
Value::Int(u as i64)
} else {
Value::Float(u as f64)
}
}
fig::Value::Float(f) => Value::Float(f),
fig::Value::Str(s) => Value::String(s),
fig::Value::Extended { text, .. } => Value::String(text),
fig::Value::Seq(items) => Value::Sequence(items.into_iter().map(Value::from).collect()),
fig::Value::Map(entries) => {
let mut map = IndexMap::with_capacity(entries.len());
for (k, v) in entries {
map.insert(fig_key_to_string(k), Value::from(v));
}
Value::Mapping(map)
}
}
}
}
fn fig_key_to_string(key: fig::Value) -> String {
match key {
fig::Value::Str(s) => s,
fig::Value::Bool(b) => b.to_string(),
fig::Value::Int(i) => i.to_string(),
fig::Value::Uint(u) => u.to_string(),
fig::Value::Null => "null".to_string(),
fig::Value::Extended { text, .. } => text,
fig::Value::Float(_) | fig::Value::Seq(_) | fig::Value::Map(_) => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "yaml")]
#[test]
fn parses_frontmatter_mapping() {
let m = parse_mapping(
"title: Hello\ncount: 42\ntags:\n- a\n- b\n",
fig::Format::Yaml,
)
.unwrap();
assert_eq!(m.get("title").and_then(Value::as_str), Some("Hello"));
assert_eq!(m.get("count"), Some(&Value::Int(42)));
assert_eq!(
m.get("tags").map(Value::link_strings),
Some(vec!["a".to_string(), "b".to_string()])
);
}
#[cfg(feature = "fig-lang")]
#[test]
fn parses_fig_dialect_mapping() {
let m = parse_mapping("title = Hello\ntags = [a, b]\n", fig::Format::Fig).unwrap();
assert_eq!(m.get("title").and_then(Value::as_str), Some("Hello"));
assert_eq!(
m.get("tags").map(Value::link_strings),
Some(vec!["a".to_string(), "b".to_string()])
);
}
#[test]
fn link_strings_handles_scalar_and_sequence() {
assert_eq!(Value::String("x".into()).link_strings(), vec!["x"]);
let seq = Value::Sequence(vec![
Value::String("a".into()),
Value::Int(3),
Value::String("b".into()),
]);
assert_eq!(seq.link_strings(), vec!["a".to_string(), "b".to_string()]);
assert!(Value::Null.link_strings().is_empty());
}
#[cfg(all(feature = "yaml", feature = "fig-lang"))]
#[test]
fn round_trips_through_fig() {
for format in [fig::Format::Yaml, fig::Format::Fig] {
let m = parse_mapping(
"title: Root\ncontents:\n- a.md\n- b.md\n",
fig::Format::Yaml,
)
.unwrap();
let out = serialize_mapping(&m, format).unwrap();
let reparsed = parse_mapping(&out, format).unwrap();
assert_eq!(m, reparsed, "round-trip through {format:?}");
}
}
}