valobj (Value Objects)
Minimal improvements on the newtype pattern, which is a common way to create value objects to wrap primitive types in Rust.
Goal
Creating value objects is a common practice in any language, and the newtype pattern is often used for this purpose in Rust. However, you sometimes need to ensure validity of your objects, which can lead to a lot of boilerplate code.
The goal of this crate is just to minimize this boilerplate code. It tries to be as simple as possible, without adding complexity. It provides a simple macro attribute to enhance the newtype pattern. This macro enforces invariants and allows to normalize / validate values at construction time, ensuring that only valid values can be created in your domain.
use ;
;
Primitive obsession
Creating new types that wrap primitives helps you avoid Primitive obsession , a code smell where primitive types like String or u64 are used directly to represent domain concepts without meaningful constraints or semantics.
When to use value objects?
If you already use newtype pattern, you can consider using value_object attribute when you need to
normalize or validate the inner value, or when you want to define invariants easily (e.g.
ensure a
UserId is always positive or that a Username is not empty...).
Reference
Construction
Value objects can be constructed with either from or try_from methods, depending on whether
validation is enabled or not.
Getter
To maintain consistency, the tuple is immutable and you cannot access the .0 field directly.
To get the inner value, a get method is generated, which returns a copy (or a &str
in case of string) to the inner value:
;
Some other traits are also implemented to allow easy access:
AsRef<T>(AsRef<str>in case of aStringtype)Deref<Target=T>
;
Validation
You can define a validation function that checks if the input value meets some requirements. If
the Validate trait is implemented, a TryFrom implementation will be generated,
allowing you to create value objects from the inner type while ensuring that the value is valid.
To enable validation, you need to :
- Add
Validateattribute to the macro:#[valobj::value_object(Validate)] - Implement the
Validatetrait for the inner type of your value object.
Normalization
You can define a normalization function that transforms the input value into a canonical form (e.g. trim whitespaces from a string, change it to lowercase...).
To enable normalization, you need to :
- Add
Normalizeattribute to the macro:#[valobj::value_object(Normalize)] - Implement the
Normalizetrait for the inner type of your value object.
The normalize method should return the normalized value.