Skip to content

Commit

Permalink
Merge pull request #44 from innFactory/feature/smithy4s-0-16-4
Browse files Browse the repository at this point in the history
updating smithy4s, implementing a GenericAPIClient and a ComplianceTest Client
  • Loading branch information
patsta32 authored Oct 18, 2022
2 parents 0d1e43b + f33dd22 commit f698065
Show file tree
Hide file tree
Showing 15 changed files with 307 additions and 86 deletions.
9 changes: 5 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import sbt.Compile
import sbt.Keys.cleanFiles

val releaseVersion = sys.env.getOrElse("TAG", "0.2.3-BETA.2")
val releaseVersion = sys.env.getOrElse("TAG", "0.2.3-BETA.3")
addCommandAlias("publishSmithy4Play", "smithy4play/publish")
addCommandAlias("publishLocalSmithy4Play", "smithy4play/publishLocal")
addCommandAlias("generateCoverage", "clean; coverage; test; coverageReport")
Expand Down Expand Up @@ -32,10 +31,12 @@ val sharedSettings = defaultProjectSettings

lazy val smithy4play = project
.in(file("smithy4play"))
.enablePlugins(Smithy4sCodegenPlugin)
.settings(
sharedSettings,
scalaVersion := Dependencies.scalaVersion,
name := "smithy4play",
scalaVersion := Dependencies.scalaVersion,
Compile / smithy4sAllowedNamespaces := List("smithy.test"),
name := "smithy4play",
scalacOptions += "-Ymacro-annotations",
Compile / compile / wartremoverWarnings ++= Warts.unsafe,
libraryDependencies ++= Dependencies.list
Expand Down
13 changes: 9 additions & 4 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ object Dependencies {
val playVersion = "2.8.13"
val typesafePlay = "com.typesafe.play" %% "play" % playVersion

val scalaVersion = "2.13.8"
val scalaVersion = "2.13.8"
val smithy4sVersion = "0.16.4"
val smithyCore = "com.disneystreaming.smithy4s" %% "smithy4s-core" % smithy4sVersion
val smithyJson = "com.disneystreaming.smithy4s" %% "smithy4s-json" % smithy4sVersion

val smithyCore = "com.disneystreaming.smithy4s" %% "smithy4s-core" % "0.16.1"
val classgraph = "io.github.classgraph" % "classgraph" % "4.8.149"
val smithyVersion = "1.24.0"

val smithyJson = "com.disneystreaming.smithy4s" %% "smithy4s-json" % "0.16.1"
val classgraph = "io.github.classgraph" % "classgraph" % "4.8.149"
val testTraits =
"software.amazon.smithy" % "smithy-protocol-test-traits" % smithyVersion

val scalatestPlus =
"org.scalatestplus.play" %% "scalatestplus-play" % "5.1.0" % Test
Expand All @@ -20,6 +24,7 @@ object Dependencies {
lazy val list = Seq(
smithyCore,
smithyJson,
testTraits,
classgraph,
scalatestPlus,
typesafePlay,
Expand Down
2 changes: 1 addition & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
addSbtPlugin("com.codecommit" % "sbt-github-packages" % "0.5.3")
addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.0.5")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
addSbtPlugin("com.disneystreaming.smithy4s" % "smithy4s-sbt-codegen" % "0.15.2")
addSbtPlugin("com.disneystreaming.smithy4s" % "smithy4s-sbt-codegen" % "0.16.4")
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.15")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3")
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package de.innfactory.smithy4play

import akka.util.ByteString
import cats.data.EitherT
import cats.data.{ EitherT, Validated }
import play.api.mvc.{
AbstractController,
ControllerComponents,
Expand All @@ -13,10 +13,11 @@ import play.api.mvc.{
Results
}
import smithy4s.{ ByteArray, Endpoint, Interpreter }
import smithy4s.http.{ CodecAPI, HttpEndpoint, Metadata, PathParams }
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 scala.concurrent.{ ExecutionContext, Future }

Expand Down Expand Up @@ -51,6 +52,13 @@ class SmithyPlayEndpoint[F[_] <: ContextRoute[_], Op[
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
Expand All @@ -68,6 +76,13 @@ class SmithyPlayEndpoint[F[_] <: ContextRoute[_], Op[
}
.getOrElse(Action(NotFound("404")))

private def validateAuthHints(metadata: Metadata) = {
for {
authSet <- endpoint.hints.get(Auth.tag)
_ <- authSet.value.find(_.value == HttpBearerAuth.id.show)
} yield metadata.headers.contains(CaseInsensitive("Authorization"))
}.getOrElse(true)

private def getPathParams(
v1: RequestHeader,
httpEp: HttpEndpoint[I]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package de.innfactory.smithy4play.client

import de.innfactory.smithy4play.{ ClientRequest, ClientResponse }
import smithy4s.{ Service, Transformation }

import scala.concurrent.ExecutionContext

private class GenericAPIClient[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _]](
service: Service[Alg, Op],
client: RequestClient
)(implicit ec: ExecutionContext) {

private val smithyPlayClient = new SmithyPlayClient("/", service, client)

/* Takes a service and creates a Transformation[Op, ClientRequest] */
private def transformer(additionalHeaders: Option[Map[String, Seq[String]]]): Alg[ClientRequest] =
service.transform(this.opToResponse(additionalHeaders))

/* uses the SmithyPlayClient to transform a Operation to a ClientResponse */
private def opToResponse(additionalHeaders: Option[Map[String, Seq[String]]]): Transformation[Op, ClientRequest] =
new Transformation[Op, ClientRequest] {
def apply[I, E, O, SI, SO](op: Op[I, E, O, SI, SO]): ClientResponse[O] =
smithyPlayClient.send(op, additionalHeaders)
}
}

object GenericAPIClient {

implicit class EnhancedGenericAPIClient[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _]](service: Service[Alg, Op]) {
def withClient(
client: RequestClient,
additionalHeaders: Option[Map[String, Seq[String]]] = None
)(implicit ec: ExecutionContext) = apply(service, additionalHeaders, client)
}
def apply[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _]](
serviceI: Service[Alg, Op],
additionalHeaders: Option[Map[String, Seq[String]]] = None,
client: RequestClient
)(implicit ec: ExecutionContext): Alg[ClientRequest] =
new GenericAPIClient(serviceI, client).transformer(additionalHeaders)

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import scala.concurrent.ExecutionContext

class SmithyPlayClient[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]](
baseUri: String,
service: smithy4s.Service[Alg, Op]
)(implicit executionContext: ExecutionContext, client: RequestClient) {
service: smithy4s.Service[Alg, Op],
client: RequestClient
)(implicit executionContext: ExecutionContext) {

def send[I, E, O, SI, SO](
op: Op[I, E, O, SI, SO],
Expand All @@ -19,7 +20,7 @@ class SmithyPlayClient[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]](
HttpEndpoint
.cast(endpoint)
.map(httpEndpoint =>
new SmithyPlayClientEndpoint(endpoint, baseUri, additionalHeaders, httpEndpoint, input).send()
new SmithyPlayClientEndpoint(endpoint, baseUri, additionalHeaders, httpEndpoint, input, client).send()
)
.get
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ private[smithy4play] class SmithyPlayClientEndpoint[Op[_, _, _, _, _], I, E, O,
baseUri: String,
additionalHeaders: Option[Map[String, Seq[String]]],
httpEndpoint: HttpEndpoint[I],
input: I
)(implicit executionContext: ExecutionContext, client: RequestClient) {
input: I,
client: RequestClient
)(implicit executionContext: ExecutionContext) {

private val codecs: codecs =
smithy4s.http.json.codecs(smithy4s.api.SimpleRestJson.protocol.hintMask ++ HintMask(InputOutput))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package de.innfactory.smithy4play.compliancetests

import de.innfactory.smithy4play.ClientResponse
import de.innfactory.smithy4play.client.{ SmithyPlayClientEndpointErrorResponse, SmithyPlayClientEndpointResponse }
import play.api.libs.json.Json
import smithy.test._
import smithy4s.http.HttpEndpoint
import smithy4s.{ Document, Endpoint, GenLift, Monadic, Service }

import scala.concurrent.duration.DurationInt
import scala.concurrent.{ Await, ExecutionContext }

class ComplianceClient[
Alg[_[_, _, _, _, _]],
Op[_, _, _, _, _]
](
client: Monadic[Alg, ClientResponse]
)(implicit
service: Service[Alg, Op],
ec: ExecutionContext
) {

private def clientRequest[I, E, O, SE, SO](
endpoint: Endpoint[Op, I, E, O, SE, SO],
requestTestCase: Option[HttpRequestTestCase],
responseTestCase: Option[HttpResponseTestCase]
) = {

val inputFromDocument = Document.Decoder.fromSchema(endpoint.input)
val input = inputFromDocument.decode(requestTestCase.flatMap(_.params).getOrElse(Document.obj())).toOption.get

val result = service
.asTransformation[GenLift[ClientResponse]#λ](client)
.apply(endpoint.wrap(input))
.map(res => matchResponse(res, endpoint, responseTestCase))
Await.result(result, 5.seconds)
}

private def matchResponse[I, E, O, SE, SO](
response: Either[SmithyPlayClientEndpointErrorResponse, SmithyPlayClientEndpointResponse[O]],
endpoint: Endpoint[Op, I, E, O, SE, SO],
responseTestCase: Option[HttpResponseTestCase]
) = {

val httpEp = HttpEndpoint.cast(endpoint).get
val responseStatusCode = response match {
case Left(value) => value.statusCode
case Right(value) => value.statusCode
}
val expectedStatusCode = responseTestCase.map(_.code).getOrElse(httpEp.code)
// val statusAssert = expectedStatusCode == responseStatusCode

val outputFromDocument = Document.Decoder.fromSchema(endpoint.output)
val expectedOutput =
outputFromDocument.decode(responseTestCase.flatMap(_.params).getOrElse(Document.obj())).toOption

// responseTestCase.forall(_ => expectedOutput == response.toOption.flatMap(_.body)) && statusAssert
ComplianceResponse(
expectedCode = expectedStatusCode,
receivedCode = responseStatusCode,
expectedBody = expectedOutput,
receivedBody = response.toOption.flatMap(_.body),
expectedError = responseTestCase match {
case Some(value) => value.body.getOrElse("")
case None => ""
},
receivedError = response match {
case Left(value) => Json.parse(value.error).toString()
case Right(value) => ""
}
)
}

case class ComplianceResponse[O](
expectedCode: Int,
receivedCode: Int,
expectedBody: Option[O],
receivedBody: Option[O],
expectedError: String,
receivedError: String
)

def tests(suite: Option[String] = None) =
service.endpoints.flatMap { endpoint =>
val requests = endpoint.hints
.get(HttpRequestTests)
.map(_.value)
.getOrElse(Nil)
.filter(tc =>
suite.isEmpty && tc.documentation.isEmpty || tc.documentation.getOrElse("") == suite.getOrElse("")
)
val responses = endpoint.hints
.get(HttpResponseTests)
.map(_.value)
.getOrElse(Nil)
.filter(tc =>
suite.isEmpty && tc.documentation.isEmpty || tc.documentation.getOrElse("") == suite.getOrElse("")
)
val ids = requests.map(_.id).toSet ++ responses.map(_.id).toSet

ids
.map(id => (requests.find(_.id == id), responses.find(_.id == id)))
.map(x => clientRequest(endpoint, x._1, x._2))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ package object smithy4play {

type ClientResponse[O] = Future[Either[SmithyPlayClientEndpointErrorResponse, SmithyPlayClientEndpointResponse[O]]]

type ClientRequest[I, E, O, SI, SO] = ClientResponse[O]

type RouteResult[O] = EitherT[Future, ContextRouteError, O]

type ContextRoute[O] = Kleisli[RouteResult, RoutingContext, O]
Expand Down
4 changes: 4 additions & 0 deletions smithy4playTest/app/controller/TestController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,8 @@ class TestController @Inject() (implicit
override def statusCode: Int = 500
})
}

override def testAuth(): ContextRoute[Unit] = Kleisli { rc =>
EitherT.rightT[Future, ContextRouteError](())
}
}
52 changes: 0 additions & 52 deletions smithy4playTest/test/TestControllerClient.scala

This file was deleted.

Loading

0 comments on commit f698065

Please sign in to comment.