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

GitHub Crates.io Docs.rs License

Introduction

FieldX is a declarative object orchestrator that streamlines object and dependency management. Key features include:

  • Lazy initialization of fields via builder methods, simplifying implicit dependency management
  • Accessor and setter methods generation
  • Inner mutability pattern for fields
  • Sync, async, and plain (unsync) modes of operation
  • Integration with serde for serialization and deserialization
  • Builder pattern for object creation
  • Post-build hooks for validation and adjustment
  • Generic structs support
  • Reference-counted objects
  • And more!

The crate doesn't have a strictly outlined purpose and can be helpful in various scenarios, such as:

  • Dependency manager implementation
  • Implicit dependency management and automated construction and initialization for objects
  • Automation of provisioning of object interfaces
  • Simplifying integration of complex object management logic with serialization and deserialization
  • ... and counting.

The functionality of FieldX can be extended by third-party crates. At the moment there is an experimental fieldx_plus crate that helps with implementing parent/child and application/agent relationships between objects.

To sum up, FieldX is well-suited for:

  • general application development
  • concurrent and asynchronous environments
  • scenarios where object internals should remain encapsulated
  • cases in which boilerplate for object management and initialization becomes tedious and annoying

The Book Purpose

This book is intended to provide a comprehensive overview of the FieldX crate, its features, and how to use it effectively. What it is not intended for is to provide a complete reference for the crate. For the latter, please refer to the FieldX documentation.

Example

This code focuses on lazy initialization of fields and shows how implicit dependencies work. Essentially, it forms the basis of a dependency manager or a large object requiring complex initialization logic.

A quick explanation:

  • The lazy argument of the fxstruct macro declares that all fields of the struct are initialized lazily by default. The exception is the order field, which opts out using lazy(off).
  • The count field is assigned a default value of 42. Thus, a newly created instance of Foo will have this value from the start, but since it’s lazy it can be reset to an uninitialized state with the clear_count method (introduced by the clearer attribute). The next read will initialize it by calling build_count. The predicate argument also provides a has_count method to check whether the field currently holds a value.
  • The comment field is purely lazy, without a default value.

Because laziness means a field only gets its value when first accessed, all fields automatically receive accessor methods. The count field is special here: its accessor is explicitly requested with get(copy), so instead of returning a reference (the usual behavior), it returns the usize value directly, which is more ergonomic since usize implements Copy.

The overall point of this sample is to demonstrate how the comment field gets its value constructed using the count field. As soon as the count changes, the comment gets a new value after re-initialization.

The example also logs each call to the corresponding builder methods of count and comment into the order field. By inspecting its content later, we can prove that the builders were only invoked when really needed, and that the order of invocation was determined by the initialization logic of the fields.

use fieldx::fxstruct;

#[fxstruct(lazy)]
struct Foo {
    #[fieldx(get(copy), predicate, clearer, default(42))]
    count:   usize,
    #[fieldx(predicate, clearer)]
    comment: String,

    #[fieldx(lazy(off), inner_mut, get, get_mut)]
    order: Vec<&'static str>,
}

impl Foo {
    fn build_count(&self) -> usize {
        self.order_mut().push("Building count.");
        12
    }

    fn build_comment(&self) -> String {
        self.order_mut().push("Building foo.");
        // If `count` isn't initialized yet it will be initialized lazily. during the call to the accessor method.
        format!("foo is using count: {}", self.count())
    }
}

let mut foo = Foo::new();
// No call to the accessor method has been made yet, the field remains uninitialized.
assert!(!foo.has_comment());

// The `count` field has a default value, so it is initialized.
assert!(foo.has_count());

// No builder methods have been called yet, so the order is empty.
assert!(foo.order().is_empty());

// For the first time the count is 42, the default value. The builder method for `comment` is using that.
assert_eq!(foo.comment(), "foo is using count: 42");

// Now we reset the count field to uninitialized state.
foo.clear_count();
assert!(!foo.has_count());

// `comment` is still initialized and reflects the original default value of `count`.
assert_eq!(foo.comment(), "foo is using count: 42");

// Reset `comment` to uninitialized state.
foo.clear_comment();

// Make sure it is unset.
assert!(!foo.has_comment());

// This time the `count` field will have its value from the builder method.
assert_eq!(foo.comment(), "foo is using count: 12");

// Both `comment` and `count` has values, so this call as just returns the value of `comment`.
assert_eq!(foo.comment(), "foo is using count: 12");

// Every call of `count` and `comment` builder methods are pushing their actions to the order field. At this point
// it must contain three entries:
// - one for the first call to `comment` where `count` had its default value and thus its builder wasn't involved;
// - and one for the call to `comment` after both fields was cleared where `count` was built by its builder method;
assert_eq!(foo.order().len(), 3);
assert_eq!(foo.order()[0], "Building foo.");
assert_eq!(foo.order()[1], "Building foo.");
assert_eq!(foo.order()[2], "Building count.");

Basics

The fieldx module provides two attributes: fxstruct, which is the actual macro and configures named structs (no unions or enums are supported), and fieldx, which adjusts field parameters.

When applied, the fxstruct macro transforms the annotated struct based on its own arguments and any subsidiary fieldx attributes. Notable changes and additions may include, but are not limited to:

  • Wrapping field types in container types (see The Inner Workings). In the introduction example, comment and count become OnceCell<String> and OnceCell<usize>, while order remains unchanged.

  • Generating helper methods and associated functions for the struct. This includes accessor methods and the new() associated method.

  • Implementing the Default trait.

  • Generating a builder type and the builder() associated method.

  • Generating a shadow struct for serialization/deserialization with serde.

The following chapters will introduce you into FieldX, covering basics of these and other topics.

Terminology

Attribute Arguments and Sub-Arguments

FieldX adopts a function-like syntax for specifying struct parameters through attributes. These parameters are referred to as arguments. Some arguments may require further customization, in which case they also take a function-like form and are referred to as sub-arguments.

This design choice supports deep nesting of parameters. For example:

#[fxstruct(
    builder(
        attributes_fn(allow(dead_code)),
        doc("Builder for the struct."),
    )
)]

Struct- and field-level arguments

As mentioned in the Basics, the fxstruct macro is applied to the struct itself and utilizes parameters provided by fieldx attributes. Consequently, this book frequently refers to various entities (primarily attribute arguments) as either struct-level or field-level.

For instance, it may be noted that a struct-level argument defines a default value, while a field-level argument overrides it. Similarly, certain functionalities might be available at the struct level but not at the field level.

Helper Arguments

Some arguments in FieldX are associated with implementation methods, whether generated by FieldX itself or provided by the user. These arguments are referred to as helper arguments, and they share some common sub-arguments. A more detailed overview of them will be provided later in this book, but for now, let's mention a literal string sub-argument that allows custom names to be defined for the argument-bound methods. Depending on the context, the argument may specify either the full name of the method or just a part of it, most commonly a prefix:

#[fxstruct(get("get_"))]
#[fieldx(get("field_alias"))]

Argument Disabler

Many arguments in FieldX are boolean flags by default, meaning they enable certain functionality. However, there are cases where it may be necessary to disable functionality instead. The two most common scenarios for this are:

  • Debugging or testing purposes, where functionality needs to be temporarily disabled.
  • Overriding a struct-level default at the field level.

To address this, FieldX introduces the concept of an argument disabler: a sub-argument named off.

#[fxstruct(get)]
struct MyStruct {
    // Override the struct-level `get`
    #[fieldx(get(off))]
    field: String,
    ...
}

Builder and Builder Methods

The term builder in FieldX can be confusing because it is used in two distinct contexts:

  1. Builder type: A type generated by the fxstruct macro that facilitates step-by-step construction of an instance of the annotated struct. This concept aligns with the builder pattern.
  2. Builder methods: User-defined methods for initializing fields lazily.

The term builder methods originated from similar implementations of the lazy initialization pattern in Perl and Raku modules. It was adopted in FieldX before the builder pattern was introduced, leading to the dual usage of the term. In most cases, the intended meaning is either clear from the context or explicitly clarified.

Modes of Operation

FieldX supports three modes of operation: sync, async, and plain (unsync). While their names are largely self-explanatory, here is a brief overview:

  • Sync: Designed for synchronous code, this mode ensures thread-safe access to fields.
  • Async: Tailored for asynchronous code, this mode facilitates field access in an async context. It is particularly useful for lazy fields, enabling users to implement async builder methods.
  • Plain: This mode does not enforce thread safety or async compatibility. Depending on the field configuration, it may explicitly lack Sync and Send traits due to its container wrapper type, or it might "incidentally" be thread-safe due to the inherent properties of the field type.

The implications of sync and async modes are explored further in the corresponding section.

Fallible and Infallible

By default, FieldX expects user-provided code to be infallible, meaning that it does not return a Result1. However, this constraint can be challenging to meet in practice. To address this, FieldX offers an escape hatch: support for fallible code that can return a Result type. The only requirement is that users declare the error type their code may return.


  1. Of course, it is always possible to panic, but it's the behavior being frowned upon.

The First Steps

