rust 如何反序列化异步函数名并使用反序列化参数调用它

plupiseo  于 2023-04-21  发布在  其他
关注(0)|答案(1)|浏览(103)

我有一个Rust项目的想法,我希望有一个可重用的库,允许TOML文件指定要调用的函数序列,沿着传递给这些函数的所需参数。这些函数需要是异步的。
我想要的是:

#[derive(Serialize, Deserialize)]
struct Config {
    functions: IndexMap<String, dyn Fn() -> dyn Future<Output = Result<(), ()>>>,
}

但这并不起作用(我认为有多种原因)。
我已经设法让PoC使用Enum作为中间类型:

use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use toml::toml;

async fn without_args() -> Result<(), ()> {
    println!("Function without args");
    Ok(())
}

async fn with_args(arg: &str) -> Result<(), ()> {
    println!("Function with the arg = {arg}!");
    Ok(())
}

async fn sum_args(x: u64, y: u64) -> Result<(), ()> {
    println!("Function for the sum {x} + {y} = {}!", x + y);
    Ok(())
}

#[derive(Deserialize, Serialize)]
#[serde(tag = "function", content = "args")]
enum FnOptions {
    Without,
    With(String),
    Sum(u64, u64),
}

impl FnOptions {
    async fn perform(&self) -> Result<(), ()> {
        match self {
            FnOptions::Without => without_args().await,
            FnOptions::With(arg) => with_args(arg).await,
            FnOptions::Sum(x, y) => sum_args(*x, *y).await,
        }
    }
}

#[derive(Serialize, Deserialize)]
struct Config {
    functions: IndexMap<String, FnOptions>,
}

#[tokio::main]
async fn main() {
    let toml = toml! {
        [functions.foo]
        function = "Without"

        [functions.bar]
        function = "With"
        args = "baz"

        [functions.sum]
        function = "Sum"
        args = [ 1, 2 ]
    };
    let config: Config = toml.try_into().unwrap();

    for func in config.functions {
        func.1.perform().await.unwrap();
    }
}

我也可以删除Enum,只使用一个match语句,将函数名直接匹配到要调用的函数,但我认为这样更干净。
然而,这对于库来说不够通用,因为用户需要手动维护Enum和perform()函数,该函数基于Enum变体调用正确的函数。
也许我可以创建一个宏来完成这个任务,但是我对宏没有太多的经验(没有使用proc-macros)。
我希望我错过了一个明显的解决方案。

oprakyz7

oprakyz71#

你可以有一个全局的函数名Map,你可以注册可以从配置中调用的函数。

use std::{
    collections::HashMap,
    sync::{Arc, Mutex},
};

use futures::future::BoxFuture;
use lazy_static::lazy_static;

pub type FnOut = BoxFuture<'static, Result<(), ()>>;

pub enum FnArgs {
    NoArgs,
    OneArg(String),
    TwoArgs(String, String),
    ThreeArgs(String, String, String),
    // ...
}
pub struct FnConfig {
    pub name: String,
    pub args: FnArgs,
}

type GenericFn = Box<dyn Fn(FnArgs) -> FnOut + Send>;

lazy_static! {
    static ref REGISTERED_FUNCTIONS: Arc<Mutex<HashMap<String, GenericFn>>> =
        Arc::new(Mutex::new(HashMap::new()));
}

pub fn register(name: impl Into<String>, f: GenericFn) {
    REGISTERED_FUNCTIONS.lock().unwrap().insert(name.into(), f);
}

pub fn perform(f: FnConfig) -> FnOut {
    let functions = REGISTERED_FUNCTIONS.lock().unwrap();
    functions.get(&f.name).unwrap()(f.args)
}

然后你会像这样使用图书馆

use futures::FutureExt;
use my_lib::{perform, register, FnArgs, FnConfig, FnOut};
use serde::{Deserialize, Serialize};
use toml::toml;

// Better implement Deserializer for FnArgs instead of using intermediate struct.
#[derive(Serialize, Deserialize)]
struct _FnConfig {
    name: String,
    args: Vec<String>,
}
impl From<_FnConfig> for FnConfig {
    fn from(value: _FnConfig) -> Self {
        Self {
            name: value.name,
            args: match value.args.as_slice() {
                [] => FnArgs::NoArgs,
                [arg1] => FnArgs::OneArg(arg1.to_owned()),
                [arg1, arg2] => FnArgs::TwoArgs(arg1.to_owned(), arg2.to_owned()),
                [arg1, arg2, arg3] => {
                    FnArgs::ThreeArgs(arg1.to_owned(), arg2.to_owned(), arg3.to_owned())
                }
                _ => unimplemented!(),
            },
        }
    }
}

#[derive(Serialize, Deserialize)]
struct Config {
    functions: Vec<_FnConfig>,
}

fn without_args(args: FnArgs) -> FnOut {
    if let FnArgs::NoArgs = args {
        async move {
            println!("Function without args");
            Ok(())
        }
        .boxed()
    } else {
        unimplemented!()
    }
}

fn with_args(args: FnArgs) -> FnOut {
    if let FnArgs::OneArg(arg) = args {
        async move {
            println!("Function with the arg = {arg}!");
            Ok(())
        }
        .boxed()
    } else {
        unimplemented!()
    }
}

fn sum_args(args: FnArgs) -> FnOut {
    if let FnArgs::TwoArgs(arg1, arg2) = args {
        let (arg1, arg2): (u64, u64) = (arg1.parse().unwrap(), arg2.parse().unwrap());
        async move {
            println!("Function for the sum {arg1} + {arg2} = {}!", arg1 + arg2);
            Ok(())
        }
        .boxed()
    } else {
        unimplemented!()
    }
}

#[tokio::main]
async fn main() {
    register("Without", Box::new(without_args));
    register("With", Box::new(with_args));
    register("Sum", Box::new(sum_args));

    let toml = toml! {
        functions = [
            { name = "Without", args = [] },
            { name = "With", args = ["baz"] },
            { name = "Sum", args = ["1", "2"] },
        ]
    };
    let config: Config = toml.try_into().unwrap();
    for func in config.functions.into_iter().map(Into::into) {
        perform(func).await.unwrap();
    }
}

这只是一个快速实现,它肯定可以改进。这只是给予你一个想法。例如,注册的函数不必是全局的。你可以有一个包含函数的结构体,并有方法registerperform。你可以为FnArgs实现反序列化器,而不是使用中间结构体进行转换。你可以使用Vec<String>作为args,但是我使用了枚举作为例子,所以你可以有一些变体来避免解析(例如,你可以有一个特殊的整型参数变体)。这主要是为了给予你一些想法,你可以如何实现这个功能。
缺点是所有可以通过config访问的函数必须有相同的签名,并且需要解析它们的参数。使用proc宏可以删除一些样板文件,但我没有太多经验。

相关问题