Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Builder Pattern

The builder pattern implemented with the builder argument added to the fxstruct macro or to one of the field fieldx attributes. The First Steps chapter already mentions this, so let's just borrow its example:

use fieldx::fxstruct;
#[fxstruct(builder, get)]
struct Book {
    title: String,
    author: String,
    #[fieldx(get(copy))]
    year: u32,
    // How many books are available in the depository.
    #[fieldx(get(copy), builder(off))]
    available: u32,
}

let my_struct = Book::builder()
    .title("The Hitchhiker's Guide to the Galaxy".to_string())
    .author("Douglas Adams".to_string())
    .year(1979)
    .build()
    .expect("Failed to build Book object");

assert_eq!(my_struct.title(), "The Hitchhiker's Guide to the Galaxy");
assert_eq!(my_struct.author(), "Douglas Adams");
assert_eq!(my_struct.year(), 1979);
assert_eq!(my_struct.available(), 0);

There are two aspects worth mentioning here.

The first is the builder(off) argument of the available field. Apparently, with this flag, the field is not settable via the builder type. But since we still need a value to be put into the field, the only one we can use is the default one. For u32, it is 0. If any other value is needed, it can be set via the default argument of the fieldx attribute:

#[fieldx(get(copy), builder(off), default(123))]

The second aspect is the .expect("...") method call at the end of the builder chain. This way, we handle possible errors that may occur when a required value is not set. Imagine commenting out the .year(1979) call in the example above. This is a pure run-time error that the compiler cannot catch, and we must handle it ourselves at run time.

This brings us to the next topic, which is discussed in the next chapter. For now, we have a bit more to discuss here.

Opt-in Approach

For a big struct where only a couple of fields need to receive values from the user, it could be quite tedious to add builder(off) to each one that is not settable via the builder. There is a way in FieldX to make this easier: use the "opt-in" approach:

use fieldx::fxstruct;

#[fxstruct(get, builder(opt_in))]
struct Book {
    #[fieldx(builder)]
    title:  String,
    #[fieldx(builder)]
    author: String,
    #[fieldx(builder)]
    year:   u32,
    #[fieldx(get(copy), get_mut, inner_mut)]
    available: u16,
    #[fieldx(
        set("place_into"),
        inner_mut,
        default("unknown".to_string())
    )]
    location: String,
}

let book = BookBuilder::new()
    .title("The Hitchhiker's Guide to the Galaxy".to_string())
    .author("Douglas Adams".to_string())
    .year(1979)
    .build()
    .expect("Failed to create book");

This example isn't perfect because there are more buildable fields than non-buildable ones, but it demonstrates the point. Attempting to add an .available(123) call to the builder chain will result in a compile-time error.

The same result can be achieved by simply adding the builder argument to the fields where we want it. In this case, FieldX will imply that we want the opt-in scheme used for the struct. Why the opt_in sub-argument, you may ask? Sometimes one may want it for readability purposes, but more importantly, it allows specifying additional struct-level sub-arguments with the builder argument without resulting in the need to go the every-field-builder(off) route.

use fieldx::fxstruct;

#[fxstruct(get, builder("BookConstructor", opt_in, prefix("set_")))]
struct Book {
    #[fieldx(builder)]
    title:  String,
    #[fieldx(builder)]
    author: String,
    #[fieldx(builder)]
    year:   u32,
    #[fieldx(get(copy), get_mut, inner_mut)]
    available: u16,
    #[fieldx(
        set("place_into"),
        inner_mut,
        default("unknown".to_string())
    )]
    location: String,
}

let book = BookConstructor::new()
    .set_title("The Hitchhiker's Guide to the Galaxy".to_string())
    .set_author("Douglas Adams".to_string())
    .set_year(1979)
    .build()
    .expect("Failed to create book");

In this snippet, we have two changes that can only be done at the struct level:

  • We've given a new name to the builder type.
  • We added a common prefix to the names of build setter methods.

Default Value

When there is a default value specified for a field, FieldX takes it into account when generating the builder type. The implication it has is that the corresponding setter method for the field could be omitted from the builder chain. Sure enough, in this case, the field will be initialized with its default:

use fieldx::fxstruct;

#[fxstruct(get)]
struct Book {
    #[fieldx(builder)]
    title:  String,
    #[fieldx(builder)]
    author: String,
    #[fieldx(builder)]
    year:   u32,
    #[fieldx(get(copy), get_mut, inner_mut)]
    available: u16,
    #[fieldx(
        set("place_into"),
        inner_mut,
        default("unknown".to_string()),
        builder
    )]
    location: String,
}

let book = Book::builder()
    .title("The Hitchhiker's Guide to the Galaxy".to_string())
    .author("Douglas Adams".to_string())
    .year(1979)
    .build()
    .expect("Failed to create book");

assert_eq!(*book.location(), "unknown");

BTW

Here we also implement the above-mentioned approach where fields are given the builder argument and the struct-level builder(opt_in) is assumed.

Coercion

Similar to the setter methods, the builder methods can also coerce their arguments into the field type. As with the setters, this is achieved using the into sub-argument:

use fieldx::fxstruct;

#[fxstruct(get(clone), builder(into))]
struct Book {
    title:  String,
    author: String,
    #[fieldx(get(copy), builder(into(off)))]
    year:   u32,
    #[fieldx(optional)]
    signed_by: String,
    #[fieldx(
        set("place_into"),
        inner_mut,
        default("unknown".to_string())
    )]
    location: String,
}

let book = Book::builder()
    .title("The Hitchhiker's Guide to the Galaxy")
    .author("Douglas Adams")
    .year(1979)
    .signed_by("Douglas Adams")
    .build()
    .expect("Failed to create book");

assert_eq!(book.title(), "The Hitchhiker's Guide to the Galaxy".to_string());
assert_eq!(book.author(), "Douglas Adams".to_string());
assert_eq!(book.year(), 1979);
assert_eq!(book.signed_by(), Some("Douglas Adams".to_string()));

This example demonstrates several concepts simultaneously:

  • The use of into as the struct-level default.
  • Field-level override for the default.
  • Usage with an optional field.