Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(derive): Add nested opt #19

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,9 @@ Instructs the macro to skip wrapping the generated field in `Option<T>`, instead
Instructs the macro to use the provided type instead of `Option<T>` when generating the field. Note that the provided type will be used verbatim, so if you expect an `Option<T>` value, you'll need to manually specify that.

Note: When using `as_type`, the given type must `Into<BaseType>` where `BaseType` is the original field type. This is required for `Partial` trait implementation.

#### nested

> Usage example: `#[partially(nested)]`

Indicates to the macro that this field is `Partial`. The type in the generated struct will then be the associated `Partial::Item` type of the field's type and the struct's `Partial::apply_some` implementation will call `Partial::apply_some` on the field.
3 changes: 3 additions & 0 deletions crates/partially/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
/// > Usage example: `#[partially(as_type = "Option<f32>")]`.
/// Instructs the macro to use the provided type instead of [`Option<T>`] when generating the field. Note that the provided type will be used verbatim, so if you expect an [`Option<T>`] value, you'll need to manually specify that.
/// Note: When using `as_type`, the given type must `Into<BaseType>` where `BaseType` is the original field type. This is required for `Partial` trait implementation.
/// ### nested
/// > Usage example: `#[partially(nested)]`
/// Indicates to the macro that this field is [`Partial`]. The type in the generated struct will then be the associated [`Partial::Item`] type of the field's type and the struct's [`Partial::apply_some`] implementation will call [`Partial::apply_some`] on the field.
///
/// ## Example
/// ```
Expand Down
9 changes: 5 additions & 4 deletions crates/partially/tests/derive/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod basic;
mod container_attrs;
mod generic;
mod retyped;
mod basic;
mod container_attrs;
mod generic;
mod nested;
mod retyped;
42 changes: 42 additions & 0 deletions crates/partially/tests/derive/nested.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use partially::Partial;

#[derive(Debug, partially_derive::Partial)]
#[partially(derive(Default))]
struct Data2 {
v: String,
}

#[derive(Debug, partially_derive::Partial)]
#[partially(derive(Default))]
struct Data1 {
v: String,
#[partially(nested)]
d2: Data2,
}


#[test]
fn nested_apply_some() {
let mut data = Data1 {
v: "v1".to_string(),
d2: Data2 {
v: "v2".to_string(),
},
};

let empty_partial = PartialData1::default();
let full_partial = PartialData1 {
v: Some("v3".to_string()),
d2: PartialData2 {
v: Some("v4".to_string()),
},
};

data.apply_some(empty_partial);
assert_eq!(data.v, "v1");
assert_eq!(data.d2.v, "v2");

data.apply_some(full_partial);
assert_eq!(data.v, "v3");
assert_eq!(data.d2.v, "v4");
}
19 changes: 15 additions & 4 deletions crates/partially_derive/src/internal/derive_receiver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use darling::{
};
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{Attribute, Generics, Ident, Path, Visibility};
use syn::{parse_quote, Attribute, Generics, Ident, Path, Visibility};

use super::{
field_receiver::FieldReceiver,
Expand Down Expand Up @@ -89,6 +89,13 @@ impl ToTokens for DeriveReceiver {
ref krate,
} = *self;

// parse the crate config, or use `partially` for the crate path
let krate = if let Some(krate) = krate {
krate.to_owned()
} else {
parse_quote!(partially)
};

let (_, ty, wher) = generics.split_for_impl();

let fields: Vec<_> = data
Expand Down Expand Up @@ -132,7 +139,11 @@ impl ToTokens for DeriveReceiver {
#additional_attrs
});

let field_tokens = TokenVec::new_with_vec_and_sep(fields.clone(), Separator::CommaNewline);
let mut field_tokens = TokenStream::new();
for field in &fields {
field.to_tokens(&mut field_tokens, &krate);
field_tokens.extend(quote!(,));
}

