mercredi 7 décembre 2022

How do I inspect function arguments at runtime in Rust?

Say I have a trait that looks like this:

use std::{error::Error, fmt::Debug};
use super::CheckResult;

/// A Checker is a component that is responsible for checking a
/// particular aspect of the node under investigation, be that metrics,
/// system information, API checks, load tests, etc.
#[async_trait::async_trait]
pub trait Checker: Debug + Sync + Send {
    type Input: Debug;

    /// This function is expected to take input, whatever that may be,
    /// and return a vec of check results.
    async fn check(&self, input: &Self::Input) -> anyhow::Result<Vec<CheckResult>>;
}

And say I have two implementations of this trait:

pub struct ApiData {
    some_response: String,
}

pub MetricsData {
    number_of_events: u64,
}

pub struct ApiChecker;

impl Checker for ApiChecker { 
    type Input = ApiData;

    // implement check function
}
 
pub struct MetricsChecker;

impl Checker for MetricsChecker { 
    type Input = MetricsData;

    // implement check function
}  

In my code I have a Vec of these Checkers that looks like this:

pub struct MyServer {
    checkers: Vec<Box<dyn Checker>>,
}

What I want to do is figure out, based on what Checkers are in this Vec, what data I need to fetch. For example, if it just contained an ApiChecker, I would only need to fetch the ApiData. If both ApiChecker and MetricsChecker were there, I'd need both ApiData and MetricsData. You can also imagine a third checker where Input = (ApiData, MetricsData). In that case I'd still just need to fetch ApiData and MetricsData once.

I imagine an approach where the Checker trait has an additional function on it that looks like this:

fn required_data(&self) -> HashSet<DataId>;

This could then return something like [DataId::Api, DataId::Metrics]. I would then run this for all Checkers in my vec and then I'd end up a complete list of data I need to get. I could then do some complicated set of checks like this:

let mut required_data = HashSet::new();
for checker in checkers {
    required_data.union(&mut checker.required_data());
}

let api_data: Option<ApiData> = None;
if required_data.contains(DataId::Api) {
    api_data = Some(get_api_data());
}

And so on for each of the data types.

I'd then pass them into the check calls like this:

api_checker.check(
    api_data.expect("There was some logic error and we didn't get the API data even though a Checker declared that it needed it")
);

The reasons I want to fetch the data outside of the Checkers is:

  1. To avoid fetching the same data multiple times.
  2. To support memoization between unrelated calls where the arguments are the same (this could be done inside some kind of Fetcher trait implementation for example).
  3. To support generic retry logic.

By now you can probably see that I've got two big problems:

  1. The declaration of what data a specific Checker needs is duplicated, once in the function signature and again from the required_data function. This naturally introduces bug potential. Ideally this information would only be declared once.
  2. Similarly, in the calling code, I have to trust that the data that the Checkers said they needed was actually accurate (the expect in the previous snippet). If it's not, and we didn't get data we needed, there will be problems.

I think both of these problems would be solved if the function signature, and specifically the Input associated type, was able to express this "required data" declaration on its own. Unfortunately I'm not sure how to do that. I see there is a nightly feature in any that implements Provider and Demand: https://doc.rust-lang.org/std/any/index.html#provider-and-demand. This sort of sounds like what I want, but I have to use stable Rust, plus I figure I must be missing something and there is an easier way to do this without going rogue with semi dynamic typing.

tl;dr: How can I inspect what types the arguments are for a function (keeping in mind that the input might be more complex than just one thing, such as a struct or tuple) at runtime from outside the trait implementer? Alternatively, is there a better way to design this code that would eliminate the need for this kind of reflection?





Aucun commentaire:

Enregistrer un commentaire