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 get_path(&self, path: &str) -> Option<&Value> {
93 path.split('.').try_fold(self, |value, key| value.get(key))
94 }
95
96 pub fn link_strings(&self) -> Vec<String> {
100 match self {
101 Value::String(s) => vec![s.clone()],
102 Value::Sequence(seq) => seq
103 .iter()
104 .filter_map(|v| v.as_str().map(str::to_owned))
105 .collect(),
106 _ => Vec::new(),
107 }
108 }
109}
110
111pub fn insert_path(map: &mut Mapping, path: &str, value: Value) {
116 let mut segments = path.split('.').peekable();
117 let mut map = map;
118 while let Some(key) = segments.next() {
119 if segments.peek().is_none() {
120 map.insert(key.to_string(), value);
121 return;
122 }
123 let entry = map
124 .entry(key.to_string())
125 .or_insert_with(|| Value::Mapping(Mapping::new()));
126 if !matches!(entry, Value::Mapping(_)) {
127 *entry = Value::Mapping(Mapping::new());
128 }
129 let Value::Mapping(inner) = entry else {
130 unreachable!()
131 };
132 map = inner;
133 }
134}
135
136pub fn link_strings(value: &fig::Value) -> Vec<String> {
142 match value {
143 fig::Value::Str(s) => vec![s.clone()],
144 fig::Value::Seq(seq) => seq
145 .iter()
146 .filter_map(|v| v.as_str().map(str::to_owned))
147 .collect(),
148 _ => Vec::new(),
149 }
150}
151
152pub fn indexed_link_strings(value: &fig::Value) -> Vec<(Option<usize>, String)> {
157 match value {
158 fig::Value::Str(s) => vec![(None, s.clone())],
159 fig::Value::Seq(seq) => seq
160 .iter()
161 .enumerate()
162 .filter_map(|(i, v)| v.as_str().map(|s| (Some(i), s.to_owned())))
163 .collect(),
164 _ => Vec::new(),
165 }
166}
167
168pub fn parse_value(s: &str, format: fig::Format) -> Result<Value> {
172 let doc = fig::Document::parse(s.as_bytes(), format)?;
173 Ok(Value::from(doc.to_value()?))
174}
175
176pub fn parse_mapping(s: &str, format: fig::Format) -> Result<Mapping> {
179 match parse_value(s, format)? {
180 Value::Mapping(m) => Ok(m),
181 Value::Null => Ok(Mapping::new()),
182 _ => Err(crate::error::Error::Structure(
183 "frontmatter must be a mapping".into(),
184 )),
185 }
186}
187
188pub fn serialize_mapping(map: &Mapping, format: fig::Format) -> Result<String> {
195 let value = fig::Value::from(&Value::Mapping(map.clone()));
196 Ok(value.serialize_with(format, fig::SerializeOptions::default().width(1))?)
197}
198
199pub fn serialize_value(value: &Value, format: fig::Format) -> Result<String> {
203 Ok(
204 fig::Value::from(value)
205 .serialize_with(format, fig::SerializeOptions::default().width(1))?,
206 )
207}
208
209impl From<&Value> for fig::Value {
214 fn from(value: &Value) -> Self {
215 match value {
216 Value::Null => fig::Value::Null,
217 Value::Bool(b) => fig::Value::Bool(*b),
218 Value::Int(i) => fig::Value::Int(*i),
219 Value::Float(f) => fig::Value::Float(*f),
220 Value::String(s) => fig::Value::Str(s.clone()),
221 Value::Sequence(seq) => fig::Value::Seq(seq.iter().map(fig::Value::from).collect()),
222 Value::Mapping(map) => fig::Value::Map(
223 map.iter()
224 .map(|(k, v)| (fig::Value::Str(k.clone()), fig::Value::from(v)))
225 .collect(),
226 ),
227 }
228 }
229}
230
231impl From<fig::Value> for Value {
232 fn from(value: fig::Value) -> Self {
233 match value {
234 fig::Value::Null => Value::Null,
235 fig::Value::Bool(b) => Value::Bool(b),
236 fig::Value::Int(i) => Value::Int(i),
237 fig::Value::Uint(u) => {
238 if u <= i64::MAX as u64 {
239 Value::Int(u as i64)
240 } else {
241 Value::Float(u as f64)
242 }
243 }
244 fig::Value::Float(f) => Value::Float(f),
245 fig::Value::Str(s) => Value::String(s),
246 fig::Value::Extended { text, .. } => Value::String(text),
249 fig::Value::Seq(items) => Value::Sequence(items.into_iter().map(Value::from).collect()),
250 fig::Value::Map(entries) => {
251 let mut map = IndexMap::with_capacity(entries.len());
252 for (k, v) in entries {
253 map.insert(fig_key_to_string(k), Value::from(v));
254 }
255 Value::Mapping(map)
256 }
257 }
258 }
259}
260
261fn fig_key_to_string(key: fig::Value) -> String {
264 match key {
265 fig::Value::Str(s) => s,
266 fig::Value::Bool(b) => b.to_string(),
267 fig::Value::Int(i) => i.to_string(),
268 fig::Value::Uint(u) => u.to_string(),
269 fig::Value::Null => "null".to_string(),
270 fig::Value::Extended { text, .. } => text,
271 fig::Value::Float(_) | fig::Value::Seq(_) | fig::Value::Map(_) => String::new(),
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[cfg(feature = "yaml")]
280 #[test]
281 fn parses_frontmatter_mapping() {
282 let m = parse_mapping(
283 "title: Hello\ncount: 42\ntags:\n- a\n- b\n",
284 fig::Format::Yaml,
285 )
286 .unwrap();
287 assert_eq!(m.get("title").and_then(Value::as_str), Some("Hello"));
288 assert_eq!(m.get("count"), Some(&Value::Int(42)));
289 assert_eq!(
290 m.get("tags").map(Value::link_strings),
291 Some(vec!["a".to_string(), "b".to_string()])
292 );
293 }
294
295 #[cfg(feature = "fig-lang")]
296 #[test]
297 fn parses_fig_dialect_mapping() {
298 let m = parse_mapping("title = Hello\ntags = [a, b]\n", fig::Format::Fig).unwrap();
299 assert_eq!(m.get("title").and_then(Value::as_str), Some("Hello"));
300 assert_eq!(
301 m.get("tags").map(Value::link_strings),
302 Some(vec!["a".to_string(), "b".to_string()])
303 );
304 }
305
306 #[test]
307 fn get_path_reaches_into_a_mapping_and_insert_path_writes_there() {
308 let mut m = Mapping::new();
309 insert_path(&mut m, "generated.how", Value::String("drafted".into()));
310 insert_path(&mut m, "title", Value::String("x".into()));
311 let v = Value::Mapping(m);
312 assert_eq!(v.get_path("title").and_then(Value::as_str), Some("x"));
313 assert_eq!(
314 v.get_path("generated.how").and_then(Value::as_str),
315 Some("drafted")
316 );
317 assert!(v.get_path("generated.by").is_none());
318 assert!(v.get_path("title.how").is_none());
319 assert!(v.get_path("missing").is_none());
320 }
321
322 #[test]
323 fn link_strings_handles_scalar_and_sequence() {
324 assert_eq!(Value::String("x".into()).link_strings(), vec!["x"]);
325 let seq = Value::Sequence(vec![
326 Value::String("a".into()),
327 Value::Int(3),
328 Value::String("b".into()),
329 ]);
330 assert_eq!(seq.link_strings(), vec!["a".to_string(), "b".to_string()]);
331 assert!(Value::Null.link_strings().is_empty());
332 }
333
334 #[cfg(all(feature = "yaml", feature = "fig-lang"))]
335 #[test]
336 fn round_trips_through_fig() {
337 for format in [fig::Format::Yaml, fig::Format::Fig] {
338 let m = parse_mapping(
339 "title: Root\ncontents:\n- a.md\n- b.md\n",
340 fig::Format::Yaml,
341 )
342 .unwrap();
343 let out = serialize_mapping(&m, format).unwrap();
344 let reparsed = parse_mapping(&out, format).unwrap();
345 assert_eq!(m, reparsed, "round-trip through {format:?}");
346 }
347 }
348}