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 thefxstruct
macro declares that all fields of the struct are initialized lazily by default. The exception is theorder
field, which opts out usinglazy(off)
. - The
count
field is assigned a default value of 42. Thus, a newly created instance ofFoo
will have this value from the start, but since it’s lazy it can be reset to an uninitialized state with theclear_count
method (introduced by theclearer
attribute). The next read will initialize it by callingbuild_count
. Thepredicate
argument also provides ahas_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
andcount
becomeOnceCell<String>
andOnceCell<usize>
, whileorder
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:
- 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. - 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
andSend
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 Result
1. 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.
-
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"] }
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 meansfalse
. -
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 tofalse
:#[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 theoff
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 theClone
trait, such asString
, 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 theyear
field andget(clone)
for theauthor
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());
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");
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.
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
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.
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.
-
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! ↩
-
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 thefxstruct
macro.#[fxstruct(default)] struct Foo { is_set: bool, }
-
When the argument
default
is provided and active for any field'sfieldx
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:
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");
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?
-
Note that the
optional
keyword is omitted in this case becausepredicate
andclear
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 RefCell
1. 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,
}
-
The
inner_mut
andlock
arguments are aliases for the same functionality insync
andasync
modes. Thelock
argument exists for readability and convenience, as marking a field as locked automatically implies that it issync
. ↩
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.
Using inner_mut
with explicit sync
or mode(sync)
has the same effect as using the lock
argument.
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:
- Implicit sync mode when the
lock
argument is used. This means there is no need to specifysync
ormode(sync)
per field if it is already locked. - The
lock
argument itself can be implicit whenreader
orwriter
arguments are used. Apparently, thesync
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:
- 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.
- The value for the initialization is provided by a lazy builder method, which is supplied by the user.
- FieldX guarantees that in sync or async modes of operation, rule #1 also implies that the builder method is called exactly once.
- The builder method is expected not to mutate its object unless it is unavoidable and done with the utmost care.
- 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)?)
}
}
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:
- First of all, all fields of the struct are marked as lazily initialized.
- 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 declarationfallible(off, error(AppError))
means that we turn off the fallible mode for all fields by default but set the default error type toAppError
. - 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. - Finally, we implement the builder methods for the fields. The methods return a
Result
type withAppError
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.
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");
}
}
Here we use reference counting to perform a self-check in a parallel thread while continuing to serve reader requests.
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:
- The immediate declaration for an entity:
get(vis(pub))
. - Field-level declaration:
#[fieldx(vis(pub), get)]
. - Struct-level default for the entity:
#[fxstruct(get(vis(pub)))]
. - Struct-level default:
#[fxstruct(vis(pub))]
. - Field explicit declaration:
pub foo: usize
. - Struct declaration:
pub struct Foo { ... }
.
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 structsattributes_fn
- for generated methodsattributes_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):
Serialization
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")
{
"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)))]
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
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.
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.
-
The
serde
feature flag actually simply depends on theclonable-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");
-
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.
Field Parameters | Plain Type | Sync Type | Async Type |
---|---|---|---|
inner_mut | RefCell<T> | sync::FXRwLock<T> or parking_lot::RwLock | async::FXRwLock<T> , or tokio::sync::RwLock or async_lock::RwLock |
lazy | once_cell::unsync::OnceCell<T> | once_cell::sync::OnceCell<T> | tokio::sync::OnceCell<T> or async_lock::OnceCell<T> |
lazy + lock | N/A | sync::FXProxy<O, T> | async::FXProxy<O,T> |
optional (also activated with clearer and predicate ) | Option<T> | sync::FXRwLock<Option<T>> | async::FXRwLock<Option<T>> |
lock , reader and/or writer | N/A | sync::FXRwLock<T> | async::FXRwLock<T> |
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:
Feature | Description |
---|---|
sync | Support for sync-safe mode of operation |
async | Support for async mode of operation |
tokio-backend | Selects the Tokio backend for async mode. A no-op without the async feature. |
async-lock-backend | Selects the async-lock backend for async mode. A no-op without the async feature. |
async-tokio | Combines async and tokio-backend features. |
async-lock | Combines async and async-lock-backend features. |
clonable-lock | Enables the clonable lock wrapper type. |
send_guard | See corresponding feature of the parking_lot crate |
serde | Enable support for serde marshalling. |
diagnostics | Enable additional diagnostics for compile time errors. Experimental, requires Rust nightly toolset. |
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
- FieldX documentation
- Utility FieldX crates:
- Full list of
fxstruct
andfieldx
arguments - FieldX GitHub repository
- Issue tracker – use it to report any problems about this book too.
- FieldX Plus - an experimental crate extending FieldX functionality for parent/child and application/agent relationships
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
- v0.2.0 - 2025-06-27
- v0.2.0-beta.1 - 2025-06-16
- v0.1.19 - 2025-06-06
- v0.1.18 - 2025-06-06
- v0.1.17 - 2025-06-01
- v0.1.16 - 2025-04-30
- v0.1.15 - 2025-04-23
- v0.1.14 - 2025-04-19
- v0.1.13 - 2025-04-18
- v0.1.12 - 2025-03-21
- v0.1.11 - 2025-03-21
- fieldx-v0.1.10 - 2025-02-22
- fieldx-v0.1.9 - 2025-01-16
- fieldx-v0.1.8 - 2024-12-05
- fieldx-v0.1.7 - 2024-11-22
- fieldx-v0.1.6 - 2024-10-19
- fieldx-v0.1.5 - 2024-10-03
- fieldx-v0.1.3 - 2024-08-02
- fieldx-v0.1.2 - 2024-06-19
- fieldx-v0.1.1 - 2024-06-02
- v0.1.0 - 2024-05-31
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 theasync-lock
crate to implementRwLock
andOnceCell
. This change also updates the feature flags: theasync
feature must be paired with eithertokio-backend
orasync-lock-backend
. Alternatively, enable async support and select an implementation in one step with the umbrella flagsasync-tokio
orasync-lock
.tokio
remains in the requirements for tests. -
Add support for
clonable-lock
feature flagWith this flag,
FXRwLock<T>
types from eithersync
orasync
modules will be used instead ofRwLock<T>
fromparking_lot
,tokio
, orasync-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
targetstest-versions
andupdate-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 withlazy
, which can now be used with bothlock
andinner_mut
. However, the combination offallible
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
, andinner_mut
are now less clear than they used to be. There is a good reason to get it withlazy
: unless it combined withlock
, the only way get lazy initialization is to use the getter.
- ⚠️ The reasons why getter was given with
-
Implement simplified
lazy
implementation for non-lock fieldsIt 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 thelock
orinner_mut
attributes (which are just aliases in thesync
andasync
modes), lazy evaluation will be implemented usingOnceCell
from eitheronce_cell
ortokio
for thesync
andasync
contexts, respectively.
Bug Fixes
get(as_ref)
wasn't respected forsync
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()
syntaxPreviously, 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 nowa_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 whenserde
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
subargumentThis allows to override struct-level defaults for
copy
,clone
, andas_ref
flags. -
Don't require
lock
field types to implement Default -
Introduce
new
helper and deprecateno_new
This also uncouples
new
fromdefault
, allowing for use of theDrop
trait withfieldx
structs.
Bug Fixes
-
An old bug in the
sync
test -
myself
method in post_build of ref-counted structsPreviously,
post_build
was invoked on reference-counted objects before they were wrapped inRc
orArc
. This led to the failure of themyself
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 codeAnd 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
structureThis shouldn't happen without explicit
lock
being set. This is a bug inherited from the times whenoptional
on async
struct was implying thelock
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 theprefix
sub-argument to thebuilder
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, usevis(...)
instead - ⚠️ struct-level
serde
literal string argument is now used asrename
argument of#[serde()]
attribute. - ⚠️
optional
is not implyinglock
forsync
structs anymore. Explitcitlock
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 fromCargo.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
forFXProxyAsync
- 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 toDefault::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 acceptinto(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
andattributes_impl
forfxstruct
Bug Fixes
- Make sure that Copy trait bound check doesn't fail for generics
Documentation
- Document new argument
lock
- Document
attributes
andattributes_impl
offxstruct
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 underwindows
- (cliff) Allow scoping for
feat
,fix
, andmaint
groups
️ Miscellaneous Tasks
- Release fieldx version 0.1.2
Main
- Should've not use
publish
withcargo 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 thepublish
target