about summary refs log tree commit diff
path: root/tvix/nix-compat-derive/src
diff options
context:
space:
mode:
authorBrian Olsen <brian@maven-group.org>2024-11-03T19·42+0100
committerclbot <clbot@tvl.fyi>2024-11-04T20·02+0000
commitb88579ade41244b09555bbb68296033fc300043f (patch)
tree4bfeb72e232a711e1d632a907a80b6fb4d0d6ba0 /tvix/nix-compat-derive/src
parent6582fa69f15c8337cb3a16e74062037a7278020f (diff)
feat(tvix/nix-compat): Add nix serialization support r/8893
This change implements the serialization part that is needed to
implement the nix daemon protocol. Previously was add deserialization
and derivers for that and this then adds the other part of that equation
so that you can write types that can then be read using deserialization.

Change-Id: I2917de634980a93822a4f5a8ad38897b9ce16d89
Reviewed-on: https://cl.tvl.fyi/c/depot/+/12729
Autosubmit: Brian Olsen <me@griff.name>
Reviewed-by: flokli <flokli@flokli.de>
Tested-by: BuildkiteCI
Diffstat (limited to 'tvix/nix-compat-derive/src')
-rw-r--r--tvix/nix-compat-derive/src/de.rs5
-rw-r--r--tvix/nix-compat-derive/src/internal/attrs.rs148
-rw-r--r--tvix/nix-compat-derive/src/internal/mod.rs4
-rw-r--r--tvix/nix-compat-derive/src/internal/symbol.rs3
-rw-r--r--tvix/nix-compat-derive/src/lib.rs175
-rw-r--r--tvix/nix-compat-derive/src/ser.rs227
6 files changed, 548 insertions, 14 deletions
diff --git a/tvix/nix-compat-derive/src/de.rs b/tvix/nix-compat-derive/src/de.rs
index 2214254e2b32..e678b50b0533 100644
--- a/tvix/nix-compat-derive/src/de.rs
+++ b/tvix/nix-compat-derive/src/de.rs
@@ -34,6 +34,11 @@ pub fn expand_nix_deserialize_remote(
 ) -> syn::Result<TokenStream> {
     let cx = Context::new();
     let remote = Remote::from_ast(&cx, crate_path, input);
+    if let Some(attrs) = remote.as_ref().map(|r| &r.attrs) {
+        if attrs.from_str.is_none() && attrs.type_from.is_none() && attrs.type_try_from.is_none() {
+            cx.error_spanned(input, "Missing from_str, from or try_from attribute");
+        }
+    }
     cx.check()?;
     let remote = remote.unwrap();
 
diff --git a/tvix/nix-compat-derive/src/internal/attrs.rs b/tvix/nix-compat-derive/src/internal/attrs.rs
index 9ed84aaf8745..d0fa3b008e22 100644
--- a/tvix/nix-compat-derive/src/internal/attrs.rs
+++ b/tvix/nix-compat-derive/src/internal/attrs.rs
@@ -3,7 +3,9 @@ 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::symbol::{
+    Symbol, CRATE, DEFAULT, DISPLAY, FROM, FROM_STR, INTO, NIX, TRY_FROM, TRY_INTO, VERSION,
+};
 use super::Context;
 
 #[derive(Debug, PartialEq, Eq)]
@@ -104,6 +106,9 @@ pub struct Container {
     pub from_str: Option<syn::Path>,
     pub type_from: Option<syn::Type>,
     pub type_try_from: Option<syn::Type>,
+    pub type_into: Option<syn::Type>,
+    pub type_try_into: Option<syn::Type>,
+    pub display: Default,
     pub crate_path: Option<syn::Path>,
 }
 
@@ -113,6 +118,9 @@ impl Container {
         let mut type_try_from = None;
         let mut crate_path = None;
         let mut from_str = None;
+        let mut type_into = None;
+        let mut type_try_into = None;
+        let mut display = Default::None;
 
         for attr in attrs {
             if attr.path() != NIX {
@@ -125,6 +133,18 @@ impl Container {
                     type_try_from = parse_lit(ctx, &meta, TRY_FROM)?;
                 } else if meta.path == FROM_STR {
                     from_str = Some(meta.path);
+                } else if meta.path == INTO {
+                    type_into = parse_lit(ctx, &meta, INTO)?;
+                } else if meta.path == TRY_INTO {
+                    type_try_into = parse_lit(ctx, &meta, TRY_INTO)?;
+                } else if meta.path == DISPLAY {
+                    if meta.input.peek(Token![=]) {
+                        if let Some(path) = parse_lit(ctx, &meta, DISPLAY)? {
+                            display = Default::Path(path);
+                        }
+                    } else {
+                        display = Default::Default(meta.path);
+                    }
                 } else if meta.path == CRATE {
                     crate_path = parse_lit(ctx, &meta, CRATE)?;
                 } else {
@@ -144,6 +164,9 @@ impl Container {
             from_str,
             type_from,
             type_try_from,
+            type_into,
+            type_try_into,
+            display,
             crate_path,
         }
     }
@@ -342,6 +365,46 @@ mod test {
     }
 
     #[test]
+    fn parse_container_from_str() {
+        let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(from_str)])];
+        let ctx = Context::new();
+        let container = Container::from_ast(&ctx, &attrs);
+        ctx.check().unwrap();
+        assert_eq!(
+            container,
+            Container {
+                from_str: Some(parse_quote!(from_str)),
+                type_from: None,
+                type_try_from: None,
+                type_into: None,
+                type_try_into: None,
+                display: Default::None,
+                crate_path: None,
+            }
+        );
+    }
+
+    #[test]
+    fn parse_container_from() {
+        let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(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: Some(parse_quote!(u64)),
+                type_try_from: None,
+                type_into: None,
+                type_try_into: None,
+                display: Default::None,
+                crate_path: None,
+            }
+        );
+    }
+
+    #[test]
     fn parse_container_try_from() {
         let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(try_from="u64")])];
         let ctx = Context::new();
