2 releases

0.9.1 Feb 25, 2025
0.9.0 Feb 25, 2025

#222 in Procedural macros

Download history

56 downloads per month
Used in 4 crates (3 directly)

MIT/Apache

18KB
283 lines

flmacro

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:

#[platform_async_trait()]
impl SubsystemHandler<Message> for SomeBroker {
    async fn messages(&mut self, _: Vec<Message>) -> Vec<Message> {
        todo!();
    }
}

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:

#[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:

use bytes::Bytes;
use flmacro::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:

#[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:

#[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:

#[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
}

target_send

This macro does two things:

  • for traits, it creates a version of the trait with Box at the end, and it adds + Send for non-wasm targets
  • for types, it adds + Send three characters from the end of the type in rust-macro-format

So the following

#[target_send]
trait Something<T>{}

#[target_send]
type SomethingElse<T> = Box<dyn SomethingMore<T>>;

Will be translated to:

trait Something<T>{}

type SomethingBox<T> = Box<dyn Something<T> $SEND>

type SomethingElse<T> = Box<dyn SomethingMore<T> $SEND>;

with $SEND either an empty string for wasm targets, or + Send for non-wasm targets. Specifically the handling of the type is very ugly, as it converts the type to a string, adds conditionally + Send three characters from the end, and parses the resulting string.

Kids, don't do this at home...

Dependencies

~205–640KB
~15K SLoC