Skip to content

Commit

Permalink
Adding VersionedSerde macro to manage configuration updates.
Browse files Browse the repository at this point in the history
  • Loading branch information
ineiti committed Jan 18, 2025
1 parent ef87aa1 commit 3241752
Show file tree
Hide file tree
Showing 4 changed files with 381 additions and 5 deletions.
4 changes: 2 additions & 2 deletions flarch/src/broker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,12 +677,12 @@ pub trait SubsystemTranslator<T: Async> {
type SubsystemCallback<T> = Box<dyn Fn(Vec<T>) -> BoxFuture<'static, Vec<(Destination, T)>>>;
#[cfg(target_family = "unix")]
type SubsystemCallback<T> =
Box<dyn Fn(Vec<T>) -> BoxFuture<'static, Vec<(Destination, T)>> + Send + Sync>;
Box<dyn Fn(Vec<T>) -> BoxFuture<'static, Vec<(Destination, T)>> + Send>;
#[cfg(target_family = "wasm")]
type SubsystemTranslatorCallback<T> = Box<dyn Fn(Vec<BrokerID>, T) -> BoxFuture<'static, bool>>;
#[cfg(target_family = "unix")]
type SubsystemTranslatorCallback<T> =
Box<dyn Fn(Vec<BrokerID>, T) -> BoxFuture<'static, bool> + Send + Sync>;
Box<dyn Fn(Vec<BrokerID>, T) -> BoxFuture<'static, bool> + Send>;

#[cfg(test)]
mod tests {
Expand Down
140 changes: 139 additions & 1 deletion flarch_macro/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# flarch_macro

Macros for the use in fledger.

## platrform_async_trait

This holds the macro for defining an `async_trait` either with or without the
`Send` trait.
You can use it like this:
Expand All @@ -13,4 +17,138 @@ impl SubsystemHandler<Message> for SomeBroker {
}
```

Depending on `wasm` or `unix`, it will either remove or keep the `Send` trait.
Depending on `wasm` or `unix`, it will either remove or keep the `Send` trait.

## proc_macro_derive(AsU256)

This allows to use tuple struct, or a newtype struct, based on `U256`, to export
all methods from `U256`.
Instead of using a type definition, which is not unique and can be replaced by any
of the other types, tuple structs allow for more type safety.
You can use it like this:

```rust
#[derive(AsU256)]
struct MyID(U256);
```

And now you can have `MyID::rnd()` and all the other methods from `U256`.

## proc_macro_derive(VersionedSerde, attributes(versions, serde))

To store configuration and other data in different version, you can use this
derive macro:

```rust
use bytes::Bytes;
use flarch_macro::VersionedSerde;
use serde::{Deserialize, Serialize};
use serde_with::{base64::Base64, serde_as};

#[serde_as]
#[derive(VersionedSerde, Clone, PartialEq, Debug)]
#[versions = "[ConfigV1, ConfigV2]"]
struct Config {
#[serde_as(as = "Base64")]
name3: Bytes,
}

impl From<ConfigV2> for Config {
fn from(value: ConfigV2) -> Self {
Self { name3: value.name2 }
}
}

#[derive(Serialize, Deserialize, Clone)]
struct ConfigV2 {
name2: Bytes,
}

impl From<ConfigV1> for ConfigV2 {
fn from(value: ConfigV1) -> Self {
Self { name2: value.name }
}
}

#[derive(Serialize, Deserialize, Clone)]
struct ConfigV1 {
name: Bytes,
}
```

It will do the following:
- create a copy of the `struct Config` as `struct ConfigV3` with appropriate `FROM` implementations
- create a `ConfigVersion` enum with all configs in it
- implement `serde::Serialize` and `serde::Deserialize` on `Config` which will
- wrap `Config` in the `ConfigVersion`
- serialize the `ConfigVersion`, or
- deserialize the `ConfigVersion` and convert it to `Config`

This allows you to use any serde implementation to store any version of your structure,
and retrieve always the latest version:

```rust
#[test]
fn test_config() -> Result<(), Box<dyn std::error::Error>> {
// Simulate the storage of an old configuration.
let v1 = ConfigV1 { name: "123".into() };
let cv1 = ConfigVersion::V1(v1);
let c: Config = cv1.clone().into();
let cv1_str = serde_yaml::to_string(&cv1)?;

// Now the old version is recovered, and automatically converted
// to the latest version.
let c_recover: Config = serde_yaml::from_str(&cv1_str)?;
assert_eq!(c_recover, cv1.into());

// Storing and retrieving the latest version is always
// done using the original struct, `Config` in this case.
let c_str = serde_yaml::to_string(&c)?;
let c_recover = serde_yaml::from_str(&c_str)?;
assert_eq!(c, c_recover);
Ok(())
}
```

### Usage of serde_as and others

To allow usage of `serde_as`, the `VersionedSerde` also defines the `serde` attribute.
However, `VersionedSerde` does not use it itself.

### Usage of a new configuration structure

When you start with a new configuration structure, the `versions` can be omitted:

```rust
#[derive(VersionedSerde, Clone)]
struct NewStruct {
field: String
}
```

When converting this using `serde`, it will store it as `V1`.
So whenever you create a new version, you can add it with
a converter to the latest structure:

```rust
#[derive(VersionedSerde, Clone)]
#[versions = "[NewStructV1]"]
struct NewStruct {
field: String,
other: String
}

impl From<NewStructV1> for NewStruct {
fn from(value: NewStructV1) -> Self {
Self {
field: value.field,
other: "default".into(),
}
}
}

#[derive(Serialize, Deserialize, Clone)]
struct NewStructV1 {
field: String
}
```
159 changes: 157 additions & 2 deletions flarch_macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Define a custom attribute macro for platform-specific async_trait
use proc_macro::TokenStream;
use quote::quote;
use syn::{ItemImpl, ItemTrait, parse_macro_input, DeriveInput};
use quote::{format_ident, quote};
use syn::*;

#[proc_macro_attribute]
pub fn platform_async_trait(_attr: TokenStream, input: TokenStream) -> TokenStream {
Expand Down Expand Up @@ -136,3 +136,158 @@ pub fn as_u256_derive(input: TokenStream) -> TokenStream {

TokenStream::from(expanded)
}

#[proc_macro_derive(VersionedSerde, attributes(versions, serde))]
pub fn versioned_serde(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;

// Extract the older configurations
let versions: Vec<syn::Ident> = input
.attrs
.iter()
.find(|attr| attr.path().is_ident("versions"))
.and_then(|attr| match &attr.meta {
Meta::NameValue(meta) => {
if let Expr::Lit(ExprLit {
lit: Lit::Str(lit), ..
}) = &meta.value
{
lit.parse::<syn::ExprArray>().ok().map(|list| {
list.elems
.into_iter()
.filter_map(|expr| {
if let syn::Expr::Path(expr_path) = expr {
expr_path.path.get_ident().cloned()
} else {
None
}
})
.collect()
})
} else {
None
}
}
_ => None,
})
.unwrap_or_default();

let enum_name = format_ident!("{}Version", struct_name);
let mut variants = vec![];
let mut conversions = vec![];
for (idx, version) in versions.iter().enumerate() {
let version_number = idx + 1;
let variant_name = format_ident!("V{}", version_number);
variants.push(quote! {
#variant_name(#version)
});
let next_variant_name = format_ident!("V{}", version_number + 1);
conversions.push(quote! {
#enum_name::#variant_name(#variant_name) => #enum_name::#next_variant_name(#variant_name.into()).into()
});
}

// Add current variant to the enum and to the match conversion
let mut latest_version = input.clone();
let latest_version_name_m1 = format_ident!("{}V{}", struct_name, versions.len());
let latest_version_name = format_ident!("{}V{}", struct_name, versions.len() + 1);
latest_version.ident = latest_version_name.clone();
// Don't copy 'versions' attribute, as it won't work recursively.
latest_version
.attrs
.retain(|a| !a.path().is_ident("versions"));

let m1_conversion = if versions.len() > 0 {
quote! {
impl From<#latest_version_name_m1> for #latest_version_name {
fn from(value: #latest_version_name_m1) -> Self {
Into::<#struct_name>::into(value).into()
}
}
}
} else {
quote! {}
};

let latest_enum = format_ident!("V{}", versions.len() + 1);
variants.push(quote! {
#latest_enum(#latest_version_name)
});
conversions.push(quote! {
#enum_name::#latest_enum(orig) => orig.into()
});

let fields = match &input.data {
syn::Data::Struct(struct_data) => match &struct_data.fields {
syn::Fields::Named(fields_named) => &fields_named.named,
_ => panic!("Only named fields are supported"),
},
_ => panic!("Only structs are supported"),
};

let field_names: Vec<_> = fields.iter().map(|f| &f.ident).collect();

let result = quote! {
#[derive(serde::Serialize, serde::Deserialize, Clone)]
enum #enum_name {
#(#variants),*
}

#[derive(serde::Serialize, serde::Deserialize, Clone)]
#latest_version

#m1_conversion

impl From<#struct_name> for #latest_version_name {
fn from(orig: #struct_name) -> Self {
Self {
#(#field_names: orig.#field_names),*
}
}
}

impl From<#latest_version_name> for #struct_name {
fn from(new: #latest_version_name) -> Self {
Self {
#(#field_names: new.#field_names),*
}
}
}

impl From<#enum_name> for #struct_name {
fn from(value: #enum_name) -> #struct_name {
return match value {
#(#conversions),*
}
}
}

impl serde::Serialize for #struct_name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
#enum_name::#latest_enum(self.clone().into()).serialize(serializer)
}
}

impl<'de> serde::Deserialize<'de> for #struct_name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let version = #enum_name::deserialize(deserializer)?;
Ok(version.into())
}
}
};

// let output_str = format!("{:?}", latest_version.attrs.to_vec().get(0).unwrap());
// let output_str = result.to_string().replace("\\n", "\n");
// return quote! {
// compile_error!(#output_str);
// }
// .into();
result.into()
}
Loading

0 comments on commit 3241752

Please sign in to comment.