From 607ceff654aab820c87ec72c12f8bde8ae100001 Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Wed, 16 Nov 2022 14:53:52 -0700 Subject: [PATCH 01/11] add endpoint specific middleware --- build.sbt | 8 +- modules/guides/smithy/auth.smithy | 41 ++++++ modules/guides/src/smithy4s/guides/Auth.scala | 132 ++++++++++++++++++ .../http4s/EndpointBasedMiddleware.scala | 26 ++++ .../http4s/SimpleProtocolBuilder.scala | 25 +++- .../smithy4s/http4s/SmithyHttp4sRouter.scala | 23 ++- .../SmithyHttp4sServerEndpoint.scala | 42 ++++-- 7 files changed, 271 insertions(+), 26 deletions(-) create mode 100644 modules/guides/smithy/auth.smithy create mode 100644 modules/guides/src/smithy4s/guides/Auth.scala create mode 100644 modules/http4s/src/smithy4s/http4s/EndpointBasedMiddleware.scala diff --git a/build.sbt b/build.sbt index ca93bd723..734c440e6 100644 --- a/build.sbt +++ b/build.sbt @@ -777,9 +777,13 @@ lazy val guides = projectMatrix .in(file("modules/guides")) .dependsOn(http4s) .settings( - Compile / allowedNamespaces := Seq("smithy4s.guides.hello"), + Compile / allowedNamespaces := Seq( + "smithy4s.guides.hello", + "smithy4s.guides.auth" + ), smithySpecs := Seq( - (ThisBuild / baseDirectory).value / "modules" / "guides" / "smithy" / "hello.smithy" + (ThisBuild / baseDirectory).value / "modules" / "guides" / "smithy" / "hello.smithy", + (ThisBuild / baseDirectory).value / "modules" / "guides" / "smithy" / "auth.smithy" ), (Compile / sourceGenerators) := Seq(genSmithyScala(Compile).taskValue), isCE3 := true, diff --git a/modules/guides/smithy/auth.smithy b/modules/guides/smithy/auth.smithy new file mode 100644 index 000000000..a4845a03f --- /dev/null +++ b/modules/guides/smithy/auth.smithy @@ -0,0 +1,41 @@ +$version: "2" + +namespace smithy4s.guides.auth + +use alloy#simpleRestJson + +@simpleRestJson +@httpBearerAuth // add this here +service HelloWorldAuthService { + version: "1.0.0", + operations: [SayWorld, HealthCheck] + errors: [NotAuthorizedError] +} + + +@readonly +@http(method: "GET", uri: "/hello", code: 200) +operation SayWorld { + output: World +} + +@readonly +@http(method: "GET", uri: "/health", code: 200) +@auth([]) +operation HealthCheck { + output := { + @required + message: String + } +} + +structure World { + message: String = "World !" +} + +@error("client") +@httpError(401) +structure NotAuthorizedError { + @required + message: String +} diff --git a/modules/guides/src/smithy4s/guides/Auth.scala b/modules/guides/src/smithy4s/guides/Auth.scala new file mode 100644 index 000000000..32eeff198 --- /dev/null +++ b/modules/guides/src/smithy4s/guides/Auth.scala @@ -0,0 +1,132 @@ +/* + * 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.guides + +import smithy4s.guides.auth._ +import cats.effect._ +import cats.implicits._ +import org.http4s.implicits._ +import org.http4s.ember.server._ +import org.http4s._ +import com.comcast.ip4s._ +import smithy4s.http4s.SimpleRestJsonBuilder +import org.http4s.server.Middleware +import org.typelevel.ci.CIString +import scala.concurrent.duration.Duration +import smithy4s.kinds.{FunctorAlgebra, PolyFunction5, Kind1} +import smithy4s.Hints +import org.http4s.headers.Authorization +import cats.data.OptionT + +final case class APIKey(value: String) + +object HelloWorldAuthImpl extends HelloWorldAuthService[IO] { + def sayWorld(): IO[World] = World().pure[IO] + def healthCheck(): IO[HealthCheckOutput] = HealthCheckOutput("Okay!").pure[IO] +} + +trait AuthChecker { + def isAuthorized(token: APIKey): IO[Boolean] +} + +object AuthChecker extends AuthChecker { + def isAuthorized(token: APIKey): IO[Boolean] = { + IO.pure( + token.value.nonEmpty + ) // put your logic here, currently just makes sure the token is not empty + } +} + +object AuthExampleRoutes { + import org.http4s.server.middleware._ + + private val helloRoutes: Resource[IO, HttpRoutes[IO]] = + SimpleRestJsonBuilder + .routes(HelloWorldAuthImpl) + .middleware(AuthMiddleware.smithy4sMiddleware(AuthChecker)) + .resource + + val all: Resource[IO, HttpRoutes[IO]] = + helloRoutes +} + +object AuthMiddleware { + + private def middleware( + authChecker: AuthChecker + ): HttpApp[IO] => HttpApp[IO] = { inputApp => + HttpApp[IO] { request => + val maybeKey = request.headers + .get[`Authorization`] + .collect { + case Authorization( + Credentials.Token(AuthScheme.Bearer, value) + ) => + value + } + .map { APIKey.apply } + + val isAuthorized = maybeKey + .map { key => + authChecker.isAuthorized(key) + } + .getOrElse(IO.pure(false)) + + isAuthorized.flatMap { + case true => inputApp(request) + case false => + IO.raiseError(new NotAuthorizedError("Not authorized!")) + } + } + } + + def smithy4sMiddleware( + authChecker: AuthChecker + ): smithy4s.http4s.EndpointSpecificMiddleware[HelloWorldAuthServiceGen, IO] = + new smithy4s.http4s.EndpointSpecificMiddleware[ + HelloWorldAuthServiceGen, + IO + ] { + def prepare(service: smithy4s.Service[HelloWorldAuthServiceGen])( + endpoint: smithy4s.Endpoint[service.Operation, _, _, _, _, _] + ): HttpApp[IO] => HttpApp[IO] = { + service.hints.get[smithy.api.HttpBearerAuth] match { + case Some(_) => + val mid = middleware(authChecker) + endpoint.hints.get[smithy.api.Auth] match { + case Some(auths) if auths.value.isEmpty => identity + case _ => mid + } + case None => identity + } + } + } +} + +// test with `curl localhost:9000/hello -H 'Authorization: Bearer Some'` +// or `curl localhost:9000/hello` +object AuthExampleMain extends IOApp.Simple { + val run = (for { + routes <- AuthExampleRoutes.all + server <- EmberServerBuilder + .default[IO] + .withPort(port"9000") + .withHost(host"localhost") + .withHttpApp(routes.orNotFound) + .build + } yield server).useForever +} diff --git a/modules/http4s/src/smithy4s/http4s/EndpointBasedMiddleware.scala b/modules/http4s/src/smithy4s/http4s/EndpointBasedMiddleware.scala new file mode 100644 index 000000000..766bf1e66 --- /dev/null +++ b/modules/http4s/src/smithy4s/http4s/EndpointBasedMiddleware.scala @@ -0,0 +1,26 @@ +package smithy4s +package http4s + +import org.http4s.HttpApp + +// format: off +trait EndpointSpecificMiddleware[Alg[_[_, _, _, _, _]], F[_]] { + def prepare(service: Service[Alg])( + endpoint: Endpoint[service.Operation, _, _, _, _, _] + ): HttpApp[F] => HttpApp[F] +} +// format: on + +object EndpointSpecificMiddleware { + + private[http4s] type EndpointMiddleware[F[_], Op[_, _, _, _, _]] = + Endpoint[Op, _, _, _, _, _] => HttpApp[F] => HttpApp[F] + + def noop[Alg[_[_, _, _, _, _]], F[_]]: EndpointSpecificMiddleware[Alg, F] = + new EndpointSpecificMiddleware[Alg, F] { + override def prepare(service: Service[Alg])( + endpoint: Endpoint[service.Operation, _, _, _, _, _] + ): HttpApp[F] => HttpApp[F] = identity + } + +} diff --git a/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala b/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala index 6d4311dd9..e9f22ea68 100644 --- a/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala +++ b/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala @@ -48,7 +48,8 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit new RouterBuilder[Alg, F]( service, impl, - PartialFunction.empty + PartialFunction.empty, + EndpointSpecificMiddleware.noop[Alg, F] ) } @@ -65,7 +66,8 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit new RouterBuilder[Alg, F]( service, impl, - PartialFunction.empty + PartialFunction.empty, + EndpointSpecificMiddleware.noop[Alg, F] ) } @@ -108,8 +110,11 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit ] private[http4s] ( service: smithy4s.Service[Alg], impl: FunctorAlgebra[Alg, F], - errorTransformation: PartialFunction[Throwable, F[Throwable]] - )(implicit F: EffectCompat[F]) { + errorTransformation: PartialFunction[Throwable, F[Throwable]], + middleware: EndpointSpecificMiddleware[Alg, F] + )(implicit + F: EffectCompat[F] + ) { val entityCompiler = EntityCompiler.fromCodecAPI(codecs) @@ -117,12 +122,17 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit def mapErrors( fe: PartialFunction[Throwable, Throwable] ): RouterBuilder[Alg, F] = - new RouterBuilder(service, impl, fe andThen (e => F.pure(e))) + new RouterBuilder(service, impl, fe andThen (e => F.pure(e)), middleware) def flatMapErrors( fe: PartialFunction[Throwable, F[Throwable]] ): RouterBuilder[Alg, F] = - new RouterBuilder(service, impl, fe) + new RouterBuilder(service, impl, fe, middleware) + + def middleware( + mid: EndpointSpecificMiddleware[Alg, F] + ): RouterBuilder[Alg, F] = + new RouterBuilder[Alg, F](service, impl, errorTransformation, mid) def make: Either[UnsupportedProtocolError, HttpRoutes[F]] = checkProtocol(service, protocolTag) @@ -133,7 +143,8 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit service, service.toPolyFunction[Kind1[F]#toKind5](impl), errorTransformation, - entityCompiler + entityCompiler, + middleware ).routes } diff --git a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala index c42baae2a..ba7d5f52e 100644 --- a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala +++ b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala @@ -23,15 +23,30 @@ import cats.implicits._ import org.http4s._ import smithy4s.http4s.internals.SmithyHttp4sServerEndpoint import smithy4s.kinds._ +import org.typelevel.vault.Key +import cats.Id +import cats.Applicative +import cats.effect.kernel.Unique // format: off class SmithyHttp4sRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]]( service: smithy4s.Service.Aux[Alg, Op], impl: FunctorInterpreter[Op, F], errorTransformation: PartialFunction[Throwable, F[Throwable]], - entityCompiler: EntityCompiler[F] + entityCompiler: EntityCompiler[F], + middleware: EndpointSpecificMiddleware[Alg, F] )(implicit effect: EffectCompat[F]) { + private val pathParamsKey = { + implicit val cheatUnique: Unique[Id] = new Unique[Id] { + def applicative: Applicative[Id] = Applicative[Id] + + def unique: Id[Unique.Token] = new Unique.Token() + + } + Key.newKey[Id, smithy4s.http.PathParams] + } + private val compilerContext = internals.CompilerContext.make(entityCompiler) val routes: HttpRoutes[F] = Kleisli { request => @@ -39,7 +54,7 @@ class SmithyHttp4sRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]]( endpoints <- perMethodEndpoint.get(request.method).toOptionT[F] path = request.uri.path.segments.map(_.decoded()).toArray (endpoint, pathParams) <- endpoints.collectFirstSome(_.matchTap(path)).toOptionT[F] - response <- OptionT.liftF(endpoint.run(pathParams, request)) + response <- OptionT.liftF(endpoint.httpApp(request.withAttribute(pathParamsKey, pathParams))) } yield response } // format: on @@ -51,7 +66,9 @@ class SmithyHttp4sRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]]( impl, ep, compilerContext, - errorTransformation + errorTransformation, + middleware.prepare(service) _, + pathParamsKey ) } .collect { case Right(http4sEndpoint) => diff --git a/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sServerEndpoint.scala b/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sServerEndpoint.scala index 0333a758c..a23322893 100644 --- a/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sServerEndpoint.scala +++ b/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sServerEndpoint.scala @@ -31,6 +31,8 @@ import smithy4s.http.Metadata import smithy4s.http._ import smithy4s.schema.Alt import smithy4s.kinds._ +import org.http4s.HttpApp +import org.typelevel.vault.Key /** * A construct that encapsulates a smithy4s endpoint, and exposes @@ -39,7 +41,7 @@ import smithy4s.kinds._ private[http4s] trait SmithyHttp4sServerEndpoint[F[_]] { def method: org.http4s.Method def matches(path: Array[String]): Option[PathParams] - def run(pathParams: PathParams, request: Request[F]): F[Response[F]] + def httpApp: HttpApp[F] def matchTap( path: Array[String] @@ -49,15 +51,19 @@ private[http4s] trait SmithyHttp4sServerEndpoint[F[_]] { private[http4s] object SmithyHttp4sServerEndpoint { + // format: off def make[F[_]: EffectCompat, Op[_, _, _, _, _], I, E, O, SI, SO]( impl: FunctorInterpreter[Op, F], endpoint: Endpoint[Op, I, E, O, SI, SO], compilerContext: CompilerContext[F], - errorTransformation: PartialFunction[Throwable, F[Throwable]] + errorTransformation: PartialFunction[Throwable, F[Throwable]], + middleware: EndpointSpecificMiddleware.EndpointMiddleware[F, Op], + pathParamsKey: Key[PathParams] ): Either[ HttpEndpoint.HttpEndpointError, SmithyHttp4sServerEndpoint[F] ] = + // format: on HttpEndpoint.cast(endpoint).flatMap { httpEndpoint => toHttp4sMethod(httpEndpoint.method) .leftMap { e => @@ -72,7 +78,9 @@ private[http4s] object SmithyHttp4sServerEndpoint { method, httpEndpoint, compilerContext, - errorTransformation + errorTransformation, + middleware, + pathParamsKey ) } } @@ -87,6 +95,8 @@ private[http4s] class SmithyHttp4sServerEndpointImpl[F[_], Op[_, _, _, _, _], I, httpEndpoint: HttpEndpoint[I], compilerContext: CompilerContext[F], errorTransformation: PartialFunction[Throwable, F[Throwable]], + middleware: EndpointSpecificMiddleware.EndpointMiddleware[F, Op], + pathParamsKey: Key[PathParams] )(implicit F: EffectCompat[F]) extends SmithyHttp4sServerEndpoint[F] { // format: on import compilerContext._ @@ -97,18 +107,22 @@ private[http4s] class SmithyHttp4sServerEndpointImpl[F[_], Op[_, _, _, _, _], I, httpEndpoint.matches(path) } - def run(pathParams: PathParams, request: Request[F]): F[Response[F]] = { - val run: F[O] = for { - metadata <- getMetadata(pathParams, request) - input <- extractInput(metadata, request) - output <- (impl(endpoint.wrap(input)): F[O]) - } yield output + private val applyMiddleware = middleware(endpoint) - run.recoverWith(transformError).attempt.flatMap { - case Left(error) => errorResponse(error) - case Right(output) => successResponse(output) - } - } + override val httpApp: HttpApp[F] = + applyMiddleware(HttpApp[F] { req => + val pathParams = req.attributes.lookup(pathParamsKey).getOrElse(Map.empty) + + val run: F[O] = for { + metadata <- getMetadata(pathParams, req) + input <- extractInput(metadata, req) + output <- (impl(endpoint.wrap(input)): F[O]) + } yield output + + run + .recoverWith(transformError) + .flatMap(successResponse) + }).handleErrorWith(error => Kleisli.liftF(errorResponse(error))) private val inputSchema: Schema[I] = endpoint.input private val outputSchema: Schema[O] = endpoint.output From 9806728b4228fe6759da667bcdde16440cc5fb76 Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Thu, 17 Nov 2022 14:19:10 -0700 Subject: [PATCH 02/11] add simple middleware version --- modules/guides/src/smithy4s/guides/Auth.scala | 17 ++++++++--------- ...e.scala => EndpointSpecificMiddleware.scala} | 13 +++++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) rename modules/http4s/src/smithy4s/http4s/{EndpointBasedMiddleware.scala => EndpointSpecificMiddleware.scala} (64%) diff --git a/modules/guides/src/smithy4s/guides/Auth.scala b/modules/guides/src/smithy4s/guides/Auth.scala index 32eeff198..8fd9ecaff 100644 --- a/modules/guides/src/smithy4s/guides/Auth.scala +++ b/modules/guides/src/smithy4s/guides/Auth.scala @@ -31,6 +31,7 @@ import smithy4s.kinds.{FunctorAlgebra, PolyFunction5, Kind1} import smithy4s.Hints import org.http4s.headers.Authorization import cats.data.OptionT +import smithy4s.http4s.EndpointSpecificMiddleware final case class APIKey(value: String) @@ -96,18 +97,16 @@ object AuthMiddleware { def smithy4sMiddleware( authChecker: AuthChecker - ): smithy4s.http4s.EndpointSpecificMiddleware[HelloWorldAuthServiceGen, IO] = - new smithy4s.http4s.EndpointSpecificMiddleware[ - HelloWorldAuthServiceGen, - IO - ] { - def prepare(service: smithy4s.Service[HelloWorldAuthServiceGen])( - endpoint: smithy4s.Endpoint[service.Operation, _, _, _, _, _] + ): EndpointSpecificMiddleware.Simple[HelloWorldAuthServiceGen, IO] = + new EndpointSpecificMiddleware.Simple[HelloWorldAuthServiceGen, IO] { + def prepareUsingHints( + serviceHints: Hints, + endpointHints: Hints ): HttpApp[IO] => HttpApp[IO] = { - service.hints.get[smithy.api.HttpBearerAuth] match { + serviceHints.get[smithy.api.HttpBearerAuth] match { case Some(_) => val mid = middleware(authChecker) - endpoint.hints.get[smithy.api.Auth] match { + endpointHints.get[smithy.api.Auth] match { case Some(auths) if auths.value.isEmpty => identity case _ => mid } diff --git a/modules/http4s/src/smithy4s/http4s/EndpointBasedMiddleware.scala b/modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala similarity index 64% rename from modules/http4s/src/smithy4s/http4s/EndpointBasedMiddleware.scala rename to modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala index 766bf1e66..6ea008712 100644 --- a/modules/http4s/src/smithy4s/http4s/EndpointBasedMiddleware.scala +++ b/modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala @@ -13,6 +13,19 @@ trait EndpointSpecificMiddleware[Alg[_[_, _, _, _, _]], F[_]] { object EndpointSpecificMiddleware { + trait Simple[Alg[_[_, _, _, _, _]], F[_]] + extends EndpointSpecificMiddleware[Alg, F] { + def prepareUsingHints( + serviceHints: Hints, + endpointHints: Hints + ): HttpApp[F] => HttpApp[F] + + final def prepare(service: Service[Alg])( + endpoint: Endpoint[service.Operation, _, _, _, _, _] + ): HttpApp[F] => HttpApp[F] = + prepareUsingHints(service.hints, endpoint.hints) + } + private[http4s] type EndpointMiddleware[F[_], Op[_, _, _, _, _]] = Endpoint[Op, _, _, _, _, _] => HttpApp[F] => HttpApp[F] From d42fde0782526a6c49b7eabe7692e7e58f4b95fc Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Fri, 18 Nov 2022 16:12:54 -0700 Subject: [PATCH 03/11] move alg from trait to method --- modules/guides/src/smithy4s/guides/Auth.scala | 4 ++-- .../http4s/EndpointSpecificMiddleware.scala | 15 +++++++-------- .../smithy4s/http4s/SimpleProtocolBuilder.scala | 8 ++++---- .../src/smithy4s/http4s/SmithyHttp4sRouter.scala | 2 +- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/modules/guides/src/smithy4s/guides/Auth.scala b/modules/guides/src/smithy4s/guides/Auth.scala index 8fd9ecaff..3d727bf21 100644 --- a/modules/guides/src/smithy4s/guides/Auth.scala +++ b/modules/guides/src/smithy4s/guides/Auth.scala @@ -97,8 +97,8 @@ object AuthMiddleware { def smithy4sMiddleware( authChecker: AuthChecker - ): EndpointSpecificMiddleware.Simple[HelloWorldAuthServiceGen, IO] = - new EndpointSpecificMiddleware.Simple[HelloWorldAuthServiceGen, IO] { + ): EndpointSpecificMiddleware.Simple[IO] = + new EndpointSpecificMiddleware.Simple[IO] { def prepareUsingHints( serviceHints: Hints, endpointHints: Hints diff --git a/modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala b/modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala index 6ea008712..e01e5fb98 100644 --- a/modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala +++ b/modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala @@ -4,8 +4,8 @@ package http4s import org.http4s.HttpApp // format: off -trait EndpointSpecificMiddleware[Alg[_[_, _, _, _, _]], F[_]] { - def prepare(service: Service[Alg])( +trait EndpointSpecificMiddleware[F[_]] { + def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])( endpoint: Endpoint[service.Operation, _, _, _, _, _] ): HttpApp[F] => HttpApp[F] } @@ -13,14 +13,13 @@ trait EndpointSpecificMiddleware[Alg[_[_, _, _, _, _]], F[_]] { object EndpointSpecificMiddleware { - trait Simple[Alg[_[_, _, _, _, _]], F[_]] - extends EndpointSpecificMiddleware[Alg, F] { + trait Simple[F[_]] extends EndpointSpecificMiddleware[F] { def prepareUsingHints( serviceHints: Hints, endpointHints: Hints ): HttpApp[F] => HttpApp[F] - final def prepare(service: Service[Alg])( + final def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])( endpoint: Endpoint[service.Operation, _, _, _, _, _] ): HttpApp[F] => HttpApp[F] = prepareUsingHints(service.hints, endpoint.hints) @@ -29,9 +28,9 @@ object EndpointSpecificMiddleware { private[http4s] type EndpointMiddleware[F[_], Op[_, _, _, _, _]] = Endpoint[Op, _, _, _, _, _] => HttpApp[F] => HttpApp[F] - def noop[Alg[_[_, _, _, _, _]], F[_]]: EndpointSpecificMiddleware[Alg, F] = - new EndpointSpecificMiddleware[Alg, F] { - override def prepare(service: Service[Alg])( + def noop[F[_]]: EndpointSpecificMiddleware[F] = + new EndpointSpecificMiddleware[F] { + override def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])( endpoint: Endpoint[service.Operation, _, _, _, _, _] ): HttpApp[F] => HttpApp[F] = identity } diff --git a/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala b/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala index e9f22ea68..d25cbf80f 100644 --- a/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala +++ b/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala @@ -49,7 +49,7 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit service, impl, PartialFunction.empty, - EndpointSpecificMiddleware.noop[Alg, F] + EndpointSpecificMiddleware.noop[F] ) } @@ -67,7 +67,7 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit service, impl, PartialFunction.empty, - EndpointSpecificMiddleware.noop[Alg, F] + EndpointSpecificMiddleware.noop[F] ) } @@ -111,7 +111,7 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit service: smithy4s.Service[Alg], impl: FunctorAlgebra[Alg, F], errorTransformation: PartialFunction[Throwable, F[Throwable]], - middleware: EndpointSpecificMiddleware[Alg, F] + middleware: EndpointSpecificMiddleware[F] )(implicit F: EffectCompat[F] ) { @@ -130,7 +130,7 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit new RouterBuilder(service, impl, fe, middleware) def middleware( - mid: EndpointSpecificMiddleware[Alg, F] + mid: EndpointSpecificMiddleware[F] ): RouterBuilder[Alg, F] = new RouterBuilder[Alg, F](service, impl, errorTransformation, mid) diff --git a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala index ba7d5f52e..8e0bb6536 100644 --- a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala +++ b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala @@ -34,7 +34,7 @@ class SmithyHttp4sRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]]( impl: FunctorInterpreter[Op, F], errorTransformation: PartialFunction[Throwable, F[Throwable]], entityCompiler: EntityCompiler[F], - middleware: EndpointSpecificMiddleware[Alg, F] + middleware: EndpointSpecificMiddleware[F] )(implicit effect: EffectCompat[F]) { private val pathParamsKey = { From 2ed20711d5f2ca7054b9c4595ccbcf97e7085e8f Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Fri, 18 Nov 2022 16:47:21 -0700 Subject: [PATCH 04/11] add client middleware --- build.sbt | 1 + .../src/smithy4s/guides/AuthClient.scala | 64 +++++++++++++++++++ modules/http4s/src-ce3/Compat.scala | 4 +- .../http4s/SimpleProtocolBuilder.scala | 14 +++- .../http4s/SmithyHttp4sReverseRouter.scala | 6 +- .../SmithyHttp4sClientEndpoint.scala | 15 +++-- 6 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 modules/guides/src/smithy4s/guides/AuthClient.scala diff --git a/build.sbt b/build.sbt index 734c440e6..b616ca7c4 100644 --- a/build.sbt +++ b/build.sbt @@ -789,6 +789,7 @@ lazy val guides = projectMatrix isCE3 := true, libraryDependencies ++= Seq( Dependencies.Http4s.emberServer.value, + Dependencies.Http4s.emberClient.value, Dependencies.Weaver.cats.value % Test ) ) diff --git a/modules/guides/src/smithy4s/guides/AuthClient.scala b/modules/guides/src/smithy4s/guides/AuthClient.scala new file mode 100644 index 000000000..97deb2fe2 --- /dev/null +++ b/modules/guides/src/smithy4s/guides/AuthClient.scala @@ -0,0 +1,64 @@ +package smithy4s.guides + +import smithy4s.guides.auth._ +import smithy4s.http4s._ +import cats.effect._ +import cats.implicits._ +import org.http4s.implicits._ +import org.http4s._ +import com.comcast.ip4s._ +import org.http4s.client._ +import org.http4s.ember.client.EmberClientBuilder +import smithy4s.Hints +import org.http4s.headers.Authorization + +object AuthClient { + def apply(http4sClient: Client[IO]): Resource[IO, HelloWorldAuthService[IO]] = + SimpleRestJsonBuilder(HelloWorldAuthService) + .client(http4sClient) + .uri(Uri.unsafeFromString("http://localhost:9000")) + .middleware(Middleware("my-token")) + .resource +} + +object Middleware { + + private def middleware(bearerToken: String): HttpApp[IO] => HttpApp[IO] = { + inputApp => + HttpApp[IO] { request => + val newRequest = request.withHeaders( + Authorization(Credentials.Token(AuthScheme.Bearer, bearerToken)) + ) + + inputApp(newRequest) + } + } + + def apply(bearerToken: String): EndpointSpecificMiddleware[IO] = + new EndpointSpecificMiddleware.Simple[IO] { + def prepareUsingHints( + serviceHints: Hints, + endpointHints: Hints + ): HttpApp[IO] => HttpApp[IO] = { + serviceHints.get[smithy.api.HttpBearerAuth] match { + case Some(_) => + val mid = middleware(bearerToken) + endpointHints.get[smithy.api.Auth] match { + case Some(auths) if auths.value.isEmpty => identity + case _ => mid + } + case None => identity + } + } + } + +} + +object AuthClientExampleMain extends IOApp.Simple { + val run = (for { + client <- EmberClientBuilder.default[IO].build + authClient <- AuthClient(client) + health <- Resource.eval(authClient.healthCheck().flatMap(IO.println)) + hello <- Resource.eval(authClient.sayWorld().flatMap(IO.println)) + } yield ()).use_ +} diff --git a/modules/http4s/src-ce3/Compat.scala b/modules/http4s/src-ce3/Compat.scala index 98051078b..d1be4e18c 100644 --- a/modules/http4s/src-ce3/Compat.scala +++ b/modules/http4s/src-ce3/Compat.scala @@ -18,7 +18,7 @@ package smithy4s.http4s private[smithy4s] object Compat { trait Package { - private[smithy4s] type EffectCompat[F[_]] = cats.effect.Concurrent[F] - private[smithy4s] val EffectCompat = cats.effect.Concurrent + private[smithy4s] type EffectCompat[F[_]] = cats.effect.Async[F] + private[smithy4s] val EffectCompat = cats.effect.Async } } diff --git a/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala b/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala index d25cbf80f..f7cacea55 100644 --- a/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala +++ b/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala @@ -78,11 +78,18 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit ] private[http4s] ( client: Client[F], val service: smithy4s.Service[Alg], - uri: Uri = uri"http://localhost:8080" + uri: Uri = uri"http://localhost:8080", + middleware: EndpointSpecificMiddleware[F] = + EndpointSpecificMiddleware.noop[F] ) { def uri(uri: Uri): ClientBuilder[Alg, F] = - new ClientBuilder[Alg, F](this.client, this.service, uri) + new ClientBuilder[Alg, F](this.client, this.service, uri, this.middleware) + + def middleware( + mid: EndpointSpecificMiddleware[F] + ): ClientBuilder[Alg, F] = + new ClientBuilder[Alg, F](this.client, this.service, this.uri, mid) def resource: Resource[F, FunctorAlgebra[Alg, F]] = use.leftWiden[Throwable].liftTo[Resource[F, *]] @@ -97,7 +104,8 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit service, client, EntityCompiler - .fromCodecAPI[F](codecs) + .fromCodecAPI[F](codecs), + middleware ) } .map(service.fromPolyFunction[Kind1[F]#toKind5](_)) diff --git a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sReverseRouter.scala b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sReverseRouter.scala index 36ab95b19..efb6e61e4 100644 --- a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sReverseRouter.scala +++ b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sReverseRouter.scala @@ -27,7 +27,8 @@ class SmithyHttp4sReverseRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]]( baseUri: Uri, service: smithy4s.Service.Aux[Alg, Op], client: Client[F], - entityCompiler: EntityCompiler[F] + entityCompiler: EntityCompiler[F], + middleware: EndpointSpecificMiddleware[F] )(implicit effect: EffectCompat[F]) extends FunctorInterpreter[Op, F] { // format: on @@ -55,7 +56,8 @@ class SmithyHttp4sReverseRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]]( baseUri, client, endpoint, - compilerContext + compilerContext, + middleware.prepare(service)(endpoint) ) .left .map { e => diff --git a/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sClientEndpoint.scala b/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sClientEndpoint.scala index 2097b074f..83c53a4fa 100644 --- a/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sClientEndpoint.scala +++ b/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sClientEndpoint.scala @@ -29,6 +29,7 @@ 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 @@ -46,7 +47,8 @@ private[http4s] object SmithyHttp4sClientEndpoint { baseUri: Uri, client: Client[F], endpoint: Endpoint[Op, I, E, O, SI, SO], - compilerContext: CompilerContext[F] + compilerContext: CompilerContext[F], + middleware: HttpApp[F] => HttpApp[F] ): Either[ HttpEndpoint.HttpEndpointError, SmithyHttp4sClientEndpoint[F, Op, I, E, O, SI, SO] @@ -65,7 +67,8 @@ private[http4s] object SmithyHttp4sClientEndpoint { method, endpoint, httpEndpoint, - compilerContext + compilerContext, + middleware ) } } @@ -79,12 +82,16 @@ private[http4s] class SmithyHttp4sClientEndpointImpl[F[_], Op[_, _, _, _, _], I, method: org.http4s.Method, endpoint: Endpoint[Op, I, E, O, SI, SO], httpEndpoint: HttpEndpoint[I], - compilerContext: CompilerContext[F] + compilerContext: CompilerContext[F], + middleware: HttpApp[F] => HttpApp[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)) + def send(input: I): F[O] = { - client + transformedClient .run(inputToRequest(input)) .use { response => outputFromResponse(response) From 35a8b908cc842514a5649647daa65c47ba53fe81 Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Fri, 18 Nov 2022 16:48:24 -0700 Subject: [PATCH 05/11] rm extra comment --- modules/guides/smithy/auth.smithy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/guides/smithy/auth.smithy b/modules/guides/smithy/auth.smithy index a4845a03f..14b95fc3e 100644 --- a/modules/guides/smithy/auth.smithy +++ b/modules/guides/smithy/auth.smithy @@ -5,7 +5,7 @@ namespace smithy4s.guides.auth use alloy#simpleRestJson @simpleRestJson -@httpBearerAuth // add this here +@httpBearerAuth service HelloWorldAuthService { version: "1.0.0", operations: [SayWorld, HealthCheck] From 79a94de2a5a24c69a28f9ac045b96a0255906541 Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Mon, 21 Nov 2022 10:14:36 -0700 Subject: [PATCH 06/11] get compiling with ce2 --- .../src/smithy4s/http4s/SmithyHttp4sRouter.scala | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala index 8e0bb6536..458b17dd3 100644 --- a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala +++ b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala @@ -24,9 +24,7 @@ import org.http4s._ import smithy4s.http4s.internals.SmithyHttp4sServerEndpoint import smithy4s.kinds._ import org.typelevel.vault.Key -import cats.Id -import cats.Applicative -import cats.effect.kernel.Unique +import cats.effect.SyncIO // format: off class SmithyHttp4sRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]]( @@ -37,15 +35,8 @@ class SmithyHttp4sRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]]( middleware: EndpointSpecificMiddleware[F] )(implicit effect: EffectCompat[F]) { - private val pathParamsKey = { - implicit val cheatUnique: Unique[Id] = new Unique[Id] { - def applicative: Applicative[Id] = Applicative[Id] - - def unique: Id[Unique.Token] = new Unique.Token() - - } - Key.newKey[Id, smithy4s.http.PathParams] - } + private val pathParamsKey = + Key.newKey[SyncIO, smithy4s.http.PathParams].unsafeRunSync() private val compilerContext = internals.CompilerContext.make(entityCompiler) From c5224db89813a8e8774e280b9d07fc255e614406 Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Mon, 21 Nov 2022 12:43:04 -0700 Subject: [PATCH 07/11] add tests --- build.sbt | 9 +- modules/guides/src/smithy4s/guides/Auth.scala | 11 +- .../src/smithy4s/guides/AuthClient.scala | 16 ++ .../http4s/EndpointSpecificMiddleware.scala | 16 ++ .../EndpointSpecificMiddlewareSpec.scala | 160 ++++++++++++++++++ sampleSpecs/hello.smithy | 2 + 6 files changed, 207 insertions(+), 7 deletions(-) create mode 100644 modules/http4s/test/src/smithy4s/http4s/EndpointSpecificMiddlewareSpec.scala diff --git a/build.sbt b/build.sbt index b616ca7c4..27e7d06f5 100644 --- a/build.sbt +++ b/build.sbt @@ -621,7 +621,14 @@ lazy val http4s = projectMatrix if (virtualAxes.value.contains(CatsEffect2Axis)) moduleName.value + "-ce2" else moduleName.value - } + }, + Test / allowedNamespaces := Seq( + "smithy4s.hello" + ), + Test / smithySpecs := Seq( + (ThisBuild / baseDirectory).value / "sampleSpecs" / "hello.smithy" + ), + (Test / sourceGenerators) := Seq(genSmithyScala(Test).taskValue) ) .http4sPlatform(allJvmScalaVersions, jvmDimSettings) diff --git a/modules/guides/src/smithy4s/guides/Auth.scala b/modules/guides/src/smithy4s/guides/Auth.scala index 3d727bf21..1bb3de368 100644 --- a/modules/guides/src/smithy4s/guides/Auth.scala +++ b/modules/guides/src/smithy4s/guides/Auth.scala @@ -87,17 +87,16 @@ object AuthMiddleware { } .getOrElse(IO.pure(false)) - isAuthorized.flatMap { - case true => inputApp(request) - case false => - IO.raiseError(new NotAuthorizedError("Not authorized!")) - } + isAuthorized.ifM( + ifTrue = inputApp(request), + ifFalse = IO.raiseError(new NotAuthorizedError("Not authorized!")) + ) } } def smithy4sMiddleware( authChecker: AuthChecker - ): EndpointSpecificMiddleware.Simple[IO] = + ): EndpointSpecificMiddleware[IO] = new EndpointSpecificMiddleware.Simple[IO] { def prepareUsingHints( serviceHints: Hints, diff --git a/modules/guides/src/smithy4s/guides/AuthClient.scala b/modules/guides/src/smithy4s/guides/AuthClient.scala index 97deb2fe2..8b0b22d07 100644 --- a/modules/guides/src/smithy4s/guides/AuthClient.scala +++ b/modules/guides/src/smithy4s/guides/AuthClient.scala @@ -1,3 +1,19 @@ +/* + * 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.guides import smithy4s.guides.auth._ diff --git a/modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala b/modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala index e01e5fb98..d17929e54 100644 --- a/modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala +++ b/modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala @@ -1,3 +1,19 @@ +/* + * 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 diff --git a/modules/http4s/test/src/smithy4s/http4s/EndpointSpecificMiddlewareSpec.scala b/modules/http4s/test/src/smithy4s/http4s/EndpointSpecificMiddlewareSpec.scala new file mode 100644 index 000000000..1211c8ee4 --- /dev/null +++ b/modules/http4s/test/src/smithy4s/http4s/EndpointSpecificMiddlewareSpec.scala @@ -0,0 +1,160 @@ +/* + * 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 weaver._ +import smithy4s.hello._ +import org.http4s.HttpApp +import cats.effect.IO +import cats.data.OptionT +import org.http4s.Uri +import org.http4s._ +import fs2.Collector +import org.http4s.client.Client +import cats.Eq + +object EndpointSpecificMiddlewareSpec extends SimpleIOSuite { + + private implicit val greetingEq: Eq[Greeting] = Eq.fromUniversalEquals + private implicit val throwableEq: Eq[Throwable] = Eq.fromUniversalEquals + + test("server - middleware is applied") { + serverMiddlewareTest( + shouldFailInMiddleware = true, + Request[IO](Method.POST, Uri.unsafeFromString("/bob")), + response => + IO.pure(expect.eql(response.status, Status.InternalServerError)) + ) + } + + test( + "server - middleware allows passing through to underlying implementation" + ) { + serverMiddlewareTest( + shouldFailInMiddleware = false, + Request[IO](Method.POST, Uri.unsafeFromString("/bob")), + response => { + response.body.compile + .to(Collector.supportsArray(Array)) + .map(new String(_)) + .map { body => + expect.eql(response.status, Status.Ok) && + expect.eql(body, """{"message":"Hello, bob"}""") + } + } + ) + } + + test("client - middleware is applied") { + clientMiddlewareTest( + shouldFailInMiddleware = true, + service => + service.hello("bob").attempt.map { result => + expect.eql(result, Left(new GenericServerError(Some("failed")))) + } + ) + } + + test("client - send request through middleware") { + clientMiddlewareTest( + shouldFailInMiddleware = false, + service => + service.hello("bob").attempt.map { result => + expect.eql(result, Right(Greeting("Hello, bob"))) + } + ) + } + + private def serverMiddlewareTest( + shouldFailInMiddleware: Boolean, + request: Request[IO], + expect: Response[IO] => IO[Expectations] + )(implicit pos: SourceLocation): IO[Expectations] = { + val service = + SimpleRestJsonBuilder + .routes(HelloImpl) + .middleware(new TestMiddleware(shouldFail = shouldFailInMiddleware)) + .make + .toOption + .get + + service(request) + .flatMap(res => OptionT.liftF(expect(res))) + .getOrElse( + failure("unable to run request") + ) + } + + private def clientMiddlewareTest( + shouldFailInMiddleware: Boolean, + expect: HelloWorldService[IO] => IO[Expectations] + ): IO[Expectations] = { + val serviceNoMiddleware: HttpApp[IO] = + SimpleRestJsonBuilder + .routes(HelloImpl) + .make + .toOption + .get + .orNotFound + + val client: HelloWorldService[IO] = { + val http4sClient = Client.fromHttpApp(serviceNoMiddleware) + SimpleRestJsonBuilder(HelloWorldService) + .client(http4sClient) + .middleware(new TestMiddleware(shouldFail = shouldFailInMiddleware)) + .use + .toOption + .get + } + + expect(client) + } + + private object HelloImpl extends HelloWorldService[IO] { + def hello(name: String, town: Option[String]): IO[Greeting] = IO.pure( + Greeting(s"Hello, $name") + ) + } + + private final class TestMiddleware(shouldFail: Boolean) + extends EndpointSpecificMiddleware.Simple[IO] { + def prepareUsingHints( + serviceHints: Hints, + endpointHints: Hints + ): HttpApp[IO] => HttpApp[IO] = { inputApp => + HttpApp[IO] { request => + val hasTag: (Hints, String) => Boolean = (hints, tagName) => + hints.get[smithy.api.Tags].exists(_.value.contains(tagName)) + // check for tags in hints to test that proper hints are sent into the prepare method + if ( + hasTag(serviceHints, "testServiceTag") && + hasTag(endpointHints, "testOperationTag") + ) { + if (shouldFail) { + IO.raiseError(new GenericServerError(Some("failed"))) + } else { + inputApp(request) + } + } else { + IO.raiseError(new Exception("didn't find tags in hints")) + } + } + } + } + +} diff --git a/sampleSpecs/hello.smithy b/sampleSpecs/hello.smithy index c3115c3b8..8a47e2ed2 100644 --- a/sampleSpecs/hello.smithy +++ b/sampleSpecs/hello.smithy @@ -3,6 +3,7 @@ namespace smithy4s.hello use alloy#simpleRestJson @simpleRestJson +@tags(["testServiceTag"]) service HelloWorldService { version: "1.0.0", // Indicates that all operations in `HelloWorldService`, @@ -18,6 +19,7 @@ structure GenericServerError { } @http(method: "POST", uri: "/{name}", code: 200) +@tags(["testOperationTag"]) operation Hello { input: Person, output: Greeting From 525195e892251a72deed3cf003be7018f64f173b Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Mon, 21 Nov 2022 14:09:04 -0700 Subject: [PATCH 08/11] add docs --- build.sbt | 1 + .../markdown/06-guides/endpoint-middleware.md | 296 ++++++++++++++++++ modules/guides/src/smithy4s/guides/Auth.scala | 19 +- .../src/smithy4s/guides/AuthClient.scala | 2 +- 4 files changed, 305 insertions(+), 13 deletions(-) create mode 100644 modules/docs/markdown/06-guides/endpoint-middleware.md diff --git a/build.sbt b/build.sbt index 27e7d06f5..48c2e9ed3 100644 --- a/build.sbt +++ b/build.sbt @@ -116,6 +116,7 @@ lazy val docs = Compile / smithySpecs := Seq( (Compile / sourceDirectory).value / "smithy", (ThisBuild / baseDirectory).value / "sampleSpecs" / "test.smithy", + (ThisBuild / baseDirectory).value / "modules" / "guides" / "smithy" / "auth.smithy", (ThisBuild / baseDirectory).value / "sampleSpecs" / "hello.smithy", (ThisBuild / baseDirectory).value / "sampleSpecs" / "kvstore.smithy" ) diff --git a/modules/docs/markdown/06-guides/endpoint-middleware.md b/modules/docs/markdown/06-guides/endpoint-middleware.md new file mode 100644 index 000000000..b0d4616d1 --- /dev/null +++ b/modules/docs/markdown/06-guides/endpoint-middleware.md @@ -0,0 +1,296 @@ +--- +sidebar_label: Endpoint Specific Middleware +title: Endpoint Specific Middleware +--- + +It used to be the case that any middleware implemented for smithy4s services would have to operate at the http4s level, without any knowledge of smithy4s or access to the constructs to utilizes. + +As of version `0.17.x` of smithy4s, we have changed this by providing a new mechanism to build and provide middleware. This mechanism is aware of the smithy4s service and endpoints that are derived from your smithy specifications. As such, this unlocks the possibility to build middleware that utilizes and is compliant to the traits and shapes of your smithy specification. + +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 + +`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: + +```scala +def prepareUsingHints( + serviceHints: Hints, + endpointHints: Hints + ): HttpApp[F] => HttpApp[F] +``` + +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. + +## Smithy Spec + +Let's look at the smithy specification that we will use for this guide. First, let's define the service. + +```kotlin +$version: "2" + +namespace smithy4s.guides.auth + +use alloy#simpleRestJson + +@simpleRestJson +@httpBearerAuth +service HelloWorldAuthService { + version: "1.0.0", + operations: [SayWorld, HealthCheck] + errors: [NotAuthorizedError] +} +``` + +Here we defined a service that has two operations, `SayWorld` and `HealthCheck`. We defined it such that any of these operations may return an `NotAuthorizedError`. Finally, we annotated the service with the `@httpBearerAuth` [trait](https://smithy.io/2.0/spec/authentication-traits.html#httpbearerauth-trait) to indicate that the service supports authentication via a bearer token. If you are using a different authentication scheme, you can still follow this guide and adapt it for your needs. You can find a full list of smithy-provided schemes [here](https://smithy.io/2.0/spec/authentication-traits.html). If none of the provided traits suit your use case, you can always create a custom trait too. + +Next, let's define our first operation, `SayWorld`: + +```kotlin +@readonly +@http(method: "GET", uri: "/hello", code: 200) +operation SayWorld { + output: World +} + +structure World { + message: String = "World !" +} +``` + +There is nothing authentication-specific defined with this operation, this means that the operation inherits the service-defined authentication scheme (`httpBearerAuth` in this case). Let's contrast this with the `HealthCheck` operation: + +```kotlin +@readonly +@http(method: "GET", uri: "/health", code: 200) +@auth([]) +operation HealthCheck { + output := { + @required + message: String + } +} +``` + +Notice that on this operation we have added the `@auth([])` trait with an empty array. This means that there is no authentication required for this endpoint. In other words, although the service defines an authentication scheme of `httpBearerAuth`, that scheme will not apply to this endpoint. + +Finally, let's define the `NotAuthorizedError` that will be returned when an authentication token is missing or invalid. + +```kotlin +@error("client") +@httpError(401) +structure NotAuthorizedError { + @required + message: String +} +``` + +There is nothing authentication specific about this error, this is a standard smithy http error that will have a 401 status code when returned. + +If you want to see the full smithy model we defined above, you can do so [here](https://github.com/disneystreaming/smithy4s/blob/main/modules/guides/smithy/auth.smithy). + +## Server-side Middleware + +To see the **full code** example of what we walk through below, go [here](https://github.com/disneystreaming/smithy4s/tree/main/modules/guides/src/smithy4s/guides/Auth.scala). + +We will create a server-side middleware that implements the authentication as defined in the smithy spec above. Let's start by creating a few classes that we will use in our middleware. + +```scala mdoc:invisible +import smithy4s.guides.auth._ +import cats.effect._ +import cats.implicits._ +import org.http4s.implicits._ +import org.http4s._ +import smithy4s.http4s.SimpleRestJsonBuilder +import smithy4s._ +import org.http4s.headers.Authorization +import smithy4s.http4s.EndpointSpecificMiddleware +``` + +#### AuthChecker + +```scala mdoc:silent +case class ApiToken(value: String) + +trait AuthChecker { + def isAuthorized(token: ApiToken): IO[Boolean] +} + +object AuthChecker extends AuthChecker { + def isAuthorized(token: ApiToken): IO[Boolean] = { + IO.pure( + token.value.nonEmpty + ) // put your logic here, currently just makes sure the token is not empty + } +} +``` + +This is a simple class that we will use to check the validity of a given token. This will be more complex in your own service, but we are keeping it simple here since it is out of the scope of this article and implementations will vary widely depending on your specific application. + +#### The Inner Middleware Implementation + +This function is what is called once we have made sure that the middleware is applicable for a given endpoint. We will show in the next step how to tell if the middleware is applicable or not. For now though, we will just focus on what the middleware does once we know that it needs to be applied to a given endpoint. + +```scala mdoc:silent +def middleware( + authChecker: AuthChecker // 1 +): HttpApp[IO] => HttpApp[IO] = { inputApp => // 2 + HttpApp[IO] { request => // 3 + val maybeKey = request.headers // 4 + .get[`Authorization`] + .collect { + case Authorization( + Credentials.Token(AuthScheme.Bearer, value) + ) => + value + } + .map { ApiToken.apply } + + val isAuthorized = maybeKey + .map { key => + authChecker.isAuthorized(key) // 5 + } + .getOrElse(IO.pure(false)) + + isAuthorized.ifM( + ifTrue = inputApp(request), // 6 + ifFalse = IO.raiseError(new NotAuthorizedError("Not authorized!")) // 7 + ) + } +} +``` + +Let's break down what we did above step by step. The step numbers below correspond to the comment numbers above. + +1. Pass an instance of `AuthChecker` that we can use to verify auth tokens are valid in this middleware +2. `inputApp` is the `HttpApp[IO]` that we are transforming in this middleware. +3. Here we create a new HttpApp, the one that we will be returning from this function we are creating. +4. Here we extract the value of the `Authorization` header, if it is present. +5. If the header had a value, we now send that value into the `AuthChecker` to see if it is valid. +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 + +Next, let's create our middleware by implementing the `EndpointSpecificMiddleware.Simple` interface we discussed above. + +```scala mdoc:silent +object AuthMiddleware { + def apply( + authChecker: AuthChecker // 1 + ): EndpointSpecificMiddleware[IO] = + new EndpointSpecificMiddleware.Simple[IO] { + private val mid: HttpApp[IO] => HttpApp[IO] = middleware(authChecker) // 2 + def prepareUsingHints( + serviceHints: Hints, + endpointHints: Hints + ): HttpApp[IO] => HttpApp[IO] = { + serviceHints.get[smithy.api.HttpBearerAuth] match { // 3 + case Some(_) => + endpointHints.get[smithy.api.Auth] match { // 4 + case Some(auths) if auths.value.isEmpty => identity // 5 + case _ => mid // 6 + } + case None => identity + } + } + } +} +``` + +1. Pass in an instance of `AuthChecker` for the middleware to use. This is how the middleware will know if a given token is valid or not. +2. This is the function that we defined in the step above. +3. Check and see if the service at hand does in fact have the `httpBearerAuth` trait on it. If it doesn't, then we will not do our auth checks. If it does, then we will proceed. +4. Here we are getting the `@auth` trait from the operation (endpoint in smithy4s lingo). We need to check for this trait because of step 5. +5. Here we are checking that IF the auth trait is on this endpoint AND the auth trait contains an empty array THEN we are performing NO authentication checks. This is how we handle the `@auth([])` trait that is present on the `HealthCheck` operation we defined above. +6. IF the auth trait is NOT present on the operation, OR it is present AND it contains one or more authentication schemes, we apply the middleware. + +#### Using the Middleware + +From here, we can pass our middleware into our `SimpleRestJsonBuilder` as follows: + +```scala mdoc:silent +object HelloWorldAuthImpl extends HelloWorldAuthService[IO] { + def sayWorld(): IO[World] = World().pure[IO] + def healthCheck(): IO[HealthCheckOutput] = HealthCheckOutput("Okay!").pure[IO] +} + +val routes = SimpleRestJsonBuilder + .routes(HelloWorldAuthImpl) + .middleware(AuthMiddleware(AuthChecker)) + .resource +``` + +And that's it. Now we have a middleware that will apply an authentication check on incoming requests whenever relevant, as defined in our smithy file. + +## Client-side Middleware + +To see the **full code** example of what we walk through below, go [here](https://github.com/disneystreaming/smithy4s/tree/main/modules/guides/src/smithy4s/guides/AuthClient.scala). + +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 + +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:silent +object Middleware { + + private def middleware(bearerToken: String): HttpApp[IO] => HttpApp[IO] = { // 1 + inputApp => + HttpApp[IO] { request => + val newRequest = request.withHeaders( // 2 + Authorization(Credentials.Token(AuthScheme.Bearer, bearerToken)) + ) + + inputApp(newRequest) + } + } + + def apply(bearerToken: String): EndpointSpecificMiddleware[IO] = // 3 + new EndpointSpecificMiddleware.Simple[IO] { + private val mid = middleware(bearerToken) + def prepareUsingHints( + serviceHints: Hints, + endpointHints: Hints + ): HttpApp[IO] => HttpApp[IO] = { + serviceHints.get[smithy.api.HttpBearerAuth] match { + case Some(_) => + endpointHints.get[smithy.api.Auth] match { + case Some(auths) if auths.value.isEmpty => identity + case _ => mid + } + case None => identity + } + } + } + +} +``` + +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. + +#### 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 +def apply(http4sClient: Client[IO]): Resource[IO, HelloWorldAuthService[IO]] = + SimpleRestJsonBuilder(HelloWorldAuthService) + .client(http4sClient) + .uri(Uri.unsafeFromString("http://localhost:9000")) + .middleware(Middleware("my-token")) // creating our middleware here + .resource +``` + +## Conclusion + +Once again, if you want to see the **full code** examples of the above, you can find them [here](https://github.com/disneystreaming/smithy4s/tree/main/modules/guides/src/smithy4s/guides/). + +Hopefully this guide gives you a good idea of how you can create a middleware that takes your smithy specification into account. This guide shows a very simple use case of what is possible with a middleware like this. If you have a more advanced use case, you can use this guide as a reference and as always you can reach out to us for insight or help. diff --git a/modules/guides/src/smithy4s/guides/Auth.scala b/modules/guides/src/smithy4s/guides/Auth.scala index 1bb3de368..246e84cd6 100644 --- a/modules/guides/src/smithy4s/guides/Auth.scala +++ b/modules/guides/src/smithy4s/guides/Auth.scala @@ -24,16 +24,11 @@ import org.http4s.ember.server._ import org.http4s._ import com.comcast.ip4s._ import smithy4s.http4s.SimpleRestJsonBuilder -import org.http4s.server.Middleware -import org.typelevel.ci.CIString -import scala.concurrent.duration.Duration -import smithy4s.kinds.{FunctorAlgebra, PolyFunction5, Kind1} import smithy4s.Hints import org.http4s.headers.Authorization -import cats.data.OptionT import smithy4s.http4s.EndpointSpecificMiddleware -final case class APIKey(value: String) +final case class ApiToken(value: String) object HelloWorldAuthImpl extends HelloWorldAuthService[IO] { def sayWorld(): IO[World] = World().pure[IO] @@ -41,11 +36,11 @@ object HelloWorldAuthImpl extends HelloWorldAuthService[IO] { } trait AuthChecker { - def isAuthorized(token: APIKey): IO[Boolean] + def isAuthorized(token: ApiToken): IO[Boolean] } object AuthChecker extends AuthChecker { - def isAuthorized(token: APIKey): IO[Boolean] = { + def isAuthorized(token: ApiToken): IO[Boolean] = { IO.pure( token.value.nonEmpty ) // put your logic here, currently just makes sure the token is not empty @@ -58,7 +53,7 @@ object AuthExampleRoutes { private val helloRoutes: Resource[IO, HttpRoutes[IO]] = SimpleRestJsonBuilder .routes(HelloWorldAuthImpl) - .middleware(AuthMiddleware.smithy4sMiddleware(AuthChecker)) + .middleware(AuthMiddleware(AuthChecker)) .resource val all: Resource[IO, HttpRoutes[IO]] = @@ -79,7 +74,7 @@ object AuthMiddleware { ) => value } - .map { APIKey.apply } + .map { ApiToken.apply } val isAuthorized = maybeKey .map { key => @@ -94,17 +89,17 @@ object AuthMiddleware { } } - def smithy4sMiddleware( + def apply( authChecker: AuthChecker ): EndpointSpecificMiddleware[IO] = new EndpointSpecificMiddleware.Simple[IO] { + private val mid: HttpApp[IO] => HttpApp[IO] = middleware(authChecker) def prepareUsingHints( serviceHints: Hints, endpointHints: Hints ): HttpApp[IO] => HttpApp[IO] = { serviceHints.get[smithy.api.HttpBearerAuth] match { case Some(_) => - val mid = middleware(authChecker) endpointHints.get[smithy.api.Auth] match { case Some(auths) if auths.value.isEmpty => identity case _ => mid diff --git a/modules/guides/src/smithy4s/guides/AuthClient.scala b/modules/guides/src/smithy4s/guides/AuthClient.scala index 8b0b22d07..8413d8256 100644 --- a/modules/guides/src/smithy4s/guides/AuthClient.scala +++ b/modules/guides/src/smithy4s/guides/AuthClient.scala @@ -52,13 +52,13 @@ object Middleware { def apply(bearerToken: String): EndpointSpecificMiddleware[IO] = new EndpointSpecificMiddleware.Simple[IO] { + private val mid = middleware(bearerToken) def prepareUsingHints( serviceHints: Hints, endpointHints: Hints ): HttpApp[IO] => HttpApp[IO] = { serviceHints.get[smithy.api.HttpBearerAuth] match { case Some(_) => - val mid = middleware(bearerToken) endpointHints.get[smithy.api.Auth] match { case Some(auths) if auths.value.isEmpty => identity case _ => mid From 98641b57b3175ae2d68979ec8d285f8f3b50a7c5 Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Mon, 21 Nov 2022 14:10:02 -0700 Subject: [PATCH 09/11] rename method --- modules/docs/markdown/06-guides/endpoint-middleware.md | 6 +++--- modules/guides/src/smithy4s/guides/Auth.scala | 2 +- modules/guides/src/smithy4s/guides/AuthClient.scala | 2 +- .../src/smithy4s/http4s/EndpointSpecificMiddleware.scala | 4 ++-- .../smithy4s/http4s/EndpointSpecificMiddlewareSpec.scala | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/docs/markdown/06-guides/endpoint-middleware.md b/modules/docs/markdown/06-guides/endpoint-middleware.md index b0d4616d1..351874df1 100644 --- a/modules/docs/markdown/06-guides/endpoint-middleware.md +++ b/modules/docs/markdown/06-guides/endpoint-middleware.md @@ -14,7 +14,7 @@ In this guide, we will show how you can implement a smithy4s middleware that is `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: ```scala -def prepareUsingHints( +def prepareWithHints( serviceHints: Hints, endpointHints: Hints ): HttpApp[F] => HttpApp[F] @@ -181,7 +181,7 @@ object AuthMiddleware { ): EndpointSpecificMiddleware[IO] = new EndpointSpecificMiddleware.Simple[IO] { private val mid: HttpApp[IO] => HttpApp[IO] = middleware(authChecker) // 2 - def prepareUsingHints( + def prepareWithHints( serviceHints: Hints, endpointHints: Hints ): HttpApp[IO] => HttpApp[IO] = { @@ -250,7 +250,7 @@ object Middleware { def apply(bearerToken: String): EndpointSpecificMiddleware[IO] = // 3 new EndpointSpecificMiddleware.Simple[IO] { private val mid = middleware(bearerToken) - def prepareUsingHints( + def prepareWithHints( serviceHints: Hints, endpointHints: Hints ): HttpApp[IO] => HttpApp[IO] = { diff --git a/modules/guides/src/smithy4s/guides/Auth.scala b/modules/guides/src/smithy4s/guides/Auth.scala index 246e84cd6..954f16fff 100644 --- a/modules/guides/src/smithy4s/guides/Auth.scala +++ b/modules/guides/src/smithy4s/guides/Auth.scala @@ -94,7 +94,7 @@ object AuthMiddleware { ): EndpointSpecificMiddleware[IO] = new EndpointSpecificMiddleware.Simple[IO] { private val mid: HttpApp[IO] => HttpApp[IO] = middleware(authChecker) - def prepareUsingHints( + def prepareWithHints( serviceHints: Hints, endpointHints: Hints ): HttpApp[IO] => HttpApp[IO] = { diff --git a/modules/guides/src/smithy4s/guides/AuthClient.scala b/modules/guides/src/smithy4s/guides/AuthClient.scala index 8413d8256..e2cd30087 100644 --- a/modules/guides/src/smithy4s/guides/AuthClient.scala +++ b/modules/guides/src/smithy4s/guides/AuthClient.scala @@ -53,7 +53,7 @@ object Middleware { def apply(bearerToken: String): EndpointSpecificMiddleware[IO] = new EndpointSpecificMiddleware.Simple[IO] { private val mid = middleware(bearerToken) - def prepareUsingHints( + def prepareWithHints( serviceHints: Hints, endpointHints: Hints ): HttpApp[IO] => HttpApp[IO] = { diff --git a/modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala b/modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala index d17929e54..75cd6dbd7 100644 --- a/modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala +++ b/modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala @@ -30,7 +30,7 @@ trait EndpointSpecificMiddleware[F[_]] { object EndpointSpecificMiddleware { trait Simple[F[_]] extends EndpointSpecificMiddleware[F] { - def prepareUsingHints( + def prepareWithHints( serviceHints: Hints, endpointHints: Hints ): HttpApp[F] => HttpApp[F] @@ -38,7 +38,7 @@ object EndpointSpecificMiddleware { final def prepare[Alg[_[_, _, _, _, _]]](service: Service[Alg])( endpoint: Endpoint[service.Operation, _, _, _, _, _] ): HttpApp[F] => HttpApp[F] = - prepareUsingHints(service.hints, endpoint.hints) + prepareWithHints(service.hints, endpoint.hints) } private[http4s] type EndpointMiddleware[F[_], Op[_, _, _, _, _]] = diff --git a/modules/http4s/test/src/smithy4s/http4s/EndpointSpecificMiddlewareSpec.scala b/modules/http4s/test/src/smithy4s/http4s/EndpointSpecificMiddlewareSpec.scala index 1211c8ee4..abff591a3 100644 --- a/modules/http4s/test/src/smithy4s/http4s/EndpointSpecificMiddlewareSpec.scala +++ b/modules/http4s/test/src/smithy4s/http4s/EndpointSpecificMiddlewareSpec.scala @@ -133,7 +133,7 @@ object EndpointSpecificMiddlewareSpec extends SimpleIOSuite { private final class TestMiddleware(shouldFail: Boolean) extends EndpointSpecificMiddleware.Simple[IO] { - def prepareUsingHints( + def prepareWithHints( serviceHints: Hints, endpointHints: Hints ): HttpApp[IO] => HttpApp[IO] = { inputApp => From 75313f5a407bb6021c304029c64b9a6439e14b0f Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Tue, 22 Nov 2022 10:52:22 -0700 Subject: [PATCH 10/11] change client middleware --- .../markdown/06-guides/endpoint-middleware.md | 54 +++++++++++-------- modules/guides/src/smithy4s/guides/Auth.scala | 6 +-- .../src/smithy4s/guides/AuthClient.scala | 14 ++--- .../http4s/ClientEndpointMiddleware.scala | 51 ++++++++++++++++++ ...e.scala => ServerEndpointMiddleware.scala} | 10 ++-- .../http4s/SimpleProtocolBuilder.scala | 13 +++-- .../http4s/SmithyHttp4sReverseRouter.scala | 2 +- .../smithy4s/http4s/SmithyHttp4sRouter.scala | 2 +- .../SmithyHttp4sClientEndpoint.scala | 8 ++- .../SmithyHttp4sServerEndpoint.scala | 4 +- .../EndpointSpecificMiddlewareSpec.scala | 43 +++++++++++++-- 11 files changed, 149 insertions(+), 58 deletions(-) create mode 100644 modules/http4s/src/smithy4s/http4s/ClientEndpointMiddleware.scala rename modules/http4s/src/smithy4s/http4s/{EndpointSpecificMiddleware.scala => ServerEndpointMiddleware.scala} (87%) diff --git a/modules/docs/markdown/06-guides/endpoint-middleware.md b/modules/docs/markdown/06-guides/endpoint-middleware.md index 351874df1..33163c2ba 100644 --- a/modules/docs/markdown/06-guides/endpoint-middleware.md +++ b/modules/docs/markdown/06-guides/endpoint-middleware.md @@ -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( @@ -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. @@ -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 @@ -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, @@ -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 { @@ -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 diff --git a/modules/guides/src/smithy4s/guides/Auth.scala b/modules/guides/src/smithy4s/guides/Auth.scala index 954f16fff..1c85b3804 100644 --- a/modules/guides/src/smithy4s/guides/Auth.scala +++ b/modules/guides/src/smithy4s/guides/Auth.scala @@ -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) @@ -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, diff --git a/modules/guides/src/smithy4s/guides/AuthClient.scala b/modules/guides/src/smithy4s/guides/AuthClient.scala index e2cd30087..4c245c872 100644 --- a/modules/guides/src/smithy4s/guides/AuthClient.scala +++ b/modules/guides/src/smithy4s/guides/AuthClient.scala @@ -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 { diff --git a/modules/http4s/src/smithy4s/http4s/ClientEndpointMiddleware.scala b/modules/http4s/src/smithy4s/http4s/ClientEndpointMiddleware.scala new file mode 100644 index 000000000..84f865662 --- /dev/null +++ b/modules/http4s/src/smithy4s/http4s/ClientEndpointMiddleware.scala @@ -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 + } + +} diff --git a/modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala b/modules/http4s/src/smithy4s/http4s/ServerEndpointMiddleware.scala similarity index 87% rename from modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala rename to modules/http4s/src/smithy4s/http4s/ServerEndpointMiddleware.scala index 75cd6dbd7..ca3557a2f 100644 --- a/modules/http4s/src/smithy4s/http4s/EndpointSpecificMiddleware.scala +++ b/modules/http4s/src/smithy4s/http4s/ServerEndpointMiddleware.scala @@ -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 @@ -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 diff --git a/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala b/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala index f7cacea55..184fac486 100644 --- a/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala +++ b/modules/http4s/src/smithy4s/http4s/SimpleProtocolBuilder.scala @@ -49,7 +49,7 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit service, impl, PartialFunction.empty, - EndpointSpecificMiddleware.noop[F] + ServerEndpointMiddleware.noop[F] ) } @@ -67,7 +67,7 @@ abstract class SimpleProtocolBuilder[P](val codecs: CodecAPI)(implicit service, impl, PartialFunction.empty, - EndpointSpecificMiddleware.noop[F] + ServerEndpointMiddleware.noop[F] ) } @@ -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) @@ -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] ) { @@ -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) diff --git a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sReverseRouter.scala b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sReverseRouter.scala index efb6e61e4..784ad135a 100644 --- a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sReverseRouter.scala +++ b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sReverseRouter.scala @@ -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 diff --git a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala index 458b17dd3..47cc0ec9e 100644 --- a/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala +++ b/modules/http4s/src/smithy4s/http4s/SmithyHttp4sRouter.scala @@ -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 = diff --git a/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sClientEndpoint.scala b/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sClientEndpoint.scala index 83c53a4fa..a2fea9599 100644 --- a/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sClientEndpoint.scala +++ b/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sClientEndpoint.scala @@ -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 @@ -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] @@ -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 diff --git a/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sServerEndpoint.scala b/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sServerEndpoint.scala index a23322893..a49892dfb 100644 --- a/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sServerEndpoint.scala +++ b/modules/http4s/src/smithy4s/http4s/internals/SmithyHttp4sServerEndpoint.scala @@ -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, @@ -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 diff --git a/modules/http4s/test/src/smithy4s/http4s/EndpointSpecificMiddlewareSpec.scala b/modules/http4s/test/src/smithy4s/http4s/EndpointSpecificMiddlewareSpec.scala index abff591a3..d9f2e6955 100644 --- a/modules/http4s/test/src/smithy4s/http4s/EndpointSpecificMiddlewareSpec.scala +++ b/modules/http4s/test/src/smithy4s/http4s/EndpointSpecificMiddlewareSpec.scala @@ -27,8 +27,9 @@ import org.http4s._ import fs2.Collector import org.http4s.client.Client import cats.Eq +import cats.effect.Resource -object EndpointSpecificMiddlewareSpec extends SimpleIOSuite { +object ServerEndpointMiddlewareSpec extends SimpleIOSuite { private implicit val greetingEq: Eq[Greeting] = Eq.fromUniversalEquals private implicit val throwableEq: Eq[Throwable] = Eq.fromUniversalEquals @@ -88,7 +89,9 @@ object EndpointSpecificMiddlewareSpec extends SimpleIOSuite { val service = SimpleRestJsonBuilder .routes(HelloImpl) - .middleware(new TestMiddleware(shouldFail = shouldFailInMiddleware)) + .middleware( + new TestServerMiddleware(shouldFail = shouldFailInMiddleware) + ) .make .toOption .get @@ -116,7 +119,9 @@ object EndpointSpecificMiddlewareSpec extends SimpleIOSuite { val http4sClient = Client.fromHttpApp(serviceNoMiddleware) SimpleRestJsonBuilder(HelloWorldService) .client(http4sClient) - .middleware(new TestMiddleware(shouldFail = shouldFailInMiddleware)) + .middleware( + new TestClientMiddleware(shouldFail = shouldFailInMiddleware) + ) .use .toOption .get @@ -131,8 +136,8 @@ object EndpointSpecificMiddlewareSpec extends SimpleIOSuite { ) } - private final class TestMiddleware(shouldFail: Boolean) - extends EndpointSpecificMiddleware.Simple[IO] { + private final class TestServerMiddleware(shouldFail: Boolean) + extends ServerEndpointMiddleware.Simple[IO] { def prepareWithHints( serviceHints: Hints, endpointHints: Hints @@ -157,4 +162,32 @@ object EndpointSpecificMiddlewareSpec extends SimpleIOSuite { } } + private final class TestClientMiddleware(shouldFail: Boolean) + extends ClientEndpointMiddleware.Simple[IO] { + def prepareWithHints( + serviceHints: Hints, + endpointHints: Hints + ): Client[IO] => Client[IO] = { inputClient => + Client[IO] { request => + val hasTag: (Hints, String) => Boolean = (hints, tagName) => + hints.get[smithy.api.Tags].exists(_.value.contains(tagName)) + // check for tags in hints to test that proper hints are sent into the prepare method + if ( + hasTag(serviceHints, "testServiceTag") && + hasTag(endpointHints, "testOperationTag") + ) { + if (shouldFail) { + Resource.eval(IO.raiseError(new GenericServerError(Some("failed")))) + } else { + inputClient.run(request) + } + } else { + Resource.eval( + IO.raiseError(new Exception("didn't find tags in hints")) + ) + } + } + } + } + } From 67b7819093cbde7b8fe0439bf643eb0e508e8167 Mon Sep 17 00:00:00 2001 From: Jeff Lewis Date: Tue, 22 Nov 2022 10:55:37 -0700 Subject: [PATCH 11/11] don't need Async anymore --- modules/http4s/src-ce3/Compat.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/http4s/src-ce3/Compat.scala b/modules/http4s/src-ce3/Compat.scala index d1be4e18c..98051078b 100644 --- a/modules/http4s/src-ce3/Compat.scala +++ b/modules/http4s/src-ce3/Compat.scala @@ -18,7 +18,7 @@ package smithy4s.http4s private[smithy4s] object Compat { trait Package { - private[smithy4s] type EffectCompat[F[_]] = cats.effect.Async[F] - private[smithy4s] val EffectCompat = cats.effect.Async + private[smithy4s] type EffectCompat[F[_]] = cats.effect.Concurrent[F] + private[smithy4s] val EffectCompat = cats.effect.Concurrent } }