Skip to content

Implement hooks via a trait and autoref specialization #1121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

ahl
Copy link
Collaborator

@ahl ahl commented Apr 19, 2025

So far we've allowed progenitor consumers to customize the generated code to calls to custom code before (pre-) and after (post-) the execution of all HTTP requests. This has been fine, but we've introduced now 4 variants with both overlap and significant limitation. In particular, when we wanted to include additional debugging output to our generated CLI, it turned out to be quite awkward and it wasn't clear how we could achieve our goals without also screwing up the SDK's interface.

This PR proposes a new trait that looks like this:

#[allow(async_fn_in_trait)]
pub trait ClientHooks<Inner = ()>
where
    Self: ClientInfo<Inner>,
{
    /// Implement to execute code prior to the execution of all requests.
    async fn pre<T>(&self, _request: &mut reqwest::Request) -> std::result::Result<(), Error<T>> {
        Ok(())
    }

    /// Implement to execute code after the execution of all requests.
    async fn post<T>(
        &self,
        _result: &reqwest::Result<reqwest::Response>,
    ) -> std::result::Result<(), Error<T>> {
        Ok(())
    }

    /// Implement to bracket the `execute` Future with your own code. Do not
    /// forget to `execute.await` at some point.
    async fn wrap(
        &self,
        execute: impl std::future::Future<Output = reqwest::Result<reqwest::Response>>,
    ) -> reqwest::Result<reqwest::Response> {
        execute.await
    }

    /// Implement to customize the execution of the given request.
    async fn exec(&self, request: reqwest::Request) -> reqwest::Result<reqwest::Response> {
        self.client().execute(request).await
    }
}

Generated code will implement this by default for &Client. Consumers who wish to customize things get to do so by simply impl ClientHooks for Client { .. } adjacent to the generated code. Today, hooks are specified in hard-to-debug quote! { } generated TokenStreams; with this change, we can effectively do the same thing--and more--in normal Rust code.

I'm not wed to aspects of the interface. For example, I thought that wrap would be useful, but now I'm not sure it is, so perhaps we remove it. I'm not sure of the appropriate return type for all of these methods--we may want to further modify the progenitor_client::Error type to adapt it to this new modality.

If this works out, I would definitely like to deprecate and/or remove the existing hooks as they're finicky, weird, poorly document, and hard to document.

You can see an example if this in action here: oxidecomputer/oxide.rs#1081. It does seem a bit more reasonable than the current mechanism.

@ahl ahl requested a review from wfchandler April 19, 2025 01:06
Copy link
Contributor

@wfchandler wfchandler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this looks very clean. Just to validate how robust this is, I tried implementing the trait in oxide.rs against &Client and got a very understandable error.


/// Implement to bracket the `execute` Future with your own code. Do not
/// forget to `execute.await` at some point.
async fn wrap(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the elegance of this method, but it has a very large overlap with exec and doesn't grant access to the request, which I think most consumers would want.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants