diff options
Diffstat (limited to 'tvix/nix-compat-derive')
-rw-r--r-- | tvix/nix-compat-derive/Cargo.toml | 1 | ||||
-rw-r--r-- | tvix/nix-compat-derive/src/de.rs | 5 | ||||
-rw-r--r-- | tvix/nix-compat-derive/src/internal/attrs.rs | 148 | ||||
-rw-r--r-- | tvix/nix-compat-derive/src/internal/mod.rs | 4 | ||||
-rw-r--r-- | tvix/nix-compat-derive/src/internal/symbol.rs | 3 | ||||
-rw-r--r-- | tvix/nix-compat-derive/src/lib.rs | 175 | ||||
-rw-r--r-- | tvix/nix-compat-derive/src/ser.rs | 227 |
7 files changed, 549 insertions, 14 deletions
diff --git a/tvix/nix-compat-derive/Cargo.toml b/tvix/nix-compat-derive/Cargo.toml index da6d6744e650..bc656b42842c 100644 --- a/tvix/nix-compat-derive/Cargo.toml +++ b/tvix/nix-compat-derive/Cargo.toml @@ -14,6 +14,7 @@ syn = { version = "2.0.76", features = ["full", "extra-traits"] } [dev-dependencies] hex-literal = { workspace = true } pretty_assertions = { workspace = true } +proptest = { workspace = true, features = ["std", "alloc", "tempfile"] } rstest = { workspace = true } tokio-test = { workspace = true } tokio = { workspace = true, features = ["io-util", "macros"] } 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 + } +} |