Skip to content
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

Allow for third-party routing logic to tap into the Tapir backends #3770

Open
Baccata opened this issue May 17, 2024 · 3 comments
Open

Allow for third-party routing logic to tap into the Tapir backends #3770

Baccata opened this issue May 17, 2024 · 3 comments

Comments

@Baccata
Copy link

Baccata commented May 17, 2024

Hi there 👋 I'm the creator/maintainer of smithy4s, a scala toolset around the smithy IDL.

Smithy4s provides a code-generator that creates protocol-agnostic and third-party-library-agnostic scala code, with a number or abstractions that allow for implementing integrations with various protocols and libraries. Out of the box, it provides an integration with http4s. Internally at $work however, it contains a few more integrations with, in particular, other http libraries.

In essence, smithy4s does some things that are pretty similar to what Tapir does under the hood : when using it server-side, it turns a "front-end" (which comes in the form of domain-specific generated interfaces that users implement) into some routing logic that produce a HttpRequest => Option[F[HttpResponse]], that can be wired to a third party http library to translate that into some router construct specific to that library.

So the fact that smithy4s comes with an http4s integration out of the box doesn't imply that the generated code produced by smithy4s is biased towards a specific flavour of Scala : smithy4s can be used with direct-style scala, cats-effect, zio, scala-futures, etc.

Thing is, we don't have the resources to provide integrations with a similar number of libraries to what Tapir does. I do believe however that with rather small, backward-compatible changes to Tapir, it'd be possible for Tapir to allow third-party front-ends (like Smithy4s) to integrate with its many back-ends.

The POC

Rather than doing a lengthy description, here's a POC

(NB : the demo is using smithy4s-deriving, an experimental library for code-first smithy4s, but it proves that it'd work for smithy4s itself)

Devil's advocate thoughts

I like poking holes into my own ideas so here you go :

Would it be desirable for Tapir maintainers to allow for this, when it essentially helps what could be seen a competing product ? Well, I honestly don't know. The way I see it, I don't think Smithy4s and Tapir are directly competing with one another : Smithy4s aims at offering a smithy-centric, highly focused developer experience that hides the intricacies of protocols away from the users, letting them focus on domain-specific concerns in a rather constrained way. In the case of http, this comes at the cost of not allowing to tap into everything that http allows for, as the notion of "protocols" in smithy equates bounded sets of semantics that facilitate the implementation of inter-compatible SDKs.

Tapir has two main axes of value :

  1. the endpoint DSL (front-end), that provides a sane way to users of tapping into as many HTTP semantics as possible
  2. integrations with virtually all http libraries from the java and scala ecosystems (back-end).

I personally think that the decoupling of those two axes is valuable, in a similar way that the LSP is valuable to decouple language support from edits. A similar decoupling is present in Smithy4s, which has allowed for smithy4s-deriving to see the light of day. Not sure anyone will actually use it, but the possibility is interesting in itself.

Note to the Tapir maintainers :

If the maintainers decide the change is not desirable, I totally respect that 😄. I understand the cost that comes with being an open-source maintainer and I understand that wanting to set a certain direction for the project means saying no to people asking for things.

Thank you for reading, and for your consideration.

@adamw
Copy link
Member

adamw commented May 20, 2024

I wouldn't at all mind integrating with smithy4s, though I'm not sure if this integration would make much sense. Going along with your proposition, we would have to add a method like:

def toRoutes(interpreter: ThirdPartyInterpreter[F]): HttpRoutes[F]

to each of our interpreters. This workload seems equivalent to simply adding these integrations in the smithy repo? I think the only piece of code that you would reuse is the RequestBody / ToResponseBody implementations. Although, they are quite low level, and where only ever envisioned to be used by server interpreters - not when implementing endpoints (same with RawBodyType, I wouldn't expect an end-user to use this). And do you need their flexibility - that is, how would you use the RequestBody and ToResponseBody implementations in your integration?

@Baccata
Copy link
Author

Baccata commented May 21, 2024

though I'm not sure if this integration would make much sense

I'm afraid integrating with the high-level Tapir "Endpoint" construct would require a significant amount of (albeit interesting/fun) work, and I'm quite unsure it'd allow to cater to some of the edge cases that are inherent to smithy protocols that we're already catering to in smithy4s.

Providing access to lower level constructs that'd allow to circumvent the Tapir Endpoint construct and tap directly into the lower levels http requests would allow for the integration to benefit from a large area-surface of Tapir, as well as a large area-surface of Smithy4s, without inducing a large amount of glue code to translate high-level concepts from one side to the other. That's what the POC aims at proving.

Going along with your proposition, we would have to add a method like [...] to each of our interpreters

That is unfortunately correct. I 100% understand why it seems annoying. I however think that it'd allow potentially other third parties than Smithy4s to integrate with Tapir in a similar fashion, which may be desirable (or not, depending on your point of view).

This workload seems equivalent to simply adding these integrations in the smithy repo

Not really, because a lot of the details of the translation from third-party request/response models have been made private in Tapir. The proposed solution doesn't require making those constructs to become public. Moreover Tapir already publishes artifacts for all the libraries it integrates with, which means that the build infrastructure, as well as the test infrastructure required to test the integrations is already present.

It's also worth noting that the ThirdPartyIntepreter interface in the proof-of-concept PR is pretty close to the existing ServerInterpreter construct in Tapir, the main difference being that the B and S type parameters are moved away from the interface and into the method, allowing for a larger degree of polymorphism, at the cost of hiding away the underlying streaming constructs away from the user. But topically it remains something that "belongs" to Tapir, imho (though it's certainly not my prerogative to say so).

Although, they are quite low level, and where only ever envisioned to be used by server interpreters - not when implementing endpoints (same with RawBodyType, I wouldn't expect an end-user to use this)

Well the point is to NOT interface with Tapir's Endpoint construct, but rather to tap into the lower level allowing to interact with the third-party specific constructs. Smithy4s has its own concept of endpoint and its own concept of HttpRequest and HttpResponse, so translating those to Tapir's own ServerRequest and ServerResponse models (and vice versa) is a lot more straightforward.

And do you need their flexibility - that is, how would you use the RequestBody and ToResponseBody implementations in your integration?

Well for better or worse, so far Smithy4s is dealing only with "unary" endpoints (as opposed to "streaming" endpoints) , so knowing exactly how the underlying body is handled by specific libraries it not really important to us at this point. RequestBody and ToResponseBody are interesting/required because of how they allow for translating common byte representations to third-party specific representations and vice versa, by catering to the lowest common denominator that is java types (byte array, byte buffer, input/output streams).

See :

  • toResponseBody usage
  • [fromRequestyBody usage](requestBody <- fromRequestBody.toRaw(request, RawBodyType.ByteArrayBody, maxBytes))

@adamw
Copy link
Member

adamw commented Jun 5, 2024

Sorry for the delayed response :)

And thanks for the details - so as I understand, the main value you'd get from tapir is the ToResponseBody & FromRequestBody abstractions, as well as having a single ServerRequest/ServerResponse to interface with - this sounds reasonable.

The only thing I'd change is the naming. I'd keep the "interpreter" nomenclature reserved for endpoint interpreters (such as we have now: server, client, docs). What you have now as "ThirdPartyInterpreter" is I suppose more of a "GenericRoute" or maybe "AnyHandler" or sth like that? Conceptually, it expresses a route or request handler: a way to translate a request to a response.

Maybe you can create a PR with the changes? We could start with 2-3 server implementations for a start.

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

No branches or pull requests

2 participants