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 <flokli@flokli.de>
This commit is contained in:
parent
49ee3e3b14
commit
90c32eec7a
8 changed files with 485 additions and 0 deletions
8
tvix/Cargo.lock
generated
8
tvix/Cargo.lock
generated
|
@ -2214,6 +2214,14 @@ dependencies = [
|
||||||
name = "tvix-nar"
|
name = "tvix-nar"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tvix-serde"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"tvix-eval",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tvix-store"
|
name = "tvix-store"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
@ -93,6 +93,16 @@ rec {
|
||||||
# File a bug if you depend on any for non-debug work!
|
# File a bug if you depend on any for non-debug work!
|
||||||
debug = internal.debugCrate { inherit packageId; };
|
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 {
|
"tvix-store" = rec {
|
||||||
packageId = "tvix-store";
|
packageId = "tvix-store";
|
||||||
build = internal.buildRustCrateWithFeatures {
|
build = internal.buildRustCrateWithFeatures {
|
||||||
|
@ -6608,6 +6618,28 @@ rec {
|
||||||
then lib.cleanSourceWith { filter = sourceFilter; src = ./nar; }
|
then lib.cleanSourceWith { filter = sourceFilter; src = ./nar; }
|
||||||
else ./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 {
|
"tvix-store" = rec {
|
||||||
crateName = "tvix-store";
|
crateName = "tvix-store";
|
||||||
|
|
|
@ -24,6 +24,7 @@ members = [
|
||||||
"eval/builtin-macros",
|
"eval/builtin-macros",
|
||||||
"nar",
|
"nar",
|
||||||
"nix_cli",
|
"nix_cli",
|
||||||
|
"serde",
|
||||||
"store",
|
"store",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
8
tvix/serde/Cargo.toml
Normal file
8
tvix/serde/Cargo.toml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "tvix-serde"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tvix-eval = { path = "../eval" }
|
||||||
|
serde = "1.0"
|
5
tvix/serde/default.nix
Normal file
5
tvix/serde/default.nix
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{ depot, ... }:
|
||||||
|
|
||||||
|
depot.tvix.crates.workspaceMembers.tvix-serde.build.override {
|
||||||
|
runTests = true;
|
||||||
|
}
|
331
tvix/serde/src/de.rs
Normal file
331
tvix/serde/src/de.rs
Normal file
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
92
tvix/serde/src/error.rs
Normal file
92
tvix/serde/src/error.rs
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
8
tvix/serde/src/lib.rs
Normal file
8
tvix/serde/src/lib.rs
Normal file
|
@ -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;
|
Loading…
Reference in a new issue