// write the struct
tokens.extend(quote! {
Expand All @@ -143,7 +154,7 @@ impl ToTokens for DeriveReceiver {

// create the impl
let impl_partial = ImplPartial {
krate,
krate: &krate,
from_ident: ident,
to_ident: &to_ident,
generics,
Expand All @@ -157,7 +168,7 @@ impl ToTokens for DeriveReceiver {

// create the partial => partial impl
let partial_impl_partial = ImplPartial {
krate,
krate: &krate,
from_ident: &to_ident,
to_ident: &to_ident,
generics,
Expand Down
64 changes: 54 additions & 10 deletions crates/partially_derive/src/internal/field_receiver.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use darling::{util::Flag, FromField, Result};
use quote::{quote, ToTokens};
use syn::{parse_quote, Ident, Type, Visibility};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{parse_quote, Ident, Path, Type, Visibility};

#[derive(Debug, FromField)]
#[darling(attributes(partially), forward_attrs, and_then = FieldReceiver::validate)]
Expand Down Expand Up @@ -39,6 +40,10 @@ pub struct FieldReceiver {
/// Note: If specified, the given [`Type`] will be used verbatim, not wrapped in an [`Option`].
/// Note: By default, [`Option<Self::ty>`] is used.
pub as_type: Option<Type>,

/// An optional flag that indicates that this field has a type that is [`Partial`] and should
/// be treated as such.
pub nested: Flag,
}

impl FieldReceiver {
Expand All @@ -52,25 +57,30 @@ impl FieldReceiver {
}

if self.omit.is_present()
&& (self.rename.is_some() || self.transparent.is_present() || self.as_type.is_some())
&& (self.rename.is_some()
|| self.transparent.is_present()
|| self.as_type.is_some()
|| self.nested.is_present())
{
acc.push(darling::Error::custom(
"cannot use omit with any other options",
));
}

if self.transparent.is_present() && self.as_type.is_some() {
if self.transparent.is_present() as i32
+ self.as_type.is_some() as i32
+ self.nested.is_present() as i32
> 1
{
acc.push(darling::Error::custom(
"cannot use both transparent and as_type",
"transparent, as_type and nested are mutually exclusive",
));
}

acc.finish_with(self)
}
}

impl ToTokens for FieldReceiver {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
pub fn to_tokens(&self, tokens: &mut TokenStream, krate: &Path) {
if self.omit.is_present() {
return;
}
Expand All @@ -89,6 +99,10 @@ impl ToTokens for FieldReceiver {
src_type.to_owned()
} else if let Some(ty) = &self.as_type {
ty.to_owned()
} else if self.nested.is_present() {
parse_quote! {
<#src_type as #krate::Partial>::Item
}
} else {
let ty: Type = parse_quote! {
Option<#src_type>
Expand Down Expand Up @@ -131,6 +145,7 @@ mod test {
omit: Flag::default(),
transparent: Flag::default(),
as_type: None,
nested: Flag::default(),
}
}

Expand Down Expand Up @@ -172,14 +187,43 @@ mod test {
}

#[test]
fn invalidate_transparent_as_type() {
fn invalidate_omit_nested() {
let mut instance = make_dummy();
instance.omit = Flag::present();
instance.nested = Flag::present();

assert!(instance.validate().is_err())
}

#[test]
fn invalidate_transparent_as_type_nested() {
// as_type + transparent
let mut instance = make_dummy();
instance.as_type = Some(syn::Type::Verbatim(quote!(NewDummyField)));
instance.transparent = Flag::present();
assert!(instance.validate().is_err());

// as_type + transparent + nested
let mut instance = make_dummy();
instance.as_type = Some(syn::Type::Verbatim(quote!(NewDummyField)));
instance.transparent = Flag::present();
instance.nested = Flag::present();
assert!(instance.validate().is_err());

assert!(instance.validate().is_err())
// as_type + nested
let mut instance = make_dummy();
instance.as_type = Some(syn::Type::Verbatim(quote!(NewDummyField)));
instance.nested = Flag::present();
assert!(instance.validate().is_err());

// nested + transparent
let mut instance = make_dummy();
instance.transparent = Flag::present();
instance.nested = Flag::present();
assert!(instance.validate().is_err());
}


#[test]
fn validate() {
let instance = make_dummy();
Expand Down
36 changes: 19 additions & 17 deletions crates/partially_derive/src/internal/impl_partial.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use quote::{quote, ToTokens};
use syn::{parse_quote, Generics, Ident, Path};
use std::iter;
use syn::{Generics, Ident, Path};

use super::{
field_receiver::FieldReceiver,
token_vec::{Separator, TokenVec},
};

pub struct ImplPartial<'a> {
pub krate: &'a Option<Path>,
pub krate: &'a Path,
pub generics: &'a Generics,
pub from_ident: &'a Ident,
pub to_ident: &'a Ident,
Expand All @@ -28,23 +29,15 @@ impl<'a> ToTokens for ImplPartial<'a> {

let (imp, ty, wher) = generics.split_for_impl();

// parse the crate config, or use `partially` for the crate path
let krate = if let Some(krate) = krate {
krate.to_owned()
} else {
parse_quote!(partially)
};

let field_is_somes = fields
.iter()
.map(|f| {
let field_is_somes = iter::once(quote!(false))
.chain(fields.iter().filter(|f| !f.nested.is_present()).map(|f| {
// this is enforced with a better error by [`FieldReceiver::validate`].
let from_ident = f.ident.as_ref().unwrap();

let to_ident = f.rename.as_ref().unwrap_or(from_ident);

quote!(partial.#to_ident.is_some())
})
}))
.collect();
let field_is_somes = TokenVec::new_with_vec_and_sep(field_is_somes, Separator::Or);

Expand All @@ -56,9 +49,18 @@ impl<'a> ToTokens for ImplPartial<'a> {

let to_ident = f.rename.as_ref().unwrap_or(from_ident);

quote! {
if let Some(#to_ident) = partial.#to_ident {
self.#from_ident = #to_ident.into();
if f.nested.is_present() {
quote! {
will_apply_some = #krate::Partial::apply_some(
&mut self.#from_ident,
partial.#to_ident
) || will_apply_some;
}
} else {
quote! {
if let Some(#to_ident) = partial.#to_ident {
self.#from_ident = #to_ident.into();
}
}
}
})
Expand All @@ -71,7 +73,7 @@ impl<'a> ToTokens for ImplPartial<'a> {
type Item = #to_ident #ty;

fn apply_some(&mut self, partial: Self::Item) -> bool {
let will_apply_some = #field_is_somes;
let mut will_apply_some = #field_is_somes;

#field_applicators

Expand Down
Loading