1use indexmap::IndexMap;
15
16use crate::error::Result;
17
18#[derive(Debug, Clone, PartialEq, Default)]
21pub enum Value {
22 #[default]
24 Null,
25 Bool(bool),
27 Int(i64),
29 Float(f64),
31 String(String),
33 Sequence(Vec<Value>),
35 Mapping(Mapping),
37}
38
39pub type Mapping = IndexMap<String, Value>;
41
42impl Value {
43 pub fn as_str(&self) -> Option<&str> {
45 match self {
46 Value::String(s) => Some(s),
47 _ => None,
48 }
49 }
50
51 pub fn as_bool(&self) -> Option<bool> {
53 match self {
54 Value::Bool(b) => Some(*b),
55 _ => None,
56 }
57 }
58
59 pub fn as_sequence(&self) -> Option<&[Value]> {
61 match self {
62 Value::Sequence(v) => Some(v),
63 _ => None,
64 }
65 }
66
67 pub fn as_mapping(&self) -> Option<&Mapping> {
69 match self {
70 Value::Mapping(m) => Some(m),
71 _ => None,
72 }
73 }
74
75 pub fn is_null(&self) -> bool {
77 matches!(self, Value::Null)
78 }
79
80 pub fn get(&self, key: &str) -> Option<&Value> {
82 self.as_mapping().and_then(|m| m.get(key))
83 }
84
85 pub fn link_strings(&self) -> Vec<String> {
89 match self {
90 Value::String(s) => vec![s.clone()],
91 Value::Sequence(seq) => seq
92 .iter()
93 .filter_map(|v| v.as_str().map(str::to_owned))
94 .collect(),
95 _ => Vec::new(),
96 }
97 }
98}
99
100pub fn link_strings(value: &fig::Value) -> Vec<String> {
106 match value {
107 fig::Value::Str(s) => vec![s.clone()],
108 fig::Value::Seq(seq) => seq
109 .iter()
110 .filter_map(|v| v.as_str().map(str::to_owned))
111 .collect(),
112 _ => Vec::new(),
113 }
114}
115
116pub fn parse_value(s: &str, format: fig::Format) -> Result<Value> {
120 let doc = fig::Document::parse(s.as_bytes(), format)?;
121 Ok(Value::from(doc.to_value()?))
122}
123
124pub fn parse_mapping(s: &str, format: fig::Format) -> Result<Mapping> {
127 match parse_value(s, format)? {
128 Value::Mapping(m) => Ok(m),
129 Value::Null => Ok(Mapping::new()),
130 _ => Err(crate::error::Error::Structure(
131 "frontmatter must be a mapping".into(),
132 )),
133 }
134}
135
136pub fn serialize_mapping(map: &Mapping, format: fig::Format) -> Result<String> {
143 let value = fig::Value::from(&Value::Mapping(map.clone()));
144 Ok(value.serialize_with(format, fig::SerializeOptions::default().width(1))?)
145}
146
147pub fn serialize_value(value: &Value, format: fig::Format) -> Result<String> {
151 Ok(
152 fig::Value::from(value)
153 .serialize_with(format, fig::SerializeOptions::default().width(1))?,
154 )
155}
156
157impl From<&Value> for fig::Value {
162 fn from(value: &Value) -> Self {
163 match value {
164 Value::Null => fig::Value::Null,
165 Value::Bool(b) => fig::Value::Bool(*b),
166 Value::Int(i) => fig::Value::Int(*i),
167 Value::Float(f) => fig::Value::Float(*f),
168 Value::String(s) => fig::Value::Str(s.clone()),
169 Value::Sequence(seq) => fig::Value::Seq(seq.iter().map(fig::Value::from).collect()),
170 Value::Mapping(map) => fig::Value::Map(
171 map.iter()
172 .map(|(k, v)| (fig::Value::Str(k.clone()), fig::Value::from(v)))
173 .collect(),
174 ),
175 }
176 }
177}
178
179impl From<fig::Value> for Value {
180 fn from(value: fig::Value) -> Self {
181 match value {
182 fig::Value::Null => Value::Null,
183 fig::Value::Bool(b) => Value::Bool(b),
184 fig::Value::Int(i) => Value::Int(i),
185 fig::Value::Uint(u) => {
186 if u <= i64::MAX as u64 {
187 Value::Int(u as i64)
188 } else {
189 Value::Float(u as f64)
190 }
191 }
192 fig::Value::Float(f) => Value::Float(f),
193 fig::Value::Str(s) => Value::String(s),
194 fig::Value::Extended { text, .. } => Value::String(text),
197 fig::Value::Seq(items) => Value::Sequence(items.into_iter().map(Value::from).collect()),
198 fig::Value::Map(entries) => {
199 let mut map = IndexMap::with_capacity(entries.len());
200 for (k, v) in entries {
201 map.insert(fig_key_to_string(k), Value::from(v));
202 }
203 Value::Mapping(map)
204 }
205 }
206 }
207}
208
209fn fig_key_to_string(key: fig::Value) -> String {
212 match key {
213 fig::Value::Str(s) => s,
214 fig::Value::Bool(b) => b.to_string(),
215 fig::Value::Int(i) => i.to_string(),
216 fig::Value::Uint(u) => u.to_string(),
217 fig::Value::Null => "null".to_string(),
218 fig::Value::Extended { text, .. } => text,
219 fig::Value::Float(_) | fig::Value::Seq(_) | fig::Value::Map(_) => String::new(),
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[cfg(feature = "yaml")]
228 #[test]
229 fn parses_frontmatter_mapping() {
230 let m = parse_mapping(
231 "title: Hello\ncount: 42\ntags:\n- a\n- b\n",
232 fig::Format::Yaml,
233 )
234 .unwrap();
235 assert_eq!(m.get("title").and_then(Value::as_str), Some("Hello"));
236 assert_eq!(m.get("count"), Some(&Value::Int(42)));
237 assert_eq!(
238 m.get("tags").map(Value::link_strings),
239 Some(vec!["a".to_string(), "b".to_string()])
240 );
241 }
242
243 #[cfg(feature = "fig-lang")]
244 #[test]
245 fn parses_fig_dialect_mapping() {
246 let m = parse_mapping("title = Hello\ntags = [a, b]\n", fig::Format::Fig).unwrap();
247 assert_eq!(m.get("title").and_then(Value::as_str), Some("Hello"));
248 assert_eq!(
249 m.get("tags").map(Value::link_strings),
250 Some(vec!["a".to_string(), "b".to_string()])
251 );
252 }
253
254 #[test]
255 fn link_strings_handles_scalar_and_sequence() {
256 assert_eq!(Value::String("x".into()).link_strings(), vec!["x"]);
257 let seq = Value::Sequence(vec![
258 Value::String("a".into()),
259 Value::Int(3),
260 Value::String("b".into()),
261 ]);
262 assert_eq!(seq.link_strings(), vec!["a".to_string(), "b".to_string()]);
263 assert!(Value::Null.link_strings().is_empty());
264 }
265
266 #[cfg(all(feature = "yaml", feature = "fig-lang"))]
267 #[test]
268 fn round_trips_through_fig() {
269 for format in [fig::Format::Yaml, fig::Format::Fig] {
270 let m = parse_mapping(
271 "title: Root\ncontents:\n- a.md\n- b.md\n",
272 fig::Format::Yaml,
273 )
274 .unwrap();
275 let out = serialize_mapping(&m, format).unwrap();
276 let reparsed = parse_mapping(&out, format).unwrap();
277 assert_eq!(m, reparsed, "round-trip through {format:?}");
278 }
279 }
280}