tidy-builder is a builder generator that is compile-time correct.

Overview

The Builder derive macro creates a compile-time correct builder which means that it only allows you to build the given struct if and only if you provide a value for all of its required fields.

From the perspective of the builder there are three types of fields:

  • Optional Fields which are fields wrapped in an Option.
  • Default Fields which are given a default value through the #[builder(default)] attribute.
  • Required Fields which are fields that do not fall into the previous categories.

Example below depicts these three types of fields:

use tidy_builder::Builder;

#[derive(Builder)]
struct Person {
    first_name: String,
    last_name: String,

    age: Option<usize>,

    #[builder(default = false)]
    employed: bool,
}

fn main() {
    let person = Person::builder()
                        .first_name("Foo".to_string())
                        .last_name("Bar".to_string())
                        .age(18)
                        .build();

    assert_eq!(person.first_name, "Foo".to_string());
    assert_eq!(person.last_name, "Bar".to_string());
    assert_eq!(person.age, Some(18));
    assert_eq!(person.employed, false);
}

As you can see, first_name and last_name are required fields, age is optional, and employed takes a default value of false. As we mentioned, in order to call build, you have to at least provide values for first_name and last_name. tidy-builder enforces this rule by creating a state machine and guarding the build function with special traits in order to make sure build is called only in the final state. Picture below shows the state machine created by tidy-builder:

For more info see What if I try to call the build function early? and How it Works.

Features

Repeated Setters

For fields that are of form Vec<T>, you can instruct the builder to create a repeated setter for you. This repeated setter gets a single value of type T and appends to the Vec. For example:

use tidy_builder::Builder;

#[derive(Builder)]
struct Input<'a> {
    #[builder(each = "arg")]
    args: Vec<&'a str>
}

fn main() {
    let input1 = Input::builder().arg("arg1").arg("arg2").build();
    let input2 = Input::builder().args(vec!["arg1", "arg2"]).build();

    assert_eq!(input1.args, vec!["arg1", "arg2"]);
    assert_eq!(input2.args, vec!["arg1", "arg2"]);
}

The builder will create another setter function named arg alongside the args function that was going to be generated anyway. Note that if the name provided for the repeated setter is the same name as the field itself, only the repeated setter will be provided by the builder since Rust does not support function overloading. For example if in the example above the repeated setter was named args, the setter that takes a Vec wouldn't be provided.

Default Values

You can provide default values for fields and make them non-required. If the field is a primitive or a String, you can specify the default value in the #[builder(default)] attribute, but if the field is not a primitive, it must implement the Default trait. For example:

use tidy_builder::Builder;

#[derive(Debug, PartialEq)]
pub struct Point {
    x: usize,
    y: usize,
}

impl Default for Point {
    fn default() -> Self {
        Point {
            x: 0,
            y: 0,
        }
    }
}

#[derive(Builder)]
struct PlayerPosition {
    #[builder(default)]
    start: Point,

    #[builder(default = 0)]
    offset: usize,
}

fn main() {
    let position = PlayerPosition::builder().build();

    assert_eq!(position.start, Point { x: 0, y: 0});
    assert_eq!(position.offset, 0);
}

Skipping Fields

You can prevent the builder from providing setters for optional and default fields. For example:

use tidy_builder::Builder;

#[derive(Builder)]
struct Vote {
    submit_url: String,

    #[builder(skip)]
    name: Option<String>,

    #[builder(skip)]
    #[builder(default = false)]
    vote: bool
}

fn main() {
    let vote = Vote::builder().submit_url("fake_submit_url.com").name("Foo".to_string()); // Fails since there is no `name` setter
}

What if I try to call the build function early?

tidy-builder uses special traits to hint at the missing required fields. For example:

use tidy_builder::Builder;

#[derive(Builder)]
struct Foo {
    bar: usize,
    baz: usize,
}

fn main() {
    let foo = Foo::builder().bar(0).build();
}

On stable Rust you'll get a compile-time error that the trait HasBaz is not implemented for the struct FooBuilder<...>. The trait HasBaz indicates that FooBuilder has a value for the baz field. So this trait not being implemented for FooBuilder means that a value is not specified for the baz field and that's why you cannot call the build function.

On nightly Rust and with the help of rustc_on_unimplemented, the Builder can hint at the compiler to show the message missing baz to inform the user that in order to call build, they should set the value of the baz field. Note that this is behind the better_error feature gate.

How it works

tidy-builder creates a state machine in order to model the behavior of the builder. The generated builder has a const generic parameter of type bool for each required field to encode whether a value has been set for the field or not. For example:

use tidy_builder::Builder;

#[derive(Builder)]
struct Foo {
    bar: usize,
    baz: usize,
}

The struct above will cause this builder to get generated:

struct FooBuilder<const P0: bool, const P1: bool> {
    bar: Option<usize>,
    baz: Option<usize>,
}

The builder will start in the FooBuilder<false, false> state when you call the builder function of Foo:

