about summary refs log tree commit diff
path: root/tvix/serde
diff options
context:
space:
mode:
authorVincent Ambo <mail@tazj.in>2022-12-31T15·13+0300
committertazjin <tazjin@tvl.su>2023-01-02T22·24+0000
commit90c32eec7a382195e6d533c6e55a3d20c58cc3a5 (patch)
tree81f70eb9ab9eb621ab9a6d11d3951a58292cf28c /tvix/serde
parent49ee3e3b148c1edb74a15e26eacb5911647d1de5 (diff)
feat(tvix/serde): initial Nix->serde::Deserialize impl r/5566
This will make it possible fairly easily use Nix to represent
arbitrary data structures, e.g. for using Nix as a config language.

Only pure Nix (i.e. no `import` etc.) is supported for now.

Not all types, specifically no struct traversal, are implemented in
this commit.

Change-Id: I9ac91a229a0d12bf818e6e3249f3e5a691599a2c
Reviewed-on: https://cl.tvl.fyi/c/depot/+/7712
Tested-by: BuildkiteCI
Reviewed-by: flokli <flokli@flokli.de>
Diffstat (limited to 'tvix/serde')
-rw-r--r--tvix/serde/Cargo.toml8
-rw-r--r--tvix/serde/default.nix5
-rw-r--r--tvix/serde/src/de.rs331
-rw-r--r--tvix/serde/src/error.rs92
-rw-r--r--tvix/serde/src/lib.rs8
5 files changed, 444 insertions, 0 deletions
diff --git a/tvix/serde/Cargo.toml b/tvix/serde/Cargo.toml
new file mode 100644
index 000000000000..37a0597e0451
--- /dev/null
+++ b/tvix/serde/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "tvix-serde"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+tvix-eval = { path = "../eval" }
+serde = "1.0"
diff --git a/tvix/serde/default.nix b/tvix/serde/default.nix
new file mode 100644
index 000000000000..5880bd24f663
--- /dev/null
+++ b/tvix/serde/default.nix
@@ -0,0 +1,5 @@
+{ depot, ... }:
+
+depot.tvix.crates.workspaceMembers.tvix-serde.build.override {
+  runTests = true;
+}
diff --git a/tvix/serde/src/de.rs b/tvix/serde/src/de.rs
new file mode 100644
index 000000000000..55313fd9a7a2
--- /dev/null
+++ b/tvix/serde/src/de.rs
@@ -0,0 +1,331 @@
+//! Deserialisation from Nix to Rust values.
+
+use serde::de;
+use tvix_eval::Value;
+
+use crate::error::Error;
+
+struct Deserializer {
+    value: tvix_eval::Value,
+}
+
+pub fn from_str<'code, T>(src: &'code str) -> Result<T, Error>
+where
+    T: serde::Deserialize<'code>,
+{
+    // First step is to evaluate the Nix code ...
+    let eval = tvix_eval::Evaluation::new(src, None);
+    let source = eval.source_map();
+    let result = eval.evaluate();
+
+    if !result.errors.is_empty() {
+        return Err(Error::NixErrors {
+            errors: result.errors,
+            source,
+        });
+    }
+
+    let de = Deserializer {
+        value: result.value.expect("value should be present on success"),
+    };
+
+    T::deserialize(de)
+}
+
+fn unexpected(expected: &'static str, got: &Value) -> Error {
+    Error::UnexpectedType {
+        expected,
+        got: got.type_of(),
+    }
+}
+
+fn visit_integer<I: TryFrom<i64>>(v: &Value) -> Result<I, Error> {
+    match v {
+        Value::Integer(i) => I::try_from(*i).map_err(|_| Error::IntegerConversion {
+            got: *i,
+            need: std::any::type_name::<I>(),
+        }),
+
+        _ => Err(unexpected("integer", v)),
+    }
+}
+
+impl<'de> de::Deserializer<'de> for Deserializer {
+    type Error = Error;
+
+    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.value {
+            Value::Null => visitor.visit_unit(),
+            Value::Bool(b) => visitor.visit_bool(b),
+            Value::Integer(i) => visitor.visit_i64(i),
+            Value::Float(f) => visitor.visit_f64(f),
+            Value::String(s) => visitor.visit_string(s.to_string()),
+            Value::Path(p) => visitor.visit_string(p.to_string_lossy().into()), // TODO: hmm
+            Value::Attrs(_) => self.deserialize_map(visitor),
+            Value::List(_) => self.deserialize_seq(visitor),
+
+            // tvix-eval types that can not be deserialized through serde.
+            Value::Closure(_)
+            | Value::Builtin(_)
+            | Value::Thunk(_)
+            | Value::AttrNotFound
+            | Value::Blueprint(_)
+            | Value::DeferredUpvalue(_)
+            | Value::UnresolvedPath(_) => Err(Error::Unserializable {
+                value_type: self.value.type_of(),
+            }),
+        }
+    }
+
+    fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.value {
+            Value::Bool(b) => visitor.visit_bool(b),
+            _ => Err(unexpected("bool", &self.value)),
+        }
+    }
+
+    fn deserialize_i8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_i8(visit_integer(&self.value)?)
+    }
+
+    fn deserialize_i16<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_i16(visit_integer(&self.value)?)
+    }
+
+    fn deserialize_i32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_i32(visit_integer(&self.value)?)
+    }
+
+    fn deserialize_i64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_i64(visit_integer(&self.value)?)
+    }
+
+    fn deserialize_u8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_u8(visit_integer(&self.value)?)
+    }
+
+    fn deserialize_u16<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_u16(visit_integer(&self.value)?)
+    }
+
+    fn deserialize_u32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_u32(visit_integer(&self.value)?)
+    }
+
+    fn deserialize_u64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_u64(visit_integer(&self.value)?)
+    }
+
+    fn deserialize_f32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if let Value::Float(f) = self.value {
+            return visitor.visit_f32(f as f32);
+        }
+
+        Err(unexpected("float", &self.value))
+    }
+
+    fn deserialize_f64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if let Value::Float(f) = self.value {
+            return visitor.visit_f64(f);
+        }
+
+        Err(unexpected("float", &self.value))
+    }
+
+    fn deserialize_char<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if let Value::String(s) = &self.value {
+            let chars = s.as_str().chars().collect::<Vec<_>>();
+            if chars.len() == 1 {
+                return visitor.visit_char(chars[0]);
+            }
+        }
+
+        Err(unexpected("char", &self.value))
+    }
+
+    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if let Value::String(s) = &self.value {
+            return visitor.visit_str(s.as_str());
+        }
+
+        Err(unexpected("string", &self.value))
+    }
+
+    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if let Value::String(s) = &self.value {
+            return visitor.visit_str(s.as_str());
+        }
+
+        Err(unexpected("string", &self.value))
+    }
+
+    fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        todo!("how to represent this?");
+    }
+
+    fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        todo!("how to represent this?");
+    }
+
+    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        todo!("how to represent this?");
+    }
+
+    fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if let Value::Null = self.value {
+            return visitor.visit_unit();
+        }
+
+        Err(unexpected("null", &self.value))
+    }
+
+    fn deserialize_unit_struct<V>(
+        self,
+        name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        todo!("how to represent this?");
+    }
+
+    fn deserialize_newtype_struct<V>(
+        self,
+        name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        todo!("how to represent this?");
+    }
+
+    fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        todo!()
+    }
+
+    fn deserialize_tuple<V>(self, len: usize, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        todo!()
+    }
+
+    fn deserialize_tuple_struct<V>(
+        self,
+        name: &'static str,
+        len: usize,
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        todo!()
+    }
+
+    fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        todo!()
+    }
+
+    fn deserialize_struct<V>(
+        self,
+        name: &'static str,
+        fields: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        todo!()
+    }
+
+    fn deserialize_enum<V>(
+        self,
+        name: &'static str,
+        variants: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        todo!()
+    }
+
+    fn deserialize_identifier<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        todo!()
+    }
+
+    fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_unit()
+    }
+}
diff --git a/tvix/serde/src/error.rs b/tvix/serde/src/error.rs
new file mode 100644
index 000000000000..fb83105cd210
--- /dev/null
+++ b/tvix/serde/src/error.rs
@@ -0,0 +1,92 @@
+//! When serialising Nix goes wrong ...
+
+use std::error;
+use std::fmt::Display;
+
+#[derive(Clone, Debug)]
+pub enum Error {
+    /// Attempted to deserialise an unsupported Nix value (such as a
+    /// function) that can not be represented by the
+    /// [`serde::Deserialize`] trait.
+    Unserializable { value_type: &'static str },
+
+    /// Expected to deserialize a value that is unsupported by Nix.
+    Unsupported { wanted: &'static str },
+
+    /// Expected a specific type, but got something else on the Nix side.
+    UnexpectedType {
+        expected: &'static str,
+        got: &'static str,
+    },
+
+    /// Deserialisation error returned from `serde::de`.
+    Deserialization(String),
+
+    /// Deserialized integer did not fit.
+    IntegerConversion { got: i64, need: &'static str },
+
+    /// Evaluation of the supplied Nix code failed while computing the
+    /// value for deserialisation.
+    NixErrors {
+        errors: Vec<tvix_eval::Error>,
+        source: tvix_eval::SourceCode,
+    },
+}
+
+impl Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Error::Unserializable { value_type } => write!(
+                f,
+                "can not deserialise a Nix '{}' into a Rust type",
+                value_type
+            ),
+
+            Error::Unsupported { wanted } => {
+                write!(f, "can not deserialize a '{}' from a Nix value", wanted)
+            }
+
+            Error::UnexpectedType { expected, got } => {
+                write!(f, "expected type {}, but got Nix type {}", expected, got)
+            }
+
+            Error::NixErrors { errors, source } => {
+                writeln!(
+                    f,
+                    "{} occured during Nix evaluation: ",
+                    if errors.len() == 1 { "error" } else { "errors" }
+                )?;
+
+                for err in errors {
+                    writeln!(f, "{}", err.fancy_format_str(&source))?;
+                }
+
+                Ok(())
+            }
+
+            Error::Deserialization(err) => write!(f, "deserialisation error occured: {}", err),
+
+            Error::IntegerConversion { got, need } => {
+                write!(f, "i64({}) does not fit in a {}", got, need)
+            }
+        }
+    }
+}
+
+impl error::Error for Error {
+    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+        match self {
+            Self::NixErrors { errors, .. } => errors.first().map(|e| e as &dyn error::Error),
+            _ => None,
+        }
+    }
+}
+
+impl serde::de::Error for Error {
+    fn custom<T>(err: T) -> Self
+    where
+        T: Display,
+    {
+        Self::Deserialization(err.to_string())
+    }
+}
diff --git a/tvix/serde/src/lib.rs b/tvix/serde/src/lib.rs
new file mode 100644
index 000000000000..b3052be596f7
--- /dev/null
+++ b/tvix/serde/src/lib.rs
@@ -0,0 +1,8 @@
+//! `tvix-serde` implements (de-)serialisation of Rust data structures
+//! to/from Nix. This is intended to make it easy to use Nix as as
+//! configuration language.
+
+mod de;
+mod error;
+
+pub use de::from_str;