@@ -353,6 +416,89 @@ mod test {
                 from_str: None,
                 type_from: None,
                 type_try_from: Some(parse_quote!(u64)),
+                type_into: None,
+                type_try_into: None,
+                display: Default::None,
+                crate_path: None,
+            }
+        );
+    }
+
+    #[test]
+    fn parse_container_into() {
+        let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(into="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: None,
+                type_into: Some(parse_quote!(u64)),
+                type_try_into: None,
+                display: Default::None,
+                crate_path: None,
+            }
+        );
+    }
+
+    #[test]
+    fn parse_container_try_into() {
+        let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(try_into="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: None,
+                type_into: None,
+                type_try_into: Some(parse_quote!(u64)),
+                display: Default::None,
+                crate_path: None,
+            }
+        );
+    }
+
+    #[test]
+    fn parse_container_display() {
+        let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(display)])];
+        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: None,
+                type_into: None,
+                type_try_into: None,
+                display: Default::Default(parse_quote!(display)),
+                crate_path: None,
+            }
+        );
+    }
+
+    #[test]
+    fn parse_container_display_path() {
+        let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(display="Path::display")])];
+        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: None,
+                type_into: None,
+                type_try_into: None,
+                display: Default::Path(parse_quote!(Path::display)),
                 crate_path: None,
             }
         );
diff --git a/tvix/nix-compat-derive/src/internal/mod.rs b/tvix/nix-compat-derive/src/internal/mod.rs
index 07ef43b6e0bb..aa42d904718d 100644
--- a/tvix/nix-compat-derive/src/internal/mod.rs
+++ b/tvix/nix-compat-derive/src/internal/mod.rs
@@ -154,10 +154,6 @@ impl<'a> Remote<'a> {
         input: &'a inputs::RemoteInput,
     ) -> Option<Remote<'a>> {
         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,
