-
Notifications
You must be signed in to change notification settings - Fork 411
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
Refactor middleware to use declarative encoding based on new machinery in zio.http.api #1501
Comments
I would like to take this up |
@afsalthaj Let's work on it together, if you don't mind! Will be done twice as fast. 😆 |
For sure. :)
…On Tue, 20 Sep 2022 at 11:37 pm, John A. De Goes ***@***.***> wrote:
@afsalthaj <https://github.com/afsalthaj> Let's work on it together, if
you don't mind! Will be done twice as fast. 😆
—
Reply to this email directly, view it on GitHub
<#1501 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABY2QJIFNGW4PJRFWSSHJO3V7H4PPANCNFSM6AAAAAAQPQBN6A>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
In our first collaboration, we discussed making the following changes:
This is only half of the design problem: we did NOT have a chance to look at how middleware should be redesigned using a declarative encoding. However, we do have some general notes on a more declarative middleware, composed of four things:
A very naive model of this is something like: trait Middleware[R, E, Input, Output] {
def preaction(in: Input): ZIO[R, E, Unit]
def postaction(response: Response): ZIO[R, E, Output]
} This is not complete (by far!), because it lacks the capabilities of existing middleware (and type parameters), and it is not clear how a single middleware concept would apply to both a service as well as an http. In addition, feeding all of In the next session, we will attempt to resolve these issues. |
One idea: object Example {
// executable middleware preserves existing functionality when needed
trait Middleware[-R, +E, +AIn, -BIn, -AOut, +BOut] {
def apply[R1 <: R, E1 >: E](http: Http[R1, E1, AIn, BIn]): Http[R1, E1, AOut, BOut]
}
// subset of middleware that can be declaratively described
sealed trait APIMiddleware[-R, +E, In, Out] extends Middleware[R, E, Request, Request, Response, Response]
object APIMiddleware {
// possible to "embed" some executable middleware into API though without docs
final case class Executable[-R, +E](executable: Middleware[R, E, Request, Request, Response, Response])
extends APIMiddleware[R, E, Unit, Unit] {
def apply[R1 <: R, E1 >: E](http: Http[R1, E1, Request, Request]): Http[R1, E1, Response, Response] =
executable(http)
}
// fixed hierarchy of fully declarative middleware
sealed trait Declarative[-R, +E, In, Out] extends APIMiddleware[R, E, In, Out] {
// can "interpret" any declarative descripton to update an appropriate app
def apply[R1 <: R, E1 >: E](http: Http[R1, E1, Request, Response]): Http[R1, E1, Request, Response] =
???
}
object Declarative {
sealed trait Input[-R, +E, In] extends Declarative[R, E, In, Unit]
object Input {
final case class AddHeader(header: Header) extends Input[Any, Nothing, Unit]
}
sealed trait Output[-R, +E, Out] extends Declarative[R, E, Unit, Out]
sealed trait Execution[-R, +E] extends Declarative[R, E, Unit, Unit]
}
}
} I'm worried about declarative middleware having an |
This might be super close to what Adam said. Conceptually type HttpMiddleware[R, E] = HttpApp[R, E] => HttpApp[R, E]
sealed trait IntrospectableMiddleware[A, B],
which is a ADT with terms representing transformation from API[A, B] to API[A, B]
Do we need any other middleware other than HttpMiddleware ? Hopefully the answer is yes. In fact the very notion of Instead of val middlewares: HttpMiddleware[Any, IOException] =
// print debug info about request and response
Middleware.debug ++
// close connection if request takes more than 3 seconds
Middleware.timeout(3 seconds) ++
// add static header
Middleware.addHeader("X-Environment", "Dev") ++
// add dynamic header
serverTime
// Run it like any simple app
val run = Server.serve(app @@ middlewares).provide(Server.default) We could simply do val app: HttpApp = ???
app
.withTimeOut(3.seconds)
.withDebug
.addResponseHeader(response => Clock.now.map(t => response.addHeader("time" -> t.toString))
|
I had to edit the above snippet a couple of times :) |
Here's the changes I was envisioning being made to final case class API[MiddlewareIn, MiddlewareOut, HandlerIn, HandlerOut](
middlewareIn: In[Query & Headers, MiddlewareIn],
middlewareOut: Out[Headers, Unit],
handlerIn: In[Route & Query & Headers & Body, HandlerIn],
handlerOut: Out[Headers & Body, HandlerOut],
doc: Doc
) (In addition to the changes described above to Now we have a precise description of what middleware requires. Then we can further divide middleware into: I think it's necessary to decrease the power of I think that's fine as for performance reasons we'll be pushing people to |
@adamgfraser I like that direction. To use my names,
In the above code sketch, I think we delete A/B in/out from Middleware (deleting its ability to do transcoding) so it becomes Perhaps, to convert a
If you did not define a middleware spec, then maybe you don't need to provide middleware. Or maybe it should work a bit different: you can implement a |
Actually, final case class API[MiddlewareIn, MiddlewareOut, HandlerIn, HandlerOut](
middlewareIn: In[Query & Headers, MiddlewareIn],
middlewareOut: In[Headers, MiddlewareOut],
handlerIn: In[Route & Query & Headers & Body, HandlerIn],
handlerOut: Out[HandlerOut]
doc: Doc
) In fact maybe we don't need final case class API[MiddlewareIn, MiddlewareOut, HandlerIn, HandlerOut](
middlewareIn: In[Query & Headers, MiddlewareIn],
middlewareOut: In[Headers, MiddlewareOut],
handlerIn: In[Route & Query & Headers & Body, HandlerIn],
handlerOut: In[Body, HandlerOut]
doc: Doc
) Now a handler for middleware must accept Factoring out final case class MiddlewareSpec[MiddlewareIn, MiddlewareOut] {
middlewareIn: In[Query & Headers, MiddlewareIn],
middlewareOut: In[Headers, MiddlewareOut]
)
final case class API[MiddlewareIn, MiddlewareOut, HandlerIn, HandlerOut](
middlewareSpec: MiddlewareSpec[MiddlewareIn, MiddlewareOut],
handlerIn: In[Route & Query & Headers & Body, HandlerIn],
handlerOut: In[Body, HandlerOut]
doc: Doc
) |
Working with these models already |
Question: will this usecase be described using the new encoding? An authentication middleware
The existing executable encoding does 1,2,3 successfully, but not 4. There are a workaround using |
@guersam To accomodate this use case cleanly (i.e. without fiber ref), a middleware would have to be able to produce some value that can be consumed by the handler. I am not sure what that would look like, but we can think about it. |
A very early stage trying to validate just the |
Here is an example in the draft PR on how this looks like in terms of usage val getUser =
API.get(literal("users") / int).out[Int] @@ MiddlewareSpec.addHeader("key", "value") It would be even ideal to have @@ at service level that inspects every API and add the middleware. i.e val addHeader =
MiddlewareSpec.addHeader("key", "value")
val getUser =
API.get(literal("users") / int).out[Int]
val getUsersService =
getUser.handle[Any, Nothing] { case (id: Int) =>
ZIO.succeedNow(1)
}
val getUserPosts =
API
.get(literal("users") / int / literal("posts") / query("name") / int)
val getUserPostsService =
getUserPosts.handle[Any, Nothing] { case (id1, query, id2) => ??? }
val services = (getUsersService ++ getUserPostsService) @@ addHeader // which delegates to `@@` in `API` case class However this implies the documentation need to rely on the API that is accessible from the service, and not the raw APIs. I hope that's the case anyway |
@afsalthaj I think it makes sense, to offer docs based on the combination of endpoints and middleware. That said, I think that it should be possible to combine docs on a higher level. But I don't see this conflicting. |
Can we close this now that we have the |
Is your feature request related to a problem? Please describe.
The middleware uses an executable encoding, which makes introspection impossible. In addition to having some performance implications, this means that it is not known in advance which headers, query parameters, or route segments a piece of middleware will inspect, or if and how a piece of middleware will modify the response headers or bodies.
This means that middleware will not form a part of the documentation generated for
API
endpoints created usingzio.http.api
. So, for example, an authentication middleware will, if used on an endpoint defined usingzio.http.api
, not contribute any relevant details to the documentation for this endpoint when it is generated. Similarly, the automatic client generated "for free" byAPIExecutor
will not be able to provide middleware with whatever it requires.Describe the solution you'd like
In order to solve this problem, as well as create many more possibilities for optimization, the design of middleware needs to be refactored to be declarative. Previously, this would not have been possible, because a declarative description of what headers, query parameters, or route segments needed by middleware did not exist.
Now, thanks to the new machinery in
zio.http.api
, it is possible to declaratively describe the inputs to a middleware. This is a necessary but not sufficient step toward a declarative encoding for middleware.The following shows an early (NOT suitable) design for a declarative encoding:
Describe alternatives you've considered
None.
The text was updated successfully, but these errors were encountered: