diff --git a/Cargo.toml b/Cargo.toml index 996dfdc..51839bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [".", "./const_typed_builder_derive", "./const_typed_builder_test"] [workspace.package] description = "Compile-time type-checked builder derive using const generics" -version = "0.2.0" +version = "0.3.0" authors = ["Koenichiwa "] repository = "https://github.com/koenichiwa/const_typed_builder/" edition = "2021" @@ -27,7 +27,7 @@ readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -const_typed_builder_derive = { path = "const_typed_builder_derive", version = "=0.2.0" } +const_typed_builder_derive = { path = "const_typed_builder_derive", version = "=0.3.0" } [dev-dependencies] trybuild = "1.0.84" diff --git a/README.md b/README.md index 994d4b8..02c2649 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,127 @@ # `Builder` Derive Macro Documentation - The `Builder` derive macro is used to generate builder methods for structs in Rust, its biggest feature in this crate is that it provides compile-time validation on the struct. The user can employ several configurations that define the validity of of a complex struct, and it is checked before the struct is ever created. +The `Builder` derive macro is used to generate builder methods for structs in Rust, its biggest feature in this crate is that it provides compile-time validation on the struct. The user can employ several configurations that define the validity of of a complex struct, and it is checked before the struct is ever created. - ## Prerequisites +The library also checks cases where the struct can never be valid or is always valid, but this is still in a work in progress. Errors are always emitted, but warnings are emitted on the nightly channel only. This is due to a limitation in [proc_macro_error](https://docs.rs/proc-macro-error/latest/proc_macro_error/). + +## Prerequisites To use the `Builder` derive macro, you should have the `const_typed_builder` crate added to your project's dependencies in your `Cargo.toml` file: ```toml [dependencies] -const_typed_builder = "0.2" +const_typed_builder = "0.3" ``` -Also, make sure you have the following import statements in your code: +Also, make sure you have the following use statement in your code: ```rust use const_typed_builder::Builder; ``` -## Overview +## Overview +The crate can be used to check validity of structs at compile time. The user can't call build until the struct is valid. This is done by checking if all mandatory fields are instantiated and all user defined "groups" are valid. -### Simple example -This derive: +### Examples +Basic usage: ```rust use const_typed_builder::Builder; -#[derive(Debug, Builder)] + +#[derive(Builder)] pub struct Foo { bar: String, } + +let foo = Foo::builder() + .bar("Hello world!".to_string()) // <- The program would not compile without this call + .build(); // <- Because this function is only implemented for the + // .. version of FooBuilder where `bar` is initialized ``` -Expands to this code: + +Subset of launchd: ```rust use const_typed_builder::Builder; -#[derive(Debug)] -pub struct Foo { - bar: String, -} -impl Builder for Foo { - type BuilderImpl = FooBuilder; - fn builder() -> Self::BuilderImpl { - Self::BuilderImpl::new() - } -} -#[derive(Debug)] -pub struct FooBuilder { - data: FooData, -} -impl FooBuilder { - pub fn new() -> FooBuilder { - Self::default() - } -} -impl Default for FooBuilder { - fn default() -> Self { - FooBuilder { - data: FooData::default(), - } - } -} -impl FooBuilder { - pub fn bar(self, bar: String) -> FooBuilder { - let mut data = self.data; - data.bar = Some(bar); - FooBuilder { data } - } -} -impl FooBuilder { - pub fn build(self) -> Foo { - self.data.into() - } -} -#[derive(Debug)] -pub struct FooData { - pub bar: Option, -} -impl From for Foo { - fn from(data: FooData) -> Foo { - Foo { - bar: data.bar.unwrap(), - } - } +use std::path::PathBuf; + +#[derive(Builder)] +pub struct ResourceLimits { + core: Option, + // ... } -impl Default for FooData { - fn default() -> Self { - FooData { bar: None } - } +#[derive(Builder)] +#[group(program = at_least(1))] +pub struct Launchd { + #[builder(mandatory)] + label: Option, + disabled: Option, + user_name: Option, + group_name: Option, + // ... + #[builder(group = program)] + program: Option, + bundle_program: Option, + #[builder(group = program)] + program_arguments: Option>, + // ... + #[builder(skip)] + on_demand: Option, // NB: deprecated (see KeepAlive), but still needed for reading old plists. + #[builder(skip)] + service_ipc: Option, // NB: "Please remove this key from your launchd.plist." + // ... + #[builder(propagate)] + soft_resource_limits: Option, + #[builder(propagate)] + hard_resource_limits: Option, + // ... } + +let launchd = Launchd::builder() + .label("my_label".to_string()) // <- 1: Mandatory + .program("./my_program".into()) // <- 2: Launchd expects that least one of these fields is set.. + .program_arguments( // <- 2: .. We can remove either one, but never both + vec!["my_arg".to_string()] + ) +// .on_demand(false) <- 3: This function doesn't exist + .soft_resource_limits(|builder| + Some(builder.core(Some(1)).build()) // <- 4: Propagating to `ResourceLimits::builder` + ) + .build(); ``` +### Attributes +This is a quick overview of the features in this library. See [`const_typed_builder_derive::Builder`] for a more in depth explanation of all the features, including examples. +**Struct** +- `#[builder(assume_mandatory)]`: Indicates that all fields in the struct should be assumed as mandatory. + If provided without an equals sign (e.g., `#[builder(assume_mandatory)]`), it sets the `mandatory` flag for fields to true. + If provided with an equals sign (e.g., `#[builder(assume_mandatory = true)]`), it sets the `mandatory` flag for fields based on the value. +- `#[group(group_name = (exact(N)|at_least(N)|at_most(N)|single)]`: + Associates fields of the struct with a group named "group_name" and specifies the group's behavior. + The `group_name` should be a string identifier. The group can have one of the following behaviors: + - `exact(N)`: Exactly N fields in the group must be set during the builder construction. + - `at_least(N)`: At least N fields in the group must be set during the builder construction. + - `at_most(N)`: At most N fields in the group can be set during the builder construction. + - `single`: Only one field in the group can be set during the builder construction. This is a shorthand for `exact(1)`. + e.g `#[group(foo = at_least(2))]` creates a group where at least 2 of the fields need to be initialized. +- `#[builder(solver = (brute_force|compiler))]`: **Use sparingly, see note at bottom of this file!** + Specifies the solver type to be used for building the struct. The `solve_type` should be one of the predefined solver types, such as `brute_force` or `compiler`. If provided with an equals sign (e.g., `#[builder(solver = brute_force)]`), + it sets the "solver type" accordingly. This attribute is still tested, and `brute_force` is the default, and only if there are problems in compilation time then you can try `compiler`. `compiler` gives less guarantees though. + +**Field** +- `#[builder(group = group_name)]`: The heart of this library. This associates the field with a group named `group_name`. + Fields in the same group are treated as a unit, and at least one of them must be set during builder construction. This attribute allows specifying the group name both as an identifier (e.g., `group = my_group`) + and as a string (e.g., `group = "my_group"`). +- `#[builder(mandatory)]`: Marks the field as mandatory, meaning it must be set during the builder + construction. If provided without an equals sign (e.g., `#[builder(mandatory)]`), it sets the field as mandatory. + If provided with an equals sign (e.g., `#[builder(mandatory = true)]`), it sets the mandatory flag based on the value. +- `#[builder(optional)]`: Marks the field as optional, this is the exact opposite of `#[builder(mandatory)]`. + If provided without an equals sign (e.g., `#[builder(optional)]`), it sets the field as optional. + If provided with an equals sign (e.g., `#[builder(optional = true)]`), it sets the optional flag based on the value. +- `#[builder(skip)]`: Marks the field as skipped, meaning that the builder will not include it. This can be used for + fields that are deprecated, but must still be deserialized. This way you can ensure that new structs will never be created with this field initialized, but that old structs can still be used. The field type has to be `Option` for this to work. +- `#[builder(propagate)]`: Indicates that the field should propagate its value when the builder is constructed. + If this attribute is present, the field's value will be copied or moved to the constructed object when the builder is used to build the object. + +Fields can either be a part of a group, mandatory, optional OR skipped. These attribute properties are mutually exclusive. `propagate` can be used on any field where the type also derives `Builder`. + > [!NOTE] > Checking the validity of each field is a problem directly related to [SAT](https://en.wikipedia.org/wiki/Boolean_satisfiability_problem), which is an NP-complete problem. This has effect especially the grouped fields. The current default implementation for checking the validity of grouped fields is `brute_force`, and this implementation currently has a complexity of $`O(2^g)`$ where $`g`$ is the amount of grouped variables. This is not a problem with a couple of fields, but it might impact compile time significantly with more fields. This can be still optimized significantly. Future editions might improve on this complexity. > diff --git a/const_typed_builder_derive/Cargo.toml b/const_typed_builder_derive/Cargo.toml index 005828d..730eeab 100644 --- a/const_typed_builder_derive/Cargo.toml +++ b/const_typed_builder_derive/Cargo.toml @@ -18,6 +18,8 @@ quote = "1.0" proc-macro2 = "1.0" either = "1.9" itertools = "0.11.0" +convert_case = "0.6" +proc-macro-error = "1.0" [lib] proc-macro = true diff --git a/const_typed_builder_derive/src/generator/builder_generator.rs b/const_typed_builder_derive/src/generator/builder_generator.rs index b23d86a..03bf7f6 100644 --- a/const_typed_builder_derive/src/generator/builder_generator.rs +++ b/const_typed_builder_derive/src/generator/builder_generator.rs @@ -2,9 +2,10 @@ use super::{ field_generator::FieldGenerator, generics_generator::GenericsGenerator, group_generator::GroupGenerator, }; -use crate::{info::SolveType, StreamResult, VecStreamResult}; +use crate::info::{FieldKind, SolveType}; +use convert_case::{Case, Casing}; use proc_macro2::TokenStream; -use quote::quote; +use quote::{format_ident, quote}; // The `BuilderGenerator` struct is responsible for generating code related to the builder struct, /// including its definition, implementation of setter methods, `new` method, and `build` method. @@ -59,24 +60,29 @@ impl<'a> BuilderGenerator<'a> { } } + fn data_field_ident(&self) -> syn::Ident { + format_ident!("__{}", self.data_name.to_string().to_case(Case::Snake)) + } + // Generates the code for the builder struct and its methods and returns a token stream. /// /// # Returns /// - /// A `StreamResult` representing the generated code for the builder struct and methods. - pub fn generate(&self) -> StreamResult { + /// A `Tokenstream` representing the generated code for the builder struct and methods. + pub fn generate(&self) -> TokenStream { let builder_struct = self.generate_struct(); - let builder_impl = self.generate_impl()?; + let builder_impl = self.generate_impl(); let tokens = quote!( #builder_struct #builder_impl ); - Ok(tokens) + tokens } /// Generates the code for the builder struct definition. fn generate_struct(&self) -> TokenStream { let data_name = self.data_name; + let data_field = self.data_field_ident(); let builder_name = self.builder_name; let generics = self.generics_gen.builder_struct_generics(); @@ -86,17 +92,22 @@ impl<'a> BuilderGenerator<'a> { let vis = self.target_vis; + let documentation = format!( + "Builder for [`{}`] derived using the `const_typed_builder` crate", + self.target_name + ); + quote!( - #[derive(Debug)] + #[doc = #documentation] #vis struct #builder_name #impl_generics #where_clause { - data: #data_name #type_generics + #data_field: #data_name #type_generics } ) } /// Generates the implementation code for the builder struct's `new`, `build` and setter methods. - fn generate_impl(&self) -> StreamResult { - let builder_setters = self.generate_setters_impl()?; + fn generate_impl(&self) -> TokenStream { + let builder_setters = self.generate_setters_impl(); let builder_new = self.generate_new_impl(); let builder_build = self.generate_build_impl(); @@ -105,19 +116,25 @@ impl<'a> BuilderGenerator<'a> { #builder_setters #builder_build ); - Ok(tokens) + tokens } /// Generates the code for the `new` method implementation. fn generate_new_impl(&self) -> TokenStream { let builder_name = self.builder_name; let data_name = self.data_name; + let data_field = self.data_field_ident(); let type_generics = self.generics_gen.const_generics_valued(false); let (impl_generics, _, where_clause) = self.generics_gen.target_generics().split_for_impl(); + let documentation = format!( + "Creates a new [`{}`] without any fields initialized", + self.builder_name + ); quote!( impl #impl_generics #builder_name #type_generics #where_clause{ + #[doc = #documentation] pub fn new() -> #builder_name #type_generics { Self::default() } @@ -125,8 +142,9 @@ impl<'a> BuilderGenerator<'a> { impl #impl_generics Default for #builder_name #type_generics #where_clause { fn default() -> Self { + #[doc = #documentation] #builder_name { - data: #data_name::default(), + #data_field: #data_name::default(), } } } @@ -137,6 +155,11 @@ impl<'a> BuilderGenerator<'a> { fn generate_build_impl(&self) -> TokenStream { let builder_name = self.builder_name; let target_name = self.target_name; + let data_field = self.data_field_ident(); + let documentation = format!( + "Build an instance of [`{}`], consuming the [`{}`]", + self.target_name, self.builder_name + ); let (impl_generics, target_type_generics, where_clause) = self.generics_gen.target_generics().split_for_impl(); @@ -152,9 +175,9 @@ impl<'a> BuilderGenerator<'a> { quote!( impl #impl_generics #builder_name #type_generics #where_clause{ - + #[doc = #documentation] pub fn build(self) -> #target_name #target_type_generics { - self.data.into() + self.#data_field.into() } } ) @@ -187,9 +210,10 @@ impl<'a> BuilderGenerator<'a> { #correctness_verifier #correctness_helper_fns + #[doc = #documentation] pub fn build(self) -> #target_name #target_type_generics { #correctness_check - self.data.into() + self.#data_field.into() } } ) @@ -198,11 +222,15 @@ impl<'a> BuilderGenerator<'a> { } /// Generates the code for the setter methods of the builder. - fn generate_setters_impl(&self) -> StreamResult { + fn generate_setters_impl(&self) -> TokenStream { let builder_name = self.builder_name; + let data_field = self.data_field_ident(); let setters = self .field_gen - .fields().iter().map(|field| { + .fields() + .iter() + .filter(|field| field.kind() != &FieldKind::Skipped) + .map(|field| { let const_idents_impl = self.generics_gen.builder_const_generic_idents_set_impl(field); let const_idents_type_input = self.generics_gen.builder_const_generic_idents_set_type(field, false); let const_idents_type_output = self.generics_gen.builder_const_generic_idents_set_type(field, true); @@ -212,23 +240,35 @@ impl<'a> BuilderGenerator<'a> { let input_type = self.field_gen.builder_set_impl_input_type(field); let input_value = self.field_gen.builder_set_impl_input_value(field); + let documentation = format!(r#" +Setter for the [`{}::{field_name}`] field. + +# Arguments + +- `{field_name}`: field to be set + +# Returns + +`Self` with `{field_name}` initialized"#, self.target_name); + let tokens = quote!( impl #const_idents_impl #builder_name #const_idents_type_input #where_clause { + #[doc = #documentation] pub fn #field_name (self, #input_type) -> #builder_name #const_idents_type_output { - let mut data = self.data; - data.#field_name = #input_value; + let mut #data_field = self.#data_field; + #data_field.#field_name = #input_value; #builder_name { - data, + #data_field, } } } ); - Ok(tokens) - }).collect::()?; + tokens + }); let tokens = quote!( #(#setters)* ); - Ok(tokens) + tokens } } diff --git a/const_typed_builder_derive/src/generator/data_generator.rs b/const_typed_builder_derive/src/generator/data_generator.rs index 94ed40d..8127f1c 100644 --- a/const_typed_builder_derive/src/generator/data_generator.rs +++ b/const_typed_builder_derive/src/generator/data_generator.rs @@ -1,5 +1,5 @@ use super::{field_generator::FieldGenerator, generics_generator::GenericsGenerator}; -use crate::StreamResult; +use proc_macro2::TokenStream; use quote::quote; /// The `DataGenerator` struct is responsible for generating code related to the data struct @@ -42,24 +42,24 @@ impl<'a> DataGenerator<'a> { /// /// # Returns /// - /// A `StreamResult` representing the generated code for the data struct and conversions. - pub fn generate(&self) -> StreamResult { - let data_struct = self.generate_struct()?; - let data_impl = self.generate_impl()?; + /// A `TokenStream` representing the generated code for the data struct and conversions. + pub fn generate(&self) -> TokenStream { + let data_struct = self.generate_struct(); + let data_impl = self.generate_impl(); let tokens = quote!( #data_struct #data_impl ); - Ok(tokens) + tokens } /// Generates the implementation code for conversions between the data struct and the target struct. - fn generate_impl(&self) -> StreamResult { + fn generate_impl(&self) -> TokenStream { let data_name = self.data_name; let struct_name = self.target_name; - let from_fields = self.field_gen.data_impl_from_fields()?; + let from_fields = self.field_gen.data_impl_from_fields(); let def_fields = self.field_gen.data_impl_default_fields(); let (impl_generics, type_generics, where_clause) = @@ -67,6 +67,7 @@ impl<'a> DataGenerator<'a> { let tokens = quote!( impl #impl_generics From<#data_name #type_generics> for #struct_name #type_generics #where_clause { + #[doc(hidden)] fn from(data: #data_name #type_generics) -> #struct_name #type_generics { #struct_name { #(#from_fields),* @@ -75,6 +76,7 @@ impl<'a> DataGenerator<'a> { } impl #impl_generics Default for #data_name #type_generics #where_clause { + #[doc(hidden)] fn default() -> Self { #data_name { #def_fields @@ -82,23 +84,23 @@ impl<'a> DataGenerator<'a> { } } ); - Ok(tokens) + tokens } /// Generates the code for the data struct itself. - fn generate_struct(&self) -> StreamResult { + fn generate_struct(&self) -> TokenStream { let data_name = self.data_name; - let fields = self.field_gen.data_struct_fields()?; + let fields = self.field_gen.data_struct_fields(); let (impl_generics, _type_generics, where_clause) = self.generics_gen.target_generics().split_for_impl(); let tokens = quote!( - #[derive(Debug)] + #[doc(hidden)] pub struct #data_name #impl_generics #where_clause{ #(#fields),* } ); - Ok(tokens) + tokens } } diff --git a/const_typed_builder_derive/src/generator/field_generator.rs b/const_typed_builder_derive/src/generator/field_generator.rs index f71163c..2382bd9 100644 --- a/const_typed_builder_derive/src/generator/field_generator.rs +++ b/const_typed_builder_derive/src/generator/field_generator.rs @@ -1,7 +1,4 @@ -use crate::{ - info::{FieldInfo, FieldKind}, - VecStreamResult, -}; +use crate::info::{FieldInfo, FieldKind}; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; @@ -34,14 +31,15 @@ impl<'a> FieldGenerator<'a> { /// /// # Returns /// - /// A `VecStreamResult` representing the generated code for the data struct fields. - pub fn data_struct_fields(&self) -> VecStreamResult { + /// A `Vec` representing the data struct fields: `pub field_name: field_type`. + pub fn data_struct_fields(&self) -> Vec { self.fields .iter() - .map(|field| { + .filter_map(|field| { let field_name = field.ident(); let data_field_type = match field.kind() { + FieldKind::Skipped => return None, FieldKind::Optional => field.ty().to_token_stream(), FieldKind::Mandatory if field.is_option_type() => field.ty().to_token_stream(), FieldKind::Mandatory => { @@ -54,7 +52,7 @@ impl<'a> FieldGenerator<'a> { let tokens = quote!( pub #field_name: #data_field_type ); - Ok(tokens) + Some(tokens) }) .collect() } @@ -63,13 +61,14 @@ impl<'a> FieldGenerator<'a> { /// /// # Returns /// - /// A `VecStreamResult` representing the generated code for the `From` trait implementation. - pub fn data_impl_from_fields(&self) -> VecStreamResult { + /// A `Vec` representing the fields for the `From` trait implementation. Either containing `unwrap`, `None` or just the type. + pub fn data_impl_from_fields(&self) -> Vec { self.fields .iter() .map(|field| { let field_name = field.ident(); let tokens = match field.kind() { + FieldKind::Skipped => quote!(#field_name: None), FieldKind::Mandatory if field.is_option_type() => { quote!(#field_name: data.#field_name) } @@ -80,7 +79,7 @@ impl<'a> FieldGenerator<'a> { quote!(#field_name: data.#field_name.unwrap()) } }; - Ok(tokens) + tokens }) .collect() } @@ -91,10 +90,14 @@ impl<'a> FieldGenerator<'a> { /// /// A `TokenStream` representing the generated default field values. pub fn data_impl_default_fields(&self) -> TokenStream { - let fields_none = self.fields.iter().map(|field| { - let field_name = field.ident(); - quote!(#field_name: None) - }); + let fields_none = self + .fields + .iter() + .filter(|field| field.kind() != &FieldKind::Skipped) + .map(|field| { + let field_name = field.ident(); + quote!(#field_name: None) + }); quote!( #(#fields_none),* ) @@ -108,14 +111,17 @@ impl<'a> FieldGenerator<'a> { /// /// # Returns /// - /// A `TokenStream` representing the generated input type for the builder setter method. - pub fn builder_set_impl_input_type(&self, field: &'a FieldInfo) -> TokenStream { + /// A `Option` representing the generated input type for the builder setter method. None if the field is skipped. + pub fn builder_set_impl_input_type(&self, field: &'a FieldInfo) -> Option { + if field.kind() == &FieldKind::Skipped { + return None; + } let field_name = field.ident(); let field_ty = field.setter_input_type(); let bottom_ty = if field.is_option_type() { field.inner_type().unwrap() } else { - field_ty + field_ty.unwrap() }; let field_ty = if field.propagate() { @@ -124,7 +130,7 @@ impl<'a> FieldGenerator<'a> { quote!(#field_ty) }; - quote!(#field_name: #field_ty) + Some(quote!(#field_name: #field_ty)) } /// Generates code for the input value of a builder setter method and returns a token stream. @@ -135,15 +141,18 @@ impl<'a> FieldGenerator<'a> { /// /// # Returns /// - /// A `TokenStream` representing the generated input value for the builder setter method. - pub fn builder_set_impl_input_value(&self, field: &'a FieldInfo) -> TokenStream { - let field_name = field.ident(); + /// A `Option` representing the generated input value for the builder setter method. None if the field is skipped. + pub fn builder_set_impl_input_value(&self, field: &'a FieldInfo) -> Option { + if field.kind() == &FieldKind::Skipped { + return None; + } + let field_name = field.ident(); let field_ty = field.setter_input_type(); let bottom_ty = if field.is_option_type() { field.inner_type().unwrap() } else { - field_ty + field_ty.unwrap() }; let field_value = if field.propagate() { @@ -153,9 +162,9 @@ impl<'a> FieldGenerator<'a> { }; if field.kind() == &FieldKind::Optional { - quote!(#field_value) + Some(quote!(#field_value)) } else { - quote!(Some(#field_value)) + Some(quote!(Some(#field_value))) } } } diff --git a/const_typed_builder_derive/src/generator/generics_generator.rs b/const_typed_builder_derive/src/generator/generics_generator.rs index 751a81b..ab652d3 100644 --- a/const_typed_builder_derive/src/generator/generics_generator.rs +++ b/const_typed_builder_derive/src/generator/generics_generator.rs @@ -45,7 +45,7 @@ impl<'a> GenericsGenerator<'a> { /// A `TokenStream` representing the generated const generics. pub fn const_generics_valued(&self, value: bool) -> TokenStream { let mut all = self.fields.iter().filter_map(|field| match field.kind() { - FieldKind::Optional => None, + FieldKind::Skipped | FieldKind::Optional => None, FieldKind::Mandatory | FieldKind::Grouped => Some(Either::Right(syn::LitBool::new( value, field.ident().span(), @@ -70,7 +70,7 @@ impl<'a> GenericsGenerator<'a> { value: bool, ) -> TokenStream { let mut all = self.fields.iter().filter_map(|field| match field.kind() { - FieldKind::Optional => None, + FieldKind::Skipped | FieldKind::Optional => None, _ if field == field_info => Some(Either::Right(syn::LitBool::new( value, field_info.ident().span(), @@ -91,7 +91,7 @@ impl<'a> GenericsGenerator<'a> { /// A `syn::Generics` instance representing the generated const generics for the setter method input type. pub fn builder_const_generic_idents_set_impl(&self, field_info: &FieldInfo) -> syn::Generics { let mut all = self.fields.iter().filter_map(|field| match field.kind() { - FieldKind::Optional => None, + FieldKind::Skipped | FieldKind::Optional => None, _ if field == field_info => None, FieldKind::Mandatory | FieldKind::Grouped => Some(field.const_ident()), }); @@ -105,7 +105,7 @@ impl<'a> GenericsGenerator<'a> { /// A `TokenStream` representing the generated const generics for the builder `build` method. pub fn builder_const_generic_idents_build(&self, true_indices: &[usize]) -> TokenStream { let mut all = self.fields.iter().filter_map(|field| match field.kind() { - FieldKind::Optional => None, + FieldKind::Skipped | FieldKind::Optional => None, FieldKind::Mandatory => Some(Either::Right(syn::LitBool::new( true, proc_macro2::Span::call_site(), @@ -128,7 +128,7 @@ impl<'a> GenericsGenerator<'a> { /// A `TokenStream` representing the generated const generics for the builder `build` method. pub fn builder_const_generic_idents_build_unset_group(&self) -> TokenStream { let mut all = self.fields.iter().filter_map(|field| match field.kind() { - FieldKind::Optional => None, + FieldKind::Skipped | FieldKind::Optional => None, FieldKind::Mandatory => Some(Either::Right(syn::LitBool::new( true, proc_macro2::Span::call_site(), @@ -145,7 +145,7 @@ impl<'a> GenericsGenerator<'a> { /// A `syn::Generics` instance representing the generated const generics for builder group partial identifiers. pub fn builder_const_generic_group_partial_idents(&self) -> syn::Generics { let mut all = self.fields.iter().filter_map(|field| match field.kind() { - FieldKind::Optional | FieldKind::Mandatory => None, + FieldKind::Skipped | FieldKind::Optional | FieldKind::Mandatory => None, FieldKind::Grouped => Some(field.const_ident()), }); self.add_const_generics_for_impl(&mut all) @@ -158,7 +158,7 @@ impl<'a> GenericsGenerator<'a> { /// A `syn::Generics` instance representing the generated const generics for the builder struct. pub fn builder_struct_generics(&self) -> syn::Generics { let mut all = self.fields.iter().filter_map(|field| match field.kind() { - FieldKind::Optional => None, + FieldKind::Skipped | FieldKind::Optional => None, FieldKind::Mandatory | FieldKind::Grouped => Some(field.const_ident()), }); self.add_const_generics_for_impl(&mut all) diff --git a/const_typed_builder_derive/src/generator/group_generator.rs b/const_typed_builder_derive/src/generator/group_generator.rs index c72f09f..edd662e 100644 --- a/const_typed_builder_derive/src/generator/group_generator.rs +++ b/const_typed_builder_derive/src/generator/group_generator.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeSet; - use crate::{ info::{GroupInfo, GroupType}, CONST_IDENT_PREFIX, @@ -7,6 +5,7 @@ use crate::{ use itertools::{Itertools, Powerset}; use proc_macro2::TokenStream; use quote::{format_ident, quote}; +use std::collections::BTreeSet; /// The `GroupGenerator` struct is responsible for generating code related to groups within the builder, including correctness checks and verifications. #[derive(Debug)] @@ -15,7 +14,7 @@ pub(super) struct GroupGenerator<'a> { } impl<'a> GroupGenerator<'a> { - /// Creates a new `GroupGenerator` instance. + /// Creates a new `GroupGenerator` instance, first checking if the groups can be valid. /// /// # Arguments /// @@ -25,6 +24,7 @@ impl<'a> GroupGenerator<'a> { /// /// A `GroupGenerator` instance initialized with the provided groups. pub fn new(groups: Vec<&'a GroupInfo>) -> Self { + groups.iter().for_each(|group| group.check()); Self { groups } } diff --git a/const_typed_builder_derive/src/generator/mod.rs b/const_typed_builder_derive/src/generator/mod.rs index 1953f64..65f41d2 100644 --- a/const_typed_builder_derive/src/generator/mod.rs +++ b/const_typed_builder_derive/src/generator/mod.rs @@ -10,7 +10,8 @@ use self::{ field_generator::FieldGenerator, generics_generator::GenericsGenerator, group_generator::GroupGenerator, target_generator::TargetGenerator, }; -use crate::{info::StructInfo, StreamResult}; +use crate::info::StructInfo; +use proc_macro2::TokenStream; use quote::quote; /// The `Generator` struct is responsible for generating code for the builder pattern based on the provided `StructInfo`. @@ -63,16 +64,16 @@ impl<'a> Generator<'a> { /// /// # Returns /// - /// A `StreamResult` representing the generated token stream. - pub fn generate(&self) -> StreamResult { + /// A `TokenStream` representing the generated token stream. + pub fn generate(&self) -> TokenStream { let target = self.target_gen.generate(); - let data = self.data_gen.generate()?; - let builder = self.builder_gen.generate()?; + let data = self.data_gen.generate(); + let builder = self.builder_gen.generate(); let tokens = quote!( #target #builder #data ); - Ok(tokens) + tokens } } diff --git a/const_typed_builder_derive/src/generator/target_generator.rs b/const_typed_builder_derive/src/generator/target_generator.rs index be71d25..f9a8d9d 100644 --- a/const_typed_builder_derive/src/generator/target_generator.rs +++ b/const_typed_builder_derive/src/generator/target_generator.rs @@ -48,14 +48,15 @@ impl<'a> TargetGenerator<'a> { let target_name = self.target_name; let builder_name = self.builder_name; let const_generics = self.generics_gen.const_generics_valued(false); - let _builder_generics = self.generics_gen.builder_struct_generics(); let (impl_generics, type_generics, where_clause) = self.generics_gen.target_generics().split_for_impl(); + let documentation = format!("Creates an instance of [`{}`]", self.builder_name); quote! { impl #impl_generics Builder for #target_name #type_generics #where_clause { type BuilderImpl = #builder_name #const_generics; + #[doc = #documentation] fn builder() -> Self::BuilderImpl { Self::BuilderImpl::new() } diff --git a/const_typed_builder_derive/src/info/field_info.rs b/const_typed_builder_derive/src/info/field_info.rs index df7a341..a8313fd 100644 --- a/const_typed_builder_derive/src/info/field_info.rs +++ b/const_typed_builder_derive/src/info/field_info.rs @@ -1,15 +1,13 @@ -use std::collections::HashSet; - use super::struct_info::StructSettings; -use proc_macro2::Span; -use quote::format_ident; -use syn::{ExprPath, Token}; - use crate::{ - symbol::{BUILDER, GROUP, MANDATORY, OPTIONAL, PROPAGATE}, - util::{inner_type, is_option}, + symbol::{BUILDER, GROUP, MANDATORY, OPTIONAL, PROPAGATE, SKIP}, CONST_IDENT_PREFIX, }; +use proc_macro2::Span; +use proc_macro_error::{emit_error, emit_warning}; +use quote::format_ident; +use std::collections::HashSet; +use syn::{ExprPath, Token}; /// Represents the information about a struct field used for code generation. #[derive(Debug, PartialEq, Eq)] @@ -30,6 +28,8 @@ pub enum FieldKind { Mandatory, /// Indicates a field that is part of one or several groups. Grouped, + /// Indicates a field that not included in the builder. + Skipped, } impl<'a> FieldInfo<'a> { @@ -43,12 +43,12 @@ impl<'a> FieldInfo<'a> { /// /// # Returns /// - /// A `Result` containing the `FieldInfo` instance or an error if the field is unnamed. + /// An otpional `FieldInfo` instance if successful. pub fn new( field: &'a syn::Field, struct_settings: &mut StructSettings, index: usize, - ) -> syn::Result { + ) -> Option { if let syn::Field { attrs, ident: Some(ident), @@ -62,9 +62,18 @@ impl<'a> FieldInfo<'a> { .default_field_settings() .clone() .with_ty(ty) - .with_attrs(attrs)?; + .with_attrs(attrs) + .ok()?; - let info = if settings.mandatory { + let info = if settings.skipped { + Self { + field, + ident, + index, + propagate: settings.propagate, + kind: FieldKind::Skipped, + } + } else if settings.mandatory { struct_settings.add_mandatory_index(index); // TODO: Check bool Self { field, @@ -75,10 +84,16 @@ impl<'a> FieldInfo<'a> { } } else if !settings.groups.is_empty() { for group_name in settings.groups { - struct_settings - .group_by_name_mut(&group_name.to_string()) - .ok_or(syn::Error::new_spanned(group_name, "Can't find group"))? - .associate(index); + if let Some(group) = struct_settings.group_by_name_mut(&group_name.to_string()) + { + group.associate(index); + } else { + emit_error!( + group_name, + format!("No group called {group_name} is available"); + hint = format!("You might want to add a #[{GROUP}(...)] attribute to the container") + ); + } } Self { @@ -98,12 +113,10 @@ impl<'a> FieldInfo<'a> { } }; - Ok(info) + Some(info) } else { - Err(syn::Error::new_spanned( - field, - "Unnamed fields are not supported", - )) + emit_error!(field, "Unnamed fields are not supported"); + None } } @@ -119,7 +132,7 @@ impl<'a> FieldInfo<'a> { /// Checks if the field's type is an Option. pub fn is_option_type(&self) -> bool { - is_option(&self.field.ty) + util::is_option(&self.field.ty) } /// Retrieves the type of the field. @@ -129,7 +142,7 @@ impl<'a> FieldInfo<'a> { /// Retrieves the inner type of the field if it is wrapped in an Option pub fn inner_type(&self) -> Option<&syn::Type> { - inner_type(&self.field.ty) + util::inner_type(&self.field.ty) } /// Retrieves the kind of the field, which can be Optional, Mandatory, or Grouped. @@ -148,16 +161,19 @@ impl<'a> FieldInfo<'a> { } /// Retrieves the input type for the builder's setter method. - pub fn setter_input_type(&self) -> &syn::Type { + pub fn setter_input_type(&self) -> Option<&syn::Type> { match self.kind() { - FieldKind::Optional => self.ty(), - FieldKind::Mandatory if self.is_option_type() => self.inner_type().expect( + FieldKind::Optional => Some(self.ty()), + FieldKind::Mandatory if self.is_option_type() => Some(self.inner_type().expect( "Couldn't read inner type of option, even though it's seen as an Option type", - ), - FieldKind::Mandatory => self.ty(), - FieldKind::Grouped => self - .inner_type() - .expect("Couldn't read inner type of option, even though it's marked as grouped"), + )), + FieldKind::Mandatory => Some(self.ty()), + FieldKind::Grouped => { + Some(self.inner_type().expect( + "Couldn't read inner type of option, even though it's marked as grouped", + )) + } + FieldKind::Skipped => None, } } } @@ -177,6 +193,8 @@ impl<'a> Ord for FieldInfo<'a> { /// Represents settings for struct field generation. #[derive(Debug, Clone)] pub struct FieldSettings { + /// Indicates that this field is skipped. + pub skipped: bool, /// Indicates if the field is mandatory. pub mandatory: bool, /// Indicates if the field should propagate values. @@ -190,6 +208,7 @@ pub struct FieldSettings { impl Default for FieldSettings { fn default() -> FieldSettings { FieldSettings { + skipped: false, mandatory: false, propagate: false, input_name: syn::Ident::new("input", Span::call_site()), @@ -214,10 +233,8 @@ impl FieldSettings { /// /// A `syn::Result` indicating success or failure of attribute handling. fn with_attrs(mut self, attrs: &[syn::Attribute]) -> syn::Result { - attrs - .iter() - .map(|attr| self.handle_attribute(attr)) - .collect::, _>>()?; + attrs.iter().for_each(|attr| self.handle_attribute(attr)); + // .collect::, _>>()?; Ok(self) } @@ -231,7 +248,7 @@ impl FieldSettings { /// /// The updated `FieldSettings` instance. fn with_ty(mut self, ty: &syn::Type) -> Self { - if !self.mandatory && !is_option(ty) { + if !self.mandatory && !util::is_option(ty) { self.mandatory = true; } self @@ -261,92 +278,187 @@ impl FieldSettings { /// # Arguments /// /// - `attr`: A reference to the `syn::Attribute` representing the builder attribute applied to the field. - /// - /// # Returns - /// - /// A `Result` indicating success or failure in handling the attribute. Errors are returned for invalid or conflicting attributes. - fn handle_attribute(&mut self, attr: &syn::Attribute) -> syn::Result<()> { - if let Some(ident) = attr.path().get_ident() { - if ident != BUILDER { - return Ok(()); + fn handle_attribute(&mut self, attr: &syn::Attribute) { + let attr_ident = match attr.path().require_ident() { + Ok(ident) if ident == BUILDER => ident, + Ok(ident) => { + emit_error!(ident, format!("{ident} can't be used as a field attribute")); + return; } - } - let list = attr.meta.require_list()?; - if list.tokens.is_empty() { - return Ok(()); + Err(err) => { + emit_error!( + attr.path(), "Can't parse attribute"; + note = err + ); + return; + } + }; + + match attr.meta.require_list() { + Ok(list) => { + if list.tokens.is_empty() { + emit_warning!(list, "Empty atrribute list"); + } + } + Err(err) => emit_error!( + attr, "Attribute expected contain a list of specifiers"; + help = "Try specifying it like #[{}(specifier)]", attr_ident; + note = err + ), } attr.parse_nested_meta(|meta| { - if meta.path == MANDATORY { - if meta.input.peek(Token![=]) { - let expr: syn::Expr = meta.value()?.parse()?; - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Bool(syn::LitBool { value, .. }), - .. - }) = expr - { - self.mandatory = value; - } - } else { - self.mandatory = true; + let path_ident = match meta.path.require_ident() { + Ok(ident) => ident, + Err(err) => { + emit_error!( + &attr.meta, "Specifier cannot be parsed"; + help = "Try specifying it like #[{}(specifier)]", attr_ident; + note = err + ); + return Ok(()); } - } - if meta.path == OPTIONAL { - if meta.input.peek(Token![=]) { - let expr: syn::Expr = meta.value()?.parse()?; - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Bool(syn::LitBool { value, .. }), - .. - }) = expr - { - self.mandatory = !value; + }; + + match (&path_ident.to_string()).into() { + SKIP => { + if meta.input.peek(Token![=]) { + let expr: syn::Expr = meta.value()?.parse()?; + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Bool(syn::LitBool { value, .. }), + .. + }) = expr + { + self.skipped = value; + } + } else { + self.skipped = true; } - } else { - self.mandatory = false; - } - } - if meta.path == GROUP { - if self.mandatory { - return Err(syn::Error::new_spanned( - &meta.path, - "Only optionals in group", - )); } - if meta.input.peek(Token![=]) { - let expr: syn::Expr = meta.value()?.parse()?; - if let syn::Expr::Path(ExprPath { path, .. }) = &expr { - let group_name = path - .get_ident() - .ok_or(syn::Error::new_spanned(path, "Can't parse group"))?; - - if !self.groups.insert(group_name.clone()) { - return Err(syn::Error::new_spanned( - &expr, - "Multiple adds to the same group", - )); + MANDATORY => { + if meta.input.peek(Token![=]) { + let expr: syn::Expr = meta.value()?.parse()?; + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Bool(syn::LitBool { value, .. }), + .. + }) = expr + { + self.mandatory = value; } + } else { + self.mandatory = true; } - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit), - .. - }) = &expr - { - if !self - .groups - .insert(syn::Ident::new(lit.value().as_str(), lit.span())) + } + OPTIONAL => { + if meta.input.peek(Token![=]) { + let expr: syn::Expr = meta.value()?.parse()?; + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Bool(syn::LitBool { value, .. }), + .. + }) = expr { - return Err(syn::Error::new_spanned( - &expr, - "Multiple adds to the same group", - )); + self.mandatory = !value; + } + } else { + self.mandatory = false; + } + } + GROUP => { + if meta.input.peek(Token![=]) { + let expr: syn::Expr = meta.value()?.parse()?; + let group_name = match expr { + syn::Expr::Path(ExprPath { path, .. }) => { + match path.require_ident() { + Ok(ident) => ident, + Err(err) => { + emit_error!( + path, "Group name not specified correctly"; + help = "Try defining it like #[{}(foo)]", BUILDER; + note = err + ); + return Ok(()); + } + }.clone() + } + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }) => { + syn::Ident::new(lit.value().as_str(), lit.span()) + } + expr => { + emit_error!(expr, "Can't parse group name"); + return Ok(()); + }, + }; + if self.groups.contains(&group_name) { + emit_error!( + group_name.span(), "Multiple adds to the same group"; + help = self.groups.get(&group_name).unwrap().span() => "Remove this attribute" + ); + } else { + self.groups.insert(group_name); } } } + PROPAGATE => { + self.propagate = true; + } + _ => { + emit_error!(&attr.meta, "Unknown attribute") + } } - if meta.path == PROPAGATE { - self.propagate = true; + + if self.mandatory && !self.groups.is_empty() { + emit_error!( + &meta.path, + format!("Can't use both {MANDATORY} and {GROUP} attributes"); + hint = "Remove either types of attribute from this field" + ); } Ok(()) }) + .unwrap_or_else(|err| emit_error!( + &attr.meta, "Unknown error"; + note = err + )) + } +} + +mod util { + pub fn is_option(ty: &syn::Type) -> bool { + if let syn::Type::Path(type_path) = ty { + if type_path.qself.is_some() { + return false; + } + if let Some(segment) = type_path.path.segments.last() { + segment.ident == syn::Ident::new("Option", segment.ident.span()) + } else { + false + } + } else { + false + } + } + + pub fn inner_type(ty: &syn::Type) -> Option<&syn::Type> { + let path = if let syn::Type::Path(type_path) = ty { + if type_path.qself.is_some() { + return None; + } + &type_path.path + } else { + return None; + }; + let segment = path.segments.last()?; + let syn::PathArguments::AngleBracketed(generic_params) = &segment.arguments else { + return None; + }; + + if let syn::GenericArgument::Type(inner) = generic_params.args.first()? { + Some(inner) + } else { + None + } } } diff --git a/const_typed_builder_derive/src/info/group_info.rs b/const_typed_builder_derive/src/info/group_info.rs index 704ff4d..43242fa 100644 --- a/const_typed_builder_derive/src/info/group_info.rs +++ b/const_typed_builder_derive/src/info/group_info.rs @@ -1,6 +1,7 @@ -use std::{collections::BTreeSet, hash::Hash}; +use proc_macro_error::{emit_error, emit_warning}; use crate::symbol::{Symbol, AT_LEAST, AT_MOST, EXACT}; +use std::{cmp::Ordering, collections::BTreeSet, hash::Hash}; /// Represents information about a group, including its name, member count, and group type. #[derive(Debug, Clone)] @@ -43,10 +44,12 @@ impl GroupInfo { } } + /// Associate a field index with this group pub fn associate(&mut self, index: usize) -> bool { self.associated_indices.insert(index) } + /// Retrieves all associated indices pub fn indices(&self) -> &BTreeSet { &self.associated_indices } @@ -76,6 +79,85 @@ impl GroupInfo { GroupType::AtMost(count) => applicable_indices_count <= count, } } + + /// Check if the group is formed correctly. Will emit errors or warnings if invalid. + pub fn check(&self) { + let valid_range = 1..self.indices().len(); + if valid_range.is_empty() { + emit_warning!(self.name, format!("There is not an valid expected count")) + } else if !valid_range.contains(&self.expected_count()) { + emit_warning!( + self.name, + format!("Expected count is outside of valid range {valid_range:#?}") + ); + } + match self.group_type() { + GroupType::Exact(expected) => { + match expected.cmp(&valid_range.start) { + Ordering::Less => emit_error!( + self.name, + "This group prevents all of the fields to be initialized"; + hint = "Remove the group and use [builder(skip)] instead" + ), + Ordering::Equal | Ordering::Greater => {} + } + match expected.cmp(&valid_range.end) { + Ordering::Less => {} + Ordering::Equal => emit_warning!( + self.name, + "Group can only be satisfied if all fields are initialized"; + hint = "Consider removing group and using [builder(mandatory)] instead" + ), + Ordering::Greater => emit_error!( + self.name, + "Group can never be satisfied"; + note = format!("Expected amount of fields: exact {}, amount of available fields: {}", expected, valid_range.end)), + } + } + GroupType::AtLeast(expected) => { + match expected.cmp(&valid_range.start) { + Ordering::Less => emit_warning!( + self.name, + "Group has no effect"; + hint = "Consider removing the group" + ), + Ordering::Equal | Ordering::Greater => {} + } + match expected.cmp(&valid_range.end) { + Ordering::Less => {} + Ordering::Equal => emit_warning!( + self.name, + "Group can only be satisfied if all fields are initialized"; + hint = "Consider removing group and using [builder(mandatory)] instead" + ), + Ordering::Greater => emit_error!( + self.name, + "Group can never be satisfied"; + note = format!("Expected amount of fields: at least {}, amount of available fields: {}", expected, valid_range.end); + ), + } + } + GroupType::AtMost(expected) => { + match expected.cmp(&valid_range.start) { + Ordering::Less => emit_error!( + self.name, + "This group prevents all of the fields to be initialized"; + hint = "Remove the group and use [builder(skip)] instead"; + note = format!("Expected amount of fields: at most {}, amount of available fields: {}", expected, valid_range.start) + ), + Ordering::Equal | Ordering::Greater => {} + } + match expected.cmp(&valid_range.end) { + Ordering::Less => {} + Ordering::Equal | Ordering::Greater => emit_warning!( + self.name, + "Group has no effect"; + hint = "Consider removing the group" + ), + } + } + } + } } impl Eq for GroupInfo {} diff --git a/const_typed_builder_derive/src/info/struct_info.rs b/const_typed_builder_derive/src/info/struct_info.rs index 181a97c..f8f638d 100644 --- a/const_typed_builder_derive/src/info/struct_info.rs +++ b/const_typed_builder_derive/src/info/struct_info.rs @@ -1,14 +1,13 @@ -use std::collections::{BTreeSet, HashMap}; - use super::field_info::{FieldInfo, FieldSettings}; use super::group_info::{GroupInfo, GroupType}; -use quote::format_ident; -use syn::Token; - use crate::symbol::{ ASSUME_MANDATORY, AT_LEAST, AT_MOST, BRUTE_FORCE, BUILDER, COMPILER, EXACT, GROUP, SINGLE, SOLVER, }; +use proc_macro_error::{emit_error, emit_warning}; +use quote::format_ident; +use std::collections::{BTreeSet, HashMap}; +use syn::Token; /// A type alias for a collection of `FieldInfo` instances. type FieldInfos<'a> = Vec>; @@ -36,6 +35,7 @@ pub struct StructInfo<'a> { groups: HashMap, /// A collection of `FieldInfo` instances representing struct fields. field_infos: FieldInfos<'a>, + /// The solver used to find all possible valid combinations for the groups solve_type: SolveType, } @@ -48,50 +48,50 @@ impl<'a> StructInfo<'a> { /// /// # Returns /// - /// A `syn::Result` containing the `StructInfo` instance if successful, - pub fn new(ast: &'a syn::DeriveInput) -> syn::Result { - if let syn::DeriveInput { - attrs, - vis, - ident, - generics, - data: - syn::Data::Struct(syn::DataStruct { - fields: syn::Fields::Named(fields), - .. - }), - } = &ast - { - if fields.named.is_empty() { - return Err(syn::Error::new_spanned(fields, "No fields found")); - } - - let mut settings = StructSettings::new().with_attrs(attrs)?; - - let field_infos = fields - .named - .iter() - .enumerate() - .map(|(index, field)| FieldInfo::new(field, &mut settings, index)) - .collect::>>()?; - - let info = StructInfo { - ident, + /// An optional `StructInfo` instance if successful, + pub fn new(ast: &'a syn::DeriveInput) -> Option { + match &ast { + syn::DeriveInput { + attrs, vis, + ident, generics, - builder_ident: format_ident!("{}{}", ident, settings.builder_suffix), - data_ident: format_ident!("{}{}", ident, settings.data_suffix), - _mandatory_indices: settings.mandatory_indices, - groups: settings.groups, - field_infos, - solve_type: settings.solver_type, - }; - Ok(info) - } else { - Err(syn::Error::new_spanned( - ast, - "Builder is only supported for named structs", - )) + data: + syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Named(fields), + .. + }), + } => { + if fields.named.is_empty() { + emit_error!(fields, "No fields found"); + } + + let mut settings = StructSettings::new().with_attrs(attrs); + + let field_infos = fields + .named + .iter() + .enumerate() + .map(|(index, field)| FieldInfo::new(field, &mut settings, index)) + .collect::>>()?; + + let info = StructInfo { + ident, + vis, + generics, + builder_ident: format_ident!("{}{}", ident, settings.builder_suffix), + data_ident: format_ident!("{}{}", ident, settings.data_suffix), + _mandatory_indices: settings.mandatory_indices, + groups: settings.groups, + field_infos, + solve_type: settings.solver_type, + }; + Some(info) + } + _ => { + emit_error!(ast, "Builder is only supported for named structs",); + None + } } } @@ -130,6 +130,7 @@ impl<'a> StructInfo<'a> { &self.groups } + /// Retrieves the solver type used to find all possible valid combinations for the groups pub fn solve_type(&self) -> SolveType { self.solve_type } @@ -146,7 +147,9 @@ pub struct StructSettings { default_field_settings: FieldSettings, /// A map of group names to their respective `GroupInfo`. groups: HashMap, + /// The indices of the mandatory fields mandatory_indices: BTreeSet, + /// The solver used to find all possible valid combinations for the groups solver_type: SolveType, } @@ -169,10 +172,12 @@ impl StructSettings { StructSettings::default() } + /// Add a field index to the set of mandatory indices pub fn add_mandatory_index(&mut self, index: usize) -> bool { self.mandatory_indices.insert(index) } + /// Get a GroupInfo by its identifier pub fn group_by_name_mut(&mut self, group_name: &String) -> Option<&mut GroupInfo> { self.groups.get_mut(group_name) } @@ -191,12 +196,9 @@ impl StructSettings { /// # Returns /// /// A `syn::Result` indicating success or failure of attribute handling. - pub fn with_attrs(mut self, attrs: &[syn::Attribute]) -> syn::Result { - attrs - .iter() - .map(|attr| self.handle_attribute(attr)) - .collect::, _>>()?; - Ok(self) + pub fn with_attrs(mut self, attrs: &[syn::Attribute]) -> Self { + attrs.iter().for_each(|attr| self.handle_attribute(attr)); + self } /// Handles the parsing and processing of attributes applied to a struct. @@ -206,21 +208,33 @@ impl StructSettings { /// /// # Arguments /// /// - `attr`: A reference to the `syn::Attribute` representing the builder attribute applied to the struct. - /// - /// # Returns - /// - /// A `Result` indicating success or failure in handling the attribute. Errors are returned for invalid or conflicting attributes. - fn handle_attribute(&mut self, attr: &syn::Attribute) -> syn::Result<()> { - if let Some(ident) = attr.path().get_ident() { - if ident == GROUP { - self.handle_group_attribute(attr) - } else if ident == BUILDER { - self.handle_builder_attribute(attr) - } else { - Ok(()) + fn handle_attribute(&mut self, attr: &syn::Attribute) { + let attr_ident = match attr.path().require_ident() { + Ok(ident) => ident, + Err(err) => { + emit_error!( + attr.path(), "Can't parse attribute"; + note = err + ); + return; } - } else { - Ok(()) + }; + match attr.meta.require_list() { + Ok(list) => { + if list.tokens.is_empty() { + emit_warning!(list, "Empty atrribute list"); + } + } + Err(err) => emit_error!( + attr, "Attribute expected contain a list of specifiers"; + help = "Try specifying it like #[{}(specifier)]", attr_ident; + note = err + ), + } + match (&attr_ident.to_string()).into() { + GROUP => self.handle_group_attribute(attr), + BUILDER => self.handle_builder_attribute(attr), + _ => emit_error!(&attr, "Unknown attribute"), } } @@ -240,52 +254,67 @@ impl StructSettings { /// # Arguments /// /// - `attr`: A reference to the `syn::Attribute` representing the builder attribute applied to the struct. - /// - /// # Returns - /// - /// A `Result` indicating success or failure in handling the attribute. Errors are returned for invalid or conflicting attributes. - fn handle_builder_attribute(&mut self, attr: &syn::Attribute) -> syn::Result<()> { - let list = attr.meta.require_list()?; - if list.tokens.is_empty() { - return Ok(()); - } - + fn handle_builder_attribute(&mut self, attr: &syn::Attribute) { attr.parse_nested_meta(|meta| { - if meta.path == ASSUME_MANDATORY { - if meta.input.peek(Token![=]) { - let expr: syn::Expr = meta.value()?.parse()?; - if let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Bool(syn::LitBool { value, .. }), - .. - }) = expr - { - self.default_field_settings.mandatory = value; + let path_ident = match meta.path.require_ident() { + Ok(ident) => ident, + Err(err) => { + emit_error!( + &attr.meta, "Specifier cannot be parsed"; + help = "Try specifying it like #[{}(specifier)]", BUILDER; + note = err + ); + return Ok(()); + } + }; + + match (&path_ident.to_string()).into() { + ASSUME_MANDATORY => { + if meta.input.peek(Token![=]) { + let expr: syn::Expr = meta.value()?.parse()?; + match &expr { + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Bool(syn::LitBool { value, .. }), + .. + }) => self.default_field_settings.mandatory = *value, + expr => emit_error!(expr, "Can't parse expression"), + } + } else { + self.default_field_settings.mandatory = true; } - } else { - self.default_field_settings.mandatory = true; } - } - if meta.path == SOLVER { - if meta.input.peek(Token![=]) { - let expr: syn::Expr = meta.value()?.parse()?; - if let syn::Expr::Path(syn::ExprPath { path, .. }) = expr { - let solve_type = path - .get_ident() - .ok_or_else(|| syn::Error::new_spanned(&path, "Can't parse solver"))?; - match (&solve_type.to_string()).into() { - BRUTE_FORCE => self.solver_type = SolveType::BruteForce, - COMPILER => self.solver_type = SolveType::Compiler, - _ => Err(syn::Error::new_spanned(&path, "Unknown solver type"))?, + SOLVER => { + if meta.input.peek(Token![=]) { + let expr: syn::Expr = meta.value()?.parse()?; + if let syn::Expr::Path(syn::ExprPath { path, .. }) = expr { + if let Some(solve_type) = path.get_ident() { + match (&solve_type.to_string()).into() { + BRUTE_FORCE => self.solver_type = SolveType::BruteForce, + COMPILER => self.solver_type = SolveType::Compiler, + _ => emit_error!(&path, "Unknown solver type"), + } + } else { + emit_error!(meta.path, "Can't parse solver specification"); + } + } else { + emit_error!(meta.path, "Can't parse solver specification"); } } else { - Err(syn::Error::new_spanned(meta.path, "Can't parse solver"))?; + emit_error!(meta.path, "Solver type needs to be specified"); } - } else { - Err(syn::Error::new_spanned(meta.path, "Can't parse solver"))?; + } + _ => { + emit_error!(meta.path, "Unknown attribute"); } } Ok(()) }) + .unwrap_or_else(|err| { + emit_error!( + &attr.meta, "Unknown error"; + note = err + ) + }) } /// Handles the parsing and processing of group attributes applied to a struct. @@ -304,77 +333,133 @@ impl StructSettings { /// # Arguments /// /// - `attr`: A reference to the `syn::Attribute` representing the group attribute applied to the struct. - /// - /// # Returns - /// - /// A `Result` indicating success or failure in handling the attribute. Errors are returned for invalid or conflicting attributes. - fn handle_group_attribute(&mut self, attr: &syn::Attribute) -> syn::Result<()> { - let list = attr.meta.require_list()?; - if list.tokens.is_empty() { - return Ok(()); - } - + fn handle_group_attribute(&mut self, attr: &syn::Attribute) { attr.parse_nested_meta(|meta| { - let group_name = meta - .path - .get_ident() - .ok_or_else(|| syn::Error::new_spanned(&attr.meta, "Can't parse group name"))? - .clone(); - - let expr: syn::Expr = meta.value()?.parse()?; + let group_name = match meta.path.require_ident() { + Ok(ident) => ident, + Err(err) => { + emit_error!( + &meta.path , "Group name is not specified correctly"; + help = "Try to define it like `#[{}(foo = {}(1))]`", GROUP, AT_LEAST; + note = err + ); + return Ok(()); + } + }; - let group_type = match &expr { + let group_type = match meta.value()?.parse()? { syn::Expr::Call(syn::ExprCall { func, args, .. }) => { - let group_type = if let syn::Expr::Path(syn::ExprPath { path, .. }) = - func.as_ref() - { - path.get_ident() - .ok_or_else(|| syn::Error::new_spanned(func, "Can't parse group type")) - } else { - Err(syn::Error::new_spanned(func, "Can't find group type")) - }?; - - let group_args = if let Some(syn::Expr::Lit(syn::ExprLit { - attrs: _, - lit: syn::Lit::Int(val), - })) = args.first() - { - val.base10_parse::() - } else { - Err(syn::Error::new_spanned(func, "Can't parse group args")) - }?; - match (&group_type.to_string()).into() { - EXACT => Ok(GroupType::Exact(group_args)), - AT_LEAST => Ok(GroupType::AtLeast(group_args)), - AT_MOST => Ok(GroupType::AtMost(group_args)), - SINGLE => Err(syn::Error::new_spanned( - args, - "`single` doesn't take any arguments", - )), - _ => Err(syn::Error::new_spanned(group_type, "Unknown group type")), + let group_type = match func.as_ref() { + syn::Expr::Path(syn::ExprPath { path, .. }) => match path.require_ident() { + Ok(ident) => ident, + Err(err) => { + emit_error!( + &meta.path , "Group type is not specified correctly"; + help = "Try to define it like `#[group({} = {}(1))]`", group_name, AT_LEAST; + note = err + ); + return Ok(()); + } + }, + _ => { + emit_error!( + &attr.meta, "No group type specified"; + help = "Try to define it like `#[group({} = {}(1))]`", group_name, AT_LEAST + ); + return Ok(()); + } + }; + + match args.first() { + Some(syn::Expr::Lit(syn::ExprLit { + attrs: _, + lit: syn::Lit::Int(val), + })) => match val.base10_parse::() { + Ok(group_args) => match (&group_type.to_string()).into() { + EXACT => GroupType::Exact(group_args), + AT_LEAST => GroupType::AtLeast(group_args), + AT_MOST => GroupType::AtMost(group_args), + SINGLE => { + emit_error!( + args, + "`{}` doesn't take any arguments", SINGLE; + help = "`{}` is shorthand for {}(1)", SINGLE, EXACT + ); + return Ok(()); + } + _ => { + emit_error!( + group_type, "Unknown group type"; + help = "Known group types are {}, {} and {}", EXACT, AT_LEAST, AT_MOST + ); + return Ok(()); + } + }, + Err(err) => { + emit_error!( + val, "Couldn't parse group argument"; + note = err + ); + return Ok(()); + } + }, + + _ => { + emit_error!(func, "Can't parse group argument"); + return Ok(()); + } } } syn::Expr::Path(syn::ExprPath { path, .. }) => { - let group_type = path - .get_ident() - .ok_or_else(|| syn::Error::new_spanned(path, "Can't parse group type"))?; + let group_type = match path.require_ident() { + Ok(ident) => ident, + Err(err) => { + emit_error!( + &meta.path , "Group type is not specified correctly"; + help = "Try to define it like `#[group({} = {}(1))]`", group_name, AT_LEAST; + note = err + ); + return Ok(()); + } + }; match (&group_type.to_string()).into() { - EXACT | AT_LEAST | AT_MOST => Err(syn::Error::new_spanned( - &attr.meta, - "Missing arguments for group type", - )), - SINGLE => Ok(GroupType::Exact(1)), - _ => Err(syn::Error::new_spanned(&attr.meta, "Can't parse group")), + EXACT | AT_LEAST | AT_MOST => { + emit_error!( + &attr.meta, + "Missing arguments for group type"; + help = "Try `{}(1)`, or any other usize", &group_type + ); + return Ok(()); + } + SINGLE => GroupType::Exact(1), + _ => { + emit_error!( + group_type, + "Unknown group type"; + help = "Known group types are {}, {} and {}", EXACT, AT_LEAST, AT_MOST + ); + return Ok(()); + } } } - _ => Err(syn::Error::new_spanned(&attr.meta, "Can't parse group")), + _ => { + emit_error!( + &attr.meta, "No group type specified"; + hint = "Try to define it like `#[group({} = {}(1))]`", group_name, AT_LEAST + ); + return Ok(()); + } }; self.groups.insert( group_name.to_string(), - GroupInfo::new(group_name, group_type?), + GroupInfo::new(group_name.clone(), group_type), ); Ok(()) }) + .unwrap_or_else(|err| emit_error!( + &attr.meta, "Unknown error"; + note = err + )) } } diff --git a/const_typed_builder_derive/src/lib.rs b/const_typed_builder_derive/src/lib.rs index b3ddd58..915df34 100644 --- a/const_typed_builder_derive/src/lib.rs +++ b/const_typed_builder_derive/src/lib.rs @@ -1,18 +1,14 @@ mod generator; mod info; mod symbol; -mod util; use generator::Generator; use info::StructInfo; use proc_macro2::TokenStream; +use proc_macro_error::proc_macro_error; +use quote::quote; use syn::DeriveInput; -/// A type alias for the result of a token stream operation. -type StreamResult = syn::Result; -/// A type alias for the result of a vector of token streams. -type VecStreamResult = syn::Result>; - const CONST_IDENT_PREFIX: &str = "__BUILDER_CONST"; /// The `derive_builder` macro is used to automatically generate builder @@ -32,11 +28,11 @@ const CONST_IDENT_PREFIX: &str = "__BUILDER_CONST"; /// This will generate a builder pattern for `MyStruct`, allowing you to /// construct instances of `MyStruct` with a fluent API. #[proc_macro_derive(Builder, attributes(builder, group))] +#[proc_macro_error] pub fn derive_builder(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let ast = syn::parse_macro_input!(input as DeriveInput); - impl_my_derive(&ast) - .unwrap_or_else(syn::Error::into_compile_error) - .into() + let stream = impl_my_derive(&ast); + quote!(#stream).into() } /// This function implements the custom derive for the `Builder` trait. @@ -52,9 +48,9 @@ pub fn derive_builder(input: proc_macro::TokenStream) -> proc_macro::TokenStream /// /// # Returns /// -/// A `StreamResult` representing the generated token stream. -fn impl_my_derive(ast: &syn::DeriveInput) -> StreamResult { +/// An optional `TokenStream` representing the generated token stream. +fn impl_my_derive(ast: &syn::DeriveInput) -> Option { let struct_info = StructInfo::new(ast)?; let generator = Generator::new(&struct_info); - generator.generate() + Some(generator.generate()) } diff --git a/const_typed_builder_derive/src/symbol.rs b/const_typed_builder_derive/src/symbol.rs index 0e4c5c2..1512a42 100644 --- a/const_typed_builder_derive/src/symbol.rs +++ b/const_typed_builder_derive/src/symbol.rs @@ -32,6 +32,8 @@ pub const SOLVER: Symbol = Symbol("solver"); pub const BRUTE_FORCE: Symbol = Symbol("brute_force"); /// Constant representing the `compiler` symbol. pub const COMPILER: Symbol = Symbol("compiler"); +/// Constant representing the `skip` symbol. +pub const SKIP: Symbol = Symbol("skip"); impl<'a> From<&'a String> for Symbol<'a> { fn from(value: &'a String) -> Self { diff --git a/const_typed_builder_derive/src/util.rs b/const_typed_builder_derive/src/util.rs deleted file mode 100644 index 33aeae2..0000000 --- a/const_typed_builder_derive/src/util.rs +++ /dev/null @@ -1,35 +0,0 @@ -pub fn is_option(ty: &syn::Type) -> bool { - if let syn::Type::Path(type_path) = ty { - if type_path.qself.is_some() { - return false; - } - if let Some(segment) = type_path.path.segments.last() { - segment.ident == syn::Ident::new("Option", segment.ident.span()) - } else { - false - } - } else { - false - } -} - -pub fn inner_type(ty: &syn::Type) -> Option<&syn::Type> { - let path = if let syn::Type::Path(type_path) = ty { - if type_path.qself.is_some() { - return None; - } - &type_path.path - } else { - return None; - }; - let segment = path.segments.last()?; - let syn::PathArguments::AngleBracketed(generic_params) = &segment.arguments else { - return None; - }; - - if let syn::GenericArgument::Type(inner) = generic_params.args.first()? { - Some(inner) - } else { - None - } -} diff --git a/const_typed_builder_test/Cargo.toml b/const_typed_builder_test/Cargo.toml index 6145395..203f16d 100644 --- a/const_typed_builder_test/Cargo.toml +++ b/const_typed_builder_test/Cargo.toml @@ -14,5 +14,5 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dev-dependencies] -const_typed_builder = { path = "../../const_typed_builder", version = "=0.2.0" } +const_typed_builder = { path = "../../const_typed_builder", version = "=0.3.0" } trybuild = "1.0.84" \ No newline at end of file diff --git a/const_typed_builder_test/compile_fail/group_1.rs b/const_typed_builder_test/compile_fail/group_1.rs index eb5aa5a..4ca314f 100644 --- a/const_typed_builder_test/compile_fail/group_1.rs +++ b/const_typed_builder_test/compile_fail/group_1.rs @@ -7,5 +7,5 @@ fn main() { #[builder(group = baz)] bar: Option, } - let foo = Foo::builder().bar("Hello world!".to_string()).build(); + let foobuilder = Foo::builder().bar("Hello world!".to_string()); } \ No newline at end of file diff --git a/const_typed_builder_test/compile_fail/group_1.stderr b/const_typed_builder_test/compile_fail/group_1.stderr index 7f13c2a..0ff3bf9 100644 --- a/const_typed_builder_test/compile_fail/group_1.stderr +++ b/const_typed_builder_test/compile_fail/group_1.stderr @@ -1,8 +1,21 @@ -error[E0599]: no method named `build` found for struct `FooBuilder` in the current scope - --> ./compile_fail/group_1.rs:10:62 +error: Group can never be satisfied + + = note: Expected amount of fields: at least 2, amount of available fields: 1 + + --> ./compile_fail/group_1.rs:5:13 + | +5 | #[group(baz = at_least(2))] + | ^^^ + +error[E0599]: no function or associated item named `builder` found for struct `Foo` in the current scope + --> ./compile_fail/group_1.rs:10:27 | -4 | #[derive(Debug, Default, PartialEq, Eq, Builder)] - | ------- method `build` not found for this struct +6 | pub struct Foo { + | -------------- function or associated item `builder` not found for this struct ... -10 | let foo = Foo::builder().bar("Hello world!".to_string()).build(); - | ^^^^^ method not found in `FooBuilder` +10 | let foobuilder = Foo::builder().bar("Hello world!".to_string()); + | ^^^^^^^ function or associated item not found in `Foo` + | + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `builder`, perhaps you need to implement it: + candidate #1: `const_typed_builder::Builder` diff --git a/const_typed_builder_test/compile_fail/group_2.rs b/const_typed_builder_test/compile_fail/group_2.rs new file mode 100644 index 0000000..f2cd9ca --- /dev/null +++ b/const_typed_builder_test/compile_fail/group_2.rs @@ -0,0 +1,13 @@ +use const_typed_builder::Builder; + +fn main() { + #[derive(Debug, Default, PartialEq, Eq, Builder)] + #[group(baz = at_least(2))] + pub struct Foo { + #[builder(group = baz)] + bar: Option, + #[builder(group = baz)] + baz: Option, + } + let foo = Foo::builder().bar("Hello world!".to_string()).build(); +} \ No newline at end of file diff --git a/const_typed_builder_test/compile_fail/group_2.stderr b/const_typed_builder_test/compile_fail/group_2.stderr new file mode 100644 index 0000000..4177a8a --- /dev/null +++ b/const_typed_builder_test/compile_fail/group_2.stderr @@ -0,0 +1,11 @@ +error[E0599]: no method named `build` found for struct `FooBuilder` in the current scope + --> ./compile_fail/group_2.rs:12:62 + | +4 | #[derive(Debug, Default, PartialEq, Eq, Builder)] + | ------- method `build` not found for this struct +... +12 | let foo = Foo::builder().bar("Hello world!".to_string()).build(); + | ^^^^^ method not found in `FooBuilder` + | + = note: the method was found for + - `FooBuilder` diff --git a/const_typed_builder_test/compile_fail/group_3.rs b/const_typed_builder_test/compile_fail/group_3.rs new file mode 100644 index 0000000..2a90345 --- /dev/null +++ b/const_typed_builder_test/compile_fail/group_3.rs @@ -0,0 +1,14 @@ +use const_typed_builder::Builder; + +fn main() { + #[derive(Debug, Default, PartialEq, Eq, Builder)] + #[group(baz = at_least(2))] + pub struct Foo { + #[builder(group = baz)] + #[builder(group = baz)] + bar: Option, + #[builder(group = baz)] + baz: Option, + } + let foobuilder = Foo::builder().bar("Hello world!".to_string()).baz("Hello world!".to_string()).build(); +} \ No newline at end of file diff --git a/const_typed_builder_test/compile_fail/group_3.stderr b/const_typed_builder_test/compile_fail/group_3.stderr new file mode 100644 index 0000000..0a11742 --- /dev/null +++ b/const_typed_builder_test/compile_fail/group_3.stderr @@ -0,0 +1,21 @@ +error: Multiple adds to the same group + + = help: Remove this attribute + + --> ./compile_fail/group_3.rs:8:27 + | +8 | #[builder(group = baz)] + | ^^^ + +error[E0599]: no function or associated item named `builder` found for struct `Foo` in the current scope + --> ./compile_fail/group_3.rs:13:27 + | +6 | pub struct Foo { + | -------------- function or associated item `builder` not found for this struct +... +13 | let foobuilder = Foo::builder().bar("Hello world!".to_string()).baz("Hello world!".to_string()).build(); + | ^^^^^^^ function or associated item not found in `Foo` + | + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `builder`, perhaps you need to implement it: + candidate #1: `const_typed_builder::Builder` diff --git a/const_typed_builder_test/compile_fail/group_and_mandatory_3.rs b/const_typed_builder_test/compile_fail/group_and_mandatory_3.rs new file mode 100644 index 0000000..e7fd1d5 --- /dev/null +++ b/const_typed_builder_test/compile_fail/group_and_mandatory_3.rs @@ -0,0 +1,15 @@ +use const_typed_builder::Builder; + +fn main() { + #[derive(Debug, Default, PartialEq, Eq, Builder)] + #[group(quz = single)] + pub struct Foo { + #[builder(group = quz)] + bar: Option, + #[builder(mandatory, group = quz)] + baz: Option, + qux: String, + } + + let _ = Foo::builder().bar("Hello".to_string()).baz("Hello".to_string()).qux("world!".to_string()).build(); +} \ No newline at end of file diff --git a/const_typed_builder_test/compile_fail/group_and_mandatory_3.stderr b/const_typed_builder_test/compile_fail/group_and_mandatory_3.stderr new file mode 100644 index 0000000..6f7697c --- /dev/null +++ b/const_typed_builder_test/compile_fail/group_and_mandatory_3.stderr @@ -0,0 +1,21 @@ +error: Can't use both mandatory and group attributes + + = help: Remove either types of attribute from this field + + --> ./compile_fail/group_and_mandatory_3.rs:9:30 + | +9 | #[builder(mandatory, group = quz)] + | ^^^^^ + +error[E0599]: no function or associated item named `builder` found for struct `Foo` in the current scope + --> ./compile_fail/group_and_mandatory_3.rs:14:18 + | +6 | pub struct Foo { + | -------------- function or associated item `builder` not found for this struct +... +14 | let _ = Foo::builder().bar("Hello".to_string()).baz("Hello".to_string()).qux("world!".to_string()).build(); + | ^^^^^^^ function or associated item not found in `Foo` + | + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `builder`, perhaps you need to implement it: + candidate #1: `const_typed_builder::Builder` diff --git a/const_typed_builder_test/compile_fail/group_and_mandatory_4.rs b/const_typed_builder_test/compile_fail/group_and_mandatory_4.rs new file mode 100644 index 0000000..8a87692 --- /dev/null +++ b/const_typed_builder_test/compile_fail/group_and_mandatory_4.rs @@ -0,0 +1,16 @@ +use const_typed_builder::Builder; + +fn main() { + #[derive(Debug, Default, PartialEq, Eq, Builder)] + #[group(quz = single)] + pub struct Foo { + #[builder(group = quz)] + bar: Option, + #[builder(group = quz)] + #[builder(mandatory)] + baz: Option, + qux: String, + } + + let _ = Foo::builder().bar("Hello".to_string()).baz("Hello".to_string()).qux("world!".to_string()).build(); +} \ No newline at end of file diff --git a/const_typed_builder_test/compile_fail/group_and_mandatory_4.stderr b/const_typed_builder_test/compile_fail/group_and_mandatory_4.stderr new file mode 100644 index 0000000..c708d76 --- /dev/null +++ b/const_typed_builder_test/compile_fail/group_and_mandatory_4.stderr @@ -0,0 +1,21 @@ +error: Can't use both mandatory and group attributes + + = help: Remove either types of attribute from this field + + --> ./compile_fail/group_and_mandatory_4.rs:10:19 + | +10 | #[builder(mandatory)] + | ^^^^^^^^^ + +error[E0599]: no function or associated item named `builder` found for struct `Foo` in the current scope + --> ./compile_fail/group_and_mandatory_4.rs:15:18 + | +6 | pub struct Foo { + | -------------- function or associated item `builder` not found for this struct +... +15 | let _ = Foo::builder().bar("Hello".to_string()).baz("Hello".to_string()).qux("world!".to_string()).build(); + | ^^^^^^^ function or associated item not found in `Foo` + | + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `builder`, perhaps you need to implement it: + candidate #1: `const_typed_builder::Builder` diff --git a/const_typed_builder_test/compile_fail/group_solver_compiler_1.rs b/const_typed_builder_test/compile_fail/group_solver_compiler_1.rs index bbf4cea..da8d080 100644 --- a/const_typed_builder_test/compile_fail/group_solver_compiler_1.rs +++ b/const_typed_builder_test/compile_fail/group_solver_compiler_1.rs @@ -4,10 +4,12 @@ use const_typed_builder::Builder; fn main() { #[derive(Debug, Default, PartialEq, Eq, Builder)] #[group(baz = at_least(2))] - #[build(solver = compiler)] + #[builder(solver = compiler)] pub struct Foo { #[builder(group = baz)] bar: Option, + #[builder(group = baz)] + baz: Option } let foo = Foo::builder().bar("Hello world!".to_string()).build(); } \ No newline at end of file diff --git a/const_typed_builder_test/compile_fail/group_solver_compiler_1.stderr b/const_typed_builder_test/compile_fail/group_solver_compiler_1.stderr index 6e623b9..5adf77a 100644 --- a/const_typed_builder_test/compile_fail/group_solver_compiler_1.stderr +++ b/const_typed_builder_test/compile_fail/group_solver_compiler_1.stderr @@ -1,14 +1,21 @@ -error: cannot find attribute `build` in this scope - --> ./compile_fail/group_solver_compiler_1.rs:7:7 +warning: unused variable: `foo` + --> ./compile_fail/group_solver_compiler_1.rs:14:9 + | +14 | let foo = Foo::builder().bar("Hello world!".to_string()).build(); + | ^^^ help: if this is intentional, prefix it with an underscore: `_foo` + | + = note: `#[warn(unused_variables)]` on by default + +error[E0080]: evaluation of `main::FooBuilder::::GROUP_VERIFIER` failed + --> ./compile_fail/group_solver_compiler_1.rs:5:45 + | +5 | #[derive(Debug, Default, PartialEq, Eq, Builder)] + | ^^^^^^^ the evaluated program panicked at '`.build()` failed because the bounds of group `baz` where not met (at_least 2)', $DIR/./compile_fail/group_solver_compiler_1.rs:5:45 | -7 | #[build(solver = compiler)] - | ^^^^^ + = note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `panic` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0599]: no method named `build` found for struct `FooBuilder` in the current scope - --> ./compile_fail/group_solver_compiler_1.rs:12:62 +note: the above error was encountered while instantiating `fn main::FooBuilder::::build` + --> ./compile_fail/group_solver_compiler_1.rs:14:15 | -5 | #[derive(Debug, Default, PartialEq, Eq, Builder)] - | ------- method `build` not found for this struct -... -12 | let foo = Foo::builder().bar("Hello world!".to_string()).build(); - | ^^^^^ method not found in `FooBuilder` +14 | let foo = Foo::builder().bar("Hello world!".to_string()).build(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/const_typed_builder_test/compile_fail/skip_field_1.rs b/const_typed_builder_test/compile_fail/skip_field_1.rs new file mode 100644 index 0000000..d9e1038 --- /dev/null +++ b/const_typed_builder_test/compile_fail/skip_field_1.rs @@ -0,0 +1,10 @@ +use const_typed_builder::Builder; +fn main() { + #[derive(Debug, PartialEq, Builder)] + pub struct Foo { + bar: String, + #[builder(skip)] + baz: Option, + } + let foo = Foo::builder().bar("Hello world!".to_string()).baz("Skipped".to_string()).build(); +} \ No newline at end of file diff --git a/const_typed_builder_test/compile_fail/skip_field_1.stderr b/const_typed_builder_test/compile_fail/skip_field_1.stderr new file mode 100644 index 0000000..3a7cac9 --- /dev/null +++ b/const_typed_builder_test/compile_fail/skip_field_1.stderr @@ -0,0 +1,8 @@ +error[E0599]: no method named `baz` found for struct `FooBuilder` in the current scope + --> ./compile_fail/skip_field_1.rs:9:62 + | +3 | #[derive(Debug, PartialEq, Builder)] + | ------- method `baz` not found for this struct +... +9 | let foo = Foo::builder().bar("Hello world!".to_string()).baz("Skipped".to_string()).build(); + | ^^^ method not found in `FooBuilder` diff --git a/const_typed_builder_test/src/lib.rs b/const_typed_builder_test/src/lib.rs index 9dc6da0..4d33e8c 100644 --- a/const_typed_builder_test/src/lib.rs +++ b/const_typed_builder_test/src/lib.rs @@ -244,10 +244,13 @@ mod test { pub struct Foo { #[builder(group = baz)] bar: Option, + #[builder(group = baz)] + baz: Option, } let expected = Foo { bar: Some("Hello world!".to_string()), + baz: None, }; let foo = Foo::builder().bar("Hello world!".to_string()).build(); assert_eq!(expected, foo); @@ -261,10 +264,13 @@ mod test { pub struct Foo { #[builder(group = baz)] bar: Option, + #[builder(group = baz)] + baz: Option, } let expected = Foo { bar: Some("Hello world!".to_string()), + baz: None, }; let foo = Foo::builder().bar("Hello world!".to_string()).build(); assert_eq!(expected, foo); @@ -369,19 +375,6 @@ mod test { .baz("Hello".to_string()) .build(); assert_eq!(expected, foo); - - // FIXME: Should fail or warn - #[derive(Debug, Default, PartialEq, Eq, Builder)] - #[group(quz = single)] - pub struct Nope { - #[builder(group = quz)] - bar: Option, - #[builder(group = quz)] - #[builder(mandatory)] - baz: Option, - #[builder(mandatory)] - qux: Option, - } } #[test] @@ -655,4 +648,30 @@ mod test { .build(); assert_eq!(expected, foo); } + + #[test] + fn no_other_derive_necessary() { + #[derive(Builder)] + pub struct Foo { + bar: String, + } + let foo = Foo::builder().bar("Hello world!".to_string()).build(); + assert_eq!(foo.bar, "Hello world!"); + } + + #[test] + fn skip_field() { + #[derive(Debug, PartialEq, Builder)] + pub struct Foo { + bar: String, + #[builder(skip)] + baz: Option, + } + let expected = Foo { + bar: "Hello world!".to_string(), + baz: None, + }; + let foo = Foo::builder().bar("Hello world!".to_string()).build(); + assert_eq!(foo, expected); + } } diff --git a/src/lib.rs b/src/lib.rs index 1d699cc..4298f62 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ /// Basic example /// ```rust /// use const_typed_builder::Builder; -/// #[derive(Debug, Builder)] +/// #[derive(Builder)] /// pub struct Foo { /// bar: String, /// baz: String, @@ -29,7 +29,7 @@ /// This will not compile without providing 'baz' /// ```compile_fail /// # use const_typed_builder::Builder; -/// #[derive(Debug, Builder)] +/// #[derive(Builder)] /// pub struct Foo { /// bar: String, /// baz: String, @@ -40,16 +40,17 @@ /// .build(); /// ``` /// -/// ## 2. Mandatory and Optional Fields +/// ## 2. Mandatory, Skipped and Optional Fields /// /// By default, all fields in the generated builder are considered mandatory, meaning they must be provided during construction. /// However, fields with the `Option` type are considered optional and can be left unset, and thus defaulted to `None`. /// +/// Fields can be either Mandatory, Skipped, Optional or Grouped (see next subsection), these are mutually exclusive. /// ### Example: /// Valid construction with optional field left unset /// ```rust /// # use const_typed_builder::Builder; -/// #[derive(Debug, Builder)] +/// #[derive(Builder)] /// pub struct Foo { /// bar: String, // Mandatory /// baz: Option, // Optional @@ -66,7 +67,7 @@ /// Invalid construction with optional field left unset: /// ```compile_fail /// # use const_typed_builder::Builder; -/// #[derive(Debug, Builder)] +/// #[derive(Builder)] /// pub struct Foo { /// bar: String, // Mandatory /// #[builder(mandatory)] @@ -82,7 +83,7 @@ /// /// ``` /// # use const_typed_builder::Builder; -/// #[derive(Debug, Builder)] +/// #[derive(Builder)] /// #[builder(assume_mandatory)] /// pub struct Foo { /// bar: Option, @@ -93,9 +94,25 @@ /// let foo = Foo::builder().bar("Hello world!".to_string()).baz("Hello world!".to_string()).build(); /// ``` /// -/// ## 3. Grouping Fields +/// You can also skip fields. This can be used if you still want to deserialize deprecated fields for instance. +/// ```compile_fail +/// # use const_typed_builder::Builder; +/// #[derive(Builder)] +/// pub struct Foo { +/// bar: String, // Mandatory +/// #[builder(skip)] +/// baz: Option, // Skipped. The builder will leave it as `None`. +/// } /// -/// Fields can be grouped together, and constraints can be applied to these groups. Groups allow you to ensure that a certain combination of fields is provided together. +/// let foo = Foo::builder() +/// .bar("Hello".to_string()) +/// .baz("World".to_string()) // This function does not exist +/// .build(); +/// ``` +/// ## 3. Grouped Fields +/// +/// Fields can be grouped together, and constraints can be applied to these groups. +/// Groups allow you to ensure that a certain combination of fields is provided together. /// There are four types of groups: `single`, `at_least`, `at_most`, and `exact`. /// /// **All** fields that are grouped need to be an `Option` type. @@ -105,12 +122,16 @@ /// - `at_most(n)`: Allows at most `n` fields in the group to be provided. /// - `exact(n)`: Requires exactly `n` fields in the group to be provided. /// +/// The range of n is 1..=k, where k is the amount of fields that are associated with this group. +/// Errors will be emitted if the group can never be valid, or can be replaced by `skip` or `mandatory`. +/// Warnings will be emitted if the group is always valid, or can be replaced by `optional`. +/// /// ### Examples: /// /// Valid construction only one field in `my_group` is provided /// ```rust /// # use const_typed_builder::Builder; -/// #[derive(Debug, Builder)] +/// #[derive(Builder)] /// #[group(my_group = single)] /// pub struct Foo { /// #[builder(group = my_group)] @@ -126,7 +147,7 @@ /// Invalid construction because both fields in `my_group` are provided /// ```compile_fail /// # use const_typed_builder::Builder; -/// #[derive(Debug, Builder)] +/// #[derive(Builder)] /// #[group(my_group = single)] /// pub struct Foo { /// #[builder(group = my_group)] @@ -143,7 +164,7 @@ /// Valid construction because at least 2 fields in `my_group` are provided: /// ```rust /// # use const_typed_builder::Builder; -/// #[derive(Debug, Builder)] +/// #[derive(Builder)] /// #[group(my_group = at_least(2))] /// pub struct Foo { /// #[builder(group = my_group)] @@ -164,7 +185,7 @@ /// Valid construction because at least 2 fields in 'least' are provided, and 'fred' had to be provided to validate the group 'most': /// ```rust /// # use const_typed_builder::Builder; -/// #[derive(Debug, Builder)] +/// #[derive(Builder)] /// #[group(least = at_least(2))] /// #[group(most = at_most(3))] /// pub struct Foo { @@ -197,13 +218,13 @@ /// Valid construction with complex struct 'Bar' created within 'Foo' /// ```rust /// # use const_typed_builder::Builder; -/// #[derive(Debug, Builder)] +/// #[derive(Builder)] /// pub struct Foo { /// #[builder(propagate)] /// bar: Bar, /// } /// -/// #[derive(Debug, Builder)] +/// #[derive(Builder)] /// pub struct Bar { /// baz: String, /// } @@ -223,7 +244,7 @@ /// Valid construction for a generic struct 'Foo' with a default generic parameter /// ```rust /// # use const_typed_builder::Builder; -/// #[derive(Debug, Builder)] +/// #[derive(Builder)] /// pub struct Foo /// where /// A: Into,