#[revisioned]
Expand description
Generates serialization and deserialization code as an implementation of
the Revisioned
trait for structs and enums.
This procedural macro attribute currently analyses the struct field and enum variant revisions, and generates custom serializer and deserializer implementations for each version. In the future, this procedural macro will also automatically remove old struct fields entirely, reducing the memory size of the struct, and ensuring that field types can be changed.
This macro works by generating a single serializer implementation for the latest revision of a struct, and multiple deserializer implementations for each historical revision of a struct. There is no limit to the maximum number of revisions that are possible to be defined for a struct or enum.
§Revisioned requirements
Currently, all struct field values, and all enum variant fields need to
implement the Revisioned
trait. This is already implemented for a number
of primitive and custom types. In addition, the Revisioned
derive macro
can not be used with generics.
§Attribute annotations
To facilitate version tolerant serialization “history metadata” is attached
to the structure or enum. This is done by using the revision
attribute for
each field. In the below example a new field is added to the structure
starting with version 2: #[revision(start = 2)]
. The struct revision must
match the maximum computed revision of every struct field or enum variant.
use revision::revisioned;
#[derive(Debug)]
#[revisioned(revision = 2)]
struct Test {
a: u32,
#[revision(start = 2)]
b: u8,
}
Multiple version annotations can be defined for a field, like for example:
#[revision(start = 2, end = 3)]
. Field was added in structure version 2
and removed in version 3. The generated code will ensure that this field
will only be deserialized for version 2 of the structure.
§Disabling serialization or deserialization
You can disable serialization or deserialization of a struct by using the
#[revision(serialize = false)]
or #[revision(deserialize = false)]
.
This is useful for data migrations when you no longer want to write the
struct but still want to read it or vice versa.
By default both serialization and deserialization are enabled.
§Example
use revision::prelude::*;
#[derive(Debug)]
#[revisioned(revision = 2, serialize = false)]
struct ReadOnlyStruct {
a: u32,
}
#[derive(Debug)]
#[revisioned(revision = 2, deserialize = false)]
struct WriteOnlyStruct {
a: u32,
}
§Supported field attributes and usage
The struct field and enum variant revision
attribute accepts several key-
value pairs to be specified in order to support struct revisions, default
values for newly added fields, and value conversion for old fields which
have been removed. The macro will automatically detect whether a conversion
function is required for a removed field or variant.
§start/end
Defines the field revision lifetime. Fields can be added by specifing the
start
revision number of the structure when first defining them and can
be removed from serialization logic by adding an end
revision number.
For example: #[revision(start = 2, end = 4)]
. The field would be present
in the structure at revisions 2 and 3, but starting with revision 4 it would
no longer be serialized or deserialized.
§default_fn
Provides an initialization value for a field when deserializing from an
older structure version which does not contain this field. If not specified
the Default
trait is used to initialize the field.
The function name needs to be specified as a string. The first function argument is the source revision that is being deserialized, and the return value is the same type as the field or an error.
use revision::Error;
use revision::revisioned;
#[derive(Debug)]
#[revisioned(revision = 2)]
struct TestStruct {
a: u32,
#[version(start = 2, default_fn = "default_b")]
b: u8,
}
impl TestStruct {
fn default_b(_revision: u16) -> Result<u8, Error> {
12u8
}
}
§convert_fn
If defined, the method is called when the field existed at some previous revision, but no longer exists in the latest revision. The implementation and behaviour is slightly different depending on whether it is applied to a removed struct field or a removed enum variant or a removed field from an enum variant. If defined, the function name needs to be specified as a string, and will be called when the field existed at a previous revision, but no longer exists in the latest revision.
When defined on a removed struct field, the first function argument is the
&mut self
of the struct to update, the second argument is the source
revision that was deserialized, and the third argument is the deserialized
value from the field which has been removed.
When working with an enum variant the convert function works with a fields
struct. This is a generated structure which has the same fields as the enum
variant. By default this struct is named
‘{enum name}{variant name}Fields
’, this name can be changed with the
fields_name
if desired.
When a field in a variant is removed the convert function takes a mutable reference to this fields struct as its first argument, it’s second argument is the revision from which this field is being deserialized and it’s third argument is the deserialized value.
When the entire variant is remove the first argument is the fields struct with it’s fields containing the values of the deserialized removed variant. In both situations the convert_fn function takes as a second argument the revision from which this was serialized. The function should return a result with either the right deserialized value or an error.
use revision::Error;
use revision::revisioned;
#[derive(Debug)]
#[revisioned(revision = 2)]
struct SomeStruct {
some_u32: u32,
#[version(end = 2, convert_fn = "convert_some_u16")]
some_u16: u16,
#[revision(start = 2)]
some_u64: u64,
}
impl SomeStruct {
fn convert_some_u16(&mut self, _revision: u16, value: u16) -> Result<(), Error> {
self.some_u64 = self.some_u16 as u64;
Ok(())
}
}
#[derive(Debug)]
#[revisioned(revision = 3)]
enum SomeTuple {
One,
#[revision(end = 2, convert_fn = "convert_variant_two")]
Two(i64, u32),
#[revision(start = 2)]
Three(i64, u64, #[revision(end = 3, convert_fn = "convert_variant_three_field")] bool),
}
impl SomeTuple {
fn convert_variant_two(fields: SomeTupleTwoFields, _revision: u16) -> Result<Self, Error> {
Ok(Self::Three(fields.a, fields.b as u64, true))
}
fn convert_variant_three_field(fields: &mut SomeTupleTwoFields, _revision: u16, v: bool) -> Result<(), Error> {
Ok(())
}
}