Skip to content

Commit

Permalink
change client middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
lewisjkl committed Nov 22, 2022
1 parent 98641b5 commit 75313f5
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 58 deletions.
54 changes: 32 additions & 22 deletions modules/docs/markdown/06-guides/endpoint-middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ As of version `0.17.x` of smithy4s, we have changed this by providing a new mech

In this guide, we will show how you can implement a smithy4s middleware that is aware of the authentication traits in your specification and is able to implement authenticate on an endpoint-by-endpoint basis. This is useful if you have different or no authentication on one or more endpoints.

## EndpointSpecificMiddleware
## ServerEndpointMiddleware / ClientEndpointMiddleware

`EndpointSpecificMiddleware` is the interface that we have provided for implementing middleware. For some use cases, you will need to use the full interface. However, for this guide and for many uses cases, you will be able to rely on the simpler interface called `EndpointSpecificMiddlewareSpec.Simple`. This interface requires a single method which looks as follows:
`ServerEndpointMiddleware` is the interface that we have provided for implementing service middleware. For some use cases, you will need to use the full interface. However, for this guide and for many use cases, you will be able to rely on the simpler interface called `ServerEndpointMiddleware.Simple`. This interface requires a single method which looks as follows:

```scala
def prepareWithHints(
Expand All @@ -22,6 +22,15 @@ def prepareWithHints(

This means that given the hints for the service and a specific endpoint, our implementation will provide a transformation of an `HttpApp`. If you are not familiar with `Hints`, they are the smithy4s construct that represents Smithy Traits. They are called hints to avoid naming conflicts and confusion with Scala `trait`s.

The `ClientEndpointMiddleware` interface is essentially the same as the one for `ServerEndpointMiddleware` with the exception that we are returning a transformation on `Client[F]` instead of `HttpApp[F]`. This looks like:

```scala
def prepareWithHints(
serviceHints: Hints,
endpointHints: Hints
): Client[F] => Client[F]
```

## Smithy Spec

Let's look at the smithy specification that we will use for this guide. First, let's define the service.
Expand Down Expand Up @@ -104,7 +113,7 @@ import org.http4s._
import smithy4s.http4s.SimpleRestJsonBuilder
import smithy4s._
import org.http4s.headers.Authorization
import smithy4s.http4s.EndpointSpecificMiddleware
import smithy4s.http4s.ServerEndpointMiddleware
```

#### AuthChecker
Expand Down Expand Up @@ -170,16 +179,16 @@ Let's break down what we did above step by step. The step numbers below correspo
6. If the token was found to be valid, we pass the request into the `inputApp` from step 2 in order to get a response.
7. If the header was found to be invalid, we return the `NotAuthorizedError` that we defined in our smithy file above.

#### EndpointSpecificMiddleware.Simple
#### ServerEndpointMiddleware.Simple

Next, let's create our middleware by implementing the `EndpointSpecificMiddleware.Simple` interface we discussed above.
Next, let's create our middleware by implementing the `ServerEndpointMiddleware.Simple` interface we discussed above.