diff --git a/tvix/nix-compat-derive/src/internal/symbol.rs b/tvix/nix-compat-derive/src/internal/symbol.rs
index ed3fe304eb5d..2bbdc069aa0f 100644
--- a/tvix/nix-compat-derive/src/internal/symbol.rs
+++ b/tvix/nix-compat-derive/src/internal/symbol.rs
@@ -11,6 +11,9 @@ 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 INTO: Symbol = Symbol("into");
+pub const TRY_INTO: Symbol = Symbol("try_into");
+pub const DISPLAY: Symbol = Symbol("display");
 pub const CRATE: Symbol = Symbol("crate");
 
 impl PartialEq<Symbol> for Path {
diff --git a/tvix/nix-compat-derive/src/lib.rs b/tvix/nix-compat-derive/src/lib.rs
index 89735cadf315..394473b1cbf8 100644
--- a/tvix/nix-compat-derive/src/lib.rs
+++ b/tvix/nix-compat-derive/src/lib.rs
@@ -6,7 +6,11 @@
 //!         1. [`#[nix(from_str)]`](#nixfrom_str)
 //!         2. [`#[nix(from = "FromType")]`](#nixfrom--fromtype)
 //!         3. [`#[nix(try_from = "FromType")]`](#nixtry_from--fromtype)
-//!         4. [`#[nix(crate = "...")]`](#nixcrate--)
+//!         4. [`#[nix(into = "IntoType")]`](#nixinto--intotype)
+//!         5. [`#[nix(try_into = "IntoType")]`](#nixtry_into--intotype)
+//!         6. [`#[nix(display)]`](#nixdisplay)
+//!         7. [`#[nix(display = "path")]`](#nixdisplay--path)
+//!         8. [`#[nix(crate = "...")]`](#nixcrate--)
 //!     2. [Variant attributes](#variant-attributes)
 //!         1. [`#[nix(version = "range")]`](#nixversion--range)
 //!     3. [Field attributes](#field-attributes)
@@ -17,20 +21,21 @@
 //! ## Overview
 //!
 //! This crate contains derive macros and function-like macros for implementing
-//! `NixDeserialize` with less boilerplate.
+//! `NixDeserialize` and `NixSerialize` with less boilerplate.
 //!
 //! ### Examples
+//!
 //! ```rust
-//! # use nix_compat_derive::NixDeserialize;
+//! # use nix_compat_derive::{NixDeserialize, NixSerialize};
 //! #
-//! #[derive(NixDeserialize)]
+//! #[derive(NixDeserialize, NixSerialize)]
 //! struct Unnamed(u64, String);
 //! ```
 //!
 //! ```rust
-//! # use nix_compat_derive::NixDeserialize;
+//! # use nix_compat_derive::{NixDeserialize, NixSerialize};
 //! #
-//! #[derive(NixDeserialize)]
+//! #[derive(NixDeserialize, NixSerialize)]
 //! struct Fields {
 //!     number: u64,
 //!     message: String,
@@ -38,9 +43,9 @@
 //! ```
 //!
 //! ```rust
-//! # use nix_compat_derive::NixDeserialize;
+//! # use nix_compat_derive::{NixDeserialize, NixSerialize};
 //! #
-//! #[derive(NixDeserialize)]
+//! #[derive(NixDeserialize, NixSerialize)]
 //! struct Ignored;
 //! ```
 //!
@@ -64,7 +69,7 @@
 //! #[derive(NixDeserialize)]
 //! #[nix(crate="nix_compat")] // <-- This is also a container attribute
 //! enum E {
-//!     #[nix(version="..=9")] // <-- This is a variant attribute
+//!     #[nix(version="..10")] // <-- This is a variant attribute
 //!     A(u64),
 //!     #[nix(version="10..")] // <-- This is also a variant attribute
 //!     B(String),
@@ -156,6 +161,114 @@
 //! }
 //! ```
 //!
+//! ##### `#[nix(into = "IntoType")]`
+//!
+//! When `into` is specified the fields are all ignored and instead the
+//! container type is converted to `IntoType` using `Into::into` and
+//! `IntoType` is then serialized. Before converting `Clone::clone` is
+//! called.
+//!
+//! This means that the container must implement `Into<IntoType>` and `Clone`
+//! and `IntoType` must implement `NixSerialize`.
+//!
+//! ###### Example
+//!
+//! ```rust
+//! # use nix_compat_derive::NixSerialize;
+//! #
+//! #[derive(Clone, NixSerialize)]
+//! #[nix(into="usize")]
+//! struct MyValue(usize);
+//! impl From<MyValue> for usize {
+//!     fn from(val: MyValue) -> Self {
+//!         val.0
+//!     }
+//! }
+//! ```
+//!
+//! ##### `#[nix(try_into = "IntoType")]`
+//!
+//! When `try_into` is specified the fields are all ignored and instead the
+//! container type is converted to `IntoType` using `TryInto::try_into` and
+//! `IntoType` is then serialized. Before converting `Clone::clone` is
+//! called.
+//!
+//! This means that the container must implement `TryInto<IntoType>` and
+//! `Clone` and `IntoType` must implement `NixSerialize`.
+//! The error returned from `try_into` also needs to implement `Display`.
+//!
+//! ###### Example
+//!
+//! ```rust
+//! # use nix_compat_derive::NixSerialize;
+//! #
+//! #[derive(Clone, NixSerialize)]
+//! #[nix(try_into="usize")]
+//! struct WrongAnswer(usize);
+//! impl TryFrom<WrongAnswer> for usize {
+//!     type Error = String;
+//!     fn try_from(val: WrongAnswer) -> Result<Self, Self::Error> {
+//!         if val.0 != 42 {
+//!             Ok(val.0)
+//!         } else {
+//!             Err("Got the answer to life the universe and everything".to_string())
+//!         }
+//!     }
+//! }
+//! ```
+//!
+//! ##### `#[nix(display)]`
+//!
+//! When `display` is specified the fields are all ignored and instead the
+//! container must implement `Display` and `NixWrite::write_display` is used to
+//! write the container.
+//!
+//! ###### Example
+//!
+//! ```rust
+//! # use nix_compat_derive::NixSerialize;
+//! # use std::fmt::{Display, Result, Formatter};
+//! #
+//! #[derive(NixSerialize)]
+//! #[nix(display)]
+//! struct WrongAnswer(usize);
+//! impl Display for WrongAnswer {
+//!     fn fmt(&self, f: &mut Formatter<'_>) -> Result {
+//!         write!(f, "Wrong Answer = {}", self.0)
+//!     }
+//! }
+//! ```
+//!
+//! ##### `#[nix(display = "path")]`
+//!
+//! When `display` is specified the fields are all ignored and instead the
+//! container the specified path must point to a function that is callable as
+//! `fn(&T) -> impl Display`. The result from this call is then written with
+//! `NixWrite::write_display`.
+//! For example `default = "my_value"` would call `my_value(&self)` and `display =
+//! "AType::empty"` would call `AType::empty(&self)`.
+//!
+//! ###### Example
+//!
+//! ```rust
+//! # use nix_compat_derive::NixSerialize;
+//! # use std::fmt::{Display, Result, Formatter};
+//! #
+//! #[derive(NixSerialize)]
+//! #[nix(display = "format_it")]
+//! struct WrongAnswer(usize);
+//! struct WrongDisplay<'a>(&'a WrongAnswer);
+//! impl<'a> Display for WrongDisplay<'a> {
+//!     fn fmt(&self, f: &mut Formatter<'_>) -> Result {
+//!         write!(f, "Wrong Answer = {}", self.0.0)
+//!     }
+//! }
+//!
+//! fn format_it(value: &WrongAnswer) -> impl Display + '_ {
+//!     WrongDisplay(value)
+//! }
+//! ```
+//!
 //! ##### `#[nix(crate = "...")]`
 //!
 //! Specify the path to the `nix-compat` crate instance to use when referring
@@ -175,6 +288,7 @@
 //!
 //! ```rust
 //! # use nix_compat_derive::NixDeserialize;
+//! #
 //! #[derive(NixDeserialize)]
 //! enum Testing {
 //!     #[nix(version="..=18")]
@@ -260,6 +374,7 @@ use syn::{parse_quote, DeriveInput};
 
 mod de;
 mod internal;
+mod ser;
 
 #[proc_macro_derive(NixDeserialize, attributes(nix))]
 pub fn derive_nix_deserialize(item: TokenStream) -> TokenStream {
@@ -270,6 +385,15 @@ pub fn derive_nix_deserialize(item: TokenStream) -> TokenStream {
         .into()
 }
 
+#[proc_macro_derive(NixSerialize, attributes(nix))]
+pub fn derive_nix_serialize(item: TokenStream) -> TokenStream {
+    let mut input = syn::parse_macro_input!(item as DeriveInput);
+    let crate_path: syn::Path = parse_quote!(::nix_compat);
+    ser::expand_nix_serialize(crate_path, &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
@@ -301,3 +425,36 @@ pub fn nix_deserialize_remote(item: TokenStream) -> TokenStream {
         .unwrap_or_else(syn::Error::into_compile_error)
         .into()
 }
+
+/// Macro to implement `NixSerialize` on a type.
+/// Sometimes you can't use the deriver to implement `NixSerialize`
+/// (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(display)]`, `#[nix(display = "path")]`,
+/// `#[nix(store_dir_display)]`, `#[nix(into = "IntoType")]` or
+/// `#[nix(try_into = "IntoType")]` if you could.
+///
+/// #### Example
+///
+/// ```rust
+/// # use nix_compat_derive::nix_serialize_remote;
+/// #
+/// #[derive(Clone)]
+/// struct MyU64(u64);
+///
+/// impl From<MyU64> for u64 {
+///     fn from(value: MyU64) -> Self {
+///         value.0
+///     }
+/// }
+///
+/// nix_serialize_remote!(#[nix(into="u64")] MyU64);
+/// ```
+#[proc_macro]
+pub fn nix_serialize_remote(item: TokenStream) -> TokenStream {
+    let input = syn::parse_macro_input!(item as RemoteInput);
+    let crate_path = parse_quote!(::nix_compat);
+    ser::expand_nix_serialize_remote(crate_path, &input)
+        .unwrap_or_else(syn::Error::into_compile_error)
+        .into()
+}
diff --git a/tvix/nix-compat-derive/src/ser.rs b/tvix/nix-compat-derive/src/ser.rs
new file mode 100644
index 000000000000..47ddfa39366d
--- /dev/null
+++ b/tvix/nix-compat-derive/src/ser.rs
@@ -0,0 +1,227 @@
+use proc_macro2::{Span, TokenStream};
+use quote::{quote, quote_spanned};
+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_serialize(crate_path: Path, input: &mut DeriveInput) -> syn::Result<TokenStream> {
+    let cx = Context::new();
+    let cont = Container::from_ast(&cx, crate_path, input);
+    cx.check()?;
+    let cont = cont.unwrap();
+
+    let ty = cont.ident_type();
+    let body = nix_serialize_body(&cont);
+    let crate_path = cont.crate_path();
+
+    Ok(nix_serialize_impl(
+        crate_path,
+        &ty,
+        &cont.original.generics,
+        body,
+    ))
+}
+
+pub fn expand_nix_serialize_remote(
+    crate_path: Path,
+    input: &RemoteInput,
+) -> syn::Result<TokenStream> {
+    let cx = Context::new();
+    let remote = Remote::from_ast(&cx, crate_path, input);
+    if let Some(attrs) = remote.as_ref().map(|r| &r.attrs) {
+        if attrs.display.is_none() && attrs.type_into.is_none() && attrs.type_try_into.is_none() {
+            cx.error_spanned(input, "Missing into, try_into or display attribute");
+        }
+    }
+    cx.check()?;
+    let remote = remote.unwrap();
+
+    let crate_path = remote.crate_path();
+    let body = nix_serialize_body_into(crate_path, &remote.attrs).expect("From tokenstream");
+    let generics = Generics::default();
+    Ok(nix_serialize_impl(crate_path, remote.ty, &generics, body))
+}
+
+fn nix_serialize_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::ser::NixSerialize for #ty #ty_generics
+            #where_clause
+        {
+            async fn serialize<W>(&self, writer: &mut W) -> std::result::Result<(), W::Error>
+                where W: #crate_path::nix_daemon::ser::NixWrite
+            {
+                use #crate_path::nix_daemon::ser::Error as _;
+                #body
+            }
+        }
+    }
+}
+
+fn nix_serialize_body_into(
+    crate_path: &syn::Path,
+    attrs: &attrs::Container,
+) -> Option<TokenStream> {
+    if let Default::Default(span) = &attrs.display {
+        Some(nix_serialize_display(span.span()))
+    } else if let Default::Path(path) = &attrs.display {
+        Some(nix_serialize_display_path(path))
+    } else if let Some(type_into) = attrs.type_into.as_ref() {
+        Some(nix_serialize_into(type_into))
+    } else {
+        attrs
+            .type_try_into
+            .as_ref()
+            .map(|type_try_into| nix_serialize_try_into(crate_path, type_try_into))
+    }
+}
+
+fn nix_serialize_body(cont: &Container) -> TokenStream {
+    if let Some(tokens) = nix_serialize_body_into(cont.crate_path(), &cont.attrs) {
+        tokens
+    } else {
+        match &cont.data {
+            Data::Struct(_style, fields) => nix_serialize_struct(fields),
+            Data::Enum(variants) => nix_serialize_enum(variants),
+        }
+    }
+}
+
+fn nix_serialize_struct(fields: &[Field<'_>]) -> TokenStream {
+    let write_fields = fields.iter().map(|f| {
+        let field = &f.member;
+        let ty = f.ty;
+        let write_value = quote_spanned! {
+            ty.span()=> writer.write_value(&self.#field).await?
+        };
+        if let Some(version) = f.attrs.version.as_ref() {
+            quote! {
+                if (#version).contains(&writer.version().minor()) {
+                    #write_value;
+                }
+            }
+        } else {
+            quote! {
+                #write_value;
+            }
+        }
+    });
+
+    quote! {
+        #(#write_fields)*
+        Ok(())
+    }
+}
+
+fn nix_serialize_variant(variant: &Variant<'_>) -> TokenStream {
+    let ident = variant.ident;
+    let write_fields = variant.fields.iter().map(|f| {
+        let field = f.var_ident();
+        let ty = f.ty;
+        let write_value = quote_spanned! {
+            ty.span()=> writer.write_value(#field).await?
+        };
+        if let Some(version) = f.attrs.version.as_ref() {
+            quote! {
+                if (#version).contains(&writer.version().minor()) {
+                    #write_value;
+                }
+            }
+        } else {
+            quote! {
+                #write_value;
+            }
+        }
+    });
+    let field_names = variant.fields.iter().map(|f| f.var_ident());
+    let destructure = match variant.style {
+        Style::Struct => {
+            quote! {
+                Self::#ident { #(#field_names),* }
+            }
+        }
+        Style::Tuple => {
+            quote! {
+                Self::#ident(#(#field_names),*)
+            }
+        }
+        Style::Unit => quote!(Self::#ident),
+    };
+    let ignore = match variant.style {
+        Style::Struct => {
+            quote! {
+                Self::#ident { .. }
+            }
+        }
+        Style::Tuple => {
+            quote! {
+                Self::#ident(_, ..)
+            }
+        }
+        Style::Unit => quote!(Self::#ident),
+    };
+    let version = &variant.attrs.version;
+    quote! {
+        #destructure if (#version).contains(&writer.version().minor()) => {
+            #(#write_fields)*
+        }
+        #ignore => {
+            return Err(W::Error::invalid_enum(format!("{} is not valid for version {}", stringify!(#ident), writer.version())));
+        }
+    }
+}
+
+fn nix_serialize_enum(variants: &[Variant<'_>]) -> TokenStream {
+    let match_variant = variants
+        .iter()
+        .map(|variant| nix_serialize_variant(variant));
+    quote! {
+        match self {
+            #(#match_variant)*
+        }
+        Ok(())
+    }
+}
+
+fn nix_serialize_into(ty: &Type) -> TokenStream {
+    quote_spanned! {
+        ty.span() =>
+        {
+            let other : #ty = <Self as Clone>::clone(self).into();
+            writer.write_value(&other).await
+        }
+    }
+}
+
+fn nix_serialize_try_into(crate_path: &Path, ty: &Type) -> TokenStream {
+    quote_spanned! {
+        ty.span() =>
+        {
+            use #crate_path::nix_daemon::ser::Error;
+            let other : #ty = <Self as Clone>::clone(self).try_into().map_err(Error::unsupported_data)?;
+            writer.write_value(&other).await
+        }
+    }
+}
+
+fn nix_serialize_display(span: Span) -> TokenStream {
+    quote_spanned! {
+        span => writer.write_display(self).await
+    }
+}
+
+fn nix_serialize_display_path(path: &syn::ExprPath) -> TokenStream {
+    quote_spanned! {
+        path.span() => writer.write_display(#path(self)).await
+    }
+}