From c831e867035e2d29a0ddcda3d8ffd13cb56a2ac0 Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Sun, 6 Mar 2022 12:42:56 -0500 Subject: [PATCH] Support custom conversions --- staged-builder-internals/Cargo.toml | 2 +- staged-builder-internals/src/lib.rs | 93 ++++++++++++++++++++++++++++- staged-builder/tests/test.rs | 24 ++++++++ 3 files changed, 116 insertions(+), 3 deletions(-) diff --git a/staged-builder-internals/Cargo.toml b/staged-builder-internals/Cargo.toml index edb4945..c84ff83 100644 --- a/staged-builder-internals/Cargo.toml +++ b/staged-builder-internals/Cargo.toml @@ -13,4 +13,4 @@ proc-macro = true heck = "0.4" proc-macro2 = "1" quote = "1" -syn = "1" +syn = { version = "1", features = ["full"] } diff --git a/staged-builder-internals/src/lib.rs b/staged-builder-internals/src/lib.rs index 791328f..31344a7 100644 --- a/staged-builder-internals/src/lib.rs +++ b/staged-builder-internals/src/lib.rs @@ -10,6 +10,8 @@ use syn::{ mod kw { syn::custom_keyword!(into); + syn::custom_keyword!(custom); + syn::custom_keyword!(convert); syn::custom_keyword!(default); syn::custom_keyword!(list); syn::custom_keyword!(set); @@ -39,9 +41,13 @@ mod kw { /// /// Options can be applied to individual fields via the `#[builder(...)]` attribute as a comma-separated sequence: /// -/// * `into` - Causes the setter method for the field to take `impl Into` rather than `FieldType` directly. /// * `default` - Causes the field to be considered optional. The [`Default`] trait is normally used to generate the /// default field value. A custom default can be specified with `default = `, where `` is an expression. +/// * `into` - Causes the setter method for the field to take `impl Into` rather than `FieldType` directly. +/// * `custom` - Causes the setter method to perform an arbitrary conversion for the field. The option expects a `type` +/// which will be used as the argument type in the setter, and a `convert` callable expression which will be invoked +/// by the setter. For example, the annotation `#[builder(into)]` on a field of type `T` is equivalent to the +/// annotation `#[builder(custom(type = impl Into, convert = Into::into))]`. /// * `list` - Causes the field to be treated as a "list style" type. It will default to an empty collection, and three /// setter methods will be generated: `push_foo` to add a single value, `foo` to set the contents, and `extend_foo` /// to exend the collection with new values. The underlying type must have a `push` method, a [`FromIterator`] @@ -62,8 +68,9 @@ mod kw { /// /// Options can be applied to the item types of collections as a comma-separated sequence: /// -/// * `type` - Indicates the type of the item in the collection. +/// * `type` - Indicates the type of the item in the collection. Required unless using `custom`. /// * `into` - Causes setter methods to take `impl>` rather than `ItemType` directly. +/// * `custom` - Causes the setter methods to perform an arbitrary conversion for the field. /// /// # Example expansion /// @@ -710,6 +717,13 @@ impl<'a> ResolvedField<'a> { assign: quote!(#name.into()), }; } + FieldOverride::Custom(config) => { + let convert = config.convert.convert; + resolved.mode = FieldMode::Normal { + type_: config.type_.type_, + assign: quote!(#convert(#name)), + } + } FieldOverride::UnaryCollection { kind, config } => { resolved.default = Some(quote!(staged_builder::__private::Default::default())); @@ -737,6 +751,7 @@ impl<'a> ResolvedField<'a> { enum FieldOverride { Default(DefaultConfig), Into(IntoConfig), + Custom(CustomConfig), UnaryCollection { kind: UnaryKind, config: UnaryCollectionConfig, @@ -751,6 +766,8 @@ impl Parse for FieldOverride { Ok(FieldOverride::Default(input.parse()?)) } else if lookahead.peek(kw::into) { Ok(FieldOverride::Into(input.parse()?)) + } else if lookahead.peek(kw::custom) { + Ok(FieldOverride::Custom(input.parse()?)) } else if lookahead.peek(kw::list) { Ok(FieldOverride::UnaryCollection { kind: UnaryKind::List, @@ -884,6 +901,69 @@ impl Parse for IntoConfig { } } +struct CustomConfig { + type_: TypeConfig, + convert: ConvertConfig, +} + +impl Parse for CustomConfig { + fn parse(input: ParseStream) -> Result { + let name = input.parse::()?; + + let content; + parenthesized!(content in input); + + let mut type_ = None; + let mut convert = None; + for override_ in content.parse_terminated::<_, Token![,]>(CustomOverride::parse)? { + match override_ { + CustomOverride::Type(config) => type_ = Some(config), + CustomOverride::Convert(config) => convert = Some(config), + } + } + + let type_ = type_.ok_or_else(|| Error::new(name.span(), "missing `type` configuration"))?; + let convert = + convert.ok_or_else(|| Error::new(name.span(), "missing `convert` configuration"))?; + + Ok(CustomConfig { type_, convert }) + } +} + +enum CustomOverride { + Type(TypeConfig), + Convert(ConvertConfig), +} + +impl Parse for CustomOverride { + fn parse(input: ParseStream) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(Token![type]) { + Ok(CustomOverride::Type(input.parse()?)) + } else if lookahead.peek(kw::convert) { + Ok(CustomOverride::Convert(input.parse()?)) + } else { + Err(lookahead.error()) + } + } +} + +struct ConvertConfig { + convert: TokenStream, +} + +impl Parse for ConvertConfig { + fn parse(input: ParseStream) -> Result { + input.parse::()?; + input.parse::()?; + let convert = input.parse::()?; + + Ok(ConvertConfig { + convert: convert.to_token_stream(), + }) + } +} + struct CollectionParamConfig { type_: TokenStream, convert_fn: Option, @@ -923,6 +1003,12 @@ impl Parse for CollectionParamConfig { match override_ { CollectionTypeOverride::Type(type_config) => type_ = Some(type_config.type_), CollectionTypeOverride::Into(_) => into = true, + CollectionTypeOverride::Custom(config) => { + return Ok(CollectionParamConfig { + type_: config.type_.type_, + convert_fn: Some(config.convert.convert), + }) + } } } @@ -942,6 +1028,7 @@ impl Parse for CollectionParamConfig { enum CollectionTypeOverride { Type(TypeConfig), Into(IntoConfig), + Custom(CustomConfig), } impl Parse for CollectionTypeOverride { @@ -951,6 +1038,8 @@ impl Parse for CollectionTypeOverride { Ok(CollectionTypeOverride::Type(input.parse()?)) } else if lookahead.peek(kw::into) { Ok(CollectionTypeOverride::Into(input.parse()?)) + } else if lookahead.peek(kw::custom) { + Ok(CollectionTypeOverride::Custom(input.parse()?)) } else { Err(lookahead.error()) } diff --git a/staged-builder/tests/test.rs b/staged-builder/tests/test.rs index df43a77..be4de47 100644 --- a/staged-builder/tests/test.rs +++ b/staged-builder/tests/test.rs @@ -1,5 +1,6 @@ use staged_builder::{staged_builder, Validate}; use std::collections::{HashMap, HashSet}; +use std::fmt::Display; #[derive(PartialEq, Debug)] #[staged_builder] @@ -133,3 +134,26 @@ fn collections_into() { .build(); assert_eq!(actual, expected); } + +#[derive(PartialEq, Debug)] +#[staged_builder] +struct Custom { + #[builder(custom(type = impl Display, convert = to_string))] + string: String, + #[builder(list(item(custom(type = impl Display, convert = to_string))))] + list: Vec, +} + +fn to_string(value: impl Display) -> String { + value.to_string() +} + +#[test] +fn custom() { + let actual = Custom::builder().string(42).push_list(true).build(); + let expected = Custom { + string: "42".to_string(), + list: vec!["true".to_string()], + }; + assert_eq!(actual, expected); +}