From 90c32eec7a382195e6d533c6e55a3d20c58cc3a5 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 31 Dec 2022 18:13:59 +0300 Subject: feat(tvix/serde): initial Nix->serde::Deserialize impl 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 --- tvix/Cargo.lock | 8 ++ tvix/Cargo.nix | 32 +++++ tvix/Cargo.toml | 1 + tvix/serde/Cargo.toml | 8 ++ tvix/serde/default.nix | 5 + tvix/serde/src/de.rs | 331 ++++++++++++++++++++++++++++++++++++++++++++++++ tvix/serde/src/error.rs | 92 ++++++++++++++ tvix/serde/src/lib.rs | 8 ++ 8 files changed, 485 insertions(+) create mode 100644 tvix/serde/Cargo.toml create mode 100644 tvix/serde/default.nix create mode 100644 tvix/serde/src/de.rs create mode 100644 tvix/serde/src/error.rs create mode 100644 tvix/serde/src/lib.rs (limited to 'tvix') diff --git a/tvix/Cargo.lock b/tvix/Cargo.lock index 9e31df0fe651..7a20c720aeb3 100644 --- a/tvix/Cargo.lock +++ b/tvix/Cargo.lock @@ -2214,6 +2214,14 @@ dependencies = [ name = "tvix-nar" version = "0.0.0" +[[package]] +name = "tvix-serde" +version = "0.1.0" +dependencies = [ + "serde", + "tvix-eval", +] + [[package]] name = "tvix-store" version = "0.1.0" diff --git a/tvix/Cargo.nix b/tvix/Cargo.nix index 57b5e2a0ff6a..361a0a3ee033 100644 --- a/tvix/Cargo.nix +++ b/tvix/Cargo.nix @@ -93,6 +93,16 @@ rec { # File a bug if you depend on any for non-debug work! debug = internal.debugCrate { inherit packageId; }; }; + "tvix-serde" = rec { + packageId = "tvix-serde"; + build = internal.buildRustCrateWithFeatures { + packageId = "tvix-serde"; + }; + + # Debug support which might change between releases. + # File a bug if you depend on any for non-debug work! + debug = internal.debugCrate { inherit packageId; }; + }; "tvix-store" = rec { packageId = "tvix-store"; build = internal.buildRustCrateWithFeatures { @@ -6608,6 +6618,28 @@ rec { then lib.cleanSourceWith { filter = sourceFilter; src = ./nar; } else ./nar; + }; + "tvix-serde" = rec { + crateName = "tvix-serde"; + version = "0.1.0"; + edition = "2021"; + # We can't filter paths with references in Nix 2.4 + # See https://github.com/NixOS/nix/issues/5410 + src = + if (lib.versionOlder builtins.nixVersion "2.4pre20211007") + then lib.cleanSourceWith { filter = sourceFilter; src = ./serde; } + else ./serde; + dependencies = [ + { + name = "serde"; + packageId = "serde"; + } + { + name = "tvix-eval"; + packageId = "tvix-eval"; + } + ]; + }; "tvix-store" = rec { crateName = "tvix-store"; diff --git a/tvix/Cargo.toml b/tvix/Cargo.toml index e12976d1f09d..748b991ddfd1 100644 --- a/tvix/Cargo.toml +++ b/tvix/Cargo.toml @@ -24,6 +24,7 @@ members = [ "eval/builtin-macros", "nar", "nix_cli", + "serde", "store", ] 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 +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>(v: &Value) -> Result { + match v { + Value::Integer(i) => I::try_from(*i).map_err(|_| Error::IntegerConversion { + got: *i, + need: std::any::type_name::(), + }), + + _ => Err(unexpected("integer", v)), + } +} + +impl<'de> de::Deserializer<'de> for Deserializer { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> Result + 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(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + match self.value { + Value::Bool(b) => visitor.visit_bool(b), + _ => Err(unexpected("bool", &self.value)), + } + } + + fn deserialize_i8(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_i8(visit_integer(&self.value)?) + } + + fn deserialize_i16(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_i16(visit_integer(&self.value)?) + } + + fn deserialize_i32(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_i32(visit_integer(&self.value)?) + } + + fn deserialize_i64(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_i64(visit_integer(&self.value)?) + } + + fn deserialize_u8(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_u8(visit_integer(&self.value)?) + } + + fn deserialize_u16(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_u16(visit_integer(&self.value)?) + } + + fn deserialize_u32(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_u32(visit_integer(&self.value)?) + } + + fn deserialize_u64(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_u64(visit_integer(&self.value)?) + } + + fn deserialize_f32(self, visitor: V) -> Result + 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(self, visitor: V) -> Result + 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(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Value::String(s) = &self.value { + let chars = s.as_str().chars().collect::>(); + if chars.len() == 1 { + return visitor.visit_char(chars[0]); + } + } + + Err(unexpected("char", &self.value)) + } + + fn deserialize_str(self, visitor: V) -> Result + 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(self, visitor: V) -> Result + 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(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + todo!("how to represent this?"); + } + + fn deserialize_byte_buf(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + todo!("how to represent this?"); + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + todo!("how to represent this?"); + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if let Value::Null = self.value { + return visitor.visit_unit(); + } + + Err(unexpected("null", &self.value)) + } + + fn deserialize_unit_struct( + self, + name: &'static str, + visitor: V, + ) -> Result + where + V: de::Visitor<'de>, + { + todo!("how to represent this?"); + } + + fn deserialize_newtype_struct( + self, + name: &'static str, + visitor: V, + ) -> Result + where + V: de::Visitor<'de>, + { + todo!("how to represent this?"); + } + + fn deserialize_seq(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + todo!() + } + + fn deserialize_tuple(self, len: usize, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + todo!() + } + + fn deserialize_tuple_struct( + self, + name: &'static str, + len: usize, + visitor: V, + ) -> Result + where + V: de::Visitor<'de>, + { + todo!() + } + + fn deserialize_map(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + todo!() + } + + fn deserialize_struct( + self, + name: &'static str, + fields: &'static [&'static str], + visitor: V, + ) -> Result + where + V: de::Visitor<'de>, + { + todo!() + } + + fn deserialize_enum( + self, + name: &'static str, + variants: &'static [&'static str], + visitor: V, + ) -> Result + where + V: de::Visitor<'de>, + { + todo!() + } + + fn deserialize_identifier(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + todo!() + } + + fn deserialize_ignored_any(self, visitor: V) -> Result + 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, + 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(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; -- cgit 1.4.1