Skip to main content

nickel_lang_core/serialize/
mod.rs

1//! Serialization of an evaluated program to various data format.
2use crate::{
3    error::{ExportErrorKind, PointedExportErrorData},
4    eval::value::{ArrayData, Container, EnumVariantData, NickelValue, ValueContentRef},
5    identifier::{Ident, LocIdent},
6    metrics,
7    term::{IndexMap, Number, TypeAnnotation, record::RecordData},
8};
9
10use serde::{
11    de::{Deserialize, Deserializer},
12    ser::{Serialize, SerializeMap, SerializeSeq, Serializer},
13};
14
15use malachite::base::{
16    num::{
17        basic::floats::PrimitiveFloat,
18        conversion::traits::{IsInteger, RoundingFrom},
19    },
20    rounding_modes::RoundingMode,
21};
22use once_cell::sync::Lazy;
23
24use std::{fmt, io};
25
26pub mod yaml;
27
28/// Available export formats.
29#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
30#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
31pub enum ExportFormat {
32    /// Evaluate a Nickel expression to a string and write that text to the output
33    /// Note: `raw` is a deprecated alias for `text`; prefer `text` instead.
34    #[cfg_attr(feature = "clap", value(alias("raw")))]
35    Text,
36    #[default]
37    Json,
38    Yaml,
39    /// Like `Yaml`, but generates multiple YAML documents in a single file,
40    /// separated by `---`. The output data must be a list: each list element is
41    /// converted into its own document.
42    YamlDocuments,
43    Toml,
44}
45
46impl fmt::Display for ExportFormat {
47    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
48        match self {
49            Self::Text => write!(f, "text"),
50            Self::Json => write!(f, "json"),
51            Self::Yaml => write!(f, "yaml"),
52            Self::YamlDocuments => write!(f, "yaml-documents"),
53            Self::Toml => write!(f, "toml"),
54        }
55    }
56}
57
58/// Available metadata export formats.
59#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
60#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
61pub enum MetadataExportFormat {
62    #[default]
63    Markdown,
64    Json,
65    Yaml,
66    Toml,
67}
68
69impl fmt::Display for MetadataExportFormat {
70    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
71        match self {
72            Self::Markdown => write!(f, "markdown"),
73            Self::Json => write!(f, "json"),
74            Self::Yaml => write!(f, "yaml"),
75            Self::Toml => write!(f, "toml"),
76        }
77    }
78}
79
80#[derive(Clone, Eq, PartialEq, Debug)]
81pub struct ParseFormatError(String);
82
83impl fmt::Display for ParseFormatError {
84    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
85        write!(f, "unsupported export format {}", self.0)
86    }
87}
88
89/// Implicitly convert numbers to primitive integers when possible, and serialize an exact
90/// representation. Note that `u128` and `i128` aren't supported for common configuration formats in
91/// serde, so we rather pick `i64` and `u64`, even if the former couple theoretically allows for a
92/// wider range of rationals to be exactly represented. We don't expect values to be that large in
93/// practice anyway: using arbitrary precision rationals is directed toward not introducing rounding
94/// errors when performing simple arithmetic operations over decimals numbers, mostly.
95///
96/// If the number doesn't fit into an `i64` or `u64`, we approximate it by the nearest `f64` and
97/// serialize this value. This may incur a loss of precision, but this is expected: we can't
98/// represent something like e.g. `1/3` exactly in JSON anyway.
99pub fn serialize_num<S>(n: &Number, serializer: S) -> Result<S::Ok, S::Error>
100where
101    S: Serializer,
102{
103    if n.is_integer() {
104        if *n < 0 {
105            if let Ok(n_as_integer) = i64::try_from(n) {
106                return n_as_integer.serialize(serializer);
107            }
108        } else if let Ok(n_as_uinteger) = u64::try_from(n) {
109            return n_as_uinteger.serialize(serializer);
110        }
111    }
112
113    f64::rounding_from(n, RoundingMode::Nearest)
114        .0
115        .serialize(serializer)
116}
117
118/// Helper function to convert a serialized primitive float to a Nickel number. Return a deserialize error if
119/// the floating point value is either NaN or infinity.
120fn number_from_float<F: PrimitiveFloat, E: serde::de::Error>(float_value: F) -> Result<Number, E>
121where
122    Number: TryFrom<
123            F,
124            Error = malachite_q::conversion::from_primitive_float::RationalFromPrimitiveFloatError,
125        >,
126{
127    Number::try_from_float_simplest(float_value).map_err(|_| {
128        E::custom(format!(
129            "couldn't convert {float_value} to a Nickel number: Nickel doesn't support NaN nor infinity"
130        ))
131    })
132}
133
134/// Deserialize a Nickel number. As for parsing, we convert the number from a 64bits float.
135pub fn deserialize_num<'de, D>(deserializer: D) -> Result<Number, D::Error>
136where
137    D: Deserializer<'de>,
138{
139    let as_f64 = f64::deserialize(deserializer)?;
140    number_from_float::<f64, D::Error>(as_f64)
141}
142
143/// Serializer for annotated values.
144pub fn serialize_annotated_value<S>(
145    _annot: &TypeAnnotation,
146    t: &NickelValue,
147    serializer: S,
148) -> Result<S::Ok, S::Error>
149where
150    S: Serializer,
151{
152    t.serialize(serializer)
153}
154
155/// Serializer for a record. Serialize fields in alphabetical order to get a deterministic output
156pub fn serialize_record<S>(record: &RecordData, serializer: S) -> Result<S::Ok, S::Error>
157where
158    S: Serializer,
159{
160    let mut entries = record
161        .iter_serializable()
162        .collect::<Result<Vec<_>, _>>()
163        .map_err(|missing_def_err| {
164            serde::ser::Error::custom(format!(
165                "missing field definition for `{}`",
166                missing_def_err.id
167            ))
168        })?;
169
170    entries.sort_by_key(|(k, _)| *k);
171
172    let mut map_ser = serializer.serialize_map(Some(entries.len()))?;
173    for (id, t) in entries.iter() {
174        map_ser.serialize_entry(&id.to_string(), &t)?
175    }
176
177    map_ser.end()
178}
179
180/// Deserialize for a record. Required to set the record attributes to default.
181pub fn deserialize_record<'de, D>(deserializer: D) -> Result<RecordData, D::Error>
182where
183    D: Deserializer<'de>,
184{
185    let fields = IndexMap::<LocIdent, _>::deserialize(deserializer)?;
186    Ok(RecordData::with_field_values(fields))
187}
188
189impl Serialize for NickelValue {
190    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
191    where
192        S: Serializer,
193    {
194        match self.content_ref() {
195            ValueContentRef::Null => serializer.serialize_none(),
196            ValueContentRef::Bool(b) => serializer.serialize_bool(b),
197            ValueContentRef::Number(n) => serialize_num(n, serializer),
198            ValueContentRef::String(s) => serializer.serialize_str(s),
199            ValueContentRef::EnumVariant(EnumVariantData { tag, arg: None }) => {
200                serializer.serialize_str(tag.label())
201            }
202            ValueContentRef::EnumVariant(EnumVariantData { tag, arg: Some(_) }) => {
203                Err(serde::ser::Error::custom(format!(
204                    "cannot serialize enum variant `'{tag}` with non-empty argument"
205                )))
206            }
207            ValueContentRef::Record(Container::Empty) => {
208                let map_ser = serializer.serialize_map(Some(0))?;
209                map_ser.end()
210            }
211            ValueContentRef::Record(Container::Alloc(record)) => {
212                serialize_record(record, serializer)
213            }
214            ValueContentRef::Array(Container::Empty) => {
215                let seq_ser = serializer.serialize_seq(Some(0))?;
216                seq_ser.end()
217            }
218            ValueContentRef::Array(Container::Alloc(ArrayData { array, .. })) => {
219                let mut seq_ser = serializer.serialize_seq(Some(array.len()))?;
220                for elt in array.iter() {
221                    seq_ser.serialize_element(elt)?
222                }
223                seq_ser.end()
224            }
225            _ => Err(serde::ser::Error::custom(format!(
226                "cannot serialize non-fully evaluated terms of type {}",
227                self.type_of().unwrap_or("unknown")
228            ))),
229        }
230    }
231}
232
233// This macro generates boilerplate visitors for the various serde number types to be included in
234// the `NickelValue` deserialize implementation.
235macro_rules! def_number_visitor {
236    ($($ty:ty),*) => {
237        $(
238            paste::paste! {
239                fn [<visit_ $ty>]<E>(self, v: $ty) -> Result<Self::Value, E>
240                where
241                    E: serde::de::Error,
242                {
243                    Ok(NickelValue::number_posless(v))
244                }
245            }
246        )*
247    };
248}
249
250impl<'de> Deserialize<'de> for NickelValue {
251    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
252    where
253        D: Deserializer<'de>,
254    {
255        struct NickelValueVisitor;
256
257        impl<'de> serde::de::Visitor<'de> for NickelValueVisitor {
258            type Value = NickelValue;
259
260            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
261                formatter.write_str("a valid Nickel value")
262            }
263
264            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
265            where
266                E: serde::de::Error,
267            {
268                Ok(NickelValue::bool_value_posless(v))
269            }
270
271            def_number_visitor!(i8, u8, i16, u16, i32, u32, i64, u64, i128, u128);
272
273            fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E>
274            where
275                E: serde::de::Error,
276            {
277                let n = number_from_float::<f32, E>(v)?;
278                Ok(NickelValue::number_posless(n))
279            }
280
281            fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
282            where
283                E: serde::de::Error,
284            {
285                let n = number_from_float::<f64, E>(v)?;
286                Ok(NickelValue::number_posless(n))
287            }
288
289            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
290            where
291                E: serde::de::Error,
292            {
293                Ok(NickelValue::string_posless(v))
294            }
295
296            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
297            where
298                E: serde::de::Error,
299            {
300                Ok(NickelValue::string_posless(v))
301            }
302
303            fn visit_none<E>(self) -> Result<Self::Value, E>
304            where
305                E: serde::de::Error,
306            {
307                Ok(NickelValue::null())
308            }
309
310            fn visit_unit<E>(self) -> Result<Self::Value, E>
311            where
312                E: serde::de::Error,
313            {
314                Ok(NickelValue::null())
315            }
316
317            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
318            where
319                A: serde::de::SeqAccess<'de>,
320            {
321                let mut elts = Vec::with_capacity(seq.size_hint().unwrap_or(0));
322
323                while let Some(elem) = seq.next_element()? {
324                    elts.push(elem);
325                }
326
327                Ok(NickelValue::array_posless(
328                    elts.into_iter().collect(),
329                    Vec::new(),
330                ))
331            }
332
333            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
334            where
335                A: serde::de::MapAccess<'de>,
336            {
337                let mut fields = IndexMap::with_capacity(map.size_hint().unwrap_or(0));
338
339                while let Some((key, value)) = map.next_entry::<String, NickelValue>()? {
340                    fields.insert(key.into(), value);
341                }
342
343                Ok(NickelValue::record_posless(RecordData::with_field_values(
344                    fields,
345                )))
346            }
347        }
348
349        deserializer.deserialize_any(NickelValueVisitor)
350    }
351}
352
353/// Element of a path to a specific value within a serialized term. See [NickelPointer].
354#[derive(Debug, PartialEq, Clone)]
355pub enum NickelPointerElem {
356    Field(Ident),
357    Index(usize),
358}
359
360impl fmt::Display for NickelPointerElem {
361    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362        match self {
363            NickelPointerElem::Field(id) => write!(f, "{id}"),
364            NickelPointerElem::Index(i) => write!(f, "[{i}]"),
365        }
366    }
367}
368
369/// A pointer to a specific value within a serialized term. The name is inspired from [JSON
370/// pointer](https://datatracker.ietf.org/doc/html/rfc6901), which is an equivalent notion.
371///
372/// In a serialized term, there can only be constants (numbers, strings, booleans, null) and two
373/// kinds of containers: records and lists. To locate a particular value within a serialized term,
374/// we can use a path of field names and indices.
375///
376/// # Example
377///
378/// In the following full evaluated program:
379///
380/// ```nickel
381/// {
382///   foo = {
383///     bar = ["hello", "world"],
384///     other = null,
385///   }
386/// }
387/// ```
388///
389/// The path to the string `"world"` is `[Field("foo"), Field("bar"), Index(1)]`. This is
390/// represented (e.g. in error messages) as `foo.bar[1]`.
391#[derive(Debug, PartialEq, Clone, Default)]
392pub struct NickelPointer(pub Vec<NickelPointerElem>);
393
394impl NickelPointer {
395    pub fn new() -> Self {
396        Self::default()
397    }
398}
399
400impl fmt::Display for NickelPointer {
401    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402        let mut it = self.0.iter();
403        let Some(first) = it.next() else {
404            return Ok(());
405        };
406
407        write!(f, "{first}")?;
408
409        for elem in it {
410            if let NickelPointerElem::Field(_) = elem {
411                write!(f, ".{elem}")?
412            } else {
413                write!(f, "{elem}")?
414            }
415        }
416
417        Ok(())
418    }
419}
420
421/// Check that a term is serializable. Serializable terms are booleans, numbers, strings, enum,
422/// arrays of serializable terms or records of serializable terms.
423pub fn validate(format: ExportFormat, value: &NickelValue) -> Result<(), PointedExportErrorData> {
424    // The max and min value that we accept to serialize as a number. Because Nickel uses arbitrary
425    // precision rationals, we could actually support a wider range of numbers, but we expect that
426    // implementations consuming the resulting JSON (or similar formats) won't necessary be able to
427    // handle values that don't fit in a 64 bits float.
428    static NUMBER_MIN: Lazy<Number> = Lazy::new(|| Number::try_from(f64::MIN).unwrap());
429    static NUMBER_MAX: Lazy<Number> = Lazy::new(|| Number::try_from(f64::MAX).unwrap());
430
431    // We need to build a field path locating a potential export error. One way would be to pass a
432    // context storing the current path to recursive calls of `validate`. However, representing
433    // this context isn't entirely trivial: using an owned `Vec` will incur a lot of copying, even
434    // in the happy path (no error), because the current path needs to be shared among all sibling
435    // fields of a record. What we want is persistent linked list (cheap to clone and to extend),
436    // but Rust doesn't have a built-in type for that. It's not very hard to implement manually, or
437    // to use an external crate, but it's still more code.
438    //
439    // Instead, what we do is to return the current field in case of an error. Then, recursive
440    // calls to `do_validate` can unwrap the error and add their own field to the path, so that
441    // when the chain of recursive calls finally returns, we have reconstructed the full path (in
442    // some sense, we're encoding the list in the OS stack). Not only this doesn't require any
443    // additional data structure, but we expect that it's performant, as in the happy path branch
444    // prediction should be able to cancel the error code path.
445    //
446    // `do_validate` is the method doing the actual validation. The only reason this code is put in
447    // a separate subfunction is that since we reconstruct the path bottom-up, it needs to be
448    // reversed before finally returning from validate.
449    fn do_validate(
450        format: ExportFormat,
451        value: &NickelValue,
452    ) -> Result<(), PointedExportErrorData> {
453        match value.content_ref() {
454            // TOML doesn't support null values
455            ValueContentRef::Null
456                if format == ExportFormat::Json || format == ExportFormat::Yaml =>
457            {
458                Ok(())
459            }
460            ValueContentRef::Null => {
461                Err(ExportErrorKind::UnsupportedNull(format, value.clone()).into())
462            }
463            ValueContentRef::Bool(_)
464            | ValueContentRef::Record(Container::Empty)
465            | ValueContentRef::Array(Container::Empty)
466            | ValueContentRef::String(_) => Ok(()),
467            ValueContentRef::EnumVariant(EnumVariantData { arg: None, .. }) => Ok(()),
468            ValueContentRef::EnumVariant(EnumVariantData { arg: Some(_), .. }) => {
469                Err(ExportErrorKind::NonSerializable(value.clone()).into())
470            }
471            ValueContentRef::Number(n) => {
472                if *n >= *NUMBER_MIN && *n <= *NUMBER_MAX {
473                    Ok(())
474                } else {
475                    Err(ExportErrorKind::NumberOutOfRange {
476                        term: value.clone(),
477                        value: n.clone(),
478                    }
479                    .into())
480                }
481            }
482            ValueContentRef::Record(Container::Alloc(record)) => {
483                record.iter_serializable().try_for_each(|binding| {
484                    // unwrap(): terms must be fully evaluated before being validated for
485                    // serialization. Otherwise, it's an internal error.
486                    let (id, value) = binding.unwrap_or_else(|err| {
487                        panic!(
488                            "encountered field without definition `{}` \
489                            during pre-serialization validation",
490                            err.id
491                        )
492                    });
493
494                    do_validate(format, value)
495                        .map_err(|err| err.with_elem(NickelPointerElem::Field(id)))
496                })?;
497                Ok(())
498            }
499            ValueContentRef::Array(Container::Alloc(array_data)) => {
500                array_data
501                    .array
502                    .iter()
503                    .enumerate()
504                    .try_for_each(|(index, val)| {
505                        do_validate(format, val)
506                            .map_err(|err| err.with_elem(NickelPointerElem::Index(index)))
507                    })?;
508                Ok(())
509            }
510            _ => Err(ExportErrorKind::NonSerializable(value.clone()).into()),
511        }
512    }
513
514    if format == ExportFormat::Text {
515        if value.as_string().is_some() {
516            Ok(())
517        } else {
518            Err(ExportErrorKind::NotAString(value.clone()).into())
519        }
520    } else {
521        let mut result = do_validate(format, value);
522
523        if let Err(PointedExportErrorData { path, .. }) = &mut result {
524            path.0.reverse();
525        }
526
527        result
528    }
529}
530
531pub fn to_writer_metadata<W, T>(
532    mut writer: W,
533    format: MetadataExportFormat,
534    item: &T,
535) -> Result<(), PointedExportErrorData>
536where
537    W: io::Write,
538    T: ?Sized + Serialize,
539{
540    // This is a near-verbatim copy of `to_writer`
541    match format {
542        MetadataExportFormat::Markdown => unimplemented!(),
543        MetadataExportFormat::Json => serde_json::to_writer_pretty(writer, &item)
544            .map_err(|err| ExportErrorKind::Other(err.to_string())),
545        MetadataExportFormat::Yaml => serde_yaml::to_writer(writer, &item)
546            .map_err(|err| ExportErrorKind::Other(err.to_string())),
547        MetadataExportFormat::Toml => toml::to_string_pretty(item)
548            .map_err(|err| ExportErrorKind::Other(err.to_string()))
549            .and_then(|s| {
550                writer
551                    .write_all(s.as_bytes())
552                    .map_err(|err| ExportErrorKind::Other(err.to_string()))
553            }),
554    }?;
555
556    Ok(())
557}
558
559pub fn to_writer<W>(
560    mut writer: W,
561    format: ExportFormat,
562    value: &NickelValue,
563) -> Result<(), PointedExportErrorData>
564where
565    W: io::Write,
566{
567    #[cfg(feature = "metrics")]
568    let start_time = std::time::Instant::now();
569
570    match format {
571        ExportFormat::Json => serde_json::to_writer_pretty(writer, &value)
572            .map_err(|err| ExportErrorKind::Other(err.to_string())),
573        ExportFormat::Yaml => serde_yaml::to_writer(writer, &value)
574            .map_err(|err| ExportErrorKind::Other(err.to_string())),
575        ExportFormat::YamlDocuments => {
576            if let Some(arr) = value.as_array() {
577                for value in arr.iter() {
578                    writeln!(writer, "---")
579                        .map_err(|err| ExportErrorKind::Other(err.to_string()))?;
580                    serde_yaml::to_writer(&mut writer, value)
581                        .map_err(|err| ExportErrorKind::Other(err.to_string()))?;
582                }
583                Ok(())
584            } else {
585                Err(ExportErrorKind::ExpectedArray {
586                    value: value.clone(),
587                })
588            }
589        }
590        ExportFormat::Toml => toml::to_string_pretty(value)
591            .map_err(|err| ExportErrorKind::Other(err.to_string()))
592            .and_then(|s| {
593                writer
594                    .write_all(s.as_bytes())
595                    .map_err(|err| ExportErrorKind::Other(err.to_string()))
596            }),
597        ExportFormat::Text => match value.as_string() {
598            Some(s) => writer
599                .write_all(s.as_bytes())
600                .map_err(|err| ExportErrorKind::Other(err.to_string())),
601            _ => Err(ExportErrorKind::Other(format!(
602                "raw export requires a `String`, got {}",
603                // unwrap(): terms must be fully evaluated before serialization,
604                // and fully evaluated terms have a definite type.
605                value.type_of().unwrap()
606            ))),
607        },
608    }?;
609
610    metrics::increment!("runtime:serialize", start_time.elapsed().as_millis() as u64);
611
612    Ok(())
613}
614
615pub fn to_string(format: ExportFormat, rt: &NickelValue) -> Result<String, PointedExportErrorData> {
616    let mut buffer: Vec<u8> = Vec::new();
617    to_writer(&mut buffer, format, rt)?;
618
619    Ok(String::from_utf8_lossy(&buffer).into_owned())
620}
621
622/// We don't use serde to deserialize toml, because
623/// - this bug: <https://github.com/toml-rs/toml/issues/798>
624/// - the machinery for getting spans for toml+serde is more code than
625///   what we have below
626///
627/// Instead, we parse the toml using `toml-edit` and then convert from their
628/// representation to a `NickelValue`. Using `toml-edit` here doesn't introduce
629/// any new dependencies, because it's used by `toml` internally anyway.
630pub mod toml_deser {
631    use crate::{
632        eval::value::NickelValue,
633        files::FileId,
634        identifier::LocIdent,
635        position::{PosTable, RawSpan, TermPos},
636        term::record::{RecordAttrs, RecordData},
637    };
638    use codespan::ByteIndex;
639    use malachite::{base::num::conversion::traits::ExactFrom as _, rational::Rational};
640    use nickel_lang_parser::ast::{
641        Ast, AstAlloc, Node,
642        record::{FieldDef, FieldMetadata, FieldPathElem},
643    };
644    use std::ops::Range;
645    use toml_edit::Value;
646
647    fn range_pos(range: Option<Range<usize>>, src_id: FileId) -> TermPos {
648        range.map_or(TermPos::None, |span| {
649            RawSpan {
650                src_id,
651                start: ByteIndex(span.start as u32),
652                end: ByteIndex(span.end as u32),
653            }
654            .into()
655        })
656    }
657
658    // Add `to_value` method to `toml_edit` types.
659    trait ToNickelValue {
660        fn to_value(&self, pos_table: &mut PosTable, src_id: FileId) -> NickelValue;
661
662        fn to_value_with_pos(
663            &self,
664            pos_table: &mut PosTable,
665            range: Option<Range<usize>>,
666            src_id: FileId,
667        ) -> NickelValue {
668            let pos = range_pos(range, src_id);
669            self.to_value(pos_table, src_id).with_pos(pos_table, pos)
670        }
671    }
672
673    impl ToNickelValue for toml_edit::Table {
674        fn to_value(&self, pos_table: &mut PosTable, src_id: FileId) -> NickelValue {
675            NickelValue::record_posless(RecordData::new(
676                self.iter()
677                    .map(|(key, val)| {
678                        (
679                            LocIdent::new(key),
680                            val.to_value_with_pos(pos_table, val.span(), src_id).into(),
681                        )
682                    })
683                    .collect(),
684                RecordAttrs::default(),
685                None,
686            ))
687        }
688    }
689
690    impl ToNickelValue for toml_edit::Value {
691        fn to_value(&self, pos_table: &mut PosTable, src_id: FileId) -> NickelValue {
692            match self {
693                Value::String(s) => NickelValue::string_posless(s.value()),
694                Value::Integer(i) => NickelValue::number_posless(*i.value()),
695                Value::Float(f) => NickelValue::number_posless(Rational::exact_from(*f.value())),
696                Value::Boolean(b) => NickelValue::bool_value_posless(*b.value()),
697                Value::Array(vs) => NickelValue::array_posless(
698                    vs.iter()
699                        .map(|val| val.to_value_with_pos(pos_table, val.span(), src_id))
700                        .collect(),
701                    Vec::new(),
702                ),
703                Value::InlineTable(t) => NickelValue::record_posless(RecordData::new(
704                    t.iter()
705                        .map(|(key, val)| {
706                            (
707                                LocIdent::new(key),
708                                val.to_value_with_pos(pos_table, val.span(), src_id).into(),
709                            )
710                        })
711                        .collect(),
712                    RecordAttrs::default(),
713                    None,
714                )),
715                // We don't have a proper type to represent datetimes currently, so we just parse
716                // this as a string.
717                Value::Datetime(dt) => NickelValue::string_posless(dt.to_string()),
718            }
719        }
720    }
721
722    impl ToNickelValue for toml_edit::Item {
723        fn to_value(&self, pos_table: &mut PosTable, src_id: FileId) -> NickelValue {
724            match self {
725                toml_edit::Item::None => NickelValue::null(),
726                toml_edit::Item::Table(t) => t.to_value(pos_table, src_id),
727                toml_edit::Item::ArrayOfTables(ts) => NickelValue::array_posless(
728                    ts.iter()
729                        .map(|val| val.to_value_with_pos(pos_table, val.span(), src_id))
730                        .collect(),
731                    Vec::new(),
732                ),
733                toml_edit::Item::Value(v) => v.to_value(pos_table, src_id),
734            }
735        }
736    }
737
738    /// Deserialize a Nickel term with position information from a TOML source provided as a
739    /// string and the file id of this source.
740    pub fn from_str(
741        pos_table: &mut PosTable,
742        s: &str,
743        file_id: FileId,
744    ) -> Result<NickelValue, toml_edit::TomlError> {
745        let doc: toml_edit::Document<_> = s.parse()?;
746        Ok(doc
747            .as_item()
748            .to_value_with_pos(pos_table, doc.span(), file_id))
749    }
750
751    trait ToAst {
752        fn to_ast<'ast>(&self, alloc: &'ast AstAlloc, file_id: FileId) -> Ast<'ast>;
753    }
754
755    fn to_record<'a, 'ast, T: ToAst + 'a, I: Iterator<Item = (&'a str, &'a T)>>(
756        alloc: &'ast AstAlloc,
757        file_id: FileId,
758        iter: I,
759    ) -> Node<'ast> {
760        Node::Record(
761            alloc.record_data(
762                [],
763                iter.map(|(key, val)| FieldDef {
764                    path: FieldPathElem::single_ident_path(alloc, LocIdent::new(key)),
765                    metadata: FieldMetadata::default(),
766                    value: Some(val.to_ast(alloc, file_id)),
767                    pos: TermPos::default(),
768                })
769                .collect::<Vec<_>>(),
770                false,
771            ),
772        )
773    }
774
775    impl ToAst for toml_edit::Table {
776        fn to_ast<'ast>(&self, alloc: &'ast AstAlloc, file_id: FileId) -> Ast<'ast> {
777            Ast::from(to_record(alloc, file_id, self.iter()))
778                .with_pos(range_pos(self.span(), file_id))
779        }
780    }
781
782    impl ToAst for toml_edit::Item {
783        fn to_ast<'ast>(&self, alloc: &'ast AstAlloc, file_id: FileId) -> Ast<'ast> {
784            let pos = range_pos(self.span(), file_id);
785            match self {
786                toml_edit::Item::None => Ast::from(Node::Null).with_pos(pos),
787                toml_edit::Item::Value(val) => val.to_ast(alloc, file_id),
788                toml_edit::Item::Table(t) => t.to_ast(alloc, file_id),
789                toml_edit::Item::ArrayOfTables(ts) => Ast::from(
790                    alloc.array(
791                        ts.iter()
792                            .map(|t| t.to_ast(alloc, file_id))
793                            .collect::<Vec<_>>(),
794                    ),
795                )
796                .with_pos(pos),
797            }
798        }
799    }
800
801    impl ToAst for toml_edit::Value {
802        fn to_ast<'ast>(&self, alloc: &'ast AstAlloc, file_id: FileId) -> Ast<'ast> {
803            let pos = range_pos(self.span(), file_id);
804            let node = match self {
805                Value::String(s) => alloc.string(s.value()),
806                Value::Integer(i) => alloc.number((*i.value()).into()),
807                Value::Float(f) => alloc.number(Rational::exact_from(*f.value())),
808                Value::Boolean(b) => Node::Bool(*b.value()),
809                Value::Datetime(dt) => alloc.string(&dt.to_string()),
810                Value::Array(array) => alloc.array(
811                    array
812                        .iter()
813                        .map(|v| v.to_ast(alloc, file_id))
814                        .collect::<Vec<_>>(),
815                ),
816                Value::InlineTable(t) => to_record(alloc, file_id, t.iter()),
817            };
818            Ast::from(node).with_pos(pos)
819        }
820    }
821
822    pub fn ast_from_str<'ast>(
823        alloc: &'ast AstAlloc,
824        s: &str,
825        file_id: FileId,
826    ) -> Result<Ast<'ast>, toml_edit::TomlError> {
827        let doc: toml_edit::Document<_> = s.parse()?;
828        Ok(doc.as_item().to_ast(alloc, file_id))
829    }
830}
831
832#[cfg(test)]
833mod tests {
834    use super::*;
835    use crate::{
836        cache::CacheHub,
837        error::NullReporter,
838        eval::{VirtualMachine, VmContext, cache::CacheImpl},
839        program::Program,
840        term::{BinaryOp, make as mk_term},
841    };
842    use serde_json::json;
843    use std::io::Cursor;
844
845    #[track_caller]
846    fn eval(s: &str) -> NickelValue {
847        let src = Cursor::new(s);
848        let mut prog = Program::<CacheImpl>::new_from_source(
849            src,
850            "<test>",
851            std::io::stderr(),
852            NullReporter {},
853        )
854        .unwrap();
855        prog.eval_full().expect("program eval should succeed")
856    }
857
858    #[track_caller]
859    fn eval_with_ctxt(vm_ctxt: &mut VmContext<CacheHub, CacheImpl>, s: &str) -> NickelValue {
860        use crate::cache::{InputFormat, SourcePath};
861
862        let file_id = vm_ctxt
863            .import_resolver
864            .sources
865            .add_source(
866                SourcePath::Path("<test>".into(), InputFormat::Nickel),
867                Cursor::new(s),
868            )
869            .unwrap();
870        let value = vm_ctxt.prepare_eval(file_id).unwrap();
871
872        VirtualMachine::<_, CacheImpl>::new(vm_ctxt)
873            .eval_full(value)
874            .unwrap()
875    }
876
877    #[track_caller]
878    fn assert_json_eq<T: Serialize>(term: &str, expected: T) {
879        assert_eq!(
880            serde_json::to_string(&eval(term)).unwrap(),
881            serde_json::to_string(&expected).unwrap()
882        )
883    }
884
885    #[track_caller]
886    fn assert_nickel_eq(
887        vm_ctxt: &mut VmContext<CacheHub, CacheImpl>,
888        value: NickelValue,
889        expected: NickelValue,
890    ) {
891        assert!(
892            VirtualMachine::<_, CacheImpl>::new_empty_env(vm_ctxt)
893                .eval(mk_term::op2(BinaryOp::Eq, value, expected))
894                .unwrap()
895                .phys_eq(&NickelValue::bool_true())
896        );
897    }
898
899    #[track_caller]
900    fn assert_pass_validation(format: ExportFormat, term: &str) {
901        validate(format, &eval(term)).unwrap();
902    }
903
904    #[track_caller]
905    fn assert_fail_validation(format: ExportFormat, term: &str) {
906        validate(format, &eval(term)).unwrap_err();
907    }
908
909    #[track_caller]
910    fn assert_involutory(term: &str) {
911        let mut vm_ctxt = VmContext::new(CacheHub::new(), std::io::stderr(), NullReporter {});
912        let evaluated = eval_with_ctxt(&mut vm_ctxt, term);
913
914        let from_json: NickelValue =
915            serde_json::from_str(&serde_json::to_string(&evaluated).unwrap()).unwrap();
916        let from_yaml: NickelValue =
917            serde_yaml::from_str(&serde_yaml::to_string(&evaluated).unwrap()).unwrap();
918        let from_toml: NickelValue = toml::from_str(&toml::to_string(&evaluated).unwrap()).unwrap();
919
920        assert_nickel_eq(&mut vm_ctxt, from_json, evaluated.clone());
921        assert_nickel_eq(&mut vm_ctxt, from_yaml, evaluated.clone());
922        assert_nickel_eq(&mut vm_ctxt, from_toml, evaluated);
923    }
924
925    #[test]
926    fn basic() {
927        assert_json_eq("1 + 1", 2);
928
929        let null: Option<()> = None;
930        assert_json_eq("null", null);
931
932        assert_json_eq("if true then false else true", false);
933        assert_json_eq(r#""Hello, %{"world"}!""#, "Hello, world!");
934        assert_json_eq("'foo", "foo");
935    }
936
937    #[test]
938    fn arrays() {
939        assert_json_eq("[]", json!([]));
940        assert_json_eq("[null, (1+1), (2+2), (3+3)]", json!([null, 2, 4, 6]));
941        assert_json_eq(
942            r#"['a, ("b" ++ "c"), "d%{"e"}f", "g"]"#,
943            json!(["a", "bc", "def", "g"]),
944        );
945        assert_json_eq(
946            r#"std.array.fold_right (fun elt acc => [[elt]] @ acc) [] [1, 2, 3, 4]"#,
947            json!([[1], [2], [3], [4]]),
948        );
949        assert_json_eq("[\"a\", 1, false, 'foo]", json!(["a", 1, false, "foo"]));
950    }
951
952    #[test]
953    fn records() {
954        assert_json_eq(
955            "{a = 1, b = 2+2, c = 3, d = null}",
956            json!({"a": 1, "b": 4, "c": 3, "d": null}),
957        );
958
959        assert_json_eq(
960            "{a = {} & {b = {c = if true then 'richtig else 'falsch}}}",
961            json!({"a": {"b": {"c": "richtig"}}}),
962        );
963
964        assert_json_eq(
965            "{foo = let z = 0.5 + 0.5 in z, \
966            bar = [\"str\", true || false], \
967            baz = {subfoo = !false} & {subbar = 1 - 1}}",
968            json!({"foo": 1, "bar": ["str", true], "baz": {"subfoo": true, "subbar": 0}}),
969        );
970    }
971
972    #[test]
973    fn meta_values() {
974        assert_json_eq(
975            "{a | default = 1, b | doc \"doc\" = 2+2, c = 3}",
976            json!({"a": 1, "b": 4, "c": 3}),
977        );
978
979        assert_json_eq(
980            "{a = {b | default = {}} & {b.c | default = (if true then 'faux else 'vrai)}}",
981            json!({"a": {"b": {"c": "faux"}}}),
982        );
983
984        assert_json_eq(
985            "{baz | default = {subfoo | default = !false} & {subbar | default = 1 - 1}}",
986            json!({"baz": {"subfoo": true, "subbar": 0}}),
987        );
988
989        assert_json_eq(
990            "{a = {b | default = {}} & {b.c | not_exported = false} & {b.d = true}}",
991            json!({"a": {"b": {"d": true}}}),
992        );
993    }
994
995    #[test]
996    fn prevalidation() {
997        assert_fail_validation(ExportFormat::Json, "{a = 1, b = {c = fun x => x}}");
998        assert_fail_validation(
999            ExportFormat::Json,
1000            "{foo.bar = let y = \"a\" in y, b = [[fun x => x]]}",
1001        );
1002        assert_pass_validation(ExportFormat::Json, "{foo = null}");
1003        assert_fail_validation(ExportFormat::Toml, "{foo = null}");
1004    }
1005
1006    #[test]
1007    fn involution() {
1008        assert_involutory("{val = 1 + 1}");
1009        assert_involutory("{val = \"Some string\"}");
1010        assert_involutory("{val = [\"a\", 3, []]}");
1011        assert_involutory("{a.foo.bar = \"2\", b = false, c = [{d = \"e\"}, {d = \"f\"}]}");
1012    }
1013}