As with any other crate, begin by adding fieldx to your Cargo.toml file:

[dependencies]
fieldx = { version = "0.2", features = ["sync"] }

Info

A detailed list of crate feature flags is available in the FieldX documentation.

Next, annotate a struct with the #[fxstruct] macro:

use fieldx::fxstruct;
#[fxstruct]
struct Book {
    #[fieldx(get, set)]
    title: String,
}

That's it! Now you can use it as follows:

let mut my_struct = Book::new();
my_struct.set_title("The Hitchhiker's Guide To The Galaxy".to_string());
assert_eq!(my_struct.title(), "The Hitchhiker's Guide To The Galaxy");

Let's say the struct grows in size and complexity, and it's time to implement the builder pattern. No problem! Simply add the builder attribute to the struct:

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);

Attributes, Arguments, and Sub-Arguments

As mentioned in the Terminology section, FieldX provides the fxstruct macro to annotate structs and the fieldx attribute to adjust field parameters. Both accept arguments that can take one of the following forms:

  • Flag: A pure boolean keyword where its presence means true and its absence means false.

  • Keyword: Similar to a flag in that it represents a boolean value. However, it can also take a function-like form with a single argument, the off flag, which sets its value to false:

    #[fxstruct(sync(off))]
  • List or Function: A complex argument that accepts a comma-separated list of sub-arguments. The term "list" originates from the definitions of Rust procedural macro entities:

    #[fxstruct(builder(attributes_fn(allow(dead_code), opt_in)))]
  • Helper: A function-like argument bound to a method of the struct implementation. The method can either be generated by the fxstruct macro or defined by the user. Helpers share common arguments, such as a string literal defining the helper's name, its visibility (if applicable), a list of Rust attributes, a doc comment, and the off flag:

    #[fieldx(builder("set_foo", vis(pub(crate)), doc("Sets the foo field.")))]

The full list of provided arguments can be found in the FieldX documentation.

Struct- and Field-Level Arguments

See also: Terminology

Arguments for the struct-level fxstruct macro serve two purposes, depending on their semantics: they either specify the struct's behavior or parameters, or define default values for field-level arguments. In the following example, the field v3 opts out of being lazily initialized and instead uses a static default value:

#[fxstruct(lazy)]
struct MyStruct {
    v1: u32,
    v2: f32,
    #[fieldx(
        lazy(off),
        default("static default".to_string())
    )]
    v3: String,
}

Interestingly, the v3 field also provides an implicit default at the struct level! Normally, FieldX avoids generating unnecessary code, and the Default trait implementation is no exception. However, if a field is given a default, it is as if the default argument were specified at the struct level, as shown in this snippet:

#[fxstruct(lazy, default)]
struct MyStruct {
    v1: u32,
    v2: f32,
    #[fieldx(
        lazy(off),
        default("static default".to_string())
    )]
    v3: String,
}

This example also demonstrates how the same arguments can have different meanings at the struct and field levels. While lazy has a consistent semantics at both levels with the struct-level argument implicitly applied to all fields, the default argument behaves differently. At the struct level, it provides a new trait implementation, while at the field level, it assigns a default value. The syntax also differs: the struct-level default argument is just a keyword.

A more prominent example of the difference between struct-level and field-level is the builder argument. At the struct level, it generates a builder type and the builder() method. At the field level, it generates a builder method for the field. Both use the same syntax but support partially different lists of sub-arguments:

#[fxstruct(builder("MyStructBuilder", opt_in))]
struct MyStruct {
    v1: u32,
    #[fieldx(builder("set_v2", into))]
    v2: f32,
}

Getters and Setters

No doubt that the first thing we need is being able to provide accessors and setter methods. While in application development these are the recommended way to expose inner data to the public, the low-level code would highly benefit from avoid them – but, still, there will be cases where the matters of code safety would require us to provide such methods.

Accessors

In FieldX, there are two ways to get an accessor method for a field:

  • Explicitly, by using the get argument.
  • Implicitly, when the use of another argument makes no sense without an accessor.

There are also two kinds of accessors: immutable and mutable. The latter is never generated implicitly and is only available with the use of the get_mut argument.

By default, an accessor returns a reference to the field value[^unless_other_args]:

use fieldx::fxstruct;

#[fxstruct(get, builder)]
struct Book {
    title:  String,
    author: String,
    year:   u32,
}

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");

let title: &str = book.title();
let author: &str = book.author();
let year: &u32 = book.year();
assert_eq!(title, "The Hitchhiker's Guide to the Galaxy");
assert_eq!(author, "Douglas Adams");
assert_eq!(year, &1979);

So far, so good! But, wait, year? This is disgusting!

use fieldx::fxstruct;

#[fxstruct(get, builder)]
struct Book {
    title:  String,
    #[fieldx(get(clone))]
    author: String,
    #[fieldx(get(copy))]
    year:   u32,
}

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");

let title: &str = book.title();
let author: String = book.author();
let year: u32 = book.year();
assert_eq!(title, "The Hitchhiker's Guide to the Galaxy");
assert_eq!(author, "Douglas Adams".to_string());
assert_eq!(year, 1979);

Now, this is much better! The get(copy) can be used with types that implement the Copy trait, such as usize, u32, etc., to get the value itself rather than a reference to it.

Along the lines, we use this sample to showcase two more things:

  • The get(clone) can be used with types that implement the Clone trait, such as String, to get a cloned value.
  • It is possible to override a struct-level default argument for a specific field, like we did with get(copy) for the year field and get(clone) for the author field.

And finally, let's have a quick look at the mutable accessors:

use fieldx::fxstruct;

#[fxstruct(get, builder)]
struct Book {
    title:  String,
    author: String,
    #[fieldx(get(copy))]
    year:   u32,
    #[fieldx(get(copy), get_mut, builder(off), default(false))]
    borrowed: bool,
}

let mut 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!(!book.borrowed());
*book.borrowed_mut() = true;
assert!(book.borrowed());

Note

As helper attributes, get and get_mut can take a literal value sub-argument that would give a name to the accessor method when used at the field level, or define a custom method name prefix for default accessor names when used at the struct level.

use fieldx::fxstruct;

#[fxstruct(get("get_"), builder)]
struct Book {
    title:  String,
    author: String,
    #[fieldx(get("published", copy))]
    year:   u32,
}

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.get_title(), "The Hitchhiker's Guide to the Galaxy");
assert_eq!(book.get_author(), "Douglas Adams");
assert_eq!(book.published(), 1979);

Setters

A field receives its setter implementation using the set helper argument:

use fieldx::fxstruct;

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

let mut book = Book::builder()
    .title("The Hitchhiker's Guide to the Galaxy".to_string())
    .author("Douglas Adams".to_string())
    .year(1979)
    .available(1)
    .location("R12.S2".to_string()) // Row 12, Section 2
    .build()
    .expect("Failed to create book");

book.set_available(2);
book.place_into("R12.S3".to_string());
assert_eq!(book.available(), 2);
assert_eq!(book.location(), "R12.S3");

Mutability

You probably have noticed that both the mutable accessor and setter require the object itself to be mutable. While this is a common requirement in Rust, it is not always possible to fulfill. This is where interior mutability comes into play. FieldX provides support for this pattern via the inner_mut argument, which, combined with the get_mut and set arguments, allows you to mutate a field even when the object itself is not mutable.

use fieldx::fxstruct;

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

let book = Book::builder()
    .title("The Hitchhiker's Guide to the Galaxy".to_string())
    .author("Douglas Adams".to_string())
    .year(1979)
    .available(1)
    .location("R12.S2".to_string()) // Row 12, Section 2
    .build()
    .expect("Failed to create book");

*book.available_mut() = 3;
book.place_into("R12.S4".to_string());
assert_eq!(book.available(), 3);
assert_eq!(*book.location(), "R12.S4");

Note

In the above example, while nothing has changed for the available field accessor, which carries the copy sub-argument, the accessor for the location field now requires dereferencing. This is because both field types are now wrapped in a RefCell container which, when borrowed, returns a Ref type. The copy sub-argument simplifies this since returning a value is straightforward. However, the location field accessor still returns a reference, just a different kind of it.

Note

In sync and async modes of operation use of the inner_mut argument is equivalent to using lock.

Coercion

This section discusses Rust's Into trait, which facilitates type conversion. FieldX supports this trait through the <a name="a001"></a>into argument, enabling automatic conversion of a value into the field type when setting its value:

use fieldx::fxstruct;

#[fxstruct(get, builder)]
struct Book {
    title:  String,
    author: String,
    year:   u32,
    #[fieldx(get(copy), set)]
    available: u16,
    #[fieldx(set("place_into", into))]
    location: String,
}

let mut book = Book::builder()
    .title("The Hitchhiker's Guide to the Galaxy".to_string())
    .author("Douglas Adams".to_string())
    .year(1979)
    .available(1)
    .location("R12.S2".to_string()) // Row 12, Section 2
    .build()
    .expect("Failed to create book");

book.place_into("R12.S3");
assert_eq!(book.location(), "R12.S3");

Compare this to the example from the Setters section:

