Skip to content

Commit

Permalink
Merge pull request #94 from Techassi/feature/method-prefix
Browse files Browse the repository at this point in the history
feat: add support for setter prefix and suffix
  • Loading branch information
idanarye committed Jun 19, 2023
2 parents d62509f + c38d686 commit 4aadd75
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 14 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
generated code itself does trigger the deprecation warning, and instead the
setter for that field now does.

### Added
- Support for setter method prefixes and suffixes `#[builder(field_defaults(setter(prefix = "...", suffix = "...")))]`.
This either prepends or appends the provided string to the setter method. This allows method names like: `set_x()`,
`with_y()`, or `set_z_value()`.

## 0.14.0 - 2023-03-08
### Added
- `build_method(into)` and `build_method(into = ...)`.
Expand Down
6 changes: 3 additions & 3 deletions examples/complicate_build.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
mod scope {
use typed_builder::TypedBuilder;

#[derive(PartialEq, TypedBuilder)]
#[derive(Debug, PartialEq, TypedBuilder)]
#[builder(build_method(vis="", name=__build))]
pub struct Foo {
// Mandatory Field:
Expand Down Expand Up @@ -36,7 +36,7 @@ mod scope {
}
}

#[derive(PartialEq)]
#[derive(Debug, PartialEq)]
pub struct Bar {
pub x: i32,
pub y: Option<i32>,
Expand All @@ -47,7 +47,7 @@ mod scope {
use scope::{Bar, Foo};

fn main() {
assert!(Foo::builder().x(1).y(2).z(3).build() == Bar { x: 2, y: Some(3), z: 4 });
assert_eq!(Foo::builder().x(1).y(2).z(3).build(), Bar { x: 2, y: Some(3), z: 4 });

// This will not compile - because `__build` is a private method
// Foo::builder().x(1).y(2).z(3).__build()
Expand Down
8 changes: 4 additions & 4 deletions examples/example.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use typed_builder::TypedBuilder;

#[derive(PartialEq, TypedBuilder)]
#[derive(Debug, PartialEq, TypedBuilder)]
struct Foo {
// Mandatory Field:
x: i32,
Expand All @@ -16,13 +16,13 @@ struct Foo {
}

fn main() {
assert!(Foo::builder().x(1).y(2).z(3).build() == Foo { x: 1, y: Some(2), z: 3 });
assert_eq!(Foo::builder().x(1).y(2).z(3).build(), Foo { x: 1, y: Some(2), z: 3 });

// Change the order of construction:
assert!(Foo::builder().z(1).x(2).y(3).build() == Foo { x: 2, y: Some(3), z: 1 });
assert_eq!(Foo::builder().z(1).x(2).y(3).build(), Foo { x: 2, y: Some(3), z: 1 });

// Optional fields are optional:
assert!(Foo::builder().x(1).build() == Foo { x: 1, y: None, z: 20 });
assert_eq!(Foo::builder().x(1).build(), Foo { x: 1, y: None, z: 20 });

// This will not compile - because we did not set x:
// Foo::builder().build();
Expand Down
39 changes: 39 additions & 0 deletions examples/example_prefix_suffix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use typed_builder::TypedBuilder;

#[derive(Debug, PartialEq, TypedBuilder)]
#[builder(field_defaults(setter(prefix = "with_", suffix = "_value")))]
struct Foo {
// Mandatory Field:
x: i32,

// #[builder(default)] without parameter - use the type's default
// #[builder(setter(strip_option))] - wrap the setter argument with `Some(...)`
#[builder(default, setter(strip_option))]
y: Option<i32>,

// Or you can set the default
#[builder(default = 20)]
z: i32,
}

fn main() {
assert_eq!(
Foo::builder().with_x_value(1).with_y_value(2).with_z_value(3).build(),
Foo { x: 1, y: Some(2), z: 3 }
);

// Change the order of construction:
assert_eq!(
Foo::builder().with_z_value(1).with_x_value(2).with_y_value(3).build(),
Foo { x: 2, y: Some(3), z: 1 }
);

// Optional fields are optional:
assert_eq!(Foo::builder().with_x_value(1).build(), Foo { x: 1, y: None, z: 20 });

// This will not compile - because we did not set x:
// Foo::builder().build();

// This will not compile - because we set y twice:
// Foo::builder().with_x_value(1).with_y_value(2).with_y_value(3);
}
10 changes: 9 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
/// // Foo::builder().x(1).y(2).y(3);
/// ```
///
/// # Customisation with attributes
/// # Customization with attributes
///
/// In addition to putting `#[derive(TypedBuilder)]` on a type, you can specify a `#[builder(…)]`
/// attribute on the type, and on any fields in it.
Expand Down Expand Up @@ -148,6 +148,14 @@
/// `param1: Type1, param2: Type2 ...` instead of the field type itself. The parameters are
/// transformed into the field type using the expression `expr`. The transformation is performed
/// when the setter is called.
///
/// - `prefix = "..."` prepends the setter method with the specified prefix. For example, setting
/// `prefix = "with_"` results in setters like `with_x` or `with_y`. This option is combinable
/// with `suffix = "..."`.
///
/// - `suffix = "..."` appends the setter method with the specified suffix. For example, setting
/// `suffix = "_value"` results in setters like `x_value` or `y_value`. This option is combinable
/// with `prefix = "..."`.
pub use typed_builder_macro::TypedBuilder;

#[doc(hidden)]
Expand Down
65 changes: 65 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,32 @@ fn test_builder_on_struct_with_keywords() {
);
}

#[test]
fn test_builder_on_struct_with_keywords_prefix_suffix() {
#[allow(non_camel_case_types)]
#[derive(PartialEq, TypedBuilder)]
#[builder(field_defaults(setter(prefix = "set_", suffix = "_value")))]
struct r#struct {
r#fn: u32,
#[builder(default, setter(strip_option))]
r#type: Option<u32>,
#[builder(default = Some(()), setter(skip))]
r#enum: Option<()>,
#[builder(setter(into))]
r#union: String,
}

assert!(
r#struct::builder().r#set_fn_value(1).r#set_union_value("two").build()
== r#struct {
r#fn: 1,
r#type: None,
r#enum: Some(()),
r#union: "two".to_owned(),
}
);
}

#[test]
fn test_field_setter_transform() {
#[derive(PartialEq)]
Expand Down Expand Up @@ -671,3 +697,42 @@ fn test_into_set_generic_impl_into() {
let bar: Bar = Foo::builder().value(42).build();
assert_eq!(bar, Bar { value: 42 });
}

#[test]
fn test_prefix() {
#[derive(Debug, PartialEq, TypedBuilder)]
#[builder(field_defaults(setter(prefix = "with_")))]
struct Foo {
x: i32,
y: i32,
}

let foo = Foo::builder().with_x(1).with_y(2).build();

Check warning on line 710 in tests/tests.rs

View workflow job for this annotation

GitHub Actions / clippy

use of a disallowed/placeholder name `foo`

warning: use of a disallowed/placeholder name `foo` --> tests/tests.rs:710:9 | 710 | let foo = Foo::builder().with_x(1).with_y(2).build(); | ^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_names
assert_eq!(foo, Foo { x: 1, y: 2 })

Check warning on line 711 in tests/tests.rs

View workflow job for this annotation

GitHub Actions / clippy

consider adding a `;` to the last statement for consistent formatting

warning: consider adding a `;` to the last statement for consistent formatting --> tests/tests.rs:711:5 | 711 | assert_eq!(foo, Foo { x: 1, y: 2 }) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: add a `;` here: `assert_eq!(foo, Foo { x: 1, y: 2 });` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#semicolon_if_nothing_returned note: the lint level is defined here --> tests/tests.rs:1:9 | 1 | #![warn(clippy::pedantic)] | ^^^^^^^^^^^^^^^^ = note: `#[warn(clippy::semicolon_if_nothing_returned)]` implied by `#[warn(clippy::pedantic)]`
}

#[test]
fn test_suffix() {
#[derive(Debug, PartialEq, TypedBuilder)]
#[builder(field_defaults(setter(suffix = "_value")))]
struct Foo {
x: i32,
y: i32,
}

let foo = Foo::builder().x_value(1).y_value(2).build();

Check warning on line 723 in tests/tests.rs

View workflow job for this annotation

GitHub Actions / clippy

use of a disallowed/placeholder name `foo`

warning: use of a disallowed/placeholder name `foo` --> tests/tests.rs:723:9 | 723 | let foo = Foo::builder().x_value(1).y_value(2).build(); | ^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_names
assert_eq!(foo, Foo { x: 1, y: 2 })

Check warning on line 724 in tests/tests.rs

View workflow job for this annotation

GitHub Actions / clippy

consider adding a `;` to the last statement for consistent formatting

warning: consider adding a `;` to the last statement for consistent formatting --> tests/tests.rs:724:5 | 724 | assert_eq!(foo, Foo { x: 1, y: 2 }) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: add a `;` here: `assert_eq!(foo, Foo { x: 1, y: 2 });` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#semicolon_if_nothing_returned
}

#[test]
fn test_prefix_and_suffix() {
#[derive(Debug, PartialEq, TypedBuilder)]
#[builder(field_defaults(setter(prefix = "with_", suffix = "_value")))]
struct Foo {
x: i32,
y: i32,
}

let foo = Foo::builder().with_x_value(1).with_y_value(2).build();

Check warning on line 736 in tests/tests.rs

View workflow job for this annotation

GitHub Actions / clippy

use of a disallowed/placeholder name `foo`

warning: use of a disallowed/placeholder name `foo` --> tests/tests.rs:736:9 | 736 | let foo = Foo::builder().with_x_value(1).with_y_value(2).build(); | ^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_names
assert_eq!(foo, Foo { x: 1, y: 2 })

Check warning on line 737 in tests/tests.rs

View workflow job for this annotation

GitHub Actions / clippy

consider adding a `;` to the last statement for consistent formatting

warning: consider adding a `;` to the last statement for consistent formatting --> tests/tests.rs:737:5 | 737 | assert_eq!(foo, Foo { x: 1, y: 2 }) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: add a `;` here: `assert_eq!(foo, Foo { x: 1, y: 2 });` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#semicolon_if_nothing_returned
}
30 changes: 28 additions & 2 deletions typed-builder-macro/src/field_info.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use proc_macro2::{Span, TokenStream};
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{parse::Error, spanned::Spanned};

use crate::util::{apply_subsections, expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix};
use crate::util::{
apply_subsections, expr_to_lit_string, expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix,
};

#[derive(Debug)]
pub struct FieldInfo<'a> {
Expand Down Expand Up @@ -73,6 +75,20 @@ impl<'a> FieldInfo<'a> {
}
}

pub fn setter_method_name(&self) -> Ident {
let name = strip_raw_ident_prefix(self.name.to_string());

if let (Some(prefix), Some(suffix)) = (&self.builder_attr.setter.prefix, &self.builder_attr.setter.suffix) {
Ident::new(&format!("{}{}{}", prefix, name, suffix), Span::call_site())
} else if let Some(prefix) = &self.builder_attr.setter.prefix {
Ident::new(&format!("{}{}", prefix, name), Span::call_site())
} else if let Some(suffix) = &self.builder_attr.setter.suffix {
Ident::new(&format!("{}{}", name, suffix), Span::call_site())
} else {
self.name.clone()
}
}

fn post_process(mut self) -> Result<Self, Error> {
if let Some(ref strip_bool_span) = self.builder_attr.setter.strip_bool {
if let Some(default_span) = self.builder_attr.default.as_ref().map(Spanned::span) {
Expand Down Expand Up @@ -110,6 +126,8 @@ pub struct SetterSettings {
pub strip_option: Option<Span>,
pub strip_bool: Option<Span>,
pub transform: Option<Transform>,
pub prefix: Option<String>,
pub suffix: Option<String>,
}

impl<'a> FieldBuilderAttr<'a> {
Expand Down Expand Up @@ -286,6 +304,14 @@ impl SetterSettings {
self.transform = Some(parse_transform_closure(assign.left.span(), *assign.right)?);
Ok(())
}
"prefix" => {
self.prefix = Some(expr_to_lit_string(&*assign.right)?);
Ok(())
}
"suffix" => {
self.suffix = Some(expr_to_lit_string(&*assign.right)?);
Ok(())
}
_ => Err(Error::new_spanned(&assign, format!("Unknown parameter {:?}", name))),
}
}
Expand Down
8 changes: 5 additions & 3 deletions typed-builder-macro/src/struct_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ impl<'a> StructInfo<'a> {
let reconstructing = self.included_fields().map(|f| f.name);

let &FieldInfo {
name: ref field_name,
name: field_name,
ty: field_type,
..
} = field;
Expand Down Expand Up @@ -262,12 +262,14 @@ impl<'a> StructInfo<'a> {
);
let repeated_fields_error_message = format!("Repeated field {}", field_name);

let method_name = field.setter_method_name();

Ok(quote! {
#[allow(dead_code, non_camel_case_types, missing_docs)]
impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause {
#deprecated
#doc
pub fn #field_name (self, #param_list) -> #builder_name <#( #target_generics ),*> {
pub fn #method_name (self, #param_list) -> #builder_name <#( #target_generics ),*> {
let #field_name = (#arg_expr,);
let ( #(#descructuring,)* ) = self.fields;
#builder_name {
Expand All @@ -285,7 +287,7 @@ impl<'a> StructInfo<'a> {
#[deprecated(
note = #repeated_fields_error_message
)]
pub fn #field_name (self, _: #repeated_fields_error_type_name) -> #builder_name <#( #target_generics ),*> {
pub fn #method_name (self, _: #repeated_fields_error_type_name) -> #builder_name <#( #target_generics ),*> {
self
}
}
Expand Down
12 changes: 11 additions & 1 deletion typed-builder-macro/src/util.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use quote::ToTokens;
use syn::parse::Parser;
use syn::{parse::Parser, Error};

pub fn path_to_single_string(path: &syn::Path) -> Option<String> {
if path.leading_colon.is_some() {
Expand Down Expand Up @@ -115,3 +115,13 @@ pub fn apply_subsections(

Ok(())
}

pub fn expr_to_lit_string(expr: &syn::Expr) -> Result<String, Error> {
match expr {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Str(str) => Ok(str.value()),
_ => return Err(Error::new_spanned(expr, "attribute only allows str values")),
},
_ => return Err(Error::new_spanned(expr, "attribute only allows str values")),
}
}

0 comments on commit 4aadd75

Please sign in to comment.