elastic icon indicating copy to clipboard operation
elastic copied to clipboard

Make it more ergonomic to return request builders

Open KodrAus opened this issue 8 years ago • 1 comments

Motivation

Right now, calls to client.search, or client.get_document etc will return a response builder with a few generic parameters like:

SearchRequestBuilder<TSender, TDocument, TBody>

This isn't very nice to include in a function signature.

Single traits

Design

Offer two traits SyncRequest and AsyncRequest that are implemented for many request builders and can be used to erase 'implementation detail' generics from request builders when they're returned from methods.

trait SyncRequest<TResponse> {
    fn send(self) -> Result<TResponse, Error>;
}

trait AsyncRequest<TResponse> {
    type Future: Future<Item = TResponse, Error = Error>;

    fn send(self) -> Self::Future;
}

// Returning an asynchronous search request
fn my_method() -> impl AsyncRequest<SearchResponse<MyType>>;

Multiple traits

Design

One idea is to use traits and impl Trait (or Box) to erase the type:

trait SyncSearchRequest<TDocument> {
    fn send(self) -> Result<SearchResponse<TDocument>>;
}

trait AsyncSearchRequest<TDocument> {
    fn send(self) -> Pending<TDocument>;
}

impl<TDocument, TBody> SyncSearchRequest<TDocument> for SearchRequestBuilder<SyncSender, TDocument, TBody> {
    fn send(self) -> Result<SearchResponse<TDocument>> {
        (self as SearchRequestBuilder<SyncSender, TDocument, TBody>).send()
    }
}

Using traits has a benefit over type definitions in that we can effectively erase generic parameters through a blanket implementation (TBody doesn't appear in the SearchRequest trait).

Open questions

  • Is this intuitive enough? We'd need to add an example for returning request builders from methods
  • What about raw requests?
  • What if the trait name conflicts with an endpoint type?
  • Could we leverage traits better for these things?
  • Is this too much extra boilerplate for request methods?
  • Should these go in the prelude or not?

KodrAus avatar Oct 14 '17 06:10 KodrAus

Another option is an even more generic trait:

trait SyncRequest<TResponse> {
    fn send(self) -> Result<TResponse, Error>;
}

trait AsyncRequest<TResponse> {
    type Future: Future<Item = TResponse, Error = Error>;

    fn send(self) -> Self::Future;
}

// Returning an asynchronous search request
fn my_method() -> impl AsyncRequest<SearchResponse<MyType>>;

In the future, if impl Trait supported type definitions then we could have aliases for these traits. I think this is probably a better approach than above because it's less code, but means callers need to have a better idea of what type to return. I think we could work around this with examples and docs.

KodrAus avatar Oct 14 '17 06:10 KodrAus