book.place_into("R12.S3".to_string());

Although, for better readability, a method from another trait – Display – is used, the concept remains the same: the into argument allows you to set a field value using a type that implements the Into trait for the field type. Since &str implements Into<String>, one could replace the to_string() method call with the into() method interchangeably, preserving the semantics of the code.

Finally, why is this section titled "Coercion"? The intent is to highlight the distinction between the explicit use of the Into trait by manually invoking the into() method and the implicit coercion performed within the methods generated by FieldX for you.

Field Or Method

Warning

This section, among technical details, contains some discussion about the ways of accessing field values which expresses the author's personal opinion.

The public view of Rust is strongly opinionated about its purpose, and most see it as a systems programming language, blockchain and cryptocurrency marvel, embedded systems darling, or a WebAssembly wonder. However, somehow application development escapes the attention of the developer community (in the broad sense of the term, not the Rust community alone). I think this is a big mistake.

But before going into the details, let's think about what makes the difference between application development and systems/blockchain/embedded ones. The one directly related to the topic is the fact that the latter ones are about performance/size, while the former is about ease and speed of development. Or, in other words, 80% performance / 20% glue and interfaces vs. 20% performance / 80% glue and interfaces, respectively1.

In the context of performance-critical code, direct access to struct fields is a way to reduce memory footprint and CPU usage.

But in the context of application development, control over access to the fields is more important than performance. This is where accessors and setters come into play. They're your interface to the outside world, whether it be some third-party user of your API or even your own code. Yes, precisely, the encapsulation in its purity!

There are also cases when the usage of accessors is nearly unavoidable, such as when you need to implement lazy initialization of a field. But then again, this is something that is rather found in non-performance-critical paths, with only a few exceptions.

Tip

Not to forget, it has to be mentioned that most of the methods generated by FieldX are marked as inlinable. Moreover, accessors carry the #[inline(always)] attribute. So, if unsure, you could always try to check the final assembly of your code to see if it is worth choosing readability over performance.

However, when there is a need to work with a field directly2, it is necessary to know what container types FieldX uses to wrap the field value. This information can be found in the Inner Workings section.


  1. I know, I know, this is a very rough approximation, and there are many exceptions to the rule. But let's not bikeshed about it!

  2. For instance, to implement your own accessor with additional functionality.

Default

FieldX can provide implicit implementation of the Default trait for a struct in one of the following cases:

  • When the argument <a name="a001"></a>default is provided and active to the fxstruct macro.

    #[fxstruct(default)]
    struct Foo {
        is_set: bool,
    }
  • When the argument default is provided and active for any field's fieldx attribute.

    #[fxstruct]
    struct Foo {
        #[fieldx(get(copy), default(3.1415926535))]
        pi: f32,
    }
    
    let foo = Foo::default();
    assert_eq!(foo.pi(), 3.1415926535);
  • When another argument, like the struct level new argument, needs it.

    #[fxstruct(new)]
    struct Bar {
        #[fieldx(get(copy), default(2.7182818284))]
        e: f32,
    }
    
    let bar = Bar::new();
    assert_eq!(bar.e(), 2.7182818284);

Construction

There are two and a half ways to instantiate a struct in FieldX:

  1. By using its default

    • by using its new method, which is just syntax sugar around the Default trait under the hood. If you don't want the implicit new method then just do:

      #[fxstruct(new(off))]
  2. By using the builder pattern.

Since there is nothing more to be added about the new()/default(), let's focus on the builder pattern.

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.

Optional Values

There is no need to explain what Option is in Rust and what role it plays. FieldX pays a bit of extra attention to it, though, because the concepts of optional values partially propagate into other patterns implemented by the crate. So, let's take this snippet as an example:

#[fxstruct]
struct Foo {
    #[fieldx(optional, get)]
    value: String,
}

It is quite apparent that the value field is optional and its final type is Option<String>. This is what a user gets. But along the lines, FieldX will use this information to generate proper code where its semantics depend on the optionality of the field. One case would be the builder implementation not failing with an error if an optional field is not set.

Predicate

In this chapter, we will discuss functionality that is more closely tied to optional values. Let's get back to the fact that there may be no value in the field. Normally, we'd check this with the is_none() method, but FieldX allows us to give the public API that our implementation provides a little touch of beauty:

#[fxstruct]
struct Foo {
    #[fieldx(optional, predicate, get)]
    value: String,
}

This gives us a has_value() method that returns true if the field is set:

let foo = Foo::new();
assert!(!foo.has_value());

Not satisfied with the name or it doesn't fit your API standards? predicate is a helper argument; so, no problem — give it another name!1

#[fxstruct]
struct Foo {
    #[fieldx(predicate("we_are_good"), get, set)]
    value: String,
}

And, of course:

let mut foo = Foo::new();
foo.set_value("Hello, world!".to_string());
assert!(foo.we_are_good());

This sample demonstrates another little perk of declaring optional fields with FieldX: there is no need to wrap the argument of the setter method into Some(), as it would be necessary with the explicit Option<String> approach.

Clearer

Where a value can be given, it can also be taken away. This is what the <a name="a003"></a>clearer argument is for1:

#[fxstruct]
struct Foo {
    #[fieldx(clearer, get)]
    value: String,
}

Let's combine this with the predicate argument and see how it works:

#[fxstruct]
struct Foo {
    #[fieldx(clearer, predicate, set)]
    value: String,
}

let mut foo = Foo::new();
assert!(!foo.has_value());
foo.set_value("Hello, world!".to_string());
assert!(foo.has_value());
let old_value = foo.clear_value();
assert!(!foo.has_value());
assert_eq!(old_value, Some("Hello, world!".to_string()));

Since clearer is a helper too, it can be renamed as well:

#[fxstruct]
struct Foo {
    #[fieldx(clearer("reset_value"), predicate, get)]
    value: String,
}

AsRef

Since by default the accessor methods return a reference to the field value, it is sometimes (often) gives us a situation where in order to do something with the Option it returns we need to convert it from &Option<T> to Option<&T>. Calling the as_ref() method every time is a bit tedious, so FieldX provides sub-argument as_ref for the get argument that does this for us automatically:

#[fxstruct]
struct Bar {
    #[fieldx(optional, get(as_ref), set)]
    value: String,
}

let mut bar = Bar::new();
bar.set_value("Привіт, світ!".to_string());
assert_eq!(bar.value(), Some(&"Привіт, світ!".to_string()));

Builder Pattern

Here is another reason why the optional keyword makes sense on its own. When generating the builder type, FieldX pays the same attention to the optionality of a field as it does to its default value:

use fieldx::fxstruct;

#[fxstruct(get, builder)]
struct Book {
    title:  String,
    author: String,
    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".to_string())
    .author("Douglas Adams".to_string())
    .year(1979)
    .build()
    .expect("Failed to create book");

assert!(book.signed_by().is_none());

About the same result could be achieved with an explicit Option-typed field by giving it the explicit default(None) argument, but doesn't the above sample look way better in its conciseness and readability?


  1. Note that the optional keyword is omitted in this case because predicate and clear arguments imply that the field is optional. ↩2

Modes Of Operation

There is no concise way to explain how a mode of operation works for every specific case in FieldX. Up to some limited extent, one could say that this entire book is dedicated to exploring this topic. However, here are a few points to help you get started.

Declaration

The arguments that set one of the modes are either named after the modes themselves: plain, sync, or r#async, where the r# is required to allow use of the async keyword as an argument name; or these keywords can be used as sub-arguments of the mode argument: mode(async). In the latter case, the r# prefix before async is not required.

Both options are entirely equivalent; choose the one that is more readable for your use case.

Plain

The plain mode is the default mode of operation for FieldX. It does not provide any guarantees regarding thread safety or concurrency.

Sync

Suppose you have a struct Foo with some fields that are not Sync or Send. If the struct is annotated with the #[fxstruct] attribute, we say it operates in plain mode. At some point, you may realize that you need to use Foo in a concurrent context. Often, it may be enough to add the sync argument to the #[fxstruct] attribute, like this:

#[fxstruct(sync)]
struct Foo {
    // ...
}

Now, Foo operates in sync mode and implements the Sync+Send traits. The specific meaning of adding the sync argument may vary for each individual field of Foo, depending on their declarations. Let's consider one example to illustrate this:

#[fxstruct(get)]
struct Foo {
    #[fieldx(inner_mut, set)]
    value: String,
}

Here, the value field is a plain one. The inner_mut attribute enables the implementation of the inner mutability pattern for the field. Technically, this means the field type is now wrapped in RefCell, which is Send but not Sync. So, let's add the argument:

#[fxstruct(sync, get)]
struct Foo {
    #[fieldx(inner_mut, set)]
    value: String,
}

Now FieldX will use RwLock instead of RefCell1. In an ideal world, not only would we not need to change any of the foo.set(new_value) calls, but as long as all our accessor calls are dereferenced, their usage will remain the same. Moreover, if for some reason we always need a clone of the value field, then with the following declaration, all uses of the value accessor will remain the same without any caveats:

#[fxstruct(sync)]
struct Foo {
    #[fieldx(inner_mut, get(clone), set)]
    value: String,
}

Async

The async mode is covered in a later chapter.

Struct or Field?

Since FieldX is primarily a field-oriented crate, it allows manipulation of operation modes for individual fields. For example, with the Foo struct from the examples above, you could do the following:

#[fxstruct]
struct Foo {
    #[fieldx(inner_mut, sync, set)]
    value: String,
    // Or `r#async` because keywords can't be used as first-level argument names.
    #[fieldx(lock, mode(async))]
    async_value: String,
}

