Skip to content

Commit

Permalink
Merge pull request #153 from innFactory/feat/middleware
Browse files Browse the repository at this point in the history
Feat/middleware
  • Loading branch information
patsta32 authored Sep 11, 2023
2 parents 23b7eee + 8c1244f commit 59384a3
Show file tree
Hide file tree
Showing 20 changed files with 294 additions and 101 deletions.
6 changes: 5 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ lazy val smithy4play = project
.settings(
sharedSettings,
scalaVersion := Dependencies.scalaVersion,
Compile / smithy4sAllowedNamespaces := List("smithy.test"),
Compile / smithy4sAllowedNamespaces := List("smithy.smithy4play"),
Compile / smithy4sInputDirs := Seq(
(ThisBuild / baseDirectory).value / "smithy4play" / "src" / "resources" / "META_INF" / "smithy"
),
Compile / smithy4sOutputDir := (ThisBuild / baseDirectory).value / "smithy4play" / "target" / "scala-2.13" / "src_managed" / "main",
name := "smithy4play",
scalacOptions += "-Ymacro-annotations",
Compile / compile / wartremoverWarnings ++= Warts.unsafe,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.innfactory.smithy4play

import de.innfactory.smithy4play.middleware.MiddlewareBase
import play.api.mvc.ControllerComponents
import play.api.routing.Router.Routes
import smithy4s.kinds.FunctorAlgebra
Expand All @@ -16,9 +17,9 @@ trait AutoRoutableController {
service: smithy4s.Service[Alg],
ec: ExecutionContext,
cc: ControllerComponents
): Routes =
new SmithyPlayRouter[Alg, F](impl).routes()
): Seq[MiddlewareBase] => Routes = (middlewares: Seq[MiddlewareBase]) =>
new SmithyPlayRouter[Alg, F](impl, service).routes(middlewares)

val routes: Routes
val router: Seq[MiddlewareBase] => Routes

}
Original file line number Diff line number Diff line change
@@ -1,37 +1,45 @@
package de.innfactory.smithy4play

import com.typesafe.config.Config
import de.innfactory.smithy4play.middleware.{ MiddlewareBase, MiddlewareRegistryBase, ValidateAuthMiddleware }
import io.github.classgraph.{ ClassGraph, ScanResult }
import play.api.Application
import play.api.mvc.ControllerComponents
import play.api.routing.Router.Routes

import javax.inject.{ Inject, Singleton }
import java.util.Optional
import javax.inject.{ Inject, Provider, Singleton }
import scala.concurrent.ExecutionContext
import scala.jdk.CollectionConverters.CollectionHasAsScala
import scala.util.Try