let builder: FooBuilder<false, false> = Foo::builder();
let builder: FooBuilder<true, false> = Foo::builder().bar(0);
let builder: FooBuilder<true, true> = Foo::builder().bar(0).baz(1);

let foo = builder.build();

assert_eq!(foo.bar, 0);
assert_eq!(foo.baz, 1);

When you call the bar function to set the value of the bar field, you cause the builder to transition to the FooBuilder<true, false> state: Similarly, when you call the baz function, you cause the builder to transition to the FooBuilder<false, true> state. So when you set the value for both fields, you end up at the FooBuilder<true, true> state, and it's in this state that you can call the build function(the state that all const generic paramters are true):

The error reporting discussed in the previous section leverages these states to inform the user of the missing fields. For example HasBar trait will be implemented for FooBuilder<true, P1> , and HasBaz will be implemented for FooBuilder<P0, true>. The build function is guarded with a where clause to make sure the builder implements all these traits:

impl<const P0: bool, const P1: bool> FooBuilder<P0, P1> {
    fn build(self) -> Foo
    where
        Self: HasBar + HasBaz
    {
        // Safety:
        //
        // It's safe since HasBar and HasBaz are implemented
        // hence self.bar and self.baz both contain valid values.
        unsafe {
            Foo {
                bar: self.bar.unwrap_unchecked(),
                baz: self.baz.unwrap_unchecked(),
            }
        }
    }
}

So if you set the value of bar and not baz, since HasBaz won't be implemented for FooBuilder<true, false>, you'll get a compile-time error that calling build is not possible.

Comments
  • [suggestion] include `build` method when all required fields are not available.

    [suggestion] include `build` method when all required fields are not available.

    In order for users to know which fields are missing we can do something like this:

    struct Builder {
       req1: Option<usize>,
       req2: Option<usize>,
       opt1: usize
    }
    
    impl Builder<false, false> {
        fn build(&self, req1: usize, req2: usize) -> ...
    }
    
    impl Builder<false, true> {
        fn build(&self, req1: usize) -> ...
    }
    
    impl Builder<true, false> {
        fn build(&self, req2: usize) -> ...
    }
    
    impl Builder<true, true> {
       fn build(&self) -> ...
    }
    
    

    The only problem is that it generates a lot of code and it may not be ideal. But if you are OK with the design, I can send a PR if you want to.

    opened by sahandevs 8
  • use traits for better error messages

    use traits for better error messages

    with this implementation we kinda have better error messages:

    error[E0277]: the trait bound `MyStructBuilder<true, false, String>: HasReq2` is not satisfied
      --> tests/ui/generics_with_where_clause.rs:15:10
       |
    15 |         .build();
       |          ^^^^^ the trait `HasReq2` is not implemented for `MyStructBuilder<true, false, String>`
       |
       = help: the trait `HasReq2` is implemented for `MyStructBuilder<P0, true, T>`
    

    still not the best error message but at least it points to the missing fields and doesn't require nightly.

    If you are ok with this implementation I will add #[rustc_on_unimplemented] in a separate pull request.

    opened by sahandevs 1
  • implement the feature for skipping default and optional fields

    implement the feature for skipping default and optional fields

    implements the #[builder(skip)] attribute. This attribute allows skipping of optional and default fields meaning there will be no setters generated for these fields. Optional fields will be set to None and default fields will be set to their default values.

    opened by maminrayej 0
  • implement the feature for specifying defaults

    implement the feature for specifying defaults

    implements support for the #[builder(default)] attribute. This attribute has two flavors:

    • #[builder(default)] which requires the type to implement the Default trait.
    • #[builder(default = 0)], #[builder(default = 1.0)], ... that allow the user to specify a default value for types like integers, floats, and boolean.

    Specifying the default attribute on an optional does not have any effect since the default value of an Option is None. Fields with the default attribute act like optional fields in the sense that they don't change the state of the state machine either. For now default and each attributes don't play together which results in each having no effect if default is provided for a field. This behavior may change in the future.

    opened by maminrayej 0
  • implements initial support for repeated setters

    implements initial support for repeated setters

    Adds support for repeated setter fields:

    #[derive(tidy_builder::Builder)]
    struct MyStruct<'a> {
        #[builder(each = "arg")]
        args: Vec<&'a str>,
    }
    

    This adds the arg function in addition to the args function and enables the user to repeatedly push values to the args field. This attribute is also applicable on optional fields.

    Limitations

    The collection that holds the args must be a Vec. It's possible to set the attribute on other collections, but it must be compatible with Vec. For example it must have a new function that takes no arguments and returns the collection. It also must have one and exactly one generic parameter. I'm not going to list everything the macro depends on since I won't officially support other compatible collections in this PR.

    There may be a way to provide a more general implementation for this feature:

    • Be general over the new as the collection initializer.
    • Be general over the number of generic parameters. This enables the user to repeatedly push values to HashMap and other similar collections which is nice.
    opened by maminrayej 0