  1. The inner_mut and lock arguments are aliases for the same functionality in sync and async modes. The lock argument exists for readability and convenience, as marking a field as locked automatically implies that it is sync.

Locks

Support for sync/async modes of operation is incomplete without providing a way to lock-protect data. FieldX can do this for you at the field level by using the lock argument.

Note

Using inner_mut with explicit sync or mode(sync) has the same effect as using the lock argument.

Note

The async mode still requires the async argument to be used.

For greater flexibility, FieldX utilizes read-write locks provided by the parking_lot crate for the sync mode, and either tokio or async-lock for the async mode, depending on which feature flag is enabled. This design decision aligns well with the immutable and mutable accessor patterns.

Making a field lockable affects the return values of its accessors. Without focusing on specific sync or async modes, the immutable accessor of such a field returns an RwLockReadGuard, while the mutable accessor returns an RwLockWriteGuard:

    #[rustfmt::skip]
#[fxstruct(get, builder(into))]
struct Book {
    title:     String,
    author:    String,
    #[fieldx(get(copy), builder(into(off)))]
    year:      u32,
    #[fieldx(optional)]
    signed_by: String,
    location:  String,
    #[fieldx(reader, writer, get(copy), get_mut, builder(into(off)))]
    available: u32,
    // Map borrower IDs to the time they borrowed the book.
    #[fieldx(lock, get_mut, builder(off))]
    borrowers: HashMap<String, Instant>,
}

let book = Book::builder()
    .title("The Catcher in the Rye")
    .author("J.D. Salinger")
    .year(1951)
    .signed_by("S.K.")
    .location("R42.S1".to_string()) // Row 12, Section 2
    .available(50)
    .build()
    .expect("Failed to create Book object");

let borrowers = 30;
let barrier = std::sync::Barrier::new(borrowers + 1);

thread::scope(|s| {
    for i in 0..borrowers {
        let book_ref = &book;
        let idx = i;
        let barrier = &barrier;
        s.spawn(move || {
            // Try to ensure real concurrency by making all threads start at the same time.
            barrier.wait();
            *book_ref.write_available() -= 1;
            book_ref.borrowers_mut().insert(format!("user{idx}"), Instant::now());
        });
    }

    barrier.wait();
});

assert_eq!(book.available(), 20);
assert_eq!(*book.read_available(), 20);
assert_eq!(book.borrowers().len(), borrowers as usize);

The use of the struct and its locked fields is straightforward in the example above. What is really worth mentioning are the following two points:

  1. Implicit sync mode when the lock argument is used. This means there is no need to specify sync or mode(sync) per field if it is already locked.
  2. The lock argument itself can be implicit when reader or writer arguments are used. Apparently, the sync mode is also implied in this case.

Readers And Writers

The reader and writer arguments are additional helpers that introduce methods always returning an RwLockReadGuard and RwLockWriteGuard, respectively. The methods are named after the field they are applied to, with the read_ and write_ prefixes added.

At this point, an immediate question arises: what makes them different from the immutable and mutable accessors? For the reader, the answer is the word "always" in the previous paragraph. Consider the copy subargument of the available field's get – with it, the locking happens inside the accessor, and we only get a u32 value.

For the writer, the situation is different and slightly more intricate. It will be discussed in the Lock'n'Lazy chapter.

Lazy Field Initialization

This section introduces lazy field initialization for those unfamiliar with the concept. If you're already familiar, feel free to skip ahead.

Consider a struct that interacts with a network resource. Suppose there is a block of data you need to retrieve from the resource and use throughout the struct's implementation or elsewhere. Retrieving this data is relatively expensive, and you might not need it immediately—or at all—depending on the context. A beginner might instinctively retrieve the data in the struct's constructor and store it in a field. However, this approach has a significant drawback: the object's construction time becomes dependent on the time it takes to retrieve the data, which might never be used.

The lazy initialization pattern addresses this issue by deferring the data retrieval until it is actually needed. Here's an example in pseudo-code:

struct NetworkResource {
    data: Option<DataType>,
}

impl NetworkResource {
    /// Accessor method to get the data.
    fn data(&mut self) -> &DataType {
        if self.data.is_none() {
            self.data = Some(self.pull_data());
        }
        self.data.as_ref().unwrap()
    }

    fn pull_data(&self) -> DataType {
        // Pull the data from the network resource
    }
}

In this implementation, the data() accessor retrieves the value only when the method gets called. If it is never called, the data is never pulled.

Now, consider a more complex scenario where the exact locator for the data is stored elsewhere on the resource. In this case, we need something like the following:

struct NetworkResource {
    data: Option<DataType>,
    location_directory: Option<LocationDirectory>,
}

impl NetworkResource {
    /// Accessor method to get the location directory.
    fn location_directory(&mut self) -> &LocationDirectory {
        if self.location_directory.is_none() {
            self.location_directory = Some(self.pull_location_directory());
        }
        self.location_directory.as_ref().unwrap()
    }

    /// Accessor method to get the data.
    fn data(&mut self, location: &str) -> &DataType {
        if self.data.is_none() {
            self.data = Some(self.pull_data(location));
        }
        self.data.as_ref().unwrap()
    }

    fn pull_data(&self, location: &str) -> DataType {
        // Pull the data from the network resource using the location
        let locator = self.location_directory().get_data_location();
        // Use the locator to pull the data
    }

    fn pull_location_directory(&self) -> LocationDirectory {
        // Pull the location directory from the network resource
    }
}

In this example, the data() method implicitly depends on the location_directory() method, but this dependency is hidden from the API. The user code remains agnostic about it.

Now, imagine a scenario where the dependency chain becomes more complex. The data retrieval might depend on user decisions, the current state of the resource, or other factors. Ultimately, the data you retrieve could depend on a combination of these factors, and the path to access it might be intricate. The implicit dependency hides these complexities from the user code, providing a simple API to access the desired value.

This concept is analogous to what a dependency manager does in dependency injection frameworks.

For a deeper dive into the lazy initialization pattern, you can refer to the Lazy Initialization article on Wikipedia. While the article provides a solid overview, it does not delve into advanced topics such as handling concurrent access patterns, which are crucial in multi-threaded environments.

Laziness Protocol

The implementation of lazy field initialization in FieldX is based on a few conventions between the crate and the user. These conventions are referred to as the "laziness protocol". The conventions are as follows:

  1. For any uninitialized field, the initialization takes place exactly once, when the field is first read from, regardless of the field's mode of operation.
  2. The value for the initialization is provided by a lazy builder method, which is supplied by the user.
  3. FieldX guarantees that in sync or async modes of operation, rule #1 also implies that the builder method is called exactly once.
  4. The builder method is expected not to mutate its object unless it is unavoidable and done with the utmost care.
  5. The builder method is expected to be infallible unless otherwise specified by the user.

There is no need to duplicate the example from the Introduction here since it already demonstrates the laziness protocol in action. Let's discuss a few other aspects here.

Fallible Builders

The ideal world has no errors that we must deal with. However, our world is far from ideal, hence sometimes a builder method may fail, and there must be a way to propagate this failure. For example, in the Lazy Field Initialization section, we use a hypothetical case where data is pulled from a network resource. The pseudo-code we used doesn't account for the possibility of a network failure or any other error that might occur during the data retrieval process. Let's address this problem now.

#[fxstruct(lazy, fallible(off, error(AppError)))]
struct NetworkResource {
    #[fieldx(fallible)]
    data: DataType,
    #[fieldx(fallible)]
    location_directory: LocationDirectory,
}

impl NetworkResource {
    fn build_location_directory(&self) -> Result<LocationDirectory, AppError> {
        Ok(self.network_request()?.get_location_directory()?)
    }

    fn build_data(&self) -> Result<DataType, AppError> {
        let location = self.location_directory()?.get_data_location();
        Ok(self.network_request()?.get_data(location)?)
    }
}

Info

In this code, we assume that the network_request method, as well as the get_* methods we call on its results, return an error type for which AppError at least implements the From trait.

Here is what's happening in this code:

