From b08f2142477f3546a1d71b4b82e8e1d26e6c09fe Mon Sep 17 00:00:00 2001
From: tenuous-guidance <105654822+tenuous-guidance@users.noreply.github.com>
Date: Tue, 8 Apr 2025 17:23:53 +0100
Subject: [PATCH 1/3] refactor(derive)!: replace forward_serde with forward

---
 confik-macros/src/lib.rs                      | 34 +++++++++----------
 confik/CHANGELOG.md                           |  9 +++++
 confik/src/lib.md                             |  8 +++--
 .../tests/{serde_forward => forward}/mod.rs   | 10 +++---
 confik/tests/main.rs                          |  4 +--
 5 files changed, 38 insertions(+), 27 deletions(-)
 rename confik/tests/{serde_forward => forward}/mod.rs (86%)

diff --git a/confik-macros/src/lib.rs b/confik-macros/src/lib.rs
index d405fdb..b6f361f 100644
--- a/confik-macros/src/lib.rs
+++ b/confik-macros/src/lib.rs
@@ -66,13 +66,13 @@ impl FromMeta for FieldTryFrom {
     }
 }
 
-/// Handles requesting to forward `serde` attributes.
+/// Handles requesting to forward attributes.
 #[derive(Debug)]
-struct ForwardSerde {
+struct Forward {
     items: Vec<NestedMeta>,
 }
 
-impl ToTokens for ForwardSerde {
+impl ToTokens for Forward {
     fn into_token_stream(self) -> TokenStream {
         self.to_token_stream()
     }
@@ -83,11 +83,11 @@ impl ToTokens for ForwardSerde {
 
     fn to_token_stream(&self) -> TokenStream {
         let Self { items } = self;
-        quote!(#[serde(#( #items ),*)])
+        quote!(#( #[ #items ] )*)
     }
 }
 
-impl FromMeta for ForwardSerde {
+impl FromMeta for Forward {
     fn from_list(items: &[NestedMeta]) -> darling::Result<Self> {
         let items = items.to_vec();
 
@@ -129,8 +129,8 @@ struct VariantImplementer {
     /// Optional explicit override of the variant's discriminant.
     discriminant: Option<Expr>,
 
-    /// Optional attributes to forward to serde.
-    forward_serde: Option<ForwardSerde>,
+    /// Optional attributes to forward to the builder's variant.
+    forward: Option<Forward>,
 }
 
 impl VariantImplementer {
@@ -140,7 +140,7 @@ impl VariantImplementer {
             ident,
             fields,
             discriminant,
-            forward_serde,
+            forward,
         } = var_impl.as_ref();
 
         let field_vec = fields
@@ -154,7 +154,7 @@ impl VariantImplementer {
             .map(|disc| quote_spanned!(disc.span() => = discriminant));
 
         Ok(quote_spanned! { var_impl.span() =>
-            #forward_serde
+            #forward
             #ident #fields #discriminant
         })
     }
@@ -330,8 +330,8 @@ struct FieldImplementer {
     /// The field type.
     ty: Type,
 
-    /// Optional attributes to forward to serde.
-    forward_serde: Option<ForwardSerde>,
+    /// Optional attributes to forward to the builder's field.
+    forward: Option<Forward>,
 }
 
 impl FieldImplementer {
@@ -380,7 +380,7 @@ impl FieldImplementer {
             ty,
             ident,
             secret,
-            forward_serde,
+            forward,
             from,
             try_from,
             ..
@@ -414,7 +414,7 @@ impl FieldImplementer {
 
         Ok(quote_spanned! { ident.span() =>
                 #[serde(default)]
-                #forward_serde
+                #forward
                 #ident #ty
         })
     }
@@ -623,8 +623,8 @@ struct RootImplementer {
     /// `pub`, `pub(crate)`, etc.
     vis: Visibility,
 
-    /// Optional attributes to forward to serde.
-    forward_serde: Option<ForwardSerde>,
+    /// Optional attributes to forward to the builder struct/enum.
+    forward: Option<Forward>,
 
     /// Derives needed by the builder, e.g. `Hash`.
     derive: Option<Derive>,
@@ -665,7 +665,7 @@ impl RootImplementer {
             data,
             generics,
             vis,
-            forward_serde,
+            forward,
             derive: additional_derives,
             ..
         } = self;
@@ -727,7 +727,7 @@ impl RootImplementer {
         Ok(quote_spanned! { target_name.span() =>
             #[derive(::std::default::Default, ::confik::__exports::__serde::Deserialize, #additional_derives )]
             #[serde(crate = "::confik::__exports::__serde")]
-            #forward_serde
+            #forward
             #vis #enum_or_struct_token #builder_name #type_generics #where_clause
                 #bracketed_data
             #terminator
diff --git a/confik/CHANGELOG.md b/confik/CHANGELOG.md
index f947c0a..92e551d 100644
--- a/confik/CHANGELOG.md
+++ b/confik/CHANGELOG.md
@@ -3,6 +3,15 @@
 ## Unreleased
 
 - Implement `Configuration` for [`js_option::JsOption`](https://docs.rs/js_option/0.1.1/js_option/enum.JsOption.html)
+- Add a new `confik(forward(...))` attribute. As well as allowing for forwarding general attributes to the builder, this:
+  - Replaces `confik(forward_serde(...))`. E.g.
+    ```rust
+    #[derive(Configuration)]
+    struct Config {
+      #[confik(forward(serde(default)))]
+      num: usize,
+    }
+    ```
 
 ## 0.13.0
 
diff --git a/confik/src/lib.md b/confik/src/lib.md
index ac64049..21806bd 100644
--- a/confik/src/lib.md
+++ b/confik/src/lib.md
@@ -76,20 +76,22 @@ struct Config {
 }
 ```
 
-### Forwarding Attributes To `Deserialize`
+### Forwarding Attributes
 
-The serde attributes used for customizing a `Deserialize` derive typically are achieved by adding `#[confik(forward_serde(...))` attributes.
+The serde attributes used for customizing a `Deserialize` derive typically are achieved by adding `#[confik(forward(serde(...)))]` attributes.
 
 For example:
 
 ```
 #[derive(confik::Configuration)]
 struct Config {
-    #[confik(forward_serde(rename = "other_data"))]
+    #[confik(forward(serde(rename = "other_data")))]
     data: usize,
 }
 ```
 
+This can also be used for non-serde attributes, but this is less commonly needed.
+
 ### Defaults
 
 Defaults are specified on a per-field basis.
diff --git a/confik/tests/serde_forward/mod.rs b/confik/tests/forward/mod.rs
similarity index 86%
rename from confik/tests/serde_forward/mod.rs
rename to confik/tests/forward/mod.rs
index 35e0e9d..1d77ca4 100644
--- a/confik/tests/serde_forward/mod.rs
+++ b/confik/tests/forward/mod.rs
@@ -1,22 +1,22 @@
 use confik::Configuration;
 
 #[derive(Configuration, Debug, PartialEq, Eq)]
-#[confik(forward_serde(rename_all = "UPPERCASE"))]
+#[confik(forward(serde(rename_all = "UPPERCASE")))]
 struct Container {
     field: usize,
 }
 
 #[derive(Configuration, Debug, PartialEq, Eq)]
 struct Inner {
-    #[confik(forward_serde(rename = "outer"))]
+    #[confik(forward(serde(rename = "outer")))]
     inner: usize,
 }
 
 #[derive(Configuration, Debug, PartialEq, Eq)]
 struct Field {
-    #[confik(forward_serde(rename = "other_name"))]
+    #[confik(forward(serde(rename = "other_name")))]
     field1: usize,
-    #[confik(forward_serde(flatten))]
+    #[confik(forward(serde(flatten)))]
     field2: Inner,
 }
 
@@ -25,7 +25,7 @@ enum Clothes {
     Hat,
     // Put some data in to force use of a custom builder
     Scarf(usize),
-    #[confik(forward_serde(alias = "Gloves", alias = "SomethingElse"))]
+    #[confik(forward(serde(alias = "Gloves", alias = "SomethingElse")))]
     Other,
 }
 
diff --git a/confik/tests/main.rs b/confik/tests/main.rs
index 35abf88..57b8151 100644
--- a/confik/tests/main.rs
+++ b/confik/tests/main.rs
@@ -3,11 +3,11 @@ mod array;
 mod common;
 mod complex_enums;
 mod defaulting_containers;
+mod forward;
 mod keyed_containers;
 mod option_builder;
 mod secret;
 mod secret_option;
-mod serde_forward;
 mod singly_nested_tests;
 mod third_party;
 mod unkeyed_containers;
@@ -84,7 +84,7 @@ mod toml {
     fn from_humantime() {
         #[derive(Debug, PartialEq, Eq, Configuration)]
         struct Config {
-            #[confik(forward_serde(with = "humantime_serde"))]
+            #[confik(forward(serde(with = "humantime_serde")))]
             timeout: Duration,
         }
 

From 5a7ffe0b91aa02546b6f28ab20d821184a2e0e8d Mon Sep 17 00:00:00 2001
From: tenuous-guidance <105654822+tenuous-guidance@users.noreply.github.com>
Date: Tue, 8 Apr 2025 17:35:45 +0100
Subject: [PATCH 2/3] refactor(derive)!: replace derive with forward

---
 confik-macros/src/lib.rs                      |  9 ++---
 confik-macros/tests/trybuild.rs               |  2 +-
 confik-macros/tests/trybuild/19-derive.rs     |  2 +-
 .../tests/trybuild/23-where-clause.rs         |  2 +-
 .../tests/trybuild/fail-derive-literal.stderr |  5 ---
 ...ive-literal.rs => fail-forward-literal.rs} |  2 +-
 .../trybuild/fail-forward-literal.stderr      | 17 ++++++++
 confik/CHANGELOG.md                           |  6 +++
 confik/examples/derives.rs                    |  2 +-
 confik/src/common.rs                          |  2 +-
 confik/src/lib.md                             | 40 ++++++++++++++-----
 confik/tests/keyed_containers/mod.rs          |  2 +-
 confik/tests/unkeyed_containers/mod.rs        |  2 +-
 13 files changed, 64 insertions(+), 29 deletions(-)
 delete mode 100644 confik-macros/tests/trybuild/fail-derive-literal.stderr
 rename confik-macros/tests/trybuild/{fail-derive-literal.rs => fail-forward-literal.rs} (62%)
 create mode 100644 confik-macros/tests/trybuild/fail-forward-literal.stderr

diff --git a/confik-macros/src/lib.rs b/confik-macros/src/lib.rs
index b6f361f..02047a4 100644
--- a/confik-macros/src/lib.rs
+++ b/confik-macros/src/lib.rs
@@ -624,10 +624,10 @@ struct RootImplementer {
     vis: Visibility,
 
     /// Optional attributes to forward to the builder struct/enum.
+    ///
+    /// This can be serde attributes e.g. `#[confik(forward(serde(default)))]` but also others like
+    /// `#[confik(forward(derive(Hash)))]`
     forward: Option<Forward>,
-
-    /// Derives needed by the builder, e.g. `Hash`.
-    derive: Option<Derive>,
 }
 
 impl RootImplementer {
@@ -666,7 +666,6 @@ impl RootImplementer {
             generics,
             vis,
             forward,
-            derive: additional_derives,
             ..
         } = self;
 
@@ -725,7 +724,7 @@ impl RootImplementer {
         let (_impl_generics, type_generics, where_clause) = generics.split_for_impl();
 
         Ok(quote_spanned! { target_name.span() =>
-            #[derive(::std::default::Default, ::confik::__exports::__serde::Deserialize, #additional_derives )]
+            #[derive(::std::default::Default, ::confik::__exports::__serde::Deserialize)]
             #[serde(crate = "::confik::__exports::__serde")]
             #forward
             #vis #enum_or_struct_token #builder_name #type_generics #where_clause
diff --git a/confik-macros/tests/trybuild.rs b/confik-macros/tests/trybuild.rs
index 64b8d1a..d89c8cd 100644
--- a/confik-macros/tests/trybuild.rs
+++ b/confik-macros/tests/trybuild.rs
@@ -34,7 +34,7 @@ fn compile_macros() {
     t.compile_fail("tests/trybuild/fail-default-invalid-expr.rs");
     t.compile_fail("tests/trybuild/fail-config-name-value.rs");
     t.compile_fail("tests/trybuild/fail-secret-extra-attr.rs");
-    t.compile_fail("tests/trybuild/fail-derive-literal.rs");
+    t.compile_fail("tests/trybuild/fail-forward-literal.rs");
     t.compile_fail("tests/trybuild/fail-field-from-unknown-type.rs");
     t.compile_fail("tests/trybuild/fail-uncreatable-type.rs");
     t.compile_fail("tests/trybuild/fail-not-a-type.rs");
diff --git a/confik-macros/tests/trybuild/19-derive.rs b/confik-macros/tests/trybuild/19-derive.rs
index cd3dfc0..a5c91f2 100644
--- a/confik-macros/tests/trybuild/19-derive.rs
+++ b/confik-macros/tests/trybuild/19-derive.rs
@@ -3,7 +3,7 @@ use std::collections::{BTreeSet, HashSet};
 use confik::Configuration;
 
 #[derive(Configuration)]
-#[confik(derive(::std::hash::Hash, std::cmp::Ord, PartialOrd, Eq, PartialEq, Clone))]
+#[confik(forward(derive(::std::hash::Hash, std::cmp::Ord, PartialOrd, Eq, PartialEq, Clone)))]
 struct Target {
     item: usize,
 }
diff --git a/confik-macros/tests/trybuild/23-where-clause.rs b/confik-macros/tests/trybuild/23-where-clause.rs
index 071f023..38b999f 100644
--- a/confik-macros/tests/trybuild/23-where-clause.rs
+++ b/confik-macros/tests/trybuild/23-where-clause.rs
@@ -13,7 +13,7 @@ impl MyTrait for () {
 }
 
 #[derive(Configuration)]
-#[confik(forward_serde(bound = "C: MyTrait + DeserializeOwned"))]
+#[confik(forward(serde(bound = "C: MyTrait + DeserializeOwned")))]
 struct Config<C>
 where
     C: MyTrait + Default + DeserializeOwned,
diff --git a/confik-macros/tests/trybuild/fail-derive-literal.stderr b/confik-macros/tests/trybuild/fail-derive-literal.stderr
deleted file mode 100644
index 60b7f86..0000000
--- a/confik-macros/tests/trybuild/fail-derive-literal.stderr
+++ /dev/null
@@ -1,5 +0,0 @@
-error: Expected a path to a derivable trait, got Lit(Lit::Str { token: "hello world" })
- --> tests/trybuild/fail-derive-literal.rs:2:17
-  |
-2 | #[confik(derive("hello world"))]
-  |                 ^^^^^^^^^^^^^
diff --git a/confik-macros/tests/trybuild/fail-derive-literal.rs b/confik-macros/tests/trybuild/fail-forward-literal.rs
similarity index 62%
rename from confik-macros/tests/trybuild/fail-derive-literal.rs
rename to confik-macros/tests/trybuild/fail-forward-literal.rs
index 9f37124..f25295d 100644
--- a/confik-macros/tests/trybuild/fail-derive-literal.rs
+++ b/confik-macros/tests/trybuild/fail-forward-literal.rs
@@ -1,5 +1,5 @@
 #[derive(confik::Configuration)]
-#[confik(derive("hello world"))]
+#[confik(forward("hello world"))]
 struct A;
 
 fn main() {}
diff --git a/confik-macros/tests/trybuild/fail-forward-literal.stderr b/confik-macros/tests/trybuild/fail-forward-literal.stderr
new file mode 100644
index 0000000..fb6249a
--- /dev/null
+++ b/confik-macros/tests/trybuild/fail-forward-literal.stderr
@@ -0,0 +1,17 @@
+error: expected identifier, found `"hello world"`
+ --> tests/trybuild/fail-forward-literal.rs:2:18
+  |
+2 | #[confik(forward("hello world"))]
+  |                  ^^^^^^^^^^^^^ expected identifier
+
+error: proc-macro derive produced unparsable tokens
+ --> tests/trybuild/fail-forward-literal.rs:1:10
+  |
+1 | #[derive(confik::Configuration)]
+  |          ^^^^^^^^^^^^^^^^^^^^^
+
+error[E0412]: cannot find type `AConfigBuilder` in this scope
+ --> tests/trybuild/fail-forward-literal.rs:3:8
+  |
+3 | struct A;
+  |        ^ not found in this scope
diff --git a/confik/CHANGELOG.md b/confik/CHANGELOG.md
index 92e551d..db00f07 100644
--- a/confik/CHANGELOG.md
+++ b/confik/CHANGELOG.md
@@ -12,6 +12,12 @@
       num: usize,
     }
     ```
+  - Replaces `confik(derive(...))`. E.g.
+    ```rust
+    #[derive(Configuration)]
+    #[confik(forward(derive(Hash)))]
+    struct Config(usize);
+    ```
 
 ## 0.13.0
 
diff --git a/confik/examples/derives.rs b/confik/examples/derives.rs
index fa70b62..ccf1f39 100644
--- a/confik/examples/derives.rs
+++ b/confik/examples/derives.rs
@@ -9,7 +9,7 @@ struct Config {
 }
 
 #[derive(Debug, Configuration, Hash, Eq, PartialEq)]
-#[confik(derive(Hash, Eq, PartialEq))]
+#[confik(forward(derive(Hash, Eq, PartialEq)))]
 struct Value {
     inner: String,
 }
diff --git a/confik/src/common.rs b/confik/src/common.rs
index 17666be..2835c80 100644
--- a/confik/src/common.rs
+++ b/confik/src/common.rs
@@ -6,7 +6,7 @@ use crate::{Configuration, MissingValue};
 
 /// The database type, used to determine the connection string format
 #[derive(Debug, Clone, PartialEq, Eq, Configuration)]
-#[confik(forward_serde(rename_all = "lowercase"))]
+#[confik(forward(serde(rename_all = "lowercase")))]
 enum DatabaseKind {
     Mysql,
     Postgres,
diff --git a/confik/src/lib.md b/confik/src/lib.md
index 21806bd..b2c60d3 100644
--- a/confik/src/lib.md
+++ b/confik/src/lib.md
@@ -69,7 +69,7 @@ If a secret is found in an insecure source, an error will be returned. You can o
 
 The derive macro is called `Configuration` and is used as normal:
 
-```
+```rust
 #[derive(confik::Configuration)]
 struct Config {
     data: usize,
@@ -78,19 +78,37 @@ struct Config {
 
 ### Forwarding Attributes
 
-The serde attributes used for customizing a `Deserialize` derive typically are achieved by adding `#[confik(forward(serde(...)))]` attributes.
+This allows forwarding any kind of attribute on to the builder.
+
+#### Serde
+
+The serde attributes used for customizing a `Deserialize` derive are achieved by adding `#[confik(forward(serde(...)))]` attributes.
 
 For example:
 
+```rust
+# use confik::Configuration;
+#[derive(Configuration, Debug, PartialEq, Eq)]
+struct Field {
+    #[confik(forward(serde(rename = "other_name")))]
+    field1: usize,
+}
 ```
-#[derive(confik::Configuration)]
-struct Config {
-    #[confik(forward(serde(rename = "other_data")))]
-    data: usize,
+#### Derives
+
+If you need additional derives for your type, these can be added via `#[confik(forward(derive...))]` attributes.
+
+For example:
+
+```rust
+# use confik::Configuration;
+#[derive(Debug, Configuration, Hash, Eq, PartialEq)]
+#[confik(forward(derive(Hash, Eq, PartialEq)))]
+struct Value {
+    inner: String,
 }
 ```
 
-This can also be used for non-serde attributes, but this is less commonly needed.
 
 ### Defaults
 
@@ -98,7 +116,7 @@ Defaults are specified on a per-field basis.
 
 - Defaults only apply if no data has been read for that field. E.g., if `data` in the below example has one value read in, it will return an error.
 
-  ```
+  ```rust
   # #[cfg(feature = "toml")]
   # {
   use confik::{Configuration, TomlSource};
@@ -150,7 +168,7 @@ Defaults are specified on a per-field basis.
 
 - Defaults can be given by any rust expression, and have [`Into::into`] run over them. E.g.,
 
-  ```
+  ```rust
   const DEFAULT_VALUE: u8 = 4;
 
   #[derive(confik::Configuration)]
@@ -166,7 +184,7 @@ Defaults are specified on a per-field basis.
 
 - Alternatively, a default without a given value called [`Default::default`]. E.g.,
 
-  ```
+  ```rust
   use confik::{Configuration};
 
   #[derive(Configuration)]
@@ -195,7 +213,7 @@ This crate provides implementations of [`Configuration`] for a number of `std` t
 
 If there's another foreign type used in your config, then you will not be able to implement [`Configuration`] for it. Instead any type that implements [`Into`] or [`TryInto`] can be used.
 
-```
+```rust
 struct ForeignType {
     data: usize,
 }
diff --git a/confik/tests/keyed_containers/mod.rs b/confik/tests/keyed_containers/mod.rs
index d99752e..0bd21b6 100644
--- a/confik/tests/keyed_containers/mod.rs
+++ b/confik/tests/keyed_containers/mod.rs
@@ -3,7 +3,7 @@ macro_rules! create_tests_for {
         use confik::Configuration;
 
         #[derive(Debug, Configuration, PartialEq, Eq, Hash, Ord, PartialOrd)]
-        #[confik(derive(Hash, PartialEq, Eq, Ord, PartialOrd))]
+        #[confik(forward(derive(Hash, PartialEq, Eq, Ord, PartialOrd)))]
         struct TwoVals {
             first: usize,
             second: usize,
diff --git a/confik/tests/unkeyed_containers/mod.rs b/confik/tests/unkeyed_containers/mod.rs
index d30cf06..b6758a1 100644
--- a/confik/tests/unkeyed_containers/mod.rs
+++ b/confik/tests/unkeyed_containers/mod.rs
@@ -3,7 +3,7 @@ macro_rules! create_tests_for {
         use confik::Configuration;
 
         #[derive(Debug, Configuration, PartialEq, Eq, Hash, Ord, PartialOrd)]
-        #[confik(derive(Hash, PartialEq, Eq, Ord, PartialOrd))]
+        #[confik(forward(derive(Hash, PartialEq, Eq, Ord, PartialOrd)))]
         struct TwoVals {
             first: usize,
             second: usize,

From c6f29828ba0526330e8ae3df50c4f43e424a4ba1 Mon Sep 17 00:00:00 2001
From: tenuous-guidance <105654822+tenuous-guidance@users.noreply.github.com>
Date: Wed, 9 Apr 2025 12:54:49 +0100
Subject: [PATCH 3/3] feat(derive): allow naming the builder and setting field
 visibility

---
 confik-macros/src/lib.rs                      | 53 +++++++++++++++----
 confik-macros/tests/trybuild.rs               |  6 ++-
 .../tests/trybuild/22-dataless-types.rs       |  2 +-
 ...m-untagged.rs => 25-pass-enum-untagged.rs} |  0
 .../tests/trybuild/26-named-builder.rs        | 15 ++++++
 .../tests/trybuild/27-field-access.rs         | 16 ++++++
 confik-macros/tests/trybuild/28-field-vis.rs  | 19 +++++++
 .../tests/trybuild/29-named-field-vis.rs      | 18 +++++++
 confik/CHANGELOG.md                           |  7 +++
 confik/src/lib.md                             | 32 +++++++++++
 10 files changed, 155 insertions(+), 13 deletions(-)
 rename confik-macros/tests/trybuild/{pass-enum-untagged.rs => 25-pass-enum-untagged.rs} (100%)
 create mode 100644 confik-macros/tests/trybuild/26-named-builder.rs
 create mode 100644 confik-macros/tests/trybuild/27-field-access.rs
 create mode 100644 confik-macros/tests/trybuild/28-field-vis.rs
 create mode 100644 confik-macros/tests/trybuild/29-named-field-vis.rs

diff --git a/confik-macros/src/lib.rs b/confik-macros/src/lib.rs
index 02047a4..02f7db6 100644
--- a/confik-macros/src/lib.rs
+++ b/confik-macros/src/lib.rs
@@ -330,6 +330,9 @@ struct FieldImplementer {
     /// The field type.
     ty: Type,
 
+    /// `pub`, `pub(crate)`, etc.
+    vis: Visibility,
+
     /// Optional attributes to forward to the builder's field.
     forward: Option<Forward>,
 }
@@ -383,6 +386,7 @@ impl FieldImplementer {
             forward,
             from,
             try_from,
+            vis,
             ..
         } = field_impl.as_ref();
 
@@ -415,7 +419,7 @@ impl FieldImplementer {
         Ok(quote_spanned! { ident.span() =>
                 #[serde(default)]
                 #forward
-                #ident #ty
+                #vis #ident #ty
         })
     }
 
@@ -628,6 +632,11 @@ struct RootImplementer {
     /// This can be serde attributes e.g. `#[confik(forward(serde(default)))]` but also others like
     /// `#[confik(forward(derive(Hash)))]`
     forward: Option<Forward>,
+
+    /// A name to use for the builder.
+    ///
+    /// Setting this also puts the builder in the local module, so that the name is accessible.
+    name: Option<Ident>,
 }
 
 impl RootImplementer {
@@ -655,7 +664,11 @@ impl RootImplementer {
     ///
     /// Use [`Self::is_dataless`] first to determine whether a builder will exist.
     fn builder_name(&self) -> Ident {
-        format_ident!("{}ConfigBuilder", self.ident)
+        if let Some(name) = self.name.as_ref() {
+            name.clone()
+        } else {
+            format_ident!("{}ConfigBuilder", self.ident)
+        }
     }
 
     /// Defines the builder for the target.
@@ -920,18 +933,36 @@ fn derive_macro_builder_inner(target_struct: &DeriveInput) -> syn::Result<proc_m
         )]
     };
 
-    let full_derive = quote! {
-        #overall_lint_overrides
-        const _: () = {
-            #impl_lint_overrides
-            #target_impl
-
+    let full_derive = if implementer.name.is_some() {
+        quote! {
+            #overall_lint_overrides
             #struct_lint_overrides
             #builder_struct
 
-            #impl_lint_overrides
-            #builder_impl
-        };
+            #overall_lint_overrides
+            const _: () = {
+                #impl_lint_overrides
+                #target_impl
+
+                #overall_lint_overrides
+                #impl_lint_overrides
+                #builder_impl
+            };
+        }
+    } else {
+        quote! {
+            #overall_lint_overrides
+            const _: () = {
+                #impl_lint_overrides
+                #target_impl
+
+                #struct_lint_overrides
+                #builder_struct
+
+                #impl_lint_overrides
+                #builder_impl
+            };
+        }
     };
 
     Ok(full_derive.into())
diff --git a/confik-macros/tests/trybuild.rs b/confik-macros/tests/trybuild.rs
index d89c8cd..6268564 100644
--- a/confik-macros/tests/trybuild.rs
+++ b/confik-macros/tests/trybuild.rs
@@ -28,7 +28,11 @@ fn compile_macros() {
     t.pass("tests/trybuild/22-dataless-types.rs");
     t.pass("tests/trybuild/23-where-clause.rs");
     t.pass("tests/trybuild/24-field-try-from.rs");
-    t.pass("tests/trybuild/pass-enum-untagged.rs");
+    t.pass("tests/trybuild/25-pass-enum-untagged.rs");
+    t.pass("tests/trybuild/26-named-builder.rs");
+    t.pass("tests/trybuild/27-field-access.rs");
+    t.pass("tests/trybuild/28-field-vis.rs");
+    t.pass("tests/trybuild/29-named-field-vis.rs");
 
     t.compile_fail("tests/trybuild/fail-default-parse.rs");
     t.compile_fail("tests/trybuild/fail-default-invalid-expr.rs");
diff --git a/confik-macros/tests/trybuild/22-dataless-types.rs b/confik-macros/tests/trybuild/22-dataless-types.rs
index d4578d2..47536d6 100644
--- a/confik-macros/tests/trybuild/22-dataless-types.rs
+++ b/confik-macros/tests/trybuild/22-dataless-types.rs
@@ -8,7 +8,7 @@ struct A;
 struct B {}
 
 #[derive(Configuration, Debug)]
-struct C ();
+struct C();
 
 fn main() {
     let _builder = A::builder().try_build().expect("No data required");
diff --git a/confik-macros/tests/trybuild/pass-enum-untagged.rs b/confik-macros/tests/trybuild/25-pass-enum-untagged.rs
similarity index 100%
rename from confik-macros/tests/trybuild/pass-enum-untagged.rs
rename to confik-macros/tests/trybuild/25-pass-enum-untagged.rs
diff --git a/confik-macros/tests/trybuild/26-named-builder.rs b/confik-macros/tests/trybuild/26-named-builder.rs
new file mode 100644
index 0000000..74e199e
--- /dev/null
+++ b/confik-macros/tests/trybuild/26-named-builder.rs
@@ -0,0 +1,15 @@
+//! Check that we can name and reference a builder.
+use confik::ConfigurationBuilder;
+
+#[derive(confik::Configuration, Debug, PartialEq)]
+#[confik(name = C)]
+struct Config {
+    #[confik(default)]
+    param: String,
+}
+
+fn main() {
+    let Config { .. } = C::default()
+        .try_build()
+        .expect("Default builder should succeed");
+}
diff --git a/confik-macros/tests/trybuild/27-field-access.rs b/confik-macros/tests/trybuild/27-field-access.rs
new file mode 100644
index 0000000..8656385
--- /dev/null
+++ b/confik-macros/tests/trybuild/27-field-access.rs
@@ -0,0 +1,16 @@
+//! Check that we can reference builder fields
+use confik::Configuration;
+
+#[derive(Configuration, Debug, PartialEq)]
+struct Config {
+    #[confik(default)]
+    param: String,
+}
+
+type Builder = <Config as Configuration>::Builder;
+
+fn main() {
+    let _ = Builder {
+        param: Default::default(),
+    };
+}
diff --git a/confik-macros/tests/trybuild/28-field-vis.rs b/confik-macros/tests/trybuild/28-field-vis.rs
new file mode 100644
index 0000000..f185a2b
--- /dev/null
+++ b/confik-macros/tests/trybuild/28-field-vis.rs
@@ -0,0 +1,19 @@
+//! Check that we can reference builder fields in a different module, when they're public
+
+pub mod config {
+    use confik::Configuration;
+
+    #[derive(Configuration, Debug, PartialEq)]
+    pub struct Config {
+        #[confik(default)]
+        pub param: String,
+    }
+
+    pub type Builder = <Config as Configuration>::Builder;
+}
+
+fn main() {
+    let _ = config::Builder {
+        param: Default::default(),
+    };
+}
diff --git a/confik-macros/tests/trybuild/29-named-field-vis.rs b/confik-macros/tests/trybuild/29-named-field-vis.rs
new file mode 100644
index 0000000..f0e6ad8
--- /dev/null
+++ b/confik-macros/tests/trybuild/29-named-field-vis.rs
@@ -0,0 +1,18 @@
+//! Check that we can reference builder fields in a different module, when they're public, using the builder's name
+
+pub mod config {
+    use confik::Configuration;
+
+    #[derive(Configuration, Debug, PartialEq)]
+    #[confik(name = Builder)]
+    pub struct Config {
+        #[confik(default)]
+        pub param: String,
+    }
+}
+
+fn main() {
+    let _ = config::Builder {
+        param: Default::default(),
+    };
+}
diff --git a/confik/CHANGELOG.md b/confik/CHANGELOG.md
index db00f07..28825df 100644
--- a/confik/CHANGELOG.md
+++ b/confik/CHANGELOG.md
@@ -18,6 +18,13 @@
     #[confik(forward(derive(Hash)))]
     struct Config(usize);
     ```
+- Add a new `confik(name = ...)` attribute, that provides a custom name for the `Configuration::Builder` `struct` or `enum`.
+  - This will also place the builder in the local module, so that its name is in a known location
+  ```rust
+  #[derive(Configuration)]
+  #[confik(name = Builder)]
+  struct Config {}
+  ```
 
 ## 0.13.0
 
diff --git a/confik/src/lib.md b/confik/src/lib.md
index b2c60d3..770d1d5 100644
--- a/confik/src/lib.md
+++ b/confik/src/lib.md
@@ -256,6 +256,38 @@ struct Config {
 }
 ```
 
+### Named builders
+
+If you want to directly access the builders, you can provide them with a name. This will also place the builder in the local module, to ensure there's a known path with which to reference them.
+
+```rust
+#[derive(confik::Configuration)]
+#[confik(name = Builder)]
+struct Config {
+    data: usize,
+}
+
+let _ = Builder { data: Default::default() };
+```
+
+### Field and Builder visibility
+
+Field and builder visibility are direclty inherited from the underlying type. E.g.
+
+```rust
+mod config {
+    #[derive(confik::Configuration)]
+    pub struct Config {
+        pub data: usize,
+    }
+}
+
+// Required as you can't use this syntax for struct initialisation.
+type Builder = <config::Config as confik::Configuration>::Builder;
+
+let _ = Builder { data: Default::default() };
+```
+
 ## Macro Limitations
 
 ### Custom `Deserialize` Implementations