summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMica White <botahamec@outlook.com>2026-06-05 15:59:04 -0400
committerMica White <botahamec@outlook.com>2026-06-05 15:59:04 -0400
commita994d9acd6a7083349b930d6b08b25b28792344f (patch)
treead5a64a5b9ac5cb2fb17215981b70cdd8fe56056
First commit
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock163
-rw-r--r--Cargo.toml13
-rw-r--r--rustfmt.toml3
-rw-r--r--src/lib.rs198
-rw-r--r--tests/basic.rs17
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: () });
+}