  1. First of all, all fields of the struct are marked as lazily initialized.
  2. Then we specify that the default error type for all fallible fields is AppError. This is a little trick where we use the fact that field-level properties get their defaults from the struct level. The declaration fallible(off, error(AppError)) means that we turn off the fallible mode for all fields by default but set the default error type to AppError.
  3. Then we mark fields that we want to be fallible. Without the struct-level default, we'd have to write fallible(error(AppError)) for each field.
  4. Finally, we implement the builder methods for the fields. The methods return a Result type with AppError as the error type.

It really takes more time and words to explain all this than to actually write the code...

OK, down to the usage. In our application code, we can now do the following:

let resource = NetworkResource::new();
match resource.data() {
    Ok(data) => {
        // Use the data
    }
    Err(err) => {
        // Handle the error
    }
}

Considering the network resource is likely to be stored in a field of some other struct, which would need the data in one of its methods, it may take the following form:

fn do_something(&self) -> Result<(), AppError> {
    let resource = self.network_resource()?;
    let data = resource.data()?;
    // Do something with the data
    Ok(())
}

The pivotal change in the API of the NetworkResource implementation is that the data() accessor now returns a Result<DataType, AppError>. Otherwise, the usage remains the same as before; i.e., we still can use copy or clone sub-arguments, and so on.

Async

The async mode of operation is identical to the sync mode in many respects. They even share the same code generator behind the scenes. Yet, there are details that warrant additional attention.

The primary use of async is with lazily initialized fields and locks. Let's have a look at why this is the case.

Locks

An early FieldX design didn't provide async support at all. While for lazies it was a nuisance, for locks it was considered acceptable only until the author was hard bitten by a lock that actually blocked a tokio task thread, effectively freezing all tasks running on the thread—whereas one of the tasks was supposed to actually release the lock. You got it!

That's all there is to it. With the use of async locks, the above situation wouldn't happen (in that particular case); or, at least, deadlocks would have a less severe impact on the system.

Lazies

Lazy field initialization is another driving force behind the introduction of the async mode. Consider initializing a network resource in an async context, for example. Either this brings us back to the boilerplate of manually implementing laziness, or to the boilerplate of creating a new runtime only to use the async code in a builder method! A choice without a choice...

Let's have a look at what we can do rather than reflecting on what we wouldn't:

#[fxstruct(get)]
struct RegistryRecord {
    location: String,
    #[fieldx(mode(async), lock, get(copy), get_mut)]
    available: u32,
    #[fieldx(optional, get(as_ref))]
    signed_by: String,
}

#[fxstruct(get, builder(into))]
struct Book {
    title:     String,
    author:    String,
    #[fieldx(get(copy), builder(into(off)))]
    year:      u32,
    #[fieldx(get(copy), builder(into(off)))]
    bar_code:  u32,
    #[fieldx(r#async, lazy)]
    registry_record: RegistryRecord,
}

impl Book {
    async fn build_registry_record(&self) -> RegistryRecord {
        self.request_registry_record(self.bar_code).await
    }
}

let book = Book::builder()
    .title("The Catcher in the Rye")
    .author("J.D. Salinger")
    .year(1951)
    .bar_code(123456)
    .build()
    .expect("Failed to create Book object");

let registry_record = book.registry_record().await;
*registry_record.available_mut().await -= 1;

As usual, nothing too complicated here. The accessor methods become async, which is expected because they are the ones that call the builder method.

Backend Choice

FieldX provides two options for the async backend: tokio and async-lock. These backends are utilized for their implementations of RwLock and OnceCell. The selection is controlled via the async-tokio or async-lock feature flags, with async-tokio being the default.

Reference Counted Structs

Another typical Rust pattern is to use Rc (or Arc) to allow multiple owners of a struct. There are various possible reasons why one might want to do this, but let us not delve into them here. The point is that sometimes we don't just need an object to be reference counted, but we need or want it to be constructed this way.

Example which we promised not to go into..

OK, let's digress into an example once. Say you need to implement a parent-child relationship between two or more structs. In this case, if there is no pressure from the performance side, the parent object can be reference counted to simplify the task of keeping a reference to it for its children.

As always, the way to achieve this pattern is as simple as adding the rc argument to the struct declaration:

#[fxstruct(rc)]

Sync/async and plain modes of operation are supported, resulting in either Arc or Rc being used as the container type respectively.

The rc argument comes with a perk but, at the same time, with a cost.

The cost is an additional implicit field being added to the struct, which holds a plain or sync Weak reference to the object itself.

The perk is two new methods in the implementation: <a name="a002"></a>myself and <a name="a003"></a>myself_downgrade. The first one returns a strong reference to the object itself, while the second one returns a weak reference. The latter is useful when you need to pass a reference to the object to some other code that may outlive the object itself, and you want to avoid keeping it alive longer than necessary.

use fieldx::fxstruct;

#[fxstruct(sync, rc, builder)]
struct LibraryInventory {
    /// Map ISBN to Book
    #[fieldx(inner_mut, get, get_mut)]
    books: HashMap<String, Book>,
}

impl LibraryInventory {
    fn make_an_inventory(&self) {
        // The inventory must be available at all times for being able to serve readers. The `books` field is
        // lock-protected. So, we take care as of not locking it for too long by utilizing this naive approach.
        let isbns = self.books().keys().cloned().collect::<Vec<_>>();

        for isbn in isbns {
            if let Some(book) = self.books_mut().get_mut(&isbn) {
                // Check whatever needs to be checked about the book
                self.check_book(book);
            }
        }
    }

    fn open_the_day(&self) {
        let myself: Arc<LibraryInventory> = self.myself().unwrap();

        let inv_task = std::thread::spawn(move || {
            myself.make_an_inventory();
        });

        self.respond_to_readers();

        inv_task.join().expect("Inventory task failed");
    }
}

Warning

Feature flag sync is required for this example to compile.

Here we use reference counting to perform a self-check in a parallel thread while continuing to serve reader requests.

Note

Since the self-reference is weak, the myself method must upgrade it first to provide us with a strong reference. Since upgrading gives us an Option, it must be unwrapped. This is safe to do in the example code because the object is alive, hence its counter is at least 1. However, within the drop() method of the Drop trait, the myself method will return None since the only possible case when the Drop trait is activated is when the reference count reaches zero.

Because the rc argument results in methods being added to the implementation, it is a helper method. Its literal string argument allows you to change the name of the myself method:

#[fxstruct(rc("my_own"))]

Visibility

It is possible to control the visibility of FieldX-generated entities. This can be done by using the vis(...) argument (or sub-argument) with corresponding visibility levels using the pub declaration. Private visibility is achieved by using no sub-argument with vis: vis(); there is also an alias private for it.

By default, the visibility level is inherited from the field or the struct declaration. More precisely, there is a priority order of visibility levels that FieldX follows:

  1. The immediate declaration for an entity: get(vis(pub)).
  2. Field-level declaration: #[fieldx(vis(pub), get)].
  3. Struct-level default for the entity: #[fxstruct(get(vis(pub)))].
  4. Struct-level default: #[fxstruct(vis(pub))].
  5. Field explicit declaration: pub foo: usize.
  6. Struct declaration: pub struct Foo { ... }.

Warning

Don't ignore the "explicit" word at the field level! It means that if the field is private, FieldX skips the step and goes directly to the struct declaration. The reason for this is that the struct is considered part of some kind of API of a module, and as such, it is better for the methods it provides to be exposed at the same level because they're part of the same API.

Think of it in terms of a field accessor where the field itself is recommended to be private, while the accessor is recommended to be accessible anywhere the struct is accessible.

use fieldx::fxstruct;

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

The levels in the snippet are somewhat arbitrary for the sake of demonstrating the feature.

Generated Entity Attributes

At a times there might be a need to annotate the entities, generated by FieldX, with additional attributes. Say, to silence down check warnings (#[allow(dead_code)]), derive additional traits, or even use some home brew procedural macros. To do so FieldX provides three arguments:

  • attributes - for generated structs
  • attributes_fn - for generated methods
  • attributes_impl - for generated implementations

Say, I've got a crazy idea of having my builder type clonable. Voila!

use fieldx::fxstruct;

#[fxstruct(
    get,
    builder(
        attributes(
            derive(Clone, PartialEq, Eq, Debug)
        ),
    )
)]
struct Book {
    #[fieldx(into)]
    title:  String,
    #[fieldx(into)]
    author: String,
    year:   u32,
    #[fieldx(optional)]
    signed_by: String,
    #[fieldx(
        set("place_into"),
        inner_mut,
        default("unknown".to_string())
    )]
    location: String,
}

let book_builder = Book::builder()
    .title("The Hitchhiker's Guide to the Galaxy")
    .author("Douglas Adams")
    .year(1979);

let book_builder_clone = book_builder.clone();

assert_eq!(book_builder_clone, book_builder);

Not all arguments that support this feature support all three sub-arguments though. For example, the helper arguments only know about the attributes_fn since they work with methods. It is better to consule with the technical documentation of the fxstruct macro.

Documentation

Documenting the generated entities could be challenging, if at all possible, unless specifically supported. FieldX provides such support by allowing you to use the doc argument. But before we get there, let's have a quick overlook of easier cases where the argument is not needed.

The most obvious case would be the struct itself. Cleared!

Then there are the fields. Their attached documentation is then used for the accessors and builder setter methods.

Then what are we left with? The builder struct; the method build() of the builder implementation; the builder() method of the main struct; the setters and the mutable accessors; ... and so on! Quite a few, huh?

This is exactly where the doc argument comes to bind them all... It has a very simple syntax:

