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

Add possibility to use a different wrapper than Option #9

Open
wants to merge 17 commits 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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ struct Opt<T> {
}
```

`optfield` supports defining visibility, documentation, attributes and merge
methods. For more details and examples check its [documentation].
`optfield` supports defining visibility, documentation, attributes, merge
methods, custom wrappers and more. For details and examples check its
[documentation].

### License
Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE)
Expand Down
100 changes: 99 additions & 1 deletion src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod kw {
syn::custom_keyword!(field_doc);
syn::custom_keyword!(field_attrs);
syn::custom_keyword!(from);
syn::custom_keyword!(wrapper);

pub mod attrs_sub {
syn::custom_keyword!(add);
Expand All @@ -28,6 +29,7 @@ pub struct Args {
pub field_doc: bool,
pub field_attrs: Option<Attrs>,
pub from: bool,
pub wrapper: Option<Wrapper>,
}

enum Arg {
Expand All @@ -38,6 +40,7 @@ enum Arg {
FieldDocs(bool),
FieldAttrs(Attrs),
From(bool),
Wrapper(Wrapper),
}

#[cfg_attr(test, derive(PartialEq))]
Expand Down Expand Up @@ -74,6 +77,11 @@ pub enum Attrs {
Add(Vec<Meta>),
}

#[cfg_attr(test, derive(Debug, PartialEq))]
pub struct Wrapper {
pub name: Ident,
}

#[derive(Debug)]
pub struct AttrList(Vec<Meta>);

Expand All @@ -87,6 +95,7 @@ struct ArgList {
field_doc: Option<Span>,
field_attrs: Option<Span>,
from: Option<Span>,
wrapper: Option<Span>,
list: Vec<Arg>,
}

Expand Down Expand Up @@ -135,6 +144,8 @@ impl Parse for ArgList {
arg_list.parse_field_attrs(input)?;
} else if lookahead.peek(kw::from) {
arg_list.parse_from(input)?;
} else if lookahead.peek(kw::wrapper) {
arg_list.parse_wrapper(input)?;
} else {
return Err(lookahead.error());
}
Expand All @@ -154,9 +165,18 @@ impl Args {
attrs: None,
field_doc: false,
field_attrs: None,
wrapper: None,
from: false,
}
}

pub(crate) fn final_wrapper(&self) -> Ident {
match &self.wrapper {
Some(w) => w.name.clone(),
// use format_ident to create an Ident without a span
None => quote::format_ident!("Option"),
}
}
}

impl ArgList {
Expand All @@ -170,6 +190,7 @@ impl ArgList {
field_doc: None,
field_attrs: None,
from: None,
wrapper: None,
list: Vec::with_capacity(6),
}
}
Expand All @@ -182,6 +203,7 @@ impl ArgList {
|| input.peek(kw::field_attrs)
|| input.peek(kw::attrs)
|| input.peek(kw::from)
|| input.peek(kw::wrapper)
}

fn parse_doc(&mut self, input: ParseStream) -> Result<()> {
Expand Down Expand Up @@ -286,6 +308,20 @@ impl ArgList {
Ok(())
}

fn parse_wrapper(&mut self, input: ParseStream) -> Result<()> {
if let Some(wrapper_span) = self.wrapper {
return ArgList::already_defined_error(input, "wrapper", wrapper_span);
}

let span = input.span();
let wrapper: Wrapper = input.parse()?;

self.wrapper = Some(span);
self.list.push(Arg::Wrapper(wrapper));

Ok(())
}

fn already_defined_error(
input: ParseStream,
arg_name: &'static str,
Expand Down Expand Up @@ -417,6 +453,26 @@ impl Parse for AttrList {
}
}

impl Parse for Wrapper {
fn parse(input: ParseStream) -> Result<Self> {
input.parse::<kw::wrapper>()?;

if !input.peek(Eq) || !input.peek2(Ident) {
return Err(input.error("wrapper needs a type name: wrapper = ..."));
}

input.parse::<Eq>()?;

let name = input.parse()?;

if name == "Option" {
return Err(input.error("custom wrapper cannot be Option"));
}

Ok(Wrapper { name })
}
}

impl From<ArgList> for Args {
fn from(arg_list: ArgList) -> Args {
use Arg::*;
Expand All @@ -432,6 +488,7 @@ impl From<ArgList> for Args {
FieldDocs(field_doc) => args.field_doc = field_doc,
FieldAttrs(field_attrs) => args.field_attrs = Some(field_attrs),
From(from) => args.from = from,
Wrapper(wrapper) => args.wrapper = Some(wrapper),
}
}

Expand All @@ -455,10 +512,14 @@ mod tests {
};

($attr:meta, $dup:meta, $expected:literal) => {
duplicate_arg_panics_test!($attr, $attr, $attr, $expected);
};

($name:meta, $attr:meta, $dup:meta, $expected:literal) => {
paste::item! {
#[test]
#[should_panic(expected = $expected)]
fn [<duplicate_ $attr _panics>]() {
fn [<duplicate_ $name _panics>]() {
parse_args(quote! {
Opt,
$attr,
Expand All @@ -476,6 +537,12 @@ mod tests {
duplicate_arg_panics_test!(field_doc, "field_doc already defined");
duplicate_arg_panics_test!(field_attrs, "field_attrs already defined");
duplicate_arg_panics_test!(from, "from already defined");
duplicate_arg_panics_test!(
wrapper,
wrapper = W,
wrapper = W2,
"wrapper already defined"
);

macro_rules! struct_name_not_first_panics {
($attr:meta) => {
Expand All @@ -499,6 +566,7 @@ mod tests {
struct_name_not_first_panics!(field_doc);
struct_name_not_first_panics!(field_attrs);
struct_name_not_first_panics!(from);
struct_name_not_first_panics!(wrapper);

#[test]
#[should_panic(expected = "expected opt struct name")]
Expand Down Expand Up @@ -735,4 +803,34 @@ mod tests {

assert!(args.from);
}

#[test]
#[should_panic(expected = "wrapper needs a type name: wrapper = ...")]
fn parse_wrapper_no_eq() {
parse_args(quote! {Opt, wrapper});
}

#[test]
#[should_panic(expected = "wrapper needs a type name: wrapper = ...")]
fn parse_wrapper_no_value() {
parse_args(quote! {Opt, wrapper =});
}

#[test]
#[should_panic(expected = "custom wrapper cannot be Option")]
fn parse_wrapper_option() {
parse_args(quote! {Opt, wrapper = Option});
}

#[test]
fn parse_wrapper() {
let args = parse_args(quote! {
Opt,
wrapper = CustomWrapper
});

let custom_wrapper: Ident = syn::parse2(quote!(CustomWrapper)).unwrap();

assert_eq!(args.wrapper.unwrap().name, custom_wrapper);
}
}
101 changes: 90 additions & 11 deletions src/fields/mod.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
use quote::quote;
use syn::{parse2, Field, Fields, ItemStruct, Path, Type, TypePath};
use syn::{parse2, Field, Fields, Ident, ItemStruct, Path, Type, TypePath};

use crate::args::Args;
use crate::error::unexpected;

mod attrs;

const OPTION: &str = "Option";

/// Wraps item fields in Option.
pub fn generate(item: &ItemStruct, args: &Args) -> Fields {
let item_name = item.ident.clone();

let mut fields = item.fields.clone();

let wrapper = args.final_wrapper();

for field in fields.iter_mut() {
field.attrs = attrs::generate(field, args);
attrs::generate(field, args);

if is_option(field) && !args.rewrap {
if is_wrapped_in(field, &wrapper) && !args.rewrap {
continue;
}

let ty = &field.ty;

let opt_type = quote! {
Option<#ty>
let wrapped_type = quote! {
#wrapper<#ty>
};

field.ty = parse2(opt_type).unwrap_or_else(|e| {
field.ty = parse2(wrapped_type).unwrap_or_else(|e| {
panic!(
"{}",
unexpected(format!("generating {} fields", item_name), e)
Expand All @@ -39,14 +39,14 @@ pub fn generate(item: &ItemStruct, args: &Args) -> Fields {
fields
}

pub fn is_option(field: &Field) -> bool {
pub fn is_wrapped_in(field: &Field, wrapper: &Ident) -> bool {
match &field.ty {
Type::Path(TypePath {
path: Path { segments, .. },
..
}) => {
if let Some(segment) = segments.first() {
segment.ident == OPTION
&segment.ident == wrapper
} else {
false
}
Expand All @@ -61,13 +61,15 @@ mod tests {

use crate::test_util::*;

use quote::format_ident;

#[test]
fn test_is_not_option() {
let field = parse_field(quote! {
field: String
});

assert!(!is_option(&field));
assert!(!is_wrapped_in(&field, &format_ident!("Option")));
}

#[test]
Expand All @@ -76,7 +78,25 @@ mod tests {
field: Option<String>
});

assert!(is_option(&field));
assert!(is_wrapped_in(&field, &format_ident!("Option")));
}

#[test]
fn test_is_not_wrapper() {
let field = parse_field(quote! {
field: String
});

assert!(!is_wrapped_in(&field, &format_ident!("Wrapper")));
}

#[test]
fn test_is_wrapper() {
let field = parse_field(quote! {
field: Wrapper<String>
});

assert!(is_wrapped_in(&field, &format_ident!("Wrapper")));
}

#[test]
Expand Down Expand Up @@ -135,4 +155,63 @@ mod tests {

assert_eq!(field_types(generated), expected_types);
}

#[test]
fn without_wrapper_rewrap() {
let (item, args) = parse_item_and_args(
quote! {
struct S<T> {
optional_string: Option<String>,
int: i32,
generic: T,
wrapped_generic: Wrapper<T>
}
},
quote! {
Opt,
wrapper = Wrapper
},
);

let expected_types = parse_types(vec![
quote! {Wrapper<Option<String>>},
quote! {Wrapper<i32>},
quote! {Wrapper<T>},
quote! {Wrapper<T>},
]);

let generated = generate(&item, &args);

assert_eq!(field_types(generated), expected_types);
}

#[test]
fn with_wrapper_rewrap() {
let (item, args) = parse_item_and_args(
quote! {
struct S<T> {
text: String,
optional_number: Option<i128>,
generic: T,
wrapped_generic: Wrapper<T>
}
},
quote! {
Opt,
wrapper = Wrapper,
rewrap
},
);

let expected_types = parse_types(vec![
quote! {Wrapper<String>},
quote! {Wrapper<Option<i128>>},
quote! {Wrapper<T>},
quote! {Wrapper<Wrapper<T>>},
]);

let generated = generate(&item, &args);

assert_eq!(field_types(generated), expected_types);
}
}
Loading
Loading