```scala mdoc:silent
object AuthMiddleware {
def apply(
authChecker: AuthChecker // 1
): EndpointSpecificMiddleware[IO] =
new EndpointSpecificMiddleware.Simple[IO] {
): ServerEndpointMiddleware[IO] =
new ServerEndpointMiddleware.Simple[IO] {
private val mid: HttpApp[IO] => HttpApp[IO] = middleware(authChecker) // 2
def prepareWithHints(
serviceHints: Hints,
Expand Down Expand Up @@ -229,31 +238,36 @@ To see the **full code** example of what we walk through below, go [here](https:

It is possible that you have a client where you want to apply a similar type of middleware that alters some part of a request depending on the endpoint being targeted. In this part of the guide, we will show how you can do this for a client using the same smithy specification we defined above. We will make it so our authentication token is only sent if we are targeting an endpoint which requires it.

#### EndpointSpecificMiddleware.Simple
#### ClientEndpointMiddleware.Simple

The interface that we define for this middleware is going to look very similar to the one we defined above. This makes sense because this middleware is effectively the dual of the middleware above.

```scala mdoc:invisible
import org.http4s.client._
import smithy4s.http4s.ClientEndpointMiddleware
```

```scala mdoc:silent
object Middleware {

private def middleware(bearerToken: String): HttpApp[IO] => HttpApp[IO] = { // 1
inputApp =>
HttpApp[IO] { request =>
private def middleware(bearerToken: String): Client[IO] => Client[IO] = { // 1
inputClient =>
Client[IO] { request =>
val newRequest = request.withHeaders( // 2
Authorization(Credentials.Token(AuthScheme.Bearer, bearerToken))
)

inputApp(newRequest)
inputClient.run(newRequest)
}
}

def apply(bearerToken: String): EndpointSpecificMiddleware[IO] = // 3
new EndpointSpecificMiddleware.Simple[IO] {
def apply(bearerToken: String): ClientEndpointMiddleware[IO] = // 3
new ClientEndpointMiddleware.Simple[IO] {
private val mid = middleware(bearerToken)
def prepareWithHints(
serviceHints: Hints,
endpointHints: Hints
): HttpApp[IO] => HttpApp[IO] = {
): Client[IO] => Client[IO] = {
serviceHints.get[smithy.api.HttpBearerAuth] match {
case Some(_) =>
endpointHints.get[smithy.api.Auth] match {
Expand All @@ -268,16 +282,12 @@ object Middleware {
}
```

1. Here we are creating an inner middleware function, just like we did above. The only difference is that this time we are adding a value to the request instead of extracting one from it.
2. Add the `Authorization` header to the request and pass it to the `inputApp` that we are transforming in this middleware.
3. This function is actually the *exact same* as the function for the middleware we implemented above. The only difference is that this apply method accepts a `bearerToken` as a parameter. This is the token that we will add into the `Authorization` header when applicable.
1. Here we are creating an inner middleware function, just like we did above. The only differences are that this time we are adding a value to the request instead of extracting one from it and we are operating on `Client` instead of `HttpApp`.
2. Add the `Authorization` header to the request and pass it to the `inputClient` that we are transforming in this middleware.
3. This function is actually the *exact same* as the function for the middleware we implemented above. The only differences are that this apply method accepts a `bearerToken` as a parameter and returns a function on `Client` instead of `HttpApp`. The provided `bearerToken` is what we will add into the `Authorization` header when applicable.

#### SimpleRestJsonBuilder

```scala mdoc:invisible
import org.http4s.client._
```

As above, we now just need to wire our middleware into our actual implementation. Here we are constructing a client and specifying the middleware we just defined.

```scala mdoc:silent
Expand Down
6 changes: 3 additions & 3 deletions modules/guides/src/smithy4s/guides/Auth.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import com.comcast.ip4s._
import smithy4s.http4s.SimpleRestJsonBuilder
import smithy4s.Hints
import org.http4s.headers.Authorization
import smithy4s.http4s.EndpointSpecificMiddleware
import smithy4s.http4s.ServerEndpointMiddleware

final case class ApiToken(value: String)

Expand Down Expand Up @@ -91,8 +91,8 @@ object AuthMiddleware {

def apply(
authChecker: AuthChecker
): EndpointSpecificMiddleware[IO] =
new EndpointSpecificMiddleware.Simple[IO] {
): ServerEndpointMiddleware[IO] =
new ServerEndpointMiddleware.Simple[IO] {
private val mid: HttpApp[IO] => HttpApp[IO] = middleware(authChecker)
def prepareWithHints(
serviceHints: Hints,
Expand Down
14 changes: 7 additions & 7 deletions modules/guides/src/smithy4s/guides/AuthClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,24 @@ object AuthClient {

object Middleware {

private def middleware(bearerToken: String): HttpApp[IO] => HttpApp[IO] = {
inputApp =>
HttpApp[IO] { request =>
private def middleware(bearerToken: String): Client[IO] => Client[IO] = {
inputClient =>
Client[IO] { request =>
val newRequest = request.withHeaders(
Authorization(Credentials.Token(AuthScheme.Bearer, bearerToken))
)

inputApp(newRequest)
inputClient.run(newRequest)
}
}

def apply(bearerToken: String): EndpointSpecificMiddleware[IO] =
new EndpointSpecificMiddleware.Simple[IO] {
def apply(bearerToken: String): ClientEndpointMiddleware[IO] =
new ClientEndpointMiddleware.Simple[IO] {
private val mid = middleware(bearerToken)
def prepareWithHints(
serviceHints: Hints,
endpointHints: Hints
): HttpApp[IO] => HttpApp[IO] = {
): Client[IO] => Client[IO] = {
serviceHints.get[smithy.api.HttpBearerAuth] match {
case Some(_) =>
endpointHints.get[smithy.api.Auth] match {
Expand Down
51 changes: 51 additions & 0 deletions modules/http4s/src/smithy4s/http4s/ClientEndpointMiddleware.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2021-2022 Disney Streaming
*
* Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://disneystreaming.github.io/TOST-1.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package smithy4s
package http4s

import org.http4s.client.Client

// format: off
trait ClientEndpointMiddleware[F[_]] {
def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])(
endpoint: Endpoint[service.Operation, _, _, _, _, _]
): Client[F] => Client[F]
}
// format: on

object ClientEndpointMiddleware {

trait Simple[F[_]] extends ClientEndpointMiddleware[F] {
def prepareWithHints(
serviceHints: Hints,
endpointHints: Hints
): Client[F] => Client[F]

final def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])(
endpoint: Endpoint[service.Operation, _, _, _, _, _]
): Client[F] => Client[F] =
prepareWithHints(service.hints, endpoint.hints)
}

def noop[F[_]]: ClientEndpointMiddleware[F] =
new ClientEndpointMiddleware[F] {
override def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])(
endpoint: Endpoint[service.Operation, _, _, _, _, _]
): Client[F] => Client[F] = identity
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ package http4s
import org.http4s.HttpApp

// format: off
trait EndpointSpecificMiddleware[F[_]] {
trait ServerEndpointMiddleware[F[_]] {
def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])(
endpoint: Endpoint[service.Operation, _, _, _, _, _]
): HttpApp[F] => HttpApp[F]
}
// format: on

object EndpointSpecificMiddleware {
object ServerEndpointMiddleware {

trait Simple[F[_]] extends EndpointSpecificMiddleware[F] {
trait Simple[F[_]] extends ServerEndpointMiddleware[F] {
def prepareWithHints(
serviceHints: Hints,
endpointHints: Hints
Expand All @@ -44,8 +44,8 @@ object EndpointSpecificMiddleware {
private[http4s] type EndpointMiddleware[F[_], Op[_, _, _, _, _]] =
Endpoint[Op, _, _, _, _, _] => HttpApp[F] => HttpApp[F]

def noop[F[_]]: EndpointSpecificMiddleware[F] =
new EndpointSpecificMiddleware[F] {
def noop[F[_]]: ServerEndpointMiddleware[F] =
new ServerEndpointMiddleware[F] {
override def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])(
endpoint: Endpoint[service.Operation, _, _, _, _, _]
): HttpApp[F] => HttpApp[F] = identity
Expand Down
13 changes: 6 additions & 7 deletions modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit
service,
impl,
PartialFunction.empty,
EndpointSpecificMiddleware.noop[F]
ServerEndpointMiddleware.noop[F]
)
}

Expand All @@ -67,7 +67,7 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit
service,
impl,
PartialFunction.empty,
EndpointSpecificMiddleware.noop[F]
ServerEndpointMiddleware.noop[F]
)

}
Expand All @@ -79,15 +79,14 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit
client: Client[F],
val service: smithy4s.Service[Alg],
uri: Uri = uri"http://localhost:8080",
middleware: EndpointSpecificMiddleware[F] =
EndpointSpecificMiddleware.noop[F]
middleware: ClientEndpointMiddleware[F] = ClientEndpointMiddleware.noop[F]
) {

def uri(uri: Uri): ClientBuilder[Alg, F] =
new ClientBuilder[Alg, F](this.client, this.service, uri, this.middleware)

def middleware(
mid: EndpointSpecificMiddleware[F]
mid: ClientEndpointMiddleware[F]
): ClientBuilder[Alg, F] =
new ClientBuilder[Alg, F](this.client, this.service, this.uri, mid)

Expand Down Expand Up @@ -119,7 +118,7 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit
service: smithy4s.Service[Alg],
impl: FunctorAlgebra[Alg, F],
errorTransformation: PartialFunction[Throwable, F[Throwable]],
middleware: EndpointSpecificMiddleware[F]
middleware: ServerEndpointMiddleware[F]
)(implicit
F: EffectCompat[F]
) {
Expand All @@ -138,7 +137,7 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit
new RouterBuilder(service, impl, fe, middleware)

def middleware(
mid: EndpointSpecificMiddleware[F]
mid: ServerEndpointMiddleware[F]
): RouterBuilder[Alg, F] =
new RouterBuilder[Alg, F](service, impl, errorTransformation, mid)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class SmithyHttp4sReverseRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]](
service: smithy4s.Service.Aux[Alg, Op],
client: Client[F],
entityCompiler: EntityCompiler[F],
middleware: EndpointSpecificMiddleware[F]
middleware: ClientEndpointMiddleware[F]
)(implicit effect: EffectCompat[F])
extends FunctorInterpreter[Op, F] {
// format: on
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class SmithyHttp4sRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]](
impl: FunctorInterpreter[Op, F],
errorTransformation: PartialFunction[Throwable, F[Throwable]],
entityCompiler: EntityCompiler[F],
middleware: EndpointSpecificMiddleware[F]
middleware: ServerEndpointMiddleware[F]
)(implicit effect: EffectCompat[F]) {

private val pathParamsKey =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import scodec.bits.ByteVector
import smithy4s.kinds._
import smithy4s.http._
import smithy4s.schema.SchemaAlt
import org.http4s.HttpApp

/**
* A construct that encapsulates interprets and a low-level
Expand All @@ -48,7 +47,7 @@ private[http4s] object SmithyHttp4sClientEndpoint {
client: Client[F],
endpoint: Endpoint[Op, I, E, O, SI, SO],
compilerContext: CompilerContext[F],
middleware: HttpApp[F] => HttpApp[F]
middleware: Client[F] => Client[F]
): Either[
HttpEndpoint.HttpEndpointError,
SmithyHttp4sClientEndpoint[F, Op, I, E, O, SI, SO]
Expand Down Expand Up @@ -83,12 +82,11 @@ private[http4s] class SmithyHttp4sClientEndpointImpl[F[_], Op[_, _, _, _, _], I,
endpoint: Endpoint[Op, I, E, O, SI, SO],
httpEndpoint: HttpEndpoint[I],
compilerContext: CompilerContext[F],
middleware: HttpApp[F] => HttpApp[F]
middleware: Client[F] => Client[F]
)(implicit effect: EffectCompat[F]) extends SmithyHttp4sClientEndpoint[F, Op, I, E, O, SI, SO] {
// format: on

private val transformedClient: Client[F] =
Client.fromHttpApp[F](middleware(client.toHttpApp))
private val transformedClient: Client[F] = middleware(client)

def send(input: I): F[O] = {
transformedClient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private[http4s] object SmithyHttp4sServerEndpoint {
endpoint: Endpoint[Op, I, E, O, SI, SO],
compilerContext: CompilerContext[F],
errorTransformation: PartialFunction[Throwable, F[Throwable]],
middleware: EndpointSpecificMiddleware.EndpointMiddleware[F, Op],
middleware: ServerEndpointMiddleware.EndpointMiddleware[F, Op],
pathParamsKey: Key[PathParams]
): Either[
HttpEndpoint.HttpEndpointError,
Expand Down Expand Up @@ -95,7 +95,7 @@ private[http4s] class SmithyHttp4sServerEndpointImpl[F[_], Op[_, _, _, _, _], I,
httpEndpoint: HttpEndpoint[I],
compilerContext: CompilerContext[F],
errorTransformation: PartialFunction[Throwable, F[Throwable]],
middleware: EndpointSpecificMiddleware.EndpointMiddleware[F, Op],
middleware: ServerEndpointMiddleware.EndpointMiddleware[F, Op],
pathParamsKey: Key[PathParams]
)(implicit F: EffectCompat[F]) extends SmithyHttp4sServerEndpoint[F] {
// format: on
Expand Down
Loading

0 comments on commit 75313f5

Please sign in to comment.