doc(
    "This is line 1",
    "then there is line 2",
    "",
    "and we keep counting!"
)

Every string in the list is an individual line of documentation, ending with an implicit newline character. I.e., the above doc contains four lines of documentation; these make two paragraphs in the generated output. Since doc accepts any syntactically valid Rust literal string, the user is free to choose what they want to see between the brackets.

Let's see how it works in practice:

/// The struct.
#[fxstruct(get, builder(doc("This is our builder for the Book struct.")))]
pub struct Book {
    title:     String,
    author:    String,
    year:      u32,
    #[fieldx(optional)]
    signed_by: String,
    #[fieldx(
        set(
            doc(
                "Set the physical location of the book.",
                "",
                "I have no idea what else to add to the above.\n\nBut I just need another line here!",
            )
        ),
        builder(doc("Initial book location.")),
        inner_mut,
        default("unknown".to_string())
    )]
    location:  String,
}

Check out the result (the methods build() and builder() are using the default documentation):

Documentation example

Documentation example

Serialization

Warning

FieldX uses the serde crate to support serialization. Understanding this chapter may sometimes require some knowledge of the crate.

Serialization in FieldX is enabled with the serde feature flag and the serde argument. However, these are not the only prerequisites, and to understand why, some explanations are needed.

The Complications and the Solutions

At this point, it's time to rewind to the Basics chapter and recall that under the hood, FieldX transforms the struct by wrapping its fields into container types if necessary. Direct serialization of some of these containers is not possible or may result in undesired outcomes. Generally speaking, using FieldX could have made serialization impossible unless the serde crate lends us a helpful hand!

The solution is to use the from and into attributes of serde, implement a copy of the user struct with containers stripped away, and use it for the actual serialization. FieldX calls this a shadow struct.

To support this functionality, the user struct must implement the Clone trait, which is a prerequisite for applying the serde argument. Since cloning a struct can be a non-trivial task, it is left to the user. In most cases, deriving it should suffice.

How To in a Nutshell

As is common with FieldX, the first step in serialization is as simple as adding the serde argument to the struct declaration:

    use fieldx::fxstruct;
    use serde::Deserialize;
    use serde::Serialize;

    #[derive(Clone)]
    #[fxstruct(get, builder, serde)]
    pub struct Book {
        title:     String,
        author:    String,
        #[fieldx(get(copy))]
        year:      u32,
        #[fieldx(optional, get(as_ref))]
        signed_by: String,
        #[fieldx(set, inner_mut, default("unknown".to_string()))]
        location:  String,
    }

And we're ready to use it:

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

        serde_json::to_string_pretty(&book).expect("Failed to serialize book")

Output

{
  "title": "The Hitchhiker's Guide to the Galaxy",
  "author": "Douglas Adams",
  "year": 1979,
  "signed_by": "Douglas Adams",
  "location": "Shelf 42"
}

And the other way around too:

let serialized = r#"{
  "title": "The Hitchhiker's Guide to the Galaxy",
  "author": "Douglas Adams",
  "year": 1979,
  "signed_by": "Douglas Adams",
  "location": "Shelf 42"
}"#;

let deserialized: super::Book = serde_json::from_str(&serialized).expect("Failed to deserialize book");
assert_eq!(*deserialized.title(), "The Hitchhiker's Guide to the Galaxy");
assert_eq!(*deserialized.author(), "Douglas Adams");
assert_eq!(deserialized.year(), 1979);
assert_eq!(deserialized.signed_by(), Some(&String::from("Douglas Adams")));
assert_eq!(*deserialized.location(), "Shelf 42");

One Way Only

A struct does not always need to be both serialized and deserialized. Which serde trait will be implemented for the struct is determined by the two sub-arguments of the serde argument, whose names are (surprise!) serialize and deserialize. For example, if we only want to serialize the struct, we simply write:

#[fxstruct(serde(serialize))]

Or

#[fxstruct(serde(deserialize(off)))]

Tip

The general rule FieldX follows here is that if the serde argument is applied and not disabled with the off flag, then the user intends for one of the two traits to be implemented, whether the desired one is enabled or the undesired one is disabled.

Default Value

It is possible to specify a default for deserialization alone, separate from the field's default value. In other words, it is possible to distinguish the default value a field receives during construction from the one it receives during deserialization if the field is omitted in the serialized data. For example:

#[derive(Clone)]
#[fxstruct(get, serde)]
pub struct Foo {
    #[fieldx(
        default("constructor".to_string()),
        serde(
            default("deserialization".to_string())
        )
    )]
    source: String,
}

let foo = Foo::new();
assert_eq!(*foo.source(), "constructor");

let deserialized = serde_json::from_str::<Foo>("{}").expect("Failed to deserialize Foo");
assert_eq!(*deserialized.source(), "deserialization");

More On Locks

Warning

Potential deadlock information is provided in this chapter.

The Basics chapter on locks explains that read-write lock types from various crates are used, depending on the mode of operation and the enabled feature flags. However, this is only part of the story. A challenge arises during serialization, where serde requires the struct to implement the Clone trait. While this is straightforward for plain-mode structs, as deriving the trait works seamlessly, sync/async mode structs may require manual implementation of cloning because none of the RwLock implementations support the Clone trait.

FieldX implements a solution for this problem by implementing a zero-cost wrapper around the RwLock types, FXRwLock<T>, which implements support for cloning. Normally its use is activated together with the serde feature flag, but can also be requested explicitly by the user with the clonable-lock feature flag.1

But the support comes with a caveat: the FXRwLock<T> wrapper has to acquire a read lock on the underlying object to clone it. While it is generally OK in the presence of other read-only locks, it poses a risk of deadlock under certain circumstances like obtaining a long-living write lock and then trying to clone the object deeper in the call stack.

The undesired "surprise" could be aggravated by the use of serialization if one forgets that it utilizes the cloning under the hood. This is where explicit deriving of the Clone trait may come to help as a visual reminder about this behavior.

Overall, the practice of modifying data that is being serialized is considered bad, therefore the above-described scenario is more of an "apocalyptic" case. For now, the convenience of automatically enabling clonable-lock with the serde feature flag is considered an acceptable trade-off that outweighs the risk.

Either way, the current implementation is experimental and the implicit feature flag dependency could be removed in a future minor release.

Future Plans

For the moment, FieldX uses FXRwLock<T> for all locks when the clonable-lock feature flag is enabled. However, it is possible that a future version will introduce better granularity by only defaulting to the wrapper for serializable structs and otherwise requiring something like a clone sub-argument of the lock argument.


  1. The serde feature flag actually simply depends on the clonable-lock.

Lock'n'Lazy

The general rule FieldX tries to follow is the KISS principle. It avoids introducing unnecessary entities without a good reason to do otherwise. In the context of this chapter, let's mention that the lazy field initialization is based upon various flavors of the OnceCell type, while locks are implemented using the RwLock<T> types (unless the clonable-lock feature flag, discussed in the previous chapter). But the combination of the two for a field leads to a couple of problems that do not have a straightforward solution.

Say, do we wrap the OnceCell in a lock or the other way around? Sync/async versions of OnceCell itself are lock-protected internally anyway, how do we avoid double locking?

Instead of trying to fuse together two things that are not meant to be fused, FieldX introduces its own solution: FXProxy types1 that implement both laziness and locking "all-in-one." And some more.

In most of the cases a user wouldn't need to worry about what container type a field is using as long as they're using method-based access to it. However, it worth paying attention to the type documentation if you plan to use the field directly – not only to understand possible nuances of the type implementation, but also find out about some additional functionality it provides.

At this point we need to focus on an aspect that is not immediately obvious. Let's recall about the Readers And Writers section in the Locks chapter. The time has come to explain the mystery around the writer argument and what makes it different from the mutable accessor.

As nearly always, a mistification revealed becomes ridiculously simple: no matter, mutable or not, an accessor remains an accessor and as such its job is to first initialize a lazy field first if it is not initialized yet. Therefore, if you only want to put a value into the field you'd anyway pay the cost of calling its builder method – which might be expensive at times!

Contrary, the writer method gives you a direct access into the container without the need to initialize it first – but without the ability to read a value from it! This functionality is backed by the FXProxy types1 which write method return FXWriter guards that have only two methods: clear and store:

    #[rustfmt::skip]
#[fxstruct(get, builder(into))]
struct Book {
    title:     String,
    author:    String,
    #[fieldx(get(copy), builder(into(off)))]
    year:      u32,
    #[fieldx(lazy, writer, get, predicate)]
    location:  String,
}

    impl Book {
        fn build_location(&self) -> String {
            String::from("unknown")
        }
    }

let book = Book::builder()
    .title("The Catcher in the Rye")
    .author("J.D. Salinger")
    .year(1951)
    .build()
    .expect("Failed to create Book object");

// Neither set nor laziliy initialized.
assert!(!book.has_location());
book.write_location().store("R42.S1".to_string());
assert_eq!(*book.location(), "R42.S1");

  1. The plural form is used here to indicate that there are multiple FXProxy types, each of which is specialized for a specific mode of operation. ↩2

Inner Workings

