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