valobj (Value Objects)
Minimal improvements on the newtype pattern.
Goal
Value objects are a common design pattern across programming languages, and Rust's newtype pattern provides a straightforward way to implement them. However, enforcing validity constraints on these objects typically requires significant boilerplate code.
This crate aims to reduce this boilerplate by providing a lightweight macro attribute that extends the newtype pattern with validation and normalization capabilities. The macro automatically enforces domain invariants at construction time, enabling you to guarantee that only valid values can exist within 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.