Owner
M.Amin Rayej
M.Amin Rayej
CLI application to run clang-tidy on a set of files specified using globs in a JSON configuration file.

run-clang-tidy CLI application for running clang-tidy for an existing .clang-tidy file on a set of files, specified using globs in a .json configurati

martin 7 Nov 4, 2022
try to find the correct word with only first letter and unknown letter count.

MOTUS Current dictionaries are provided in french and can contain some words not included in the official Motus dictionary. Additionally, dictionaries

Alexandre 6 Apr 11, 2022
languagetool-code-comments integrates the LanguageTool API to parse, spell check, and correct the grammar of your code comments!

languagetool-code-comments integrates the LanguageTool API to parse, spell check, and correct the grammar of your code comments! Overview Install MacO

Dustin Blackman 17 Dec 25, 2022
A CLI tool for CIs and build scripts, making file system based caching easy and correct (locking, eviction, etc.)

FS Dir Cache A CLI tool for CIs and build scripts, making file system based caching easy and correct (locking, eviction, etc.) When working on build s

Dawid Ciężarkiewicz 5 Aug 29, 2023
Rust library to convert RGB 24-bit colors into ANSI 256 (8-bit) color codes with zero dependencies and at compile-time.

rgb2ansi256 rgb2ansi256 is a small Rust library to convert RGB 24-bit colors into ANSI 256 (8-bit) color codes with zero dependencies and const fn. Th

Linda_pp 7 Nov 17, 2022
A strong, compile-time enforced authorization framework for rust applications.

DACquiri A compile-time enforced authorization framework for Rust applications. Authorization In typical applications, authorization checks are perfor

resync 247 Dec 20, 2022
Utilites for working with `bevy_ecs` when not all types are known at compile time

bevy_ecs_dynamic Utilities for working with bevy_ecs in situations where the types you're dealing with might not be known at compile time (e.g. script

Jakob Hellermann 17 Dec 9, 2022
Stockbook embeds 1-bit raster images in your code at compile time

stockbook Stockbook embeds 1-bit raster images in your code at compile time. Designed primarily for #![no_std] usage, in embedded or other program-mem

Karol Belina 3 Oct 27, 2022
ChatGPT powered Rust proc macro that generates code at compile-time.

gpt-macro ChatGPT powered Rust proc macro that generates code at compile-time. Implemented Macros auto_impl!{} #[auto_test(...)] Usage Get ChatGPT API

Akira Moroo 429 Apr 15, 2023
Choose Rust types at compile-time via boolean constants

condtype Choose Rust types at compile-time via boolean constants, brought to you by Nikolai Vazquez. If you find this library useful, consider starrin

Nikolai Vazquez 36 May 8, 2023
Catch Tailwindcss Errors at Compile-Time Before They Catch You, without making any change to your code! Supports overriding, extending, custom classes, custom modifiers, Plugins and many more 🚀🔥🦀

twust Twust is a powerful static checker in rust for TailwindCSS class names at compile-time. Table of Contents Overview Installation Usage Statement

null 15 Nov 8, 2023
AUR external package builder

AUR Build Server Goal This project aims to provide an external package making server based on any PKGBUILD based project. Right now it pulls AUR packa

Seïfane Idouchach 2 Sep 11, 2022
A container image builder tool for OCI (distrobox/toolbox, also podman/docker)

Distrobox Boost A container image builder tool for Open Container Initiative (distrobox/toolbox, also podman/docker). Distrobox is good enough in runn

xz-dev 6 Aug 15, 2023
The joker_query is a cute query builder, with Joker can implement most complex queries with sugar syntax

joker_query The joker_query is most sugared query builder of Rust, with joker_query can implement most complex queries with sugar syntax Features − (O

DanyalMh 8 Dec 13, 2023
belt is a command line app that can show your time from a list of selected time zones

A CLI app to show your time from a list of selected time zones, and a rust lib to parse dates in string formats that are commonly used.

Rollie Ma 23 Nov 4, 2022
Deadliner helps you keep track of the time left for your deadline by dynamically updating the wallpaper of your desktop with the time left.

Deadliner Watch the YouTube video What's Deadliner? Deadliner is a cross-platform desktop application for setting deadline for a project and keeping t

Deadliner 34 Dec 16, 2022
Helps you keep track of time for team members across different time zones & DST changes

Teamdate Helps you keep track of time for team members across different timezones and other daylight saving changes based off their location. Because

Alex Snaps 7 Jan 9, 2023
compile TypeScript or JavaScript to binaries

the powr project Development is paused until 2023. ?? powr aims to be a javascript/typescript engine to power serverless functions over the web. the j

powr.js 18 Dec 29, 2022
A series of crates that I made to compile images/video into asciinema & play them.

Bad Apple A series of crates that I made to compile images/video into asciinema & play them. The end goal is to make a kernel & legacy bootloader that

S0ra 10 Nov 29, 2022