As it was mentioned in the Basics, fieldx rewrites structures with fxstruct applied. The following table reveals the final types of fields. T in the table represents the original field type, as specified by the user; O is the original struct type.

Info

The way a particular container type is chosen depends on the combination of the enabled feature flags. With regard to the async mode operation refer to the Async Mode of Operation chapter, Backend Choice section for more details. With regard to the choice between FXRwLock and RwLock see the More on Locks chapter.

Apparently, skipped fields retain their original type. Sure enough, if such a field is of non-Send or non-Sync type the entire struct would be missing these traits despite all the efforts from the fxstruct macro.

There is also a difference in how the initialization of lazy fields is implemented. For non-locked (simple) fields the lazy builder method is called directly from the accessor method. For locked fields, however, the lazy builder is invoked by the implementation of the proxy type.

Feature Flags

The following feature flags are supported by this crate:

FeatureDescription
syncSupport for sync-safe mode of operation
asyncSupport for async mode of operation
tokio-backendSelects the Tokio backend for async mode. A no-op without the async feature.
async-lock-backendSelects the async-lock backend for async mode. A no-op without the async feature.
async-tokioCombines async and tokio-backend features.
async-lockCombines async and async-lock-backend features.
clonable-lockEnables the clonable lock wrapper type.
send_guardSee corresponding feature of the parking_lot crate
serdeEnable support for serde marshalling.
diagnosticsEnable additional diagnostics for compile time errors. Experimental, requires Rust nightly toolset.

Warning

The tokio-backend and async-lock-backend features are mutually exclusive. You can only use one of them at a time or FieldX will produce a compile-time error.

Index

accessor,
      Accessors,
      Documentation
      as_ref,
            Optional Values
      immutable accessor,
            Accessors
      mutable accessor,
            Accessors,
            Documentation
argument,
      Terminology
      field-level,
            Terminology
      struct-level,
            Terminology
argument disabler,
      Terminology
builder,
      Terminology
builder method,
      Terminology
builder pattern,
      Terminology,
      Builder Pattern,
      Construction,
      Optional Values
      builder type,
            Terminology,
            Builder Pattern,
            Optional Values
      opt-in builder,
            Builder Pattern
      setter method,
            Builder Pattern,
            Documentation
coercion, see into
conversion, see into
default,
      Default
dependency injection,
      Lazy Field Initialization
dependency manager,
      Lazy Field Initialization
fallible,
      Terminology
feature flag,
      Locks,
      Serialization,
      Feature Flags
      async-lock,
            Async Mode Of Operation,
            Feature Flags
      async-tokio,
            Async Mode Of Operation,
            Feature Flags
FXProxy,
      Lock'n'Lazy
FXWriter,
      Lock'n'Lazy
helper, see helper argument
helper argument,
      Terminology
infallible,
      Terminology
inlinable,
      Field Or Method
inner_mut,
      Inner Workings
interior mutability,
      Mutability
into,
      Coercion
laziness, see lazy field initialization
lazy field initialization,
      Lazy Field Initialization,
      Lock'n'Lazy
      fallible builder,
            Laziness Protocol
      laziness protocol,
            Laziness Protocol
      lazy,
            Inner Workings
locks,
      Locks,
      Lock'n'Lazy
      clonable-lock,
            More On Locks,
            Feature Flags
      lock,
            Locks,
            Inner Workings
      reader,
            Locks,
            Inner Workings
      writer,
            Locks,
            Inner Workings
modes of operation,
      Terminology
      async,
            Terminology,
            Async Mode Of Operation
      mode,
            Modes Of Operation
      plain,
            Terminology
      sync,
            Terminology
      unsync, see plain
myself,
      Reference Counted Structs
myself_downgrade,
      Reference Counted Structs
new,
      Construction
off, see argument disabler
optional values,
      Optional Values
      clearer,
            Optional Values
      optional,
            Inner Workings
      predicate,
            Optional Values
reference counted,
      Reference Counted Structs
serialization,
      Serialization
      serde argument,
            Serialization
      shadow struct,
            Serialization
setter,
      Setters,
      Documentation
sub-argument,
      Terminology
visibility,
      Visibility
      visibility level,
            Visibility

References

Release Notes

Chapters in this section provide summaries of the corresponding releases. More detailed technical information can be found in the CHANGELOG.

v0.2.0

This release marks the first stable version of fieldx, summarizing over a year of development based on experience gained from using FieldX in various work and personal projects. The stabilization of features ensures that:

  • All patch versions v0.2.x will maintain backwards compatibility.
  • The crate is ready for widespread public use.
  • The release can now be bundled with this book, which aims to provide a comprehensive guide on using fieldx in your projects.

With this release two utility crates are also considered ready for public use, though not yet fully documented:

These modules are designed to help extending FieldX with 3rd-party crates by exposing crate's internal structures and APIs. Basically, FieldX itself is built upon these two.

Changelog

v0.2.1 - 2025-06-28

Documentation

  • Fix errors in the documentation
  • Add the Further Reading section to the main page/README
  • Give each workspace member its own README

Maintenance

  • Add homepage to the cargo manifests
  • Add dry run info to the publish question to user
  • Try to only use specific publish feature set for fieldx package

v0.2.0 - 2025-06-27

Features

  • Allow optional alternative async-lock instead of tokio

    It’s been long planned to relax the crate’s dependency on tokio. You can now use the async-lock crate to implement RwLock and OnceCell. This change also updates the feature flags: the async feature must be paired with either tokio-backend or async-lock-backend. Alternatively, enable async support and select an implementation in one step with the umbrella flags async-tokio or async-lock.

    tokio remains in the requirements for tests.

  • Add support for clonable-lock feature flag

    With this flag, FXRwLock<T> types from either sync or async modules will be used instead of RwLock<T> from parking_lot, tokio, or async-lock crates.

Bug Fixes

  • Don't use unsound mapped rwlock guards

    Replace them with FXProxyReadGuard and FXProxyWriteGuard wrappers.

  • Builder's prefix must be struct level only sub-argument

  • prefix was disrespected for field builder method name

  • Build Docker image for nightly Rust

  • Sync mode is not set for a field when lock is used

Refactor

  • ️‼️ breaking Get rid of Sync/Async suffixes in type names where possible

    Use namespaces to address the types. I.e. sync::FXProxy,

    • ⚠️ The last breaking change before the 0.2.0 release.

Documentation

  • Release the 'FieldX Object Manager' book

Testing

  • Add some test descriptions

  • Update compilation tests for changes in feature flags and MSRV

  • Implement parallelized testing with Docker

    cargo make targets test-versions and update-versions are rather heavy both runtime- and space-wise. Now each version of Rust toolchain we test for will be tests in its own Docker container and all versions will be run in parallel.

  • Add examples to the testing

  • Better support for containerization of update-versions target

Maintenance

  • Bump MSRV to 1.78
  • Another attempt to fix error reporting in the Makefile.toml
  • Skip compilation tests for nightly at the earliest possible stage

v0.2.0-beta.1 - 2025-06-16

Documentation

  • Reflect some of the previous versions changes

Testing

  • Propagate reace condition fix from sync.rs test to sync_generics.rs

v0.1.19 - 2025-06-06

Bug Fixes

  • Quick fix for an overlooked case with fallible

    The fallible argument makes sense when used with lazy, which can now be used with both lock and inner_mut. However, the combination of fallible with these two was unconditionally prohibited.

  • Take care of new warnings from the nightly compiler

  • One more location where the new lifetime warning springs up

v0.1.18 - 2025-06-06

Features

  • ️‼️ breaking Getter is not give by default only with the lazy attribute

    • ⚠️ The reasons why getter was given with clearer, predicate, and inner_mut are now less clear than they used to be. There is a good reason to get it with lazy: unless it combined with lock, the only way get lazy initialization is to use the getter.
  • Implement simplified lazy implementation for non-lock fields

    It is not always necessary to lock-protect lazy fields; this is especially true for read-only ones. Therefore, unless the lazy attribute is accompanied by the lock or inner_mut attributes (which are just aliases in the sync and async modes), lazy evaluation will be implemented using OnceCell from either once_cell or tokio for the sync and async contexts, respectively.

Bug Fixes

  • get(as_ref) wasn't respected for sync fields
  • Add now required get attribute

Refactor

  • Move plain-related decls into fieldx::plain module

Documentation

  • Fix an autocompletion error
  • Add notes on lazy enabling the accessor methods

v0.1.17 - 2025-06-01

Features

  • Make vast parts of fieldx_derive reusable via fieldx_core crate

  • Add support for associated types to FXImplConstructor

  • Make doc_props macro public

  • Implement ToTokens trait for some key types

  • Allow keyword args to also accept a foo() syntax

    Previously, this syntax triggered 'expected ...' errors. While it worked for manually written code, it failed for token streams produced by the same types when wrapped in FXNestingAttr, because their keyword forms generated the aforementioned function-call-like syntax.

  • Implement FXSetState trait for FXProp

    This allowed cleaning up some trait implementations. So, where previously a_prop.into() was used, it is now a_prop.is_set().

  • Implement to_arg_tokens method for FXFieldReceiver

