From ced05a2bb6b66d30208520d0791f4fa298c429e2 Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Mon, 22 Jul 2024 16:51:42 +0200 Subject: [PATCH] feat(tvix/nix-compat-derive): Add deriver for NixDeserialize This adds a nix-compat-derive derive crate that implements a deriver for NixDeserialize implementations. This is to reduce the amount of code needed to implement deserialization for all the types used by the Nix daemon protocol. Change-Id: I484724b550e8a1d5e9adad9555d9dc1374ae95c2 Reviewed-on: https://cl.tvl.fyi/c/depot/+/12022 Autosubmit: Brian Olsen Tested-by: BuildkiteCI Reviewed-by: flokli --- tvix/Cargo.lock | 44 ++ tvix/Cargo.nix | 172 +++++++- tvix/Cargo.toml | 2 + tvix/nix-compat-derive-tests/Cargo.toml | 27 ++ tvix/nix-compat-derive-tests/default.nix | 5 + .../tests/read_derive.rs | 417 ++++++++++++++++++ tvix/nix-compat-derive-tests/tests/ui.rs | 6 + .../tests/ui/deserialize_bad_type.rs | 10 + .../tests/ui/deserialize_bad_type.stderr | 21 + .../ui/deserialize_enum_non_exaustive.rs | 13 + .../ui/deserialize_enum_non_exaustive.stderr | 8 + .../tests/ui/deserialize_from_missing.rs | 7 + .../tests/ui/deserialize_from_missing.stderr | 5 + .../deserialize_from_str_error_not_display.rs | 20 + ...erialize_from_str_error_not_display.stderr | 13 + .../tests/ui/deserialize_from_str_missing.rs | 7 + .../ui/deserialize_from_str_missing.stderr | 16 + .../tests/ui/deserialize_missing_default.rs | 12 + .../ui/deserialize_missing_default.stderr | 12 + .../ui/deserialize_missing_default_path.rs | 12 + .../deserialize_missing_default_path.stderr | 8 + .../ui/deserialize_remote_missing_attr.rs | 15 + .../ui/deserialize_remote_missing_attr.stderr | 5 + .../deserialize_try_from_error_not_display.rs | 19 + ...erialize_try_from_error_not_display.stderr | 13 + .../tests/ui/deserialize_try_from_missing.rs | 7 + .../ui/deserialize_try_from_missing.stderr | 8 + .../tests/ui/parse_bad_default.rs | 9 + .../tests/ui/parse_bad_default.stderr | 5 + .../tests/ui/parse_bad_default_path.rs | 9 + .../tests/ui/parse_bad_default_path.stderr | 5 + .../tests/ui/parse_bad_nix.rs | 9 + .../tests/ui/parse_bad_nix.stderr | 5 + .../tests/ui/parse_bad_version.rs | 9 + .../tests/ui/parse_bad_version.stderr | 5 + .../tests/ui/parse_mising_version.rs | 9 + .../tests/ui/parse_mising_version.stderr | 5 + tvix/nix-compat-derive/Cargo.toml | 32 ++ tvix/nix-compat-derive/default.nix | 5 + tvix/nix-compat-derive/src/de.rs | 272 ++++++++++++ tvix/nix-compat-derive/src/internal/attrs.rs | 358 +++++++++++++++ tvix/nix-compat-derive/src/internal/ctx.rs | 50 +++ tvix/nix-compat-derive/src/internal/inputs.rs | 110 +++++ tvix/nix-compat-derive/src/internal/mod.rs | 183 ++++++++ tvix/nix-compat-derive/src/internal/symbol.rs | 32 ++ tvix/nix-compat-derive/src/lib.rs | 348 +++++++++++++++ tvix/nix-compat/Cargo.toml | 8 +- .../src/nix_daemon/protocol_version.rs | 7 + 48 files changed, 2376 insertions(+), 3 deletions(-) create mode 100644 tvix/nix-compat-derive-tests/Cargo.toml create mode 100644 tvix/nix-compat-derive-tests/default.nix create mode 100644 tvix/nix-compat-derive-tests/tests/read_derive.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_bad_type.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_bad_type.stderr create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_enum_non_exaustive.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_enum_non_exaustive.stderr create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_from_missing.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_from_missing.stderr create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_from_str_error_not_display.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_from_str_error_not_display.stderr create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_from_str_missing.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_from_str_missing.stderr create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_missing_default.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_missing_default.stderr create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_missing_default_path.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_missing_default_path.stderr create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_remote_missing_attr.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_remote_missing_attr.stderr create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_try_from_error_not_display.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_try_from_error_not_display.stderr create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_try_from_missing.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui/deserialize_try_from_missing.stderr create mode 100644 tvix/nix-compat-derive-tests/tests/ui/parse_bad_default.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui/parse_bad_default.stderr create mode 100644 tvix/nix-compat-derive-tests/tests/ui/parse_bad_default_path.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui/parse_bad_default_path.stderr create mode 100644 tvix/nix-compat-derive-tests/tests/ui/parse_bad_nix.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui/parse_bad_nix.stderr create mode 100644 tvix/nix-compat-derive-tests/tests/ui/parse_bad_version.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui/parse_bad_version.stderr create mode 100644 tvix/nix-compat-derive-tests/tests/ui/parse_mising_version.rs create mode 100644 tvix/nix-compat-derive-tests/tests/ui/parse_mising_version.stderr create mode 100644 tvix/nix-compat-derive/Cargo.toml create mode 100644 tvix/nix-compat-derive/default.nix create mode 100644 tvix/nix-compat-derive/src/de.rs create mode 100644 tvix/nix-compat-derive/src/internal/attrs.rs create mode 100644 tvix/nix-compat-derive/src/internal/ctx.rs create mode 100644 tvix/nix-compat-derive/src/internal/inputs.rs create mode 100644 tvix/nix-compat-derive/src/internal/mod.rs create mode 100644 tvix/nix-compat-derive/src/internal/symbol.rs create mode 100644 tvix/nix-compat-derive/src/lib.rs diff --git a/tvix/Cargo.lock b/tvix/Cargo.lock index 11014d201..7bf659475 100644 --- a/tvix/Cargo.lock +++ b/tvix/Cargo.lock @@ -2342,6 +2342,7 @@ dependencies = [ "hex-literal", "lazy_static", "mimalloc", + "nix-compat-derive", "nom", "num-traits", "pin-project-lite", @@ -2358,6 +2359,35 @@ dependencies = [ "zstd", ] +[[package]] +name = "nix-compat-derive" +version = "0.1.0" +dependencies = [ + "hex-literal", + "nix-compat", + "pretty_assertions", + "proc-macro2", + "quote", + "rstest", + "syn 2.0.72", + "tokio", + "tokio-test", +] + +[[package]] +name = "nix-compat-derive-tests" +version = "0.1.0" +dependencies = [ + "hex-literal", + "nix-compat", + "nix-compat-derive", + "pretty_assertions", + "rstest", + "tokio", + "tokio-test", + "trybuild", +] + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -4708,6 +4738,20 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "trybuild" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207aa50d36c4be8d8c6ea829478be44a372c6a77669937bb39c698e52f1491e8" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "termcolor", + "toml 0.8.15", +] + [[package]] name = "tvix-build" version = "0.1.0" diff --git a/tvix/Cargo.nix b/tvix/Cargo.nix index 5321a97e8..274d3d1d1 100644 --- a/tvix/Cargo.nix +++ b/tvix/Cargo.nix @@ -55,6 +55,26 @@ rec { # File a bug if you depend on any for non-debug work! debug = internal.debugCrate { inherit packageId; }; }; + "nix-compat-derive" = rec { + packageId = "nix-compat-derive"; + build = internal.buildRustCrateWithFeatures { + packageId = "nix-compat-derive"; + }; + + # Debug support which might change between releases. + # File a bug if you depend on any for non-debug work! + debug = internal.debugCrate { inherit packageId; }; + }; + "nix-compat-derive-tests" = rec { + packageId = "nix-compat-derive-tests"; + build = internal.buildRustCrateWithFeatures { + packageId = "nix-compat-derive-tests"; + }; + + # 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-build" = rec { packageId = "tvix-build"; build = internal.buildRustCrateWithFeatures { @@ -7395,6 +7415,12 @@ rec { name = "mimalloc"; packageId = "mimalloc"; } + { + name = "nix-compat-derive"; + packageId = "nix-compat-derive"; + optional = true; + usesDefaultFeatures = false; + } { name = "nom"; packageId = "nom"; @@ -7488,12 +7514,115 @@ rec { features = { "async" = [ "tokio" ]; "bytes" = [ "dep:bytes" ]; - "default" = [ "async" "wire" ]; + "default" = [ "async" "wire" "nix-compat-derive" ]; + "nix-compat-derive" = [ "dep:nix-compat-derive" ]; "pin-project-lite" = [ "dep:pin-project-lite" ]; "tokio" = [ "dep:tokio" ]; "wire" = [ "tokio" "pin-project-lite" "bytes" ]; }; - resolvedDefaultFeatures = [ "async" "bytes" "default" "pin-project-lite" "tokio" "wire" ]; + resolvedDefaultFeatures = [ "async" "bytes" "default" "nix-compat-derive" "pin-project-lite" "test" "tokio" "wire" ]; + }; + "nix-compat-derive" = rec { + crateName = "nix-compat-derive"; + version = "0.1.0"; + edition = "2021"; + src = lib.cleanSourceWith { filter = sourceFilter; src = ./nix-compat-derive; }; + procMacro = true; + libName = "nix_compat_derive"; + dependencies = [ + { + name = "proc-macro2"; + packageId = "proc-macro2"; + features = [ "proc-macro" ]; + } + { + name = "quote"; + packageId = "quote"; + features = [ "proc-macro" ]; + } + { + name = "syn"; + packageId = "syn 2.0.72"; + features = [ "full" "extra-traits" ]; + } + ]; + devDependencies = [ + { + name = "hex-literal"; + packageId = "hex-literal"; + } + { + name = "nix-compat"; + packageId = "nix-compat"; + usesDefaultFeatures = false; + features = [ "async" "wire" "test" ]; + } + { + name = "pretty_assertions"; + packageId = "pretty_assertions"; + } + { + name = "rstest"; + packageId = "rstest"; + } + { + name = "tokio"; + packageId = "tokio"; + features = [ "io-util" "macros" ]; + } + { + name = "tokio-test"; + packageId = "tokio-test"; + } + ]; + features = { + "default" = [ "external" ]; + }; + resolvedDefaultFeatures = [ "default" "external" ]; + }; + "nix-compat-derive-tests" = rec { + crateName = "nix-compat-derive-tests"; + version = "0.1.0"; + edition = "2021"; + src = lib.cleanSourceWith { filter = sourceFilter; src = ./nix-compat-derive-tests; }; + devDependencies = [ + { + name = "hex-literal"; + packageId = "hex-literal"; + } + { + name = "nix-compat"; + packageId = "nix-compat"; + features = [ "test" "wire" ]; + } + { + name = "nix-compat-derive"; + packageId = "nix-compat-derive"; + } + { + name = "pretty_assertions"; + packageId = "pretty_assertions"; + } + { + name = "rstest"; + packageId = "rstest"; + } + { + name = "tokio"; + packageId = "tokio"; + features = [ "io-util" "macros" ]; + } + { + name = "tokio-test"; + packageId = "tokio-test"; + } + { + name = "trybuild"; + packageId = "trybuild"; + } + ]; + features = { }; + resolvedDefaultFeatures = [ "compile-tests" ]; }; "nohash-hasher" = rec { crateName = "nohash-hasher"; @@ -15498,6 +15627,45 @@ rec { ]; }; + "trybuild" = rec { + crateName = "trybuild"; + version = "1.0.99"; + edition = "2021"; + sha256 = "1s4i2hpyb66676xkg6b6fxm2qdsawj5lfad8ds68vgn46q6sayi0"; + authors = [ + "David Tolnay " + ]; + dependencies = [ + { + name = "glob"; + packageId = "glob"; + } + { + name = "serde"; + packageId = "serde"; + } + { + name = "serde_derive"; + packageId = "serde_derive"; + } + { + name = "serde_json"; + packageId = "serde_json"; + } + { + name = "termcolor"; + packageId = "termcolor"; + } + { + name = "toml"; + packageId = "toml 0.8.15"; + } + ]; + features = { + "diff" = [ "dissimilar" ]; + "dissimilar" = [ "dep:dissimilar" ]; + }; + }; "tvix-build" = rec { crateName = "tvix-build"; version = "0.1.0"; diff --git a/tvix/Cargo.toml b/tvix/Cargo.toml index 53b9134a5..175125e15 100644 --- a/tvix/Cargo.toml +++ b/tvix/Cargo.toml @@ -27,6 +27,8 @@ members = [ "glue", "nar-bridge", "nix-compat", + "nix-compat-derive", + "nix-compat-derive-tests", "serde", "store", "tracing", diff --git a/tvix/nix-compat-derive-tests/Cargo.toml b/tvix/nix-compat-derive-tests/Cargo.toml new file mode 100644 index 000000000..31a334b92 --- /dev/null +++ b/tvix/nix-compat-derive-tests/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "nix-compat-derive-tests" +version = "0.1.0" +edition = "2021" + +[features] +compile-tests = [] + +[dev-dependencies] +hex-literal = "0.4.1" +pretty_assertions = "1.4.0" +rstest = "0.19.0" +tokio-test = "0.4.3" +trybuild = "1.0.96" + +[dev-dependencies.nix-compat] +version = "0.1.0" +path = "../nix-compat" +features = ["test", "wire"] + +[dev-dependencies.nix-compat-derive] +version = "0.1.0" +path = "../nix-compat-derive" + +[dev-dependencies.tokio] +version = "^1.38" +features = ["io-util", "macros"] diff --git a/tvix/nix-compat-derive-tests/default.nix b/tvix/nix-compat-derive-tests/default.nix new file mode 100644 index 000000000..cabe9ad13 --- /dev/null +++ b/tvix/nix-compat-derive-tests/default.nix @@ -0,0 +1,5 @@ +{ depot, ... }: + +depot.tvix.crates.workspaceMembers.nix-compat-derive-tests.build.override { + runTests = true; +} diff --git a/tvix/nix-compat-derive-tests/tests/read_derive.rs b/tvix/nix-compat-derive-tests/tests/read_derive.rs new file mode 100644 index 000000000..055d70cf0 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/read_derive.rs @@ -0,0 +1,417 @@ +use std::str::FromStr; + +use nix_compat::nix_daemon::de::mock::{Builder, Error}; +use nix_compat::nix_daemon::de::NixRead; +use nix_compat_derive::NixDeserialize; + +#[derive(Debug, PartialEq, Eq, NixDeserialize)] +pub struct UnitTest; + +#[derive(Debug, PartialEq, Eq, NixDeserialize)] +pub struct EmptyTupleTest(); + +#[derive(Debug, PartialEq, Eq, NixDeserialize)] +pub struct StructTest { + first: u64, + second: String, +} + +#[derive(Debug, PartialEq, Eq, NixDeserialize)] +pub struct TupleTest(u64, String); + +#[derive(Debug, PartialEq, Eq, NixDeserialize)] +pub struct StructVersionTest { + test: u64, + #[nix(version = "20..")] + hello: String, +} + +fn default_test() -> StructVersionTest { + StructVersionTest { + test: 89, + hello: String::from("klomp"), + } +} + +#[derive(Debug, PartialEq, Eq, NixDeserialize)] +pub struct TupleVersionTest(u64, #[nix(version = "25..")] String); + +#[derive(Debug, PartialEq, Eq, NixDeserialize)] +pub struct TupleVersionDefaultTest( + u64, + #[nix(version = "..25", default = "default_test")] StructVersionTest, +); + +#[tokio::test] +async fn read_unit() { + let mut mock = Builder::new().build(); + let v: UnitTest = mock.read_value().await.unwrap(); + assert_eq!(UnitTest, v); +} + +#[tokio::test] +async fn read_empty_tuple() { + let mut mock = Builder::new().build(); + let v: EmptyTupleTest = mock.read_value().await.unwrap(); + assert_eq!(EmptyTupleTest(), v); +} + +#[tokio::test] +async fn read_struct() { + let mut mock = Builder::new().read_number(89).read_slice(b"klomp").build(); + let v: StructTest = mock.read_value().await.unwrap(); + assert_eq!( + StructTest { + first: 89, + second: String::from("klomp"), + }, + v + ); +} + +#[tokio::test] +async fn read_tuple() { + let mut mock = Builder::new().read_number(89).read_slice(b"klomp").build(); + let v: TupleTest = mock.read_value().await.unwrap(); + assert_eq!(TupleTest(89, String::from("klomp")), v); +} + +#[tokio::test] +async fn read_struct_version() { + let mut mock = Builder::new() + .version((1, 20)) + .read_number(89) + .read_slice(b"klomp") + .build(); + let v: StructVersionTest = mock.read_value().await.unwrap(); + assert_eq!(default_test(), v); +} + +#[tokio::test] +async fn read_struct_without_version() { + let mut mock = Builder::new().version((1, 19)).read_number(89).build(); + let v: StructVersionTest = mock.read_value().await.unwrap(); + assert_eq!( + StructVersionTest { + test: 89, + hello: String::new(), + }, + v + ); +} + +#[tokio::test] +async fn read_tuple_version() { + let mut mock = Builder::new() + .version((1, 26)) + .read_number(89) + .read_slice(b"klomp") + .build(); + let v: TupleVersionTest = mock.read_value().await.unwrap(); + assert_eq!(TupleVersionTest(89, "klomp".into()), v); +} + +#[tokio::test] +async fn read_tuple_without_version() { + let mut mock = Builder::new().version((1, 19)).read_number(89).build(); + let v: TupleVersionTest = mock.read_value().await.unwrap(); + assert_eq!(TupleVersionTest(89, String::new()), v); +} + +#[tokio::test] +async fn read_complex_1() { + let mut mock = Builder::new() + .version((1, 19)) + .read_number(999) + .read_number(666) + .build(); + let v: TupleVersionDefaultTest = mock.read_value().await.unwrap(); + assert_eq!( + TupleVersionDefaultTest( + 999, + StructVersionTest { + test: 666, + hello: String::new() + } + ), + v + ); +} + +#[tokio::test] +async fn read_complex_2() { + let mut mock = Builder::new() + .version((1, 20)) + .read_number(999) + .read_number(666) + .read_slice(b"The quick brown \xF0\x9F\xA6\x8A jumps over 13 lazy \xF0\x9F\x90\xB6.") + .build(); + let v: TupleVersionDefaultTest = mock.read_value().await.unwrap(); + assert_eq!( + TupleVersionDefaultTest( + 999, + StructVersionTest { + test: 666, + hello: String::from("The quick brown 🦊 jumps over 13 lazy 🐶.") + } + ), + v + ); +} + +#[tokio::test] +async fn read_complex_3() { + let mut mock = Builder::new().version((1, 25)).read_number(999).build(); + let v: TupleVersionDefaultTest = mock.read_value().await.unwrap(); + assert_eq!( + TupleVersionDefaultTest( + 999, + StructVersionTest { + test: 89, + hello: String::from("klomp") + } + ), + v + ); +} + +#[tokio::test] +async fn read_complex_4() { + let mut mock = Builder::new().version((1, 26)).read_number(999).build(); + let v: TupleVersionDefaultTest = mock.read_value().await.unwrap(); + assert_eq!( + TupleVersionDefaultTest( + 999, + StructVersionTest { + test: 89, + hello: String::from("klomp") + } + ), + v + ); +} + +#[tokio::test] +async fn read_field_invalid_data() { + let mut mock = Builder::new() + .read_number(666) + .read_slice(b"The quick brown \xED\xA0\x80 jumped.") + .build(); + let err = mock.read_value::().await.unwrap_err(); + assert_eq!( + Error::InvalidData("invalid utf-8 sequence of 1 bytes from index 16".into()), + err + ); +} + +#[tokio::test] +async fn read_field_missing_data() { + let mut mock = Builder::new().read_number(666).build(); + let err = mock.read_value::().await.unwrap_err(); + assert_eq!(Error::MissingData("unexpected end-of-file".into()), err); +} + +#[tokio::test] +async fn read_field_no_data() { + let mut mock = Builder::new().build(); + let err = mock.read_value::().await.unwrap_err(); + assert_eq!(Error::MissingData("unexpected end-of-file".into()), err); +} + +#[tokio::test] +async fn read_field_reader_error_first() { + let mut mock = Builder::new() + .read_number_error(Error::InvalidData("Bad reader".into())) + .build(); + let err = mock.read_value::().await.unwrap_err(); + assert_eq!(Error::InvalidData("Bad reader".into()), err); +} + +#[tokio::test] +async fn read_field_reader_error_later() { + let mut mock = Builder::new() + .read_number(999) + .read_bytes_error(Error::InvalidData("Bad reader".into())) + .build(); + let err = mock.read_value::().await.unwrap_err(); + assert_eq!(Error::InvalidData("Bad reader".into()), err); +} + +#[derive(Debug, PartialEq, Eq, NixDeserialize)] +#[nix(from_str)] +struct TestFromStr; + +impl FromStr for TestFromStr { + type Err = String; + + fn from_str(s: &str) -> Result { + if s == "test" { + Ok(TestFromStr) + } else { + Err(s.into()) + } + } +} + +#[tokio::test] +async fn read_from_str() { + let mut mock = Builder::new().read_slice(b"test").build(); + let value = mock.read_value::().await.unwrap(); + assert_eq!(TestFromStr, value); +} + +#[tokio::test] +async fn read_from_str_invalid_data() { + let mut mock = Builder::new().read_slice(b"wrong string").build(); + let err = mock.read_value::().await.unwrap_err(); + assert_eq!(Error::InvalidData("wrong string".into()), err); +} + +#[tokio::test] +async fn read_from_str_invalid_string() { + let mut mock = Builder::new() + .read_slice(b"The quick brown \xED\xA0\x80 jumped.") + .build(); + let err = mock.read_value::().await.unwrap_err(); + assert_eq!( + Error::InvalidData("invalid utf-8 sequence of 1 bytes from index 16".into()), + err + ); +} + +#[tokio::test] +async fn read_from_str_reader_error() { + let mut mock = Builder::new() + .read_bytes_error(Error::InvalidData("Bad reader".into())) + .build(); + let err = mock.read_value::().await.unwrap_err(); + assert_eq!(Error::InvalidData("Bad reader".into()), err); +} + +#[derive(Debug, PartialEq, Eq, NixDeserialize)] +#[nix(try_from = "u64")] +struct TestTryFromU64; + +impl TryFrom for TestTryFromU64 { + type Error = u64; + + fn try_from(value: u64) -> Result { + if value == 42 { + Ok(TestTryFromU64) + } else { + Err(value) + } + } +} + +#[tokio::test] +async fn read_try_from_u64() { + let mut mock = Builder::new().read_number(42).build(); + let value = mock.read_value::().await.unwrap(); + assert_eq!(TestTryFromU64, value); +} + +#[tokio::test] +async fn read_try_from_u64_invalid_data() { + let mut mock = Builder::new().read_number(666).build(); + let err = mock.read_value::().await.unwrap_err(); + assert_eq!(Error::InvalidData("666".into()), err); +} + +#[tokio::test] +async fn read_try_from_u64_reader_error() { + let mut mock = Builder::new() + .read_number_error(Error::InvalidData("Bad reader".into())) + .build(); + let err = mock.read_value::().await.unwrap_err(); + assert_eq!(Error::InvalidData("Bad reader".into()), err); +} + +#[derive(Debug, PartialEq, Eq, NixDeserialize)] +#[nix(from = "u64")] +struct TestFromU64; + +impl From for TestFromU64 { + fn from(_value: u64) -> TestFromU64 { + TestFromU64 + } +} + +#[tokio::test] +async fn read_from_u64() { + let mut mock = Builder::new().read_number(42).build(); + let value = mock.read_value::().await.unwrap(); + assert_eq!(TestFromU64, value); +} + +#[tokio::test] +async fn read_from_u64_reader_error() { + let mut mock = Builder::new() + .read_number_error(Error::InvalidData("Bad reader".into())) + .build(); + let err = mock.read_value::().await.unwrap_err(); + assert_eq!(Error::InvalidData("Bad reader".into()), err); +} + +#[derive(Debug, PartialEq, Eq, NixDeserialize)] +enum TestEnum { + #[nix(version = "..=19")] + Pre20(TestTryFromU64), + #[nix(version = "20..")] + Post20(StructVersionTest), +} + +#[tokio::test] +async fn read_enum_19() { + let mut mock = Builder::new().version((1, 19)).read_number(42).build(); + let value = mock.read_value::().await.unwrap(); + assert_eq!(TestEnum::Pre20(TestTryFromU64), value); +} + +#[tokio::test] +async fn read_enum_20() { + let mut mock = Builder::new() + .version((1, 20)) + .read_number(42) + .read_slice(b"klomp") + .build(); + let value = mock.read_value::().await.unwrap(); + assert_eq!( + TestEnum::Post20(StructVersionTest { + test: 42, + hello: "klomp".into(), + }), + value + ); +} + +#[tokio::test] +async fn read_enum_reader_error() { + let mut mock = Builder::new() + .version((1, 19)) + .read_number_error(Error::InvalidData("Bad reader".into())) + .build(); + let err = mock.read_value::().await.unwrap_err(); + assert_eq!(Error::InvalidData("Bad reader".into()), err); +} + +#[tokio::test] +async fn read_enum_invalid_data_19() { + let mut mock = Builder::new().version((1, 19)).read_number(666).build(); + let err = mock.read_value::().await.unwrap_err(); + assert_eq!(Error::InvalidData("666".into()), err); +} + +#[tokio::test] +async fn read_enum_invalid_data_20() { + let mut mock = Builder::new() + .version((1, 20)) + .read_number(666) + .read_slice(b"The quick brown \xED\xA0\x80 jumped.") + .build(); + let err = mock.read_value::().await.unwrap_err(); + assert_eq!( + Error::InvalidData("invalid utf-8 sequence of 1 bytes from index 16".into()), + err + ); +} diff --git a/tvix/nix-compat-derive-tests/tests/ui.rs b/tvix/nix-compat-derive-tests/tests/ui.rs new file mode 100644 index 000000000..6a7bffeaf --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui.rs @@ -0,0 +1,6 @@ +#[cfg(feature = "compile-tests")] +#[test] +fn ui() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/*.rs"); +} diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_bad_type.rs b/tvix/nix-compat-derive-tests/tests/ui/deserialize_bad_type.rs new file mode 100644 index 000000000..f77469679 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_bad_type.rs @@ -0,0 +1,10 @@ +use nix_compat_derive::NixDeserialize; + +pub struct BadType; + +#[derive(NixDeserialize)] +pub struct Test { + version: BadType, +} + +fn main() {} diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_bad_type.stderr b/tvix/nix-compat-derive-tests/tests/ui/deserialize_bad_type.stderr new file mode 100644 index 000000000..12ffdc83c --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_bad_type.stderr @@ -0,0 +1,21 @@ +error[E0277]: the trait bound `BadType: NixDeserialize` is not satisfied + --> tests/ui/deserialize_bad_type.rs:7:14 + | +7 | version: BadType, + | ^^^^^^^ the trait `NixDeserialize` is not implemented for `BadType` + | + = help: the following other types implement trait `NixDeserialize`: + BTreeMap + String + Test + Vec + bool + bytes::bytes::Bytes + i64 + u64 + usize +note: required by a bound in `try_read_value` + --> $WORKSPACE/nix-compat/src/nix_daemon/de/mod.rs + | + | fn try_read_value( + | ^^^^^^^^^^^^^^ required by this bound in `NixRead::try_read_value` diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_enum_non_exaustive.rs b/tvix/nix-compat-derive-tests/tests/ui/deserialize_enum_non_exaustive.rs new file mode 100644 index 000000000..ab559f2b8 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_enum_non_exaustive.rs @@ -0,0 +1,13 @@ +use nix_compat_derive::NixDeserialize; + +#[derive(NixDeserialize)] +pub enum Test { + #[nix(version = "..=10")] + Old, + #[nix(version = "15..=17")] + Legacy, + #[nix(version = "50..")] + NewWay, +} + +fn main() {} diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_enum_non_exaustive.stderr b/tvix/nix-compat-derive-tests/tests/ui/deserialize_enum_non_exaustive.stderr new file mode 100644 index 000000000..8a46d9439 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_enum_non_exaustive.stderr @@ -0,0 +1,8 @@ +error[E0004]: non-exhaustive patterns: `11_u8..=14_u8` and `18_u8..=49_u8` not covered + --> tests/ui/deserialize_enum_non_exaustive.rs:3:10 + | +3 | #[derive(NixDeserialize)] + | ^^^^^^^^^^^^^^ patterns `11_u8..=14_u8` and `18_u8..=49_u8` not covered + | + = note: the matched value is of type `u8` + = note: this error originates in the derive macro `NixDeserialize` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_missing.rs b/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_missing.rs new file mode 100644 index 000000000..913b7c4f7 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_missing.rs @@ -0,0 +1,7 @@ +use nix_compat_derive::NixDeserialize; + +#[derive(NixDeserialize)] +#[nix(from = "u64")] +pub struct Test; + +fn main() {} diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_missing.stderr b/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_missing.stderr new file mode 100644 index 000000000..0124010cf --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_missing.stderr @@ -0,0 +1,5 @@ +error[E0277]: the trait bound `Test: From` is not satisfied + --> tests/ui/deserialize_from_missing.rs:4:14 + | +4 | #[nix(from = "u64")] + | ^^^^^ the trait `From` is not implemented for `Test` diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_str_error_not_display.rs b/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_str_error_not_display.rs new file mode 100644 index 000000000..36cd4b153 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_str_error_not_display.rs @@ -0,0 +1,20 @@ +use std::str::FromStr; + +use nix_compat_derive::NixDeserialize; + +#[derive(NixDeserialize)] +#[nix(from_str)] +pub struct Test; + +impl FromStr for Test { + type Err = (); + fn from_str(s: &str) -> Result { + if s == "test" { + Ok(Test) + } else { + Err(()) + } + } +} + +fn main() {} diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_str_error_not_display.stderr b/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_str_error_not_display.stderr new file mode 100644 index 000000000..8283ed534 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_str_error_not_display.stderr @@ -0,0 +1,13 @@ +error[E0277]: `()` doesn't implement `std::fmt::Display` + --> tests/ui/deserialize_from_str_error_not_display.rs:6:7 + | +6 | #[nix(from_str)] + | ^^^^^^^^ `()` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `()` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead +note: required by a bound in `invalid_data` + --> $WORKSPACE/nix-compat/src/nix_daemon/de/mod.rs + | + | fn invalid_data(msg: T) -> Self { + | ^^^^^^^^^^^^ required by this bound in `Error::invalid_data` diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_str_missing.rs b/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_str_missing.rs new file mode 100644 index 000000000..a959db57e --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_str_missing.rs @@ -0,0 +1,7 @@ +use nix_compat_derive::NixDeserialize; + +#[derive(NixDeserialize)] +#[nix(from_str)] +pub struct Test; + +fn main() {} diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_str_missing.stderr b/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_str_missing.stderr new file mode 100644 index 000000000..f68f58801 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_from_str_missing.stderr @@ -0,0 +1,16 @@ +error[E0277]: the trait bound `Test: FromStr` is not satisfied + --> tests/ui/deserialize_from_str_missing.rs:4:7 + | +4 | #[nix(from_str)] + | ^^^^^^^^ the trait `FromStr` is not implemented for `Test` + | + = help: the following other types implement trait `FromStr`: + IpAddr + Ipv4Addr + Ipv6Addr + NonZero + NonZero + NonZero + NonZero + NonZero + and $N others diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_missing_default.rs b/tvix/nix-compat-derive-tests/tests/ui/deserialize_missing_default.rs new file mode 100644 index 000000000..e9df62845 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_missing_default.rs @@ -0,0 +1,12 @@ +use nix_compat_derive::NixDeserialize; + +#[derive(NixDeserialize)] +pub struct Value(String); + +#[derive(NixDeserialize)] +pub struct Test { + #[nix(version = "20..")] + version: Value, +} + +fn main() {} diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_missing_default.stderr b/tvix/nix-compat-derive-tests/tests/ui/deserialize_missing_default.stderr new file mode 100644 index 000000000..5cc2f5974 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_missing_default.stderr @@ -0,0 +1,12 @@ +error[E0277]: the trait bound `Value: Default` is not satisfied + --> tests/ui/deserialize_missing_default.rs:6:10 + | +6 | #[derive(NixDeserialize)] + | ^^^^^^^^^^^^^^ the trait `Default` is not implemented for `Value` + | + = note: this error originates in the derive macro `NixDeserialize` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider annotating `Value` with `#[derive(Default)]` + | +4 + #[derive(Default)] +5 | pub struct Value(String); + | diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_missing_default_path.rs b/tvix/nix-compat-derive-tests/tests/ui/deserialize_missing_default_path.rs new file mode 100644 index 000000000..4f319c069 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_missing_default_path.rs @@ -0,0 +1,12 @@ +use nix_compat_derive::NixDeserialize; + +#[derive(NixDeserialize)] +pub struct Value(String); + +#[derive(NixDeserialize)] +pub struct Test { + #[nix(version = "20..", default = "Value::make_default")] + version: Value, +} + +fn main() {} diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_missing_default_path.stderr b/tvix/nix-compat-derive-tests/tests/ui/deserialize_missing_default_path.stderr new file mode 100644 index 000000000..bb9af7491 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_missing_default_path.stderr @@ -0,0 +1,8 @@ +error[E0599]: no function or associated item named `make_default` found for struct `Value` in the current scope + --> tests/ui/deserialize_missing_default_path.rs:8:39 + | +4 | pub struct Value(String); + | ---------------- function or associated item `make_default` not found for this struct +... +8 | #[nix(version = "20..", default = "Value::make_default")] + | ^^^^^^^^^^^^^^^^^^^^^ function or associated item not found in `Value` diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_remote_missing_attr.rs b/tvix/nix-compat-derive-tests/tests/ui/deserialize_remote_missing_attr.rs new file mode 100644 index 000000000..cc2ab5bfb --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_remote_missing_attr.rs @@ -0,0 +1,15 @@ +use nix_compat_derive::nix_deserialize_remote; + +pub struct Value(String); +impl From for Value { + fn from(s: String) -> Value { + Value(s) + } +} + +nix_deserialize_remote!( + #[nix()] + Value +); + +fn main() {} diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_remote_missing_attr.stderr b/tvix/nix-compat-derive-tests/tests/ui/deserialize_remote_missing_attr.stderr new file mode 100644 index 000000000..a1c18adc6 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_remote_missing_attr.stderr @@ -0,0 +1,5 @@ +error: Missing from_str, from or try_from attribute + --> tests/ui/deserialize_remote_missing_attr.rs:10:25 + | +10 | nix_deserialize_remote!(#[nix()] Value); + | ^^^^^^^^^^^^^^ diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_try_from_error_not_display.rs b/tvix/nix-compat-derive-tests/tests/ui/deserialize_try_from_error_not_display.rs new file mode 100644 index 000000000..7f8ad6bbf --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_try_from_error_not_display.rs @@ -0,0 +1,19 @@ +use nix_compat_derive::NixDeserialize; + +#[derive(NixDeserialize)] +#[nix(try_from = "u64")] +pub struct Test; + +impl TryFrom for Test { + type Error = (); + + fn try_from(value: u64) -> Result { + if value == 42 { + Ok(Test) + } else { + Err(()) + } + } +} + +fn main() {} diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_try_from_error_not_display.stderr b/tvix/nix-compat-derive-tests/tests/ui/deserialize_try_from_error_not_display.stderr new file mode 100644 index 000000000..8e55a3c56 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_try_from_error_not_display.stderr @@ -0,0 +1,13 @@ +error[E0277]: `()` doesn't implement `std::fmt::Display` + --> tests/ui/deserialize_try_from_error_not_display.rs:4:18 + | +4 | #[nix(try_from = "u64")] + | ^^^^^ `()` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `()` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead +note: required by a bound in `invalid_data` + --> $WORKSPACE/nix-compat/src/nix_daemon/de/mod.rs + | + | fn invalid_data(msg: T) -> Self { + | ^^^^^^^^^^^^ required by this bound in `Error::invalid_data` diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_try_from_missing.rs b/tvix/nix-compat-derive-tests/tests/ui/deserialize_try_from_missing.rs new file mode 100644 index 000000000..899095ae3 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_try_from_missing.rs @@ -0,0 +1,7 @@ +use nix_compat_derive::NixDeserialize; + +#[derive(NixDeserialize)] +#[nix(try_from = "u64")] +pub struct Test; + +fn main() {} diff --git a/tvix/nix-compat-derive-tests/tests/ui/deserialize_try_from_missing.stderr b/tvix/nix-compat-derive-tests/tests/ui/deserialize_try_from_missing.stderr new file mode 100644 index 000000000..9605d1f33 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/deserialize_try_from_missing.stderr @@ -0,0 +1,8 @@ +error[E0277]: the trait bound `Test: From` is not satisfied + --> tests/ui/deserialize_try_from_missing.rs:4:18 + | +4 | #[nix(try_from = "u64")] + | ^^^^^ the trait `From` is not implemented for `Test`, which is required by `Test: TryFrom` + | + = note: required for `u64` to implement `Into` + = note: required for `Test` to implement `TryFrom` diff --git a/tvix/nix-compat-derive-tests/tests/ui/parse_bad_default.rs b/tvix/nix-compat-derive-tests/tests/ui/parse_bad_default.rs new file mode 100644 index 000000000..d87831cec --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/parse_bad_default.rs @@ -0,0 +1,9 @@ +use nix_compat_derive::NixDeserialize; + +#[derive(NixDeserialize)] +pub struct Test { + #[nix(default = 12)] + version: u8, +} + +fn main() {} diff --git a/tvix/nix-compat-derive-tests/tests/ui/parse_bad_default.stderr b/tvix/nix-compat-derive-tests/tests/ui/parse_bad_default.stderr new file mode 100644 index 000000000..acb1bc2a4 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/parse_bad_default.stderr @@ -0,0 +1,5 @@ +error: expected nix attribute default to be string + --> tests/ui/parse_bad_default.rs:5:21 + | +5 | #[nix(default = 12)] + | ^^ diff --git a/tvix/nix-compat-derive-tests/tests/ui/parse_bad_default_path.rs b/tvix/nix-compat-derive-tests/tests/ui/parse_bad_default_path.rs new file mode 100644 index 000000000..fbde8ffbc --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/parse_bad_default_path.rs @@ -0,0 +1,9 @@ +use nix_compat_derive::NixDeserialize; + +#[derive(NixDeserialize)] +pub struct Test { + #[nix(default = "12")] + version: u8, +} + +fn main() {} diff --git a/tvix/nix-compat-derive-tests/tests/ui/parse_bad_default_path.stderr b/tvix/nix-compat-derive-tests/tests/ui/parse_bad_default_path.stderr new file mode 100644 index 000000000..7628d4c83 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/parse_bad_default_path.stderr @@ -0,0 +1,5 @@ +error: expected identifier + --> tests/ui/parse_bad_default_path.rs:5:21 + | +5 | #[nix(default = "12")] + | ^^^^ diff --git a/tvix/nix-compat-derive-tests/tests/ui/parse_bad_nix.rs b/tvix/nix-compat-derive-tests/tests/ui/parse_bad_nix.rs new file mode 100644 index 000000000..690e76a20 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/parse_bad_nix.rs @@ -0,0 +1,9 @@ +use nix_compat_derive::NixDeserialize; + +#[derive(NixDeserialize)] +pub struct Test { + #[nix] + version: u8, +} + +fn main() {} diff --git a/tvix/nix-compat-derive-tests/tests/ui/parse_bad_nix.stderr b/tvix/nix-compat-derive-tests/tests/ui/parse_bad_nix.stderr new file mode 100644 index 000000000..da3d2d9aa --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/parse_bad_nix.stderr @@ -0,0 +1,5 @@ +error: expected attribute arguments in parentheses: #[nix(...)] + --> tests/ui/parse_bad_nix.rs:5:7 + | +5 | #[nix] + | ^^^ diff --git a/tvix/nix-compat-derive-tests/tests/ui/parse_bad_version.rs b/tvix/nix-compat-derive-tests/tests/ui/parse_bad_version.rs new file mode 100644 index 000000000..35b3b05c2 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/parse_bad_version.rs @@ -0,0 +1,9 @@ +use nix_compat_derive::NixDeserialize; + +#[derive(NixDeserialize)] +pub struct Test { + #[nix(version = 12)] + version: u8, +} + +fn main() {} diff --git a/tvix/nix-compat-derive-tests/tests/ui/parse_bad_version.stderr b/tvix/nix-compat-derive-tests/tests/ui/parse_bad_version.stderr new file mode 100644 index 000000000..48cc817fa --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/parse_bad_version.stderr @@ -0,0 +1,5 @@ +error: expected nix attribute version to be string + --> tests/ui/parse_bad_version.rs:5:21 + | +5 | #[nix(version = 12)] + | ^^ diff --git a/tvix/nix-compat-derive-tests/tests/ui/parse_mising_version.rs b/tvix/nix-compat-derive-tests/tests/ui/parse_mising_version.rs new file mode 100644 index 000000000..9eaa743ed --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/parse_mising_version.rs @@ -0,0 +1,9 @@ +use nix_compat_derive::NixDeserialize; + +#[derive(NixDeserialize)] +pub struct Test { + #[nix(version)] + version: u8, +} + +fn main() {} diff --git a/tvix/nix-compat-derive-tests/tests/ui/parse_mising_version.stderr b/tvix/nix-compat-derive-tests/tests/ui/parse_mising_version.stderr new file mode 100644 index 000000000..79f048e11 --- /dev/null +++ b/tvix/nix-compat-derive-tests/tests/ui/parse_mising_version.stderr @@ -0,0 +1,5 @@ +error: expected `=` + --> tests/ui/parse_mising_version.rs:5:18 + | +5 | #[nix(version)] + | ^ diff --git a/tvix/nix-compat-derive/Cargo.toml b/tvix/nix-compat-derive/Cargo.toml new file mode 100644 index 000000000..dea8ab5ab --- /dev/null +++ b/tvix/nix-compat-derive/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "nix-compat-derive" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[features] +external = [] +default = ["external"] + + +[dependencies] +proc-macro2 = { version = "1.0.86", features = ["proc-macro"] } +quote = { version = "1.0.36", features = ["proc-macro"] } +syn = { version = "2.0.72", features = ["full", "extra-traits"] } + +[dev-dependencies] +hex-literal = "0.4.1" +pretty_assertions = "1.4.0" +rstest = "0.19.0" +tokio-test = "0.4.3" + +[dev-dependencies.tokio] +version = "^1.38" +features = ["io-util", "macros"] + +[dev-dependencies.nix-compat] +path = "../nix-compat" +default-features = false +features = ["async", "wire", "test"] diff --git a/tvix/nix-compat-derive/default.nix b/tvix/nix-compat-derive/default.nix new file mode 100644 index 000000000..e6636e7f2 --- /dev/null +++ b/tvix/nix-compat-derive/default.nix @@ -0,0 +1,5 @@ +{ depot, lib, ... }: + +depot.tvix.crates.workspaceMembers.nix-compat-derive.build.override { + runTests = true; +} diff --git a/tvix/nix-compat-derive/src/de.rs b/tvix/nix-compat-derive/src/de.rs new file mode 100644 index 000000000..ee79ea9d1 --- /dev/null +++ b/tvix/nix-compat-derive/src/de.rs @@ -0,0 +1,272 @@ +use proc_macro2::{Span, TokenStream}; +use quote::{quote, quote_spanned, ToTokens}; +use syn::spanned::Spanned; +use syn::{DeriveInput, Generics, Path, Type}; + +use crate::internal::attrs::Default; +use crate::internal::inputs::RemoteInput; +use crate::internal::{attrs, Container, Context, Data, Field, Remote, Style, Variant}; + +pub fn expand_nix_deserialize(nnixrs: Path, input: &mut DeriveInput) -> syn::Result { + let cx = Context::new(); + let cont = Container::from_ast(&cx, nnixrs, input); + cx.check()?; + let cont = cont.unwrap(); + + let ty = cont.ident_type(); + let body = nix_deserialize_body(&cont); + let crate_path = cont.crate_path(); + + Ok(nix_deserialize_impl( + crate_path, + &ty, + &cont.original.generics, + body, + )) +} + +pub fn expand_nix_deserialize_remote( + crate_path: Path, + input: &RemoteInput, +) -> syn::Result { + let cx = Context::new(); + let remote = Remote::from_ast(&cx, crate_path, input); + cx.check()?; + let remote = remote.unwrap(); + + let crate_path = remote.crate_path(); + let body = nix_deserialize_body_from(crate_path, &remote.attrs).expect("From tokenstream"); + let generics = Generics::default(); + Ok(nix_deserialize_impl(crate_path, remote.ty, &generics, body)) +} + +fn nix_deserialize_impl( + crate_path: &Path, + ty: &Type, + generics: &Generics, + body: TokenStream, +) -> TokenStream { + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + quote! { + #[automatically_derived] + impl #impl_generics #crate_path::nix_daemon::de::NixDeserialize for #ty #ty_generics + #where_clause + { + #[allow(clippy::manual_async_fn)] + fn try_deserialize(reader: &mut R) -> impl ::std::future::Future, R::Error>> + Send + '_ + where R: ?Sized + #crate_path::nix_daemon::de::NixRead + Send, + { + #body + } + } + } +} + +fn nix_deserialize_body_from( + crate_path: &syn::Path, + attrs: &attrs::Container, +) -> Option { + if let Some(span) = attrs.from_str.as_ref() { + Some(nix_deserialize_from_str(crate_path, span.span())) + } else if let Some(type_from) = attrs.type_from.as_ref() { + Some(nix_deserialize_from(type_from)) + } else { + attrs + .type_try_from + .as_ref() + .map(|type_try_from| nix_deserialize_try_from(crate_path, type_try_from)) + } +} + +fn nix_deserialize_body(cont: &Container) -> TokenStream { + if let Some(tokens) = nix_deserialize_body_from(cont.crate_path(), &cont.attrs) { + tokens + } else { + match &cont.data { + Data::Struct(style, fields) => nix_deserialize_struct(*style, fields), + Data::Enum(variants) => nix_deserialize_enum(variants), + } + } +} + +fn nix_deserialize_struct(style: Style, fields: &[Field<'_>]) -> TokenStream { + let read_fields = fields.iter().map(|f| { + let field = f.var_ident(); + let ty = f.ty; + let read_value = quote_spanned! { + ty.span()=> if first__ { + first__ = false; + if let Some(v) = reader.try_read_value::<#ty>().await? { + v + } else { + return Ok(None); + } + } else { + reader.read_value::<#ty>().await? + } + }; + if let Some(version) = f.attrs.version.as_ref() { + let default = match &f.attrs.default { + Default::Default => quote_spanned!(ty.span()=>::std::default::Default::default), + Default::Path(path) => path.to_token_stream(), + _ => panic!("No default for versioned field"), + }; + quote! { + let #field : #ty = if (#version).contains(&reader.version().minor()) { + #read_value + } else { + #default() + }; + } + } else { + quote! { + let #field : #ty = #read_value; + } + } + }); + + let field_names = fields.iter().map(|f| f.var_ident()); + let construct = match style { + Style::Struct => { + quote! { + Self { #(#field_names),* } + } + } + Style::Tuple => { + quote! { + Self(#(#field_names),*) + } + } + Style::Unit => quote!(Self), + }; + quote! { + #[allow(unused_assignments)] + async move { + let mut first__ = true; + #(#read_fields)* + Ok(Some(#construct)) + } + } +} + +fn nix_deserialize_variant(variant: &Variant<'_>) -> TokenStream { + let ident = variant.ident; + let read_fields = variant.fields.iter().map(|f| { + let field = f.var_ident(); + let ty = f.ty; + let read_value = quote_spanned! { + ty.span()=> if first__ { + first__ = false; + if let Some(v) = reader.try_read_value::<#ty>().await? { + v + } else { + return Ok(None); + } + } else { + reader.read_value::<#ty>().await? + } + }; + if let Some(version) = f.attrs.version.as_ref() { + let default = match &f.attrs.default { + Default::Default => quote_spanned!(ty.span()=>::std::default::Default::default), + Default::Path(path) => path.to_token_stream(), + _ => panic!("No default for versioned field"), + }; + quote! { + let #field : #ty = if (#version).contains(&reader.version().minor()) { + #read_value + } else { + #default() + }; + } + } else { + quote! { + let #field : #ty = #read_value; + } + } + }); + let field_names = variant.fields.iter().map(|f| f.var_ident()); + let construct = match variant.style { + Style::Struct => { + quote! { + Self::#ident { #(#field_names),* } + } + } + Style::Tuple => { + quote! { + Self::#ident(#(#field_names),*) + } + } + Style::Unit => quote!(Self::#ident), + }; + let version = &variant.attrs.version; + quote! { + #version => { + #(#read_fields)* + Ok(Some(#construct)) + } + } +} + +fn nix_deserialize_enum(variants: &[Variant<'_>]) -> TokenStream { + let match_variant = variants + .iter() + .map(|variant| nix_deserialize_variant(variant)); + quote! { + #[allow(unused_assignments)] + async move { + let mut first__ = true; + match reader.version().minor() { + #(#match_variant)* + } + } + } +} + +fn nix_deserialize_from(ty: &Type) -> TokenStream { + quote_spanned! { + ty.span() => + async move { + if let Some(value) = reader.try_read_value::<#ty>().await? { + Ok(Some(>::from(value))) + } else { + Ok(None) + } + } + } +} + +fn nix_deserialize_try_from(crate_path: &Path, ty: &Type) -> TokenStream { + quote_spanned! { + ty.span() => + async move { + use #crate_path::nix_daemon::de::Error; + if let Some(item) = reader.try_read_value::<#ty>().await? { + >::try_from(item) + .map_err(Error::invalid_data) + .map(Some) + } else { + Ok(None) + } + } + } +} + +fn nix_deserialize_from_str(crate_path: &Path, span: Span) -> TokenStream { + quote_spanned! { + span => + async move { + use #crate_path::nix_daemon::de::Error; + if let Some(buf) = reader.try_read_bytes().await? { + let s = ::std::str::from_utf8(&buf) + .map_err(Error::invalid_data)?; + ::from_str(s) + .map_err(Error::invalid_data) + .map(Some) + } else { + Ok(None) + } + } + } +} diff --git a/tvix/nix-compat-derive/src/internal/attrs.rs b/tvix/nix-compat-derive/src/internal/attrs.rs new file mode 100644 index 000000000..dbc959d1e --- /dev/null +++ b/tvix/nix-compat-derive/src/internal/attrs.rs @@ -0,0 +1,358 @@ +use quote::ToTokens; +use syn::meta::ParseNestedMeta; +use syn::parse::Parse; +use syn::{parse_quote, Attribute, Expr, ExprLit, ExprPath, Lit, Token}; + +use super::symbol::{Symbol, CRATE, DEFAULT, FROM, FROM_STR, NIX, TRY_FROM, VERSION}; +use super::Context; + +#[derive(Debug, PartialEq, Eq)] +pub enum Default { + None, + #[allow(clippy::enum_variant_names)] + Default, + Path(ExprPath), +} + +impl Default { + pub fn is_none(&self) -> bool { + matches!(self, Default::None) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Field { + pub default: Default, + pub version: Option, +} + +impl Field { + pub fn from_ast(ctx: &Context, attrs: &Vec) -> Field { + let mut version = None; + let mut default = Default::None; + for attr in attrs { + if attr.path() != NIX { + continue; + } + if let Err(err) = attr.parse_nested_meta(|meta| { + if meta.path == VERSION { + version = parse_lit(ctx, &meta, VERSION)?; + } else if meta.path == DEFAULT { + if meta.input.peek(Token![=]) { + if let Some(path) = parse_lit(ctx, &meta, DEFAULT)? { + default = Default::Path(path); + } + } else { + default = Default::Default; + } + } else { + let path = meta.path.to_token_stream().to_string(); + return Err(meta.error(format_args!("unknown nix field attribute '{}'", path))); + } + Ok(()) + }) { + eprintln!("{:?}", err.span().source_text()); + ctx.syn_error(err); + } + } + if version.is_some() && default.is_none() { + default = Default::Default; + } + + Field { default, version } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Variant { + pub version: syn::ExprRange, +} + +impl Variant { + pub fn from_ast(ctx: &Context, attrs: &Vec) -> Variant { + let mut version = parse_quote!(..); + for attr in attrs { + if attr.path() != NIX { + continue; + } + if let Err(err) = attr.parse_nested_meta(|meta| { + if meta.path == VERSION { + if let Some(v) = parse_lit(ctx, &meta, VERSION)? { + version = v; + } + } else { + let path = meta.path.to_token_stream().to_string(); + return Err( + meta.error(format_args!("unknown nix variant attribute '{}'", path)) + ); + } + Ok(()) + }) { + eprintln!("{:?}", err.span().source_text()); + ctx.syn_error(err); + } + } + + Variant { version } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Container { + pub from_str: Option, + pub type_from: Option, + pub type_try_from: Option, + pub crate_path: Option, +} + +impl Container { + pub fn from_ast(ctx: &Context, attrs: &Vec) -> Container { + let mut type_from = None; + let mut type_try_from = None; + let mut crate_path = None; + let mut from_str = None; + + for attr in attrs { + if attr.path() != NIX { + continue; + } + if let Err(err) = attr.parse_nested_meta(|meta| { + if meta.path == FROM { + type_from = parse_lit(ctx, &meta, FROM)?; + } else if meta.path == TRY_FROM { + type_try_from = parse_lit(ctx, &meta, TRY_FROM)?; + } else if meta.path == FROM_STR { + from_str = Some(meta.path); + } else if meta.path == CRATE { + crate_path = parse_lit(ctx, &meta, CRATE)?; + } else { + let path = meta.path.to_token_stream().to_string(); + return Err( + meta.error(format_args!("unknown nix variant attribute '{}'", path)) + ); + } + Ok(()) + }) { + eprintln!("{:?}", err.span().source_text()); + ctx.syn_error(err); + } + } + + Container { + from_str, + type_from, + type_try_from, + crate_path, + } + } +} + +pub fn get_lit_str( + ctx: &Context, + meta: &ParseNestedMeta, + attr: Symbol, +) -> syn::Result> { + let expr: Expr = meta.value()?.parse()?; + let mut value = &expr; + while let Expr::Group(e) = value { + value = &e.expr; + } + if let Expr::Lit(ExprLit { + lit: Lit::Str(s), .. + }) = value + { + Ok(Some(s.clone())) + } else { + ctx.error_spanned( + expr, + format_args!("expected nix attribute {} to be string", attr), + ); + Ok(None) + } +} + +pub fn parse_lit( + ctx: &Context, + meta: &ParseNestedMeta, + attr: Symbol, +) -> syn::Result> { + match get_lit_str(ctx, meta, attr)? { + Some(lit) => Ok(Some(lit.parse()?)), + None => Ok(None), + } +} + +#[cfg(test)] +mod test { + use syn::{parse_quote, Attribute}; + + use crate::internal::Context; + + use super::*; + + #[test] + fn parse_field_version() { + let attrs: Vec = vec![parse_quote!(#[nix(version="..34")])]; + let ctx = Context::new(); + let field = Field::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + field, + Field { + default: Default::Default, + version: Some(parse_quote!(..34)), + } + ); + } + + #[test] + fn parse_field_default() { + let attrs: Vec = vec![parse_quote!(#[nix(default)])]; + let ctx = Context::new(); + let field = Field::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + field, + Field { + default: Default::Default, + version: None, + } + ); + } + + #[test] + fn parse_field_default_path() { + let attrs: Vec = vec![parse_quote!(#[nix(default="Default::default")])]; + let ctx = Context::new(); + let field = Field::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + field, + Field { + default: Default::Path(parse_quote!(Default::default)), + version: None, + } + ); + } + + #[test] + fn parse_field_both() { + let attrs: Vec = + vec![parse_quote!(#[nix(version="..", default="Default::default")])]; + let ctx = Context::new(); + let field = Field::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + field, + Field { + default: Default::Path(parse_quote!(Default::default)), + version: Some(parse_quote!(..)), + } + ); + } + + #[test] + fn parse_field_both_rev() { + let attrs: Vec = + vec![parse_quote!(#[nix(default="Default::default", version="..")])]; + let ctx = Context::new(); + let field = Field::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + field, + Field { + default: Default::Path(parse_quote!(Default::default)), + version: Some(parse_quote!(..)), + } + ); + } + + #[test] + fn parse_field_no_attr() { + let attrs: Vec = vec![]; + let ctx = Context::new(); + let field = Field::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + field, + Field { + default: Default::None, + version: None, + } + ); + } + + #[test] + fn parse_field_no_subattrs() { + let attrs: Vec = vec![parse_quote!(#[nix()])]; + let ctx = Context::new(); + let field = Field::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + field, + Field { + default: Default::None, + version: None, + } + ); + } + + #[test] + fn parse_variant_version() { + let attrs: Vec = vec![parse_quote!(#[nix(version="..34")])]; + let ctx = Context::new(); + let variant = Variant::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + variant, + Variant { + version: parse_quote!(..34), + } + ); + } + + #[test] + fn parse_variant_no_attr() { + let attrs: Vec = vec![]; + let ctx = Context::new(); + let variant = Variant::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + variant, + Variant { + version: parse_quote!(..), + } + ); + } + + #[test] + fn parse_variant_no_subattrs() { + let attrs: Vec = vec![parse_quote!(#[nix()])]; + let ctx = Context::new(); + let variant = Variant::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + variant, + Variant { + version: parse_quote!(..), + } + ); + } + + #[test] + fn parse_container_try_from() { + let attrs: Vec = vec![parse_quote!(#[nix(try_from="u64")])]; + let ctx = Context::new(); + let container = Container::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + container, + Container { + from_str: None, + type_from: None, + type_try_from: Some(parse_quote!(u64)), + crate_path: None, + } + ); + } +} diff --git a/tvix/nix-compat-derive/src/internal/ctx.rs b/tvix/nix-compat-derive/src/internal/ctx.rs new file mode 100644 index 000000000..ba770e044 --- /dev/null +++ b/tvix/nix-compat-derive/src/internal/ctx.rs @@ -0,0 +1,50 @@ +use std::cell::RefCell; +use std::fmt; +use std::thread::panicking; + +use quote::ToTokens; + +pub struct Context { + errors: RefCell>>, +} + +impl Context { + pub fn new() -> Context { + Context { + errors: RefCell::new(Some(Vec::new())), + } + } + + pub fn syn_error(&self, error: syn::Error) { + self.errors + .borrow_mut() + .as_mut() + .take() + .unwrap() + .push(error); + } + + pub fn error_spanned(&self, tokens: T, message: D) { + self.syn_error(syn::Error::new_spanned(tokens, message)); + } + + pub fn check(&self) -> syn::Result<()> { + let mut iter = self.errors.borrow_mut().take().unwrap().into_iter(); + let mut err = match iter.next() { + None => return Ok(()), + Some(err) => err, + }; + for next_err in iter { + err.combine(next_err); + } + Err(err) + } +} + +impl Drop for Context { + fn drop(&mut self) { + if self.errors.borrow().is_some() && !panicking() { + panic!("Context dropped without checking errors"); + } + } +} diff --git a/tvix/nix-compat-derive/src/internal/inputs.rs b/tvix/nix-compat-derive/src/internal/inputs.rs new file mode 100644 index 000000000..097a141a5 --- /dev/null +++ b/tvix/nix-compat-derive/src/internal/inputs.rs @@ -0,0 +1,110 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RemoteInput { + pub attrs: Vec, + pub ident: syn::Type, +} + +impl syn::parse::Parse for RemoteInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let attrs = input.call(syn::Attribute::parse_outer)?; + + let ident = input.parse::()?; + Ok(RemoteInput { attrs, ident }) + } +} + +impl quote::ToTokens for RemoteInput { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + fn is_outer(attr: &&syn::Attribute) -> bool { + match attr.style { + syn::AttrStyle::Outer => true, + syn::AttrStyle::Inner(_) => false, + } + } + for attr in self.attrs.iter().filter(is_outer) { + attr.to_tokens(tokens); + } + self.ident.to_tokens(tokens); + } +} + +#[cfg(test)] +mod test { + use syn::parse_quote; + //use syn::parse::Parse; + + use super::*; + + #[test] + fn test_input() { + let p: RemoteInput = parse_quote!(u64); + assert_eq!( + p, + RemoteInput { + attrs: vec![], + ident: parse_quote!(u64), + } + ); + } + + #[test] + fn test_input_attr() { + let p: RemoteInput = parse_quote!( + #[nix] + u64 + ); + assert_eq!( + p, + RemoteInput { + attrs: vec![parse_quote!(#[nix])], + ident: parse_quote!(u64), + } + ); + } + + #[test] + fn test_input_attr_multiple() { + let p: RemoteInput = parse_quote!( + #[nix] + #[hello] + u64 + ); + assert_eq!( + p, + RemoteInput { + attrs: vec![parse_quote!(#[nix]), parse_quote!(#[hello])], + ident: parse_quote!(u64), + } + ); + } + + #[test] + fn test_input_attr_full() { + let p: RemoteInput = parse_quote!( + #[nix(try_from = "u64")] + usize + ); + assert_eq!( + p, + RemoteInput { + attrs: vec![parse_quote!(#[nix(try_from="u64")])], + ident: parse_quote!(usize), + } + ); + } + + #[test] + fn test_input_attr_other() { + let p: RemoteInput = parse_quote!( + #[muh] + u64 + ); + assert_eq!( + p, + RemoteInput { + attrs: vec![parse_quote!(#[muh])], + ident: parse_quote!(u64), + } + ); + } +} diff --git a/tvix/nix-compat-derive/src/internal/mod.rs b/tvix/nix-compat-derive/src/internal/mod.rs new file mode 100644 index 000000000..20b243221 --- /dev/null +++ b/tvix/nix-compat-derive/src/internal/mod.rs @@ -0,0 +1,183 @@ +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::Token; + +pub mod attrs; +mod ctx; +pub mod inputs; +mod symbol; + +pub use ctx::Context; + +pub struct Field<'a> { + pub member: syn::Member, + pub ty: &'a syn::Type, + pub attrs: attrs::Field, + pub original: &'a syn::Field, +} + +impl<'a> Field<'a> { + pub fn from_ast(ctx: &Context, idx: usize, field: &'a syn::Field) -> Field<'a> { + let attrs = attrs::Field::from_ast(ctx, &field.attrs); + let member = match &field.ident { + Some(id) => syn::Member::Named(id.clone()), + None => syn::Member::Unnamed(idx.into()), + }; + Field { + member, + attrs, + ty: &field.ty, + original: field, + } + } + + pub fn var_ident(&self) -> syn::Ident { + match &self.member { + syn::Member::Named(name) => name.clone(), + syn::Member::Unnamed(idx) => { + syn::Ident::new(&format!("field{}", idx.index), self.original.span()) + } + } + } +} + +pub struct Variant<'a> { + pub ident: &'a syn::Ident, + pub attrs: attrs::Variant, + pub style: Style, + pub fields: Vec>, + //pub original: &'a syn::Variant, +} + +impl<'a> Variant<'a> { + pub fn from_ast(ctx: &Context, variant: &'a syn::Variant) -> Self { + let attrs = attrs::Variant::from_ast(ctx, &variant.attrs); + let (style, fields) = match &variant.fields { + syn::Fields::Named(fields) => (Style::Struct, fields_ast(ctx, &fields.named)), + syn::Fields::Unnamed(fields) => (Style::Tuple, fields_ast(ctx, &fields.unnamed)), + syn::Fields::Unit => (Style::Unit, Vec::new()), + }; + Variant { + ident: &variant.ident, + attrs, + style, + fields, + //original: variant, + } + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum Style { + Struct, + Tuple, + Unit, +} + +pub enum Data<'a> { + Enum(Vec>), + Struct(Style, Vec>), +} + +pub struct Container<'a> { + pub ident: &'a syn::Ident, + pub attrs: attrs::Container, + pub data: Data<'a>, + pub crate_path: syn::Path, + pub original: &'a syn::DeriveInput, +} + +impl<'a> Container<'a> { + pub fn from_ast( + ctx: &Context, + crate_path: syn::Path, + input: &'a mut syn::DeriveInput, + ) -> Option> { + let attrs = attrs::Container::from_ast(ctx, &input.attrs); + let data = match &input.data { + syn::Data::Struct(s) => match &s.fields { + syn::Fields::Named(fields) => { + Data::Struct(Style::Struct, fields_ast(ctx, &fields.named)) + } + syn::Fields::Unnamed(fields) => { + Data::Struct(Style::Tuple, fields_ast(ctx, &fields.unnamed)) + } + syn::Fields::Unit => Data::Struct(Style::Unit, Vec::new()), + }, + syn::Data::Enum(e) => { + let variants = e + .variants + .iter() + .map(|variant| Variant::from_ast(ctx, variant)) + .collect(); + Data::Enum(variants) + } + syn::Data::Union(u) => { + ctx.error_spanned(u.union_token, "Union not supported by nixrs"); + return None; + } + }; + Some(Container { + ident: &input.ident, + attrs, + data, + crate_path, + original: input, + }) + } + + pub fn crate_path(&self) -> &syn::Path { + if let Some(crate_path) = self.attrs.crate_path.as_ref() { + crate_path + } else { + &self.crate_path + } + } + + pub fn ident_type(&self) -> syn::Type { + let path: syn::Path = self.ident.clone().into(); + let tp = syn::TypePath { qself: None, path }; + tp.into() + } +} + +pub struct Remote<'a> { + pub attrs: attrs::Container, + pub ty: &'a syn::Type, + pub crate_path: syn::Path, +} + +impl<'a> Remote<'a> { + pub fn from_ast( + ctx: &Context, + crate_path: syn::Path, + input: &'a inputs::RemoteInput, + ) -> Option> { + let attrs = attrs::Container::from_ast(ctx, &input.attrs); + if attrs.from_str.is_none() && attrs.type_from.is_none() && attrs.type_try_from.is_none() { + ctx.error_spanned(input, "Missing from_str, from or try_from attribute"); + return None; + } + Some(Remote { + ty: &input.ident, + attrs, + crate_path, + }) + } + + pub fn crate_path(&self) -> &syn::Path { + if let Some(crate_path) = self.attrs.crate_path.as_ref() { + crate_path + } else { + &self.crate_path + } + } +} + +fn fields_ast<'a>(ctx: &Context, fields: &'a Punctuated) -> Vec> { + fields + .iter() + .enumerate() + .map(|(idx, field)| Field::from_ast(ctx, idx, field)) + .collect() +} diff --git a/tvix/nix-compat-derive/src/internal/symbol.rs b/tvix/nix-compat-derive/src/internal/symbol.rs new file mode 100644 index 000000000..ed3fe304e --- /dev/null +++ b/tvix/nix-compat-derive/src/internal/symbol.rs @@ -0,0 +1,32 @@ +use std::fmt; + +use syn::Path; + +#[derive(Copy, Clone)] +pub struct Symbol(&'static str); + +pub const NIX: Symbol = Symbol("nix"); +pub const VERSION: Symbol = Symbol("version"); +pub const DEFAULT: Symbol = Symbol("default"); +pub const FROM: Symbol = Symbol("from"); +pub const TRY_FROM: Symbol = Symbol("try_from"); +pub const FROM_STR: Symbol = Symbol("from_str"); +pub const CRATE: Symbol = Symbol("crate"); + +impl PartialEq for Path { + fn eq(&self, word: &Symbol) -> bool { + self.is_ident(word.0) + } +} + +impl<'a> PartialEq for &'a Path { + fn eq(&self, word: &Symbol) -> bool { + self.is_ident(word.0) + } +} + +impl fmt::Display for Symbol { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str(self.0) + } +} diff --git a/tvix/nix-compat-derive/src/lib.rs b/tvix/nix-compat-derive/src/lib.rs new file mode 100644 index 000000000..5c0dd5c2d --- /dev/null +++ b/tvix/nix-compat-derive/src/lib.rs @@ -0,0 +1,348 @@ +//! # Using derive +//! +//! 1. [Overview](#overview) +//! 3. [Attributes](#attributes) +//! 1. [Container attributes](#container-attributes) +//! 1. [`#[nix(from_str)]`](#nixfrom_str) +//! 2. [`#[nix(from = "FromType")]`](#nixfrom--fromtype) +//! 3. [`#[nix(try_from = "FromType")]`](#nixtry_from--fromtype) +//! 4. [`#[nix(crate = "...")]`](#nixcrate--) +//! 2. [Variant attributes](#variant-attributes) +//! 1. [`#[nix(version = "range")]`](#nixversion--range) +//! 3. [Field attributes](#field-attributes) +//! 1. [`#[nix(version = "range")]`](#nixversion--range-1) +//! 2. [`#[nix(default)]`](#nixdefault) +//! 3. [`#[nix(default = "path")]`](#nixdefault--path) +//! +//! ## Overview +//! +//! This crate contains derive macros and function-like macros for implementing +//! `NixDeserialize` with less boilerplate. +//! +//! ### Examples +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! struct Unnamed(u64, String); +//! ``` +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! struct Fields { +//! number: u64, +//! message: String, +//! }; +//! ``` +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! struct Ignored; +//! ``` +//! +//! ## Attributes +//! +//! To customize the derived trait implementations you can add +//! [attributes](https://doc.rust-lang.org/reference/attributes.html) +//! to containers, fields and variants. +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! #[nix(crate="nix_compat")] // <-- This is a container attribute +//! struct Fields { +//! number: u64, +//! #[nix(version="..20")] // <-- This is a field attribute +//! message: String, +//! }; +//! +//! #[derive(NixDeserialize)] +//! #[nix(crate="nix_compat")] // <-- This is also a container attribute +//! enum E { +//! #[nix(version="..10")] // <-- This is a variant attribute +//! A(u64), +//! #[nix(version="10..")] // <-- This is also a variant attribute +//! B(String), +//! } +//! ``` +//! +//! ### Container attributes +//! +//! ##### `#[nix(from_str)]` +//! +//! When `from_str` is specified the fields are all ignored and instead a +//! `String` is first deserialized and then `FromStr::from_str` is used +//! to convert this `String` to the container type. +//! +//! This means that the container must implement `FromStr` and the error +//! returned from the `from_str` must implement `Display`. +//! +//! ###### Example +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! #[nix(from_str)] +//! struct MyString(String); +//! impl std::str::FromStr for MyString { +//! type Err = String; +//! fn from_str(s: &str) -> Result { +//! if s != "bad string" { +//! Ok(MyString(s.to_string())) +//! } else { +//! Err("Got a bad string".to_string()) +//! } +//! } +//! } +//! ``` +//! +//! ##### `#[nix(from = "FromType")]` +//! +//! When `from` is specified the fields are all ignored and instead a +//! value of `FromType` is first deserialized and then `From::from` is +//! used to convert from this value to the container type. +//! +//! This means that the container must implement `From` and +//! `FromType` must implement `NixDeserialize`. +//! +//! ###### Example +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! #[nix(from="usize")] +//! struct MyValue(usize); +//! impl From for MyValue { +//! fn from(val: usize) -> Self { +//! MyValue(val) +//! } +//! } +//! ``` +//! +//! ##### `#[nix(try_from = "FromType")]` +//! +//! With `try_from` a value of `FromType` is first deserialized and then +//! `TryFrom::try_from` is used to convert from this value to the container +//! type. +//! +//! This means that the container must implement `TryFrom` and +//! `FromType` must implement `NixDeserialize`. +//! The error returned from `try_from` also needs to implement `Display`. +//! +//! ###### Example +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! #[nix(try_from="usize")] +//! struct WrongAnswer(usize); +//! impl TryFrom for WrongAnswer { +//! type Error = String; +//! fn try_from(val: usize) -> Result { +//! if val != 42 { +//! Ok(WrongAnswer(val)) +//! } else { +//! Err("Got the answer to life the universe and everything".to_string()) +//! } +//! } +//! } +//! ``` +//! +//! ##### `#[nix(crate = "...")]` +//! +//! Specify the path to the `nix-compat` crate instance to use when referring +//! to the API in the generated code. This is usually not needed. +//! +//! ### Variant attributes +//! +//! ##### `#[nix(version = "range")]` +//! +//! Specifies the protocol version range where this variant is used. +//! When deriving an enum the `version` attribute is used to select which +//! variant of the enum to deserialize. The range is for minor version and +//! the version ranges of all variants combined must cover all versions +//! without any overlap or the first variant that matches is selected. +//! +//! ###### Example +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! #[derive(NixDeserialize)] +//! enum Testing { +//! #[nix(version="..=18")] +//! OldVersion(u64), +//! #[nix(version="19..")] +//! NewVersion(String), +//! } +//! ``` +//! +//! ### Field attributes +//! +//! ##### `#[nix(version = "range")]` +//! +//! Specifies the protocol version range where this field is included. +//! The range is for minor version. For example `version = "..20"` +//! includes the field in protocol versions `1.0` to `1.19` and skips +//! it in version `1.20` and above. +//! +//! ###### Example +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! struct Field { +//! number: u64, +//! #[nix(version="..20")] +//! messsage: String, +//! } +//! ``` +//! +//! ##### `#[nix(default)]` +//! +//! When a field is skipped because the active protocol version falls +//! outside the range specified in [`#[nix(version = "range")]`](#nixversion--range-1) +//! this attribute indicates that `Default::default()` should be used +//! to get a value for the field. This is also the default +//! when you only specify [`#[nix(version = "range")]`](#nixversion--range-1). +//! +//! ###### Example +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! struct Field { +//! number: u64, +//! #[nix(version="..20", default)] +//! messsage: String, +//! } +//! ``` +//! +//! ##### `#[nix(default = "path")]` +//! +//! When a field is skipped because the active protocol version falls +//! outside the range specified in [`#[nix(version = "range")]`](#nixversion--range-1) +//! this attribute indicates that the function in `path` should be called to +//! get a default value for the field. The given function must be callable +//! as `fn() -> T`. +//! For example `default = "my_value"` would call `my_value()` and `default = +//! "AType::empty"` would call `AType::empty()`. +//! +//! ###### Example +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! struct Field { +//! number: u64, +//! #[nix(version="..20", default="missing_string")] +//! messsage: String, +//! } +//! +//! fn missing_string() -> String { +//! "missing string".to_string() +//! } +//! ``` + +use internal::inputs::RemoteInput; +use proc_macro::TokenStream; +use syn::{parse_quote, DeriveInput}; + +mod de; +mod internal; + +#[cfg(not(feature = "external"))] +#[proc_macro_derive(NixDeserialize, attributes(nix))] +pub fn derive_nix_deserialize(item: TokenStream) -> TokenStream { + let mut input = syn::parse_macro_input!(item as DeriveInput); + let nnixrs: syn::Path = parse_quote!(crate); + de::expand_nix_deserialize(nnixrs, &mut input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[cfg(feature = "external")] +#[proc_macro_derive(NixDeserialize, attributes(nix))] +pub fn derive_nix_deserialize(item: TokenStream) -> TokenStream { + let mut input = syn::parse_macro_input!(item as DeriveInput); + let nnixrs: syn::Path = parse_quote!(::nix_compat); + de::expand_nix_deserialize(nnixrs, &mut input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Macro to implement `NixDeserialize` on a type. +/// Sometimes you can't use the deriver to implement `NixDeserialize` +/// (like when dealing with types in Rust standard library) but don't want +/// to implement it yourself. So this macro can be used for those situations +/// where you would derive using `#[nix(from_str)]`, +/// `#[nix(from = "FromType")]` or `#[nix(try_from = "FromType")]` if you +/// could. +/// +/// #### Example +/// +/// ```rust +/// # use nix_compat_derive::nix_deserialize_remote; +/// # +/// struct MyU64(u64); +/// +/// impl From for MyU64 { +/// fn from(value: u64) -> Self { +/// Self(value) +/// } +/// } +/// +/// nix_deserialize_remote!(#[nix(from="u64")] MyU64); +/// ``` +#[cfg(not(feature = "external"))] +#[proc_macro] +pub fn nix_deserialize_remote(item: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(item as RemoteInput); + let crate_path = parse_quote!(crate); + de::expand_nix_deserialize_remote(crate_path, &input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Macro to implement `NixDeserialize` on a type. +/// Sometimes you can't use the deriver to implement `NixDeserialize` +/// (like when dealing with types in Rust standard library) but don't want +/// to implement it yourself. So this macro can be used for those situations +/// where you would derive using `#[nix(from_str)]`, +/// `#[nix(from = "FromType")]` or `#[nix(try_from = "FromType")]` if you +/// could. +/// +/// #### Example +/// +/// ```rust +/// # use nix_compat_derive::nix_deserialize_remote; +/// # +/// struct MyU64(u64); +/// +/// impl From for MyU64 { +/// fn from(value: u64) -> Self { +/// Self(value) +/// } +/// } +/// +/// nix_deserialize_remote!(#[nix(from="u64")] MyU64); +/// ``` +#[cfg(feature = "external")] +#[proc_macro] +pub fn nix_deserialize_remote(item: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(item as RemoteInput); + let crate_path = parse_quote!(::nix_compat); + de::expand_nix_deserialize_remote(crate_path, &input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} diff --git a/tvix/nix-compat/Cargo.toml b/tvix/nix-compat/Cargo.toml index 9f43bb24e..87e9b1e67 100644 --- a/tvix/nix-compat/Cargo.toml +++ b/tvix/nix-compat/Cargo.toml @@ -8,9 +8,10 @@ edition = "2021" async = ["tokio"] # code emitting low-level packets used in the daemon protocol. wire = ["tokio", "pin-project-lite", "bytes"] +test = [] # Enable all features by default. -default = ["async", "wire"] +default = ["async", "wire", "nix-compat-derive"] [dependencies] bitflags = "2.4.1" @@ -33,6 +34,11 @@ tracing = "0.1.37" optional = true version = "1.6.1" +[dependencies.nix-compat-derive] +path = "../nix-compat-derive" +optional = true +default-features = false + [dependencies.tokio] optional = true version = "1.32.0" diff --git a/tvix/nix-compat/src/nix_daemon/protocol_version.rs b/tvix/nix-compat/src/nix_daemon/protocol_version.rs index 3c8fe663e..19da28d48 100644 --- a/tvix/nix-compat/src/nix_daemon/protocol_version.rs +++ b/tvix/nix-compat/src/nix_daemon/protocol_version.rs @@ -54,6 +54,13 @@ impl From for ProtocolVersion { } } +#[cfg(any(test, feature = "test"))] +impl From<(u8, u8)> for ProtocolVersion { + fn from((major, minor): (u8, u8)) -> Self { + Self::from_parts(major, minor) + } +} + impl TryFrom for ProtocolVersion { type Error = &'static str;