Any way to build an API wrapper like that?

Hi,
I am writing some API wrapper for which there are already alternatives, but just as a way to practice Rust, I wonder how it could be built so that the usage becomes as such:

Example::api::v1::info.get().await
Example::api::v1::info.post(params?).await
Example::api::v1().info(params).get().await

In this way, somehow the entire path of the URL is reflected in the way we access the API.
Is this doable using structs and traits? If yes, how?

1 Like

It's certainly doable with some sort of code generator fed with an openapi spec or something like that. There are probably many tools out there that may fit your need.

This is weird for me, but:

  1. ..::info.get() is not legal Rust. It could be either ..::info::get() or ..::info().get() or ..::info{..}.get()

  2. You'd need to map (hardcode) all possible url parts to modules. There is no handler for "some module path user imported" like you could do in ie. Python.

  3. Modules do not need to reflect fs. You can write

mod Example {
    mod api {
        mod v1 {  pub fn info() {...} ... }
    }
}
2 Likes

this looks good, thanks.
But say Example would be a struct, and I would need the info function to mutate a field in Example, would that still be possible with this solution .3?

how can info know the API key of Example Struct if imported that way?

How should we know where your API key comes from?

i mean say you would have it like : let client = Example::new(key);
and then be able to use it as such:

client.api.v1.info().get();

I know most api wrappers just have their methods listed flat. But, this API i'm using is strange, has overlaps, has methods that return different types, some endpoints are the same name but use put or post.

The motivation is the call chain to be 1:1 like the endpoint path.

  1. ..::info.get() is not legal Rust. It could be either ..::info::get() or ..::info().get() or ..::info{..}.get()

It is totally legal rust and is even used extensively for diesel DSLs. You just need to use struct info: playground:

use core::future::Future;
use std::io;

extern crate tokio;

pub mod api {
    use core::future::Future;
    use std::io;
    
    use super::Request;

    #[allow(non_camel_case_types)]
    pub struct info;
    
    impl Request for info {
        type GetResult = ();
    
        fn get(self) -> impl Future<Output = io::Result<Self::GetResult>> {
            async {
                Ok(())
            }
        }
    }
}

trait Request {
    type GetResult;

    fn get(self) -> impl Future<Output = io::Result<Self::GetResult>>;
}

#[tokio::main]
async fn main() -> io::Result<()> {
    api::info.get().await?;
    Ok(())
}

However I do not know how to make ::api::v1() and ::api::v1::... work at the same time, so Example::api::v1().info(params).get().await needs to be given up.

However if state is indeed needed this approach is not a good idea. client.api.v1.info().get(); variant is definitely theoretically possible, but practical variant should probably be something like either Example::api::v1::info.get(client) or client.api().v1().info().get().

1 Like

No, you have no flexibility with modules here.

You can do it with something similar to builder pattern:

  Example()
    .client()
    .api()
    .v1()
     info()
    .get()
    .await

Problem here is that if the API has a version of info in v1 and v2, it will overlap.

coming from JS, why does rust not have something like default exports?

I've prepared this version, which I think is close to what you're asking. I think the code is a little bit unwieldy and I'd prefer simpler alternatives, but here you go:

2 Likes

thanks, this looks very good. I wonder if we had the API_KEY only stored in Client could Info still access it? Because now api_key is a field in two structs.

I would have implemented such API by dragging reference to the key (or, better, Client) everywhere and not the key itself, but your options here are basically:

  1. Some kind of task-local variable. E.g. this. I have not used it, but it does not look very convenient.
  2. Drag something everywhere: reference if you want key (or the whole client) be reusable, owned value if not.

Since that extra field is not visible to the user dragging it everywhere should be OK. You can also do something like this to have reference in one struct only.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.