Bug Fixes

  • Clarify the use of reference counted self

    Don't perform extra checks on the validity of the weak self-reference unless necessary, and call post_build on the object itself rather than on its reference-counting container.

  • Incorrect code generation for async locks

Refactor

  • Make the codegen context generic over implementation details

    Now what is related to the particular code generation is available via the impl_ctx field and corresponding method.

v0.1.16 - 2025-04-30

Features

  • Improve functionality of FXSynTuple

    Allow it to take a wider range of Parse-implementing types.

Bug Fixes

  • Add missing FXSetState impl for FXSynTuple and FXPunctuated
  • Silence clippy warnings about introduced type complexity
  • Silence some more clippy lints

Refactor

  • Remove redundant delegations
  • Stop returning a reference by field receiver span method

Testing

  • Complete unfinished tests

v0.1.15 - 2025-04-23

Features

  • Add support for explicit builder Default trait implementation

    The builder type implements the Default trait by default. If this behavior is undesired, use builder(default(off)) instead.

Bug Fixes

  • Unrequested Default impl when serde feature is enabled

Maintenance

  • Make clippy happy with its defaults

v0.1.14 - 2025-04-19

Bug Fixes

  • Fix a regression that broke compatibility with fieldx_plus crate

v0.1.13 - 2025-04-18

Features

  • Allow accessor mode flags to take the off subargument

    This allows to override struct-level defaults for copy, clone, and as_ref flags.

  • Don't require lock field types to implement Default

  • Introduce new helper and deprecate no_new

    This also uncouples new from default, allowing for use of the Drop trait with fieldx structs.

Bug Fixes

  • An old bug in the sync test

  • myself method in post_build of ref-counted structs

    Previously, post_build was invoked on reference-counted objects before they were wrapped in Rc or Arc. This led to the failure of the myself method because the weak reference to self remained uninitialized.

  • Superfluous warning on unused builder methods

    Mark field builder methods as #[allow(unused)] if corresponding fields are optional or have alternative sources of values (such as lazy initialization or default values).

  • Regression that broke non-debug build profiles

  • Compiler error message location for post_build methods

Documentation

  • Fix CHANGELOG template
  • Document the new helper argument

Testing

  • Remove laziness from attribute to make test working again

    Builder methods of lazy fields are now marked with #[allow(unused)] to avoid unused warnings.

️ Miscellaneous Tasks

  • Don't include release commit message in changelog

v0.1.12 - 2025-03-21

Bug Fixes

  • Missing documentation key in crates metadata

️ Miscellaneous Tasks

  • Release fieldx version 0.1.12

v0.1.11 - 2025-03-21

Features

  • Generate errors for useless sub-arguments

  • Add doc argument to document generated code

    And preserve field doc comments for accessors and builder setters.

  • Restrict use of more subargs at field level

    This feature came along with refactoring aiming at unification of internal interfaces.

Bug Fixes

  • Clearer treating a field as a lock on sync structure

    This shouldn't happen without explicit lock being set. This is a bug inherited from the times when optional on a sync struct was implying the lock argument.

  • Incorrect hanlding of sync+optional by serialization

  • Option span not updated from FXNestingAttr wrapper

  • Recover lost functionality of prefixing builder setter methods

    While it was the right move to change the functionality of the builder argument's literal sub-argument to define the builder's struct name, the ability to bulk-assign prefixes to builder setter names was lost. It is now recovered by introducing the prefix sub-argument to the builder argument.

Refactor

  • ️‼️ breaking Improve how generated code is bound to the source

    With this commit the following breaking changes are introduced:

    • ⚠️ public argument is gone, use vis(...) instead
    • ⚠️ struct-level serde literal string argument is now used as rename argument of #[serde()] attribute.
    • ⚠️ optional is not implying lock for sync structs anymore. Explitcit lock argument may be required.
  • Major step towards constructor-based architecture

    No extra features added, just refactoring the codebase to make it more maintainable and extensible. If no big bugs are found, then this release could serve as a stabilization prior to the v0.2.0 release.

Documentation

  • Document the latest changes and fix some errors

️ Miscellaneous Tasks

  • Release fieldx version 0.1.11

fieldx-v0.1.10 - 2025-02-22

Features

  • Allow default to take expressions as arguments

Bug Fixes

  • Insufficiently strict handling of 'skip'
  • Lazy fields picking up optional from struct level

fieldx-v0.1.9 - 2025-01-16

Features

  • Allow inner_mut to be used in sync mode
  • Allow wider use of inner_mut

Bug Fixes

  • Remove erroneous documentation field from Cargo.toml
  • Builder methods visibility must not always be public
  • Regression, parking_lot types must be re-exported
  • Incorrect codegen for const generic params with default
  • Generics for serde shadow struct
  • Incorrect generation of serde shadow Default implementation
  • Incorrect generic handling in Self fixup
  • Sanitize the logic for choosing field concurrency mode

fieldx-v0.1.8 - 2024-12-05

Features

  • Implement async support
  • Allow use of async keyword
  • Implement support for fallible lazy builders
  • Support for builder's custom error type
  • Introduce 'sync' and 'async' features

Bug Fixes

  • Implement Clone for FXProxyAsync
  • Error diagnostics for serde-related code
  • Fieldx_derive must depend on fieldx by path in dev-deps

Refactor

  • Rename internal structs for the sake of naming consistency

Documentation

  • Update docs with async addition
  • Completed documenting fieldx_aux crate

Testing

  • Only test failing compiles under the Makefile.toml environment
  • Fix testing documentation examples of fieldx_derive crate

Fix

  • Don't implement Default for shadow if it's not needed

Styling

  • Format all sources

fieldx-v0.1.7 - 2024-11-22

Features

  • Implement builder init argument

Documentation

  • Document builder init argument

fieldx-v0.1.6 - 2024-10-19

Features

  • Allow field's default to be just a keyword so it would fallback to Default::default()
  • Make builder setter methods to use more common 'self ownership' scheme instead of using &mut

Bug Fixes

  • Avoid function name case warning
  • Reduce builder dependency on Default
  • Allow non-snake-case names for generated serde methods

️ Miscellaneous Tasks

  • Release fieldx version 0.1.5

fieldx-v0.1.5 - 2024-10-03

Features

  • Complete implementation of reference counted objects
  • Make builder's into argument accept into(off) form
  • Add support for builder(required)
  • Implement inner mutability pattern
  • Implement struct-level builder(opt_in)
  • Allow better granularity over fields concurrency mode
  • Implement PartialEq and Eq
  • Added two convenice types: FXSynValueArg and FXSynTupleArg
  • Added implementation of FXPunctuated

Bug Fixes

  • Marshalling of optional fields
  • Fix a thinko in serde deserialization of optionals
  • Suppress a harmless warning
  • Remove unused import
  • Improve some error location reporting
  • Propagate "diagnostics" feature to the darling crate

Refactor

  • Make more types available via fieldx_aux
  • Split fxproxy proxy module into submodules
  • Get rid of FXStructSync and FXStructNonSync
  • Removed unused struct

Documentation

  • Describe interior mutability pattern

️ Miscellaneous Tasks

  • Release fieldx_derive_support version 0.1.4
  • Release fieldx_aux version 0.1.4
  • Release fieldx_derive version 0.1.4
  • Release fieldx version 0.1.4
  • Release fieldx_derive_support version 0.1.5
  • Release fieldx_aux version 0.1.5
  • Release fieldx_derive version 0.1.5
  • Release fieldx version 0.1.5

fieldx-v0.1.3 - 2024-08-02

Features

  • Add feature send_guard
  • Support reference counted objects

Documentation

  • Document the new rc argument and crate features

️ Miscellaneous Tasks

  • Release fieldx version 0.1.3

fieldx-v0.1.2 - 2024-06-19

Features

  • ️‼️ breaking Allow optional unlocked fields on sync structs
  • Add support for attributes and attributes_impl for fxstruct

Bug Fixes

  • Make sure that Copy trait bound check doesn't fail for generics

Documentation

  • Document new argument lock
  • Document attributes and attributes_impl of fxstruct

Testing

  • Streamline toolchain(version)-dependent testing
  • Use stricter/safer atomic ordering
  • Refactor tests to get rid of warnings

Maintenance

  • Set some environment variables conditionally
  • (CI) Exclude nightly toolchain from testing under windows
  • (cliff) Allow scoping for feat, fix, and maint groups

️ Miscellaneous Tasks

  • Release fieldx version 0.1.2

Main

  • Should've not use publish with cargo release

fieldx-v0.1.1 - 2024-06-02

Features

  • ️‼️ breaking Full support for accessors for sync structs and lock argument
    • ⚠️ new accessors are incompatible with the previous approach

Documentation

  • Document the changes, related to the accessors of sync structs

Testing

  • Adjusted tests for the new accessors concept and lock

Maintenance

  • Fix incorrect handling of environment variables in Makefile.toml
  • Fix generation of CHANGELOG by cargo release
  • Use cargo release for the publish target

v0.1.0 - 2024-05-31