diff --git a/flarch/src/broker.rs b/flarch/src/broker.rs index feeb9144..861e0b1a 100644 --- a/flarch/src/broker.rs +++ b/flarch/src/broker.rs @@ -677,12 +677,12 @@ pub trait SubsystemTranslator { type SubsystemCallback = Box) -> BoxFuture<'static, Vec<(Destination, T)>>>; #[cfg(target_family = "unix")] type SubsystemCallback = - Box) -> BoxFuture<'static, Vec<(Destination, T)>> + Send + Sync>; + Box) -> BoxFuture<'static, Vec<(Destination, T)>> + Send>; #[cfg(target_family = "wasm")] type SubsystemTranslatorCallback = Box, T) -> BoxFuture<'static, bool>>; #[cfg(target_family = "unix")] type SubsystemTranslatorCallback = - Box, T) -> BoxFuture<'static, bool> + Send + Sync>; + Box, T) -> BoxFuture<'static, bool> + Send>; #[cfg(test)] mod tests { diff --git a/flarch_macro/README.md b/flarch_macro/README.md index e2f5eb31..5513e487 100644 --- a/flarch_macro/README.md +++ b/flarch_macro/README.md @@ -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: @@ -13,4 +17,138 @@ impl SubsystemHandler for SomeBroker { } ``` -Depending on `wasm` or `unix`, it will either remove or keep the `Send` trait. \ No newline at end of file +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 for Config { + fn from(value: ConfigV2) -> Self { + Self { name3: value.name2 } + } +} + +#[derive(Serialize, Deserialize, Clone)] +struct ConfigV2 { + name2: Bytes, +} + +impl From 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> { + // 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 for NewStruct { + fn from(value: NewStructV1) -> Self { + Self { + field: value.field, + other: "default".into(), + } + } +} + +#[derive(Serialize, Deserialize, Clone)] +struct NewStructV1 { + field: String +} +``` diff --git a/flarch_macro/src/lib.rs b/flarch_macro/src/lib.rs index 85c7496c..432cbabe 100644 --- a/flarch_macro/src/lib.rs +++ b/flarch_macro/src/lib.rs @@ -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 { @@ -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 = 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::().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(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + #enum_name::#latest_enum(self.clone().into()).serialize(serializer) + } + } + + impl<'de> serde::Deserialize<'de> for #struct_name { + fn deserialize(deserializer: D) -> Result + 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() +} diff --git a/flmodules/tests/test_config.rs b/flmodules/tests/test_config.rs new file mode 100644 index 00000000..dbba77e6 --- /dev/null +++ b/flmodules/tests/test_config.rs @@ -0,0 +1,83 @@ +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, ConfigV3]"] +struct Config { + #[serde_as(as = "Base64")] + name4: Bytes, +} + +impl From for Config { + fn from(value: ConfigV3) -> Self { + Self { name4: value.name3 } + } +} + +#[derive(Serialize, Deserialize, Clone)] +struct ConfigV3 { + name3: Bytes, +} + +impl From for ConfigV3 { + fn from(value: ConfigV2) -> Self { + Self { name3: value.name2 } + } +} + +#[derive(Serialize, Deserialize, Clone)] +struct ConfigV2 { + name2: Bytes, +} + +impl From for ConfigV2 { + fn from(value: ConfigV1) -> Self { + Self { name2: value.name } + } +} + +#[derive(Serialize, Deserialize, Clone)] +struct ConfigV1 { + name: Bytes, +} + +#[test] +fn test_config() -> Result<(), Box> { + // 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(()) +} + +#[derive(VersionedSerde, Clone, PartialEq, Debug)] +struct NewStruct { + field: String, +} + +#[test] +fn test_new_struct() -> Result<(), Box> { + let ns = NewStruct { + field: "123".into(), + }; + let ns_str = serde_yaml::to_string(&ns)?; + let ns_recover = serde_yaml::from_str(&ns_str)?; + assert_eq!(ns, ns_recover); + + Ok(()) +}