@Singleton
class AutoRouter @Inject(
) (implicit
) (validateAuthMiddleware: ValidateAuthMiddleware)(implicit
cc: ControllerComponents,
app: Application,
ec: ExecutionContext,
config: Config
) extends BaseRouter {

private val pkg = config.getString("smithy4play.autoRoutePackage")

override val controllers: Seq[Routes] = {
val pkg = config.getString("smithy4play.autoRoutePackage")
val classGraphScanner: ScanResult = new ClassGraph().enableAllInfo().acceptPackages(pkg).scan()
val controllers = classGraphScanner.getClassesImplementing(classOf[AutoRoutableController])
logger.debug(s"[AutoRouter] found ${controllers.size().toString} Controllers")
val routes = controllers.asScala.map(_.loadClass(true)).map(clazz => createFromClass(clazz)).toSeq
val middlewares = Try {
app.injector.instanceOf[MiddlewareRegistryBase].middlewares
}.toOption.getOrElse(Seq(validateAuthMiddleware))
logger.debug(s"[AutoRouter] found ${controllers.size().toString} controllers")
logger.debug(s"[AutoRouter] found ${middlewares.size.toString} middlewares")
val routes = controllers.asScala.map(_.loadClass(true)).map(clazz => createFromClass(clazz, middlewares)).toSeq
classGraphScanner.close()
routes
}

def createFromClass(clazz: Class[_]): Routes =
private def createFromClass(clazz: Class[_], middlewares: Seq[MiddlewareBase]): Routes =
app.injector.instanceOf(clazz) match {
case c: AutoRoutableController => c.routes
case c: AutoRoutableController => c.router(middlewares)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ object AutoRoutingMacro {
with ..$parentss
with de.innfactory.smithy4play.AutoRoutableController
{ $self =>
override val routes: play.api.routing.Router.Routes = this
override val router: Seq[de.innfactory.smithy4play.middleware.MiddlewareBase] => play.api.routing.Router.Routes = this
..$body }
"""
case _ =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ abstract class BaseRouter(implicit
serviceProvider: smithy4s.Service[Alg],
cc: ControllerComponents,
executionContext: ExecutionContext
): Routes =
new SmithyPlayRouter[Alg, F](impl).routes()
): SmithyPlayRouter[Alg, F] =
new SmithyPlayRouter[Alg, F](impl, serviceProvider)

def chain(
toChain: Seq[Routes]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
package de.innfactory.smithy4play

import play.api.mvc.{ RawBuffer, Request }
import play.api.mvc.{ RawBuffer, Request, RequestHeader }
import smithy4s.{ Hints, ShapeTag }

case class RoutingContext(map: Map[String, Seq[String]])
case class RoutingContext(
headers: Map[String, Seq[String]],
serviceHints: Hints,
endpointHints: Hints,
attributes: Map[String, Any],
requestHeader: RequestHeader
) {
def hasHints(s: ShapeTag.Companion[_]): Boolean = hasEndpointHints(s) || hasServiceHints(s)
def hasServiceHints(s: ShapeTag.Companion[_]): Boolean = serviceHints.has(s.tagInstance)
def hasEndpointHints(s: ShapeTag.Companion[_]): Boolean = endpointHints.has(s.tagInstance)
}

object RoutingContext {
def fromRequest(request: Request[RawBuffer]): RoutingContext =
RoutingContext(request.headers.toMap)
def fromRequest(
request: Request[RawBuffer],
sHints: Hints,
eHints: Hints,
requestHeader: RequestHeader
): RoutingContext =
RoutingContext(request.headers.toMap, sHints, eHints, Map.empty, requestHeader)
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
package de.innfactory.smithy4play

import akka.util.ByteString
import cats.data.{ EitherT, Validated }
import play.api.mvc.{
AbstractController,
ControllerComponents,
Handler,
RawBuffer,
Request,
RequestHeader,
Result,
Results
}
import smithy4s.{ ByteArray, Endpoint, Service }
import smithy4s.http.{ CaseInsensitive, CodecAPI, HttpEndpoint, Metadata, PathParams }
import smithy4s.schema.Schema
import cats.implicits._
import play.api.libs.json.Json
import smithy.api.{ Auth, HttpBearerAuth }
import cats.data.{ EitherT, Kleisli }
import cats.implicits.toBifunctorOps
import de.innfactory.smithy4play.middleware.MiddlewareBase
import play.api.mvc._
import smithy4s.http.{ CodecAPI, HttpEndpoint, Metadata, PathParams }
import smithy4s.kinds.FunctorInterpreter
import smithy4s.schema.Schema
import smithy4s.{ ByteArray, Endpoint, Service }

import scala.concurrent.{ ExecutionContext, Future }

Expand All @@ -31,13 +21,15 @@ class SmithyPlayEndpoint[Alg[_[_, _, _, _, _]], F[_] <: ContextRoute[_], Op[
], I, E, O, SI, SO](
service: Service[Alg],
impl: FunctorInterpreter[Op, F],
middleware: Seq[MiddlewareBase],
endpoint: Endpoint[Op, I, E, O, SI, SO],
codecs: CodecAPI
)(implicit cc: ControllerComponents, ec: ExecutionContext)
extends AbstractController(cc) {

private val serviceHints = service.hints
private val httpEndpoint: Either[HttpEndpoint.HttpEndpointError, HttpEndpoint[I]] = HttpEndpoint.cast(endpoint)
private val serviceHints = service.hints
private val endpointHints = endpoint.hints

private val inputSchema: Schema[I] = endpoint.input
private val outputSchema: Schema[O] = endpoint.output
Expand All @@ -58,25 +50,18 @@ class SmithyPlayEndpoint[Alg[_[_, _, _, _, _]], F[_] <: ContextRoute[_], Op[
"Try setting play.http.parser.maxMemoryBuffer in application.conf"
)
}
val result: EitherT[Future, ContextRouteError, O] = for {
pathParams <- getPathParams(v1, httpEp)
metadata = getMetadata(pathParams, v1)
input <- getInput(request, metadata)
_ <- EitherT(
Future(
Validated
.cond(validateAuthHints(metadata), (), Smithy4PlayError("Unauthorized", 401))
.toEither
)
)
res <- impl(endpoint.wrap(input))
.run(
RoutingContext
.fromRequest(request)
)
.map { case o: O =>
o
}

val result = for {
pathParams <- getPathParams(v1, httpEp)
metadata = getMetadata(pathParams, v1)
input <- getInput(request, metadata)
endpointLogic = impl(endpoint.wrap(input))
.asInstanceOf[Kleisli[RouteResult, RoutingContext, O]]
.map(mapToEndpointResult)

chainedMiddlewares = middleware.foldRight(endpointLogic)((a, b) => a.middleware(b.run))
res <-
chainedMiddlewares.run(RoutingContext.fromRequest(request, serviceHints, endpointHints, v1))
} yield res
result.value.map {
case Left(value) => handleFailure(value)
Expand All @@ -86,13 +71,27 @@ class SmithyPlayEndpoint[Alg[_[_, _, _, _, _]], F[_] <: ContextRoute[_], Op[
}
.getOrElse(Action(NotFound("404")))

private def validateAuthHints(metadata: Metadata) = {
val serviceAuthHints = serviceHints.get(HttpBearerAuth.tagInstance).map(_ => Auth(Set(HttpBearerAuth.id.show)))
for {
authSet <- endpoint.hints.get(Auth.tag) orElse serviceAuthHints
_ <- authSet.value.find(_.value == HttpBearerAuth.id.show)
} yield metadata.headers.contains(CaseInsensitive("Authorization"))
}.getOrElse(true)
private def mapToEndpointResult(o: O): EndpointResult = {
val outputMetadata = outputMetadataEncoder.encode(o)
val outputHeaders = outputMetadata.headers.map { case (k, v) =>
(k.toString.toLowerCase, v.mkString(""))
}
val contentType =
outputHeaders.getOrElse("content-type", "application/json")
val codecApi = contentType match {
case "application/json" => codecs
case _ => CodecAPI.nativeStringsAndBlob(codecs)
}
logger.debug(s"[SmithyPlayEndpoint] Headers: ${outputHeaders.mkString("|")}")

val codec = codecApi.compileCodec(outputSchema)
val expectBody = Metadata.PartialDecoder
.fromSchema(outputSchema)
.total
.isEmpty // expect body if metadata decoder is not total
val body = if (expectBody) Some(codecApi.writeToArray(codec, o)) else None
EndpointResult(body, outputHeaders)
}

private def getPathParams(
v1: RequestHeader,
Expand Down Expand Up @@ -187,35 +186,22 @@ class SmithyPlayEndpoint[Alg[_[_, _, _, _, _]], F[_] <: ContextRoute[_], Op[
query = request.queryString.map { case (k, v) => (k.trim, v) }
)

def handleFailure(error: ContextRouteError): Result =
private def handleFailure(error: ContextRouteError): Result =
Results.Status(error.statusCode)(error.toJson)

private def handleSuccess(output: O, code: Int): Result = {
val outputMetadata = outputMetadataEncoder.encode(output)
val outputHeaders = outputMetadata.headers.map { case (k, v) =>
(k.toString.toLowerCase, v.mkString(""))
}
private def handleSuccess(output: EndpointResult, code: Int): Result = {
val status = Results.Status(code)
val outputHeadersWithoutContentType = output.headers.-("content-type").toList
val contentType =
outputHeaders.getOrElse("content-type", "application/json")
val outputHeadersWithoutContentType = outputHeaders.-("content-type").toList
val codecApi = contentType match {
case "application/json" => codecs
case _ => CodecAPI.nativeStringsAndBlob(codecs)
output.headers.getOrElse("content-type", "application/json")

output.body match {
case Some(value) =>
status(value)
.as(contentType)
.withHeaders(outputHeadersWithoutContentType: _*)
case None => status("").withHeaders(outputHeadersWithoutContentType: _*)
}
logger.debug(s"[SmithyPlayEndpoint] Headers: ${outputHeaders.mkString("|")}")

val status = Results.Status(code)
val codec = codecApi.compileCodec(outputSchema)
val expectBody = Metadata.PartialDecoder
.fromSchema(outputSchema)
.total
.isEmpty // expect body if metadata decoder is not total
if (expectBody) {
status(codecApi.writeToArray(codec, output))
.as(contentType)
.withHeaders(outputHeadersWithoutContentType: _*)
} else status("")

}

}
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
package de.innfactory.smithy4play

import cats.data.{ EitherT, Kleisli }
import cats.implicits.toTraverseOps
import de.innfactory.smithy4play.middleware.MiddlewareBase
import play.api.mvc.{ AbstractController, ControllerComponents, Handler, RequestHeader }
import play.api.routing.Router.Routes
import smithy4s.HintMask
import smithy4s.http.{ HttpEndpoint, PathSegment }
import smithy4s.{ Endpoint, HintMask, Service }
import smithy4s.internals.InputOutput
import smithy4s.kinds.{ BiFunctorAlgebra, FunctorAlgebra, FunctorInterpreter, Kind1, PolyFunction5 }
import smithy4s.kinds.{ FunctorAlgebra, Kind1, PolyFunction5 }

import scala.concurrent.ExecutionContext

class SmithyPlayRouter[Alg[_[_, _, _, _, _]], F[
_
] <: ContextRoute[_]](
impl: FunctorAlgebra[Alg, F]
impl: FunctorAlgebra[Alg, F],
service: smithy4s.Service[Alg]
)(implicit cc: ControllerComponents, ec: ExecutionContext)
extends AbstractController(cc) {

def routes()(implicit
service: smithy4s.Service[Alg]
): Routes = {
def routes(middlewares: Seq[MiddlewareBase]): Routes = {

val interpreter: PolyFunction5[service.Operation, Kind1[F]#toKind5] = service.toPolyFunction[Kind1[F]#toKind5](impl)
val endpoints: Seq[service.Endpoint[_, _, _, _, _]] = service.endpoints
Expand All @@ -45,6 +46,7 @@ class SmithyPlayRouter[Alg[_[_, _, _, _, _]], F[
} yield new SmithyPlayEndpoint(
service,
interpreter,
middlewares,
endpointAndHttpEndpoint._1,
smithy4s.http.json.codecs(alloy.SimpleRestJson.protocol.hintMask ++ HintMask(InputOutput))
).handler(v1)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package de.innfactory.smithy4play.middleware

import cats.data.Kleisli
import de.innfactory.smithy4play.{ EndpointResult, RouteResult, RoutingContext }
import play.api.Logger

trait MiddlewareBase {

val logger: Logger = Logger("smithy4play")

protected def logic(
r: RoutingContext,
next: RoutingContext => RouteResult[EndpointResult]
): RouteResult[EndpointResult]

protected def skipMiddleware(r: RoutingContext): Boolean = false

def middleware(
f: RoutingContext => RouteResult[EndpointResult]
): Kleisli[RouteResult, RoutingContext, EndpointResult] =
Kleisli { r =>
if (skipMiddleware(r)) f(r)
else logic(r, f)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.innfactory.smithy4play.middleware

trait MiddlewareRegistryBase {

val middlewares: Seq[MiddlewareBase]

}
Loading

0 comments on commit 59384a3

Please sign in to comment.