diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Cargo.lock | 163 | ||||
| -rw-r--r-- | Cargo.toml | 13 | ||||
| -rw-r--r-- | rustfmt.toml | 3 | ||||
| -rw-r--r-- | src/lib.rs | 198 | ||||
| -rw-r--r-- | tests/basic.rs | 17 |
6 files changed, 395 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..331ab82 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,163 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "attribute-derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77" +dependencies = [ + "attribute-derive-macro", + "derive-where", + "manyhow", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn", +] + +[[package]] +name = "auguments" +version = "0.1.0" +dependencies = [ + "attribute-derive", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "collection_literals" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" + +[[package]] +name = "derive-where" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quote-use" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" +dependencies = [ + "quote", + "quote-use-macros", +] + +[[package]] +name = "quote-use-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..449a8f1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "auguments" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2", features = ["full"] } +quote = "1" +proc-macro2 = "1" +attribute-derive = "0.10" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..d3d0d1e --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +edition = "2024" +hard_tabs = true +newline_style = "Unix" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..056b6f1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,198 @@ +use attribute_derive::FromAttr; +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::{format_ident, quote}; +use syn::{ + Expr, ExprStruct, Ident, ItemStruct, LitBool, Type, Visibility, parse_macro_input, token::Pub, +}; + +enum Default { + None, + Impl, + Value(Expr), +} + +struct MaybeOptionalField { + visibility: Visibility, + name: Ident, + default: Default, + auto_into: bool, + ty: Type, +} + +#[derive(FromAttr)] +#[from_attr(ident = builder)] +struct BuilderFieldOptions { + vis: Option<Visibility>, + optional: bool, + default: Option<Expr>, + into: bool, +} + +#[proc_macro_derive(Builder, attributes(builder))] +pub fn derive_builder(input: TokenStream) -> TokenStream { + let structure = parse_macro_input!(input as ItemStruct); + let struct_name = &structure.ident; + let builder_name = format_ident!("{}Builder", structure.ident); + let fields = structure + .fields + .into_iter() + .map(|field| { + let attrs = BuilderFieldOptions::from_attributes(field.attrs).unwrap(); + MaybeOptionalField { + visibility: attrs.vis.unwrap_or(field.vis), + name: field.ident.unwrap(), + default: match (attrs.optional, attrs.default) { + (true, None) => Default::Impl, + (_, Some(expr)) => Default::Value(expr), + (false, None) => Default::None, + }, + auto_into: attrs.into, + ty: field.ty, + } + }) + .collect::<Box<_>>(); + + let builder_visibility = fields + .iter() + .all(|field| matches!(field.visibility, Visibility::Public(_))) + .then_some(Visibility::Public(Pub { + span: Span::call_site(), + })); + let field_names = fields + .iter() + .map(|field| field.name.clone()) + .collect::<Box<_>>(); + let field_types = fields + .iter() + .map(|field| field.ty.clone()) + .collect::<Box<_>>(); + let const_generics = fields + .iter() + .filter(|field| matches!(field.default, Default::None)) + .map(|field| Ident::new(&field.name.to_string().to_uppercase(), field.name.span())) + .collect::<Box<_>>(); + let trues = (0..const_generics.len()) + .map(|_| LitBool::new(true, Span::call_site())) + .collect::<Box<_>>(); + let falses = (0..const_generics.len()) + .map(|_| LitBool::new(false, Span::call_site())) + .collect::<Box<_>>(); + + let mut j = 0; + let setters = fields + .iter() + .enumerate() + .map(|(i, field)| { + let visibility = &field.visibility; + let setter_name = &field.name; + let ty = &field.ty; + let param_ty = if field.auto_into { quote! {#ty} } else { quote! { impl ::core::convert::Into<#ty> } }; + let field_name = &field.name; + let fields_head = &field_names[0..i]; + let fields_tail = &field_names[i+1..]; + if matches!(field.default, Default::None) { + let generics_head = &const_generics[0..j]; + let generics_tail = &const_generics[j+1..]; + j += 1; + quote! { + #visibility fn #setter_name(self, value: #param_ty>) -> #builder_name<#(#generics_head,)* true, #(#generics_tail),*> { + #builder_name { + #(#fields_head: self.#fields_head,)* + #field_name: Some(value.into()), + #(#fields_tail: self.#fields_tail,)* + } + } + } + } else { + let maybe_setter_name = format_ident!("maybe_{}", &field.name); + let clear_name = format_ident!("clear_{}", &field.name); + quote! { + #visibility fn #setter_name(self, value: #param_ty) -> #builder_name<#(#const_generics),*> { + #builder_name { + #(#fields_head: self.#fields_head,)* + #field_name: Some(value.into()), + #(#fields_tail: self.#fields_tail,)* + } + } + + #visibility fn #maybe_setter_name(self, value: core::option::Option<#param_ty>) -> #builder_name<#(#const_generics),*> { + #builder_name { + #(#fields_head: self.#fields_head,)* + #field_name: value.map(Into::into), + #(#fields_tail: self.#fields_tail,)* + } + } + + #visibility fn #clear_name(self) -> #builder_name<#(#const_generics),*> { + #builder_name { + #(#fields_head: self.#fields_head,)* + #field_name: None, + #(#fields_tail: self.#fields_tail,)* + } + } + } + } + }) + .collect::<Box<_>>(); + let build_fields = fields.iter().map(|field| { + let field_name = &field.name; + match &field.default { + Default::Value(default_value) => { + quote! { #field_name: self.#field_name.unwrap_or_else(|| #default_value) } + } + Default::Impl => quote! { #field_name: self.#field_name.unwrap_or_default() }, + Default::None => quote! { #field_name: unsafe { self.#field_name.unwrap_unchecked() } }, + } + }); + + quote! { + #builder_visibility struct #builder_name<#(const #const_generics: bool),*> { + #(#field_names: Option<#field_types>,)* + } + + impl<#(const #const_generics: bool),*> core::default::Default for #builder_name<#(#const_generics),*> { + fn default() -> Self { + Self { + #(#field_names: None,)* + } + } + } + + impl #struct_name { + #builder_visibility fn builder() -> #builder_name<#(#falses),*> { + #builder_name::default() + } + } + + impl<#(const #const_generics: bool),*> #builder_name<#(#const_generics),*> { + #(#setters)* + } + + impl #builder_name<#(#trues),*> { + #builder_visibility fn build(self) -> #struct_name { + #struct_name { + #(#build_fields,)* + } + } + } + } + .into() +} + +#[proc_macro] +pub fn build(input: TokenStream) -> TokenStream { + let structure = parse_macro_input!(input as ExprStruct); + let struct_name = &structure.path; + + let field_names = structure.fields.iter().map(|field| &field.member); + let field_values = structure.fields.iter().map(|field| &field.expr); + + quote! { + #struct_name + ::builder() + #(.#field_names(#field_values))* + .build() + } + .into() +} diff --git a/tests/basic.rs b/tests/basic.rs new file mode 100644 index 0000000..68cefb7 --- /dev/null +++ b/tests/basic.rs @@ -0,0 +1,17 @@ +use auguments::{Builder, build}; + +#[derive(Builder)] +#[allow(dead_code)] +struct Foo { + #[builder(into, vis = pub)] + bar: String, + #[builder(default = 32)] + baz: i32, + bat: (), +} + +fn main() { + let _: Foo = Foo::builder().baz(32).bar("hello").bat(()).build(); + let bar = "hello"; + let _: Foo = build!(Foo { bar, bat: () }); +} |
