Skip to content

Commit

Permalink
Merge pull request #32 from innFactory/feature/Client
Browse files Browse the repository at this point in the history
Feature/client
  • Loading branch information
patsta32 authored Sep 13, 2022
2 parents f4256f5 + 5ee4d1f commit 891a437
Show file tree
Hide file tree
Showing 28 changed files with 724 additions and 81 deletions.
19 changes: 9 additions & 10 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
name: Scala Build CI
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop
name: Scala Build and Test CI
on: push

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: run compile
run: sbt compile
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: run tests
run: sbt test
lint:
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
- name: publish to new Tag
run: sbt publish
run: sbt publishSmithy4Play
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.event.release.tag_name }}
35 changes: 29 additions & 6 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import sbt.Compile
import sbt.Keys.cleanFiles

val releaseVersion = sys.env.getOrElse("TAG", "0.2.2-1")

val releaseVersion = sys.env.getOrElse("TAG", "0.2.3-BETA")
addCommandAlias("publishSmithy4Play", "smithy4play/publish")
addCommandAlias("publishLocalSmithy4Play", "smithy4play/publishLocal")
addCommandAlias("generateCoverage", "clean; coverage; test; coverageReport")
val token = sys.env.getOrElse("GITHUB_TOKEN", "")
val githubSettings = Seq(
githubOwner := "innFactory",
Expand Down Expand Up @@ -30,14 +33,34 @@ val sharedSettings = defaultProjectSettings
lazy val smithy4play = project
.in(file("smithy4play"))
.settings(
sharedSettings
)
.settings(
sharedSettings,
scalaVersion := Dependencies.scalaVersion,
name := "smithy4play",
scalacOptions += "-Ymacro-annotations",
Compile / compile / wartremoverWarnings ++= Warts.unsafe,
libraryDependencies ++= Dependencies.list
)

lazy val root = project.in(file(".")).settings(sharedSettings).dependsOn(smithy4play).aggregate(smithy4play)
lazy val smithy4playTest = project
.in(file("smithy4playTest"))
.enablePlugins(Smithy4sCodegenPlugin, PlayScala)
.settings(
sharedSettings,
scalaVersion := Dependencies.scalaVersion,
name := "smithy4playTest",
scalacOptions += "-Ymacro-annotations",
Compile / compile / wartremoverWarnings ++= Warts.unsafe,
cleanKeepFiles += (ThisBuild / baseDirectory).value / "smithy4playTest" / "app",
cleanFiles += (ThisBuild / baseDirectory).value / "smithy4playTest" / "app" / "testDefinitions" / "test",
Compile / smithy4sInputDir := (ThisBuild / baseDirectory).value / "smithy4playTest" / "testSpecs",
Compile / smithy4sOutputDir := (ThisBuild / baseDirectory).value / "smithy4playTest" / "app",
libraryDependencies ++= Seq(
guice,
Dependencies.cats,
Dependencies.smithyCore,
Dependencies.scalatestPlus
)
)
.dependsOn(smithy4play)

lazy val root = project.in(file(".")).settings(sharedSettings).aggregate(smithy4play, smithy4playTest)
6 changes: 3 additions & 3 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ object Dependencies {

val scalaVersion = "2.13.8"

val smithyCore = "com.disneystreaming.smithy4s" %% "smithy4s-core" % "0.14.2"
val smithyJson = "com.disneystreaming.smithy4s" %% "smithy4s-json" % "0.14.2"
val smithyCore = "com.disneystreaming.smithy4s" %% "smithy4s-core" % "0.15.2"
val smithyJson = "com.disneystreaming.smithy4s" %% "smithy4s-json" % "0.15.2"
val classgraph = "io.github.classgraph" % "classgraph" % "4.8.149"

val scalatestPlus =
"org.scalatestplus.play" %% "scalatestplus-play" % "5.1.0" % Test
val cats = "org.typelevel" %% "cats-core" % "2.7.0"
val cats = "org.typelevel" %% "cats-core" % "2.8.0"

lazy val list = Seq(
smithyCore,
Expand Down
9 changes: 6 additions & 3 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,3 +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.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.typesafe.play" % "sbt-plugin" % "2.8.15")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3")
4 changes: 4 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ Autorouting
```scala
-> / de.innfactory.smithy4play.AutoRouter
```
- add package name to configuration
```scala
smithy4play.autoRoutePackage = "your.package.name"
```

Selfbinding

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ trait AutoRoutableController {
cc: ControllerComponents
): Routes =
new SmithyPlayRouter[Alg, Op, F](impl).routes()

val routes: Routes

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class AutoRouter @Inject(
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()} Controllers")
logger.debug(s"[AutoRouter] found ${controllers.size().toString} Controllers")
val routes = controllers.asScala.map(_.loadClass(true)).map(clazz => createFromClass(clazz)).toSeq
classGraphScanner.close()
routes
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.innfactory.smithy4play

import akka.util.ByteString
import cats.data.EitherT
import play.api.mvc.{
AbstractController,
Expand Down Expand Up @@ -32,15 +33,15 @@ class SmithyPlayEndpoint[F[_] <: ContextRoute[_], Op[
)(implicit cc: ControllerComponents, ec: ExecutionContext)
extends AbstractController(cc) {

private val httpEndpoint = HttpEndpoint.cast(endpoint)
private val httpEndpoint: Option[HttpEndpoint[I]] = HttpEndpoint.cast(endpoint)

private val inputSchema: Schema[I] = endpoint.input
private val outputSchema: Schema[O] = endpoint.output

private val inputMetadataDecoder =
private val inputMetadataDecoder: Metadata.PartialDecoder[I] =
Metadata.PartialDecoder.fromSchema(inputSchema)

private val outputMetadataEncoder =
private val outputMetadataEncoder: Metadata.Encoder[O] =
Metadata.Encoder.fromSchema(outputSchema)

def handler(v1: RequestHeader): Handler =
Expand All @@ -67,13 +68,6 @@ class SmithyPlayEndpoint[F[_] <: ContextRoute[_], Op[
}
.getOrElse(Action(NotFound("404")))

def handleFailure(error: ContextRouteError): Result =
Results.Status(error.statusCode)(
Json.toJson(
RoutingErrorResponse(error.message, error.additionalInfoErrorCode)
)
)

private def getPathParams(
v1: RequestHeader,
httpEp: HttpEndpoint[I]
Expand Down Expand Up @@ -107,15 +101,17 @@ class SmithyPlayEndpoint[F[_] <: ContextRoute[_], Op[
)
}
case None =>
request.contentType.get match {
println(request.contentType.getOrElse("application/json"))
request.contentType.getOrElse("application/json") match {
case "application/json" => parseJson(request, metadata)
case _ => parseRaw(request, metadata)
}

})
)

private def parseJson(request: Request[RawBuffer], metadata: Metadata) =
private def parseJson(request: Request[RawBuffer], metadata: Metadata): Either[ContextRouteError, I] = {
val codec = codecs.compileCodec(inputSchema)
for {
metadataPartial <- inputMetadataDecoder
.decode(metadata)
Expand All @@ -126,18 +122,18 @@ class SmithyPlayEndpoint[F[_] <: ContextRoute[_], Op[
500
)
}
codec = codecs.compileCodec(inputSchema)
c <- codecs
.decodeFromByteBufferPartial(
codec,
request.body.asBytes().get.toByteBuffer
request.body.asBytes().getOrElse(ByteString.empty).toByteBuffer
)
.leftMap(e => Smithy4PlayError(s"expected: ${e.expected}", 400))
} yield metadataPartial.combine(c)
}

private def parseRaw(request: Request[RawBuffer], metadata: Metadata) = {
private def parseRaw(request: Request[RawBuffer], metadata: Metadata): Either[ContextRouteError, I] = {
val nativeCodec: CodecAPI = CodecAPI.nativeStringsAndBlob(codecs)
val input = ByteArray(request.body.asBytes().get.toArray)
val input = ByteArray(request.body.asBytes().getOrElse(ByteString.empty).toArray)
val codec = nativeCodec
.compileCodec(inputSchema)
for {
Expand All @@ -156,14 +152,21 @@ class SmithyPlayEndpoint[F[_] <: ContextRoute[_], Op[
} yield metadataPartial.combine(bodyPartial)
}

private def getMetadata(pathParams: PathParams, request: RequestHeader) =
private def getMetadata(pathParams: PathParams, request: RequestHeader): Metadata =
Metadata(
path = pathParams,
headers = getHeaders(request),
headers = getHeaders(request.headers),
query = request.queryString.map { case (k, v) => (k.trim, v) }
)

private def handleSuccess(output: O, code: Int) = {
def handleFailure(error: ContextRouteError): Result =
Results.Status(error.statusCode)(
Json.toJson(
RoutingErrorResponse(error.message, error.additionalInfoErrorCode)
)
)

private def handleSuccess(output: O, code: Int): Result = {
val outputMetadata = outputMetadataEncoder.encode(output)
val outputHeaders = outputMetadata.headers.map { case (k, v) =>
(k.toString, v.mkString(""))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package de.innfactory.smithy4play

import play.api.mvc.{ ControllerComponents, Handler, RequestHeader }
import cats.implicits.toTraverseOps
import play.api.mvc.{ AbstractController, ControllerComponents, Handler, RequestHeader }
import play.api.routing.Router.Routes
import smithy4s.http.{ matchPath, HttpEndpoint, HttpMethod, PathSegment }
import smithy4s.http.{ HttpEndpoint, PathSegment }
import smithy4s.{ Endpoint, GenLift, HintMask, Monadic, Service, Transformation }
import smithy4s.internals.InputOutput

Expand All @@ -12,7 +13,8 @@ class SmithyPlayRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[
_
] <: ContextRoute[_]](
impl: Monadic[Alg, F]
)(implicit cc: ControllerComponents, ec: ExecutionContext) {
)(implicit cc: ControllerComponents, ec: ExecutionContext)
extends AbstractController(cc) {

def routes()(implicit
serviceProvider: smithy4s.Service.Provider[Alg, Op]
Expand All @@ -21,32 +23,27 @@ class SmithyPlayRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[
val service: Service[Alg, Op] = serviceProvider.service
val interpreter: Transformation[Op, GenLift[F]#λ] = service.asTransformation[GenLift[F]#λ](impl)
val endpoints: Seq[Endpoint[Op, _, _, _, _, _]] = service.endpoints
val httpEndpoints = endpoints.map(HttpEndpoint.cast(_).get)
val httpEndpoints: Seq[Option[HttpEndpoint[_]]] = endpoints.map(HttpEndpoint.cast(_))

new PartialFunction[RequestHeader, Handler] {
override def isDefinedAt(x: RequestHeader): Boolean = {
logger.debug("[SmithyPlayRouter] calling isDefinedAt on service: " + service.id.name + "for path: " + x.path)
httpEndpoints.exists(ep => checkIfRequestHeaderMatchesEndpoint(x, ep))
logger.debug("[SmithyPlayRouter] calling isDefinedAt on service: " + service.id.name + " for path: " + x.path)
httpEndpoints.exists(ep => ep.exists(checkIfRequestHeaderMatchesEndpoint(x, _)))
}

override def apply(v1: RequestHeader): Handler = {
logger.debug("[SmithyPlayRouter] calling apply on: " + service.id.name)

val validEndpoint = endpoints.find(endpoint =>
checkIfRequestHeaderMatchesEndpoint(
v1,
HttpEndpoint.cast(endpoint).get
)
)
new SmithyPlayEndpoint(
for {
zippedEndpoints <- endpoints.map(ep => HttpEndpoint.cast(ep).map((ep, _))).sequence
endpointAndHttpEndpoint <- zippedEndpoints.find(ep => checkIfRequestHeaderMatchesEndpoint(v1, ep._2))
} yield new SmithyPlayEndpoint(
interpreter,
validEndpoint.get,
smithy4s.http.json.codecs(
smithy4s.api.SimpleRestJson.protocol.hintMask ++ HintMask(
InputOutput
)
)
endpointAndHttpEndpoint._1,
smithy4s.http.json.codecs(smithy4s.api.SimpleRestJson.protocol.hintMask ++ HintMask(InputOutput))
).handler(v1)
} match {
case Some(value) => value
case None => throw new Exception("Could not cast Endpoint to HttpEndpoint, likely a bug in smithy4s")
}

}
Expand All @@ -56,20 +53,13 @@ class SmithyPlayRouter[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[
x: RequestHeader,
ep: HttpEndpoint[_]
) = {
ep.path.foreach {
case PathSegment.StaticSegment(value) =>
if (value.contains(" "))
logger.info("following pathSegment contains a space: " + value)
case PathSegment.LabelSegment(value) =>
if (value.contains(" "))
logger.info("following pathSegment contains a space: " + value)
case PathSegment.GreedySegment(value) =>
if (value.contains(" "))
logger.info("following pathSegment contains a space: " + value)
ep.path.map {
case PathSegment.StaticSegment(value) => value
case PathSegment.LabelSegment(value) => value
case PathSegment.GreedySegment(value) => value
}
matchRequestPath(x, ep).isDefined && x.method
.equals(
ep.method.showUppercase
)
.filter(_.contains(" "))
.foreach(value => logger.info("following pathSegment contains a space: " + value))
matchRequestPath(x, ep).isDefined && x.method == ep.method.showUppercase
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package de.innfactory.smithy4play.client

import play.api.mvc.Headers

import scala.concurrent.Future

case class SmithyClientResponse(body: Option[Array[Byte]], headers: Map[String, Seq[String]], statusCode: Int)

trait RequestClient {
def send(
method: String,
path: String,
headers: Map[String, Seq[String]],
body: Option[Array[Byte]]
): Future[SmithyClientResponse]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package de.innfactory.smithy4play.client

import de.innfactory.smithy4play.ClientResponse
import smithy4s.http.HttpEndpoint

import scala.concurrent.ExecutionContext

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

def send[I, E, O, SI, SO](
op: Op[I, E, O, SI, SO],
additionalHeaders: Option[Map[String, Seq[String]]]
): ClientResponse[O] = {

val (input, endpoint) = service.endpoint(op)
HttpEndpoint
.cast(endpoint)
.map(httpEndpoint =>
new SmithyPlayClientEndpoint(endpoint, baseUri, additionalHeaders, httpEndpoint, input).send()
)
.get
}

}
Loading

0 comments on commit 891a437

Please sign in to comment.