Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Play support #234

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- checkout
- restore_cache:
key: sbtcache
- run: sbt ++2.12.10! coreJVM/test http4s/compile akkaHttp/compile finch/compile examples/compile catsInteropJVM/compile benchmarks/compile codegen/test clientJVM/test monixInterop/compile
- run: sbt ++2.12.10! coreJVM/test http4s/compile akkaHttp/compile finch/compile examples/compile catsInteropJVM/compile benchmarks/compile codegen/test clientJVM/test monixInterop/compile play_27/compile play_28/compile
- save_cache:
key: sbtcache
paths:
Expand All @@ -35,7 +35,7 @@ jobs:
- checkout
- restore_cache:
key: sbtcache
- run: sbt ++2.13.1! coreJVM/test http4s/compile akkaHttp/compile finch/compile examples/compile catsInteropJVM/compile monixInterop/compile clientJVM/test
- run: sbt ++2.13.1! coreJVM/test http4s/compile akkaHttp/compile finch/compile examples/compile catsInteropJVM/compile monixInterop/compile clientJVM/test play_27/compile play_28/compile
- save_cache:
key: sbtcache
paths:
Expand Down Expand Up @@ -131,4 +131,4 @@ workflows:
branches:
only:
- master


31 changes: 29 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ val catsEffectVersion = "2.1.2"
val http4sVersion = "0.21.1"
val silencerVersion = "1.6.0"
val sttpVersion = "2.0.4"
val play28Version = "2.8.1"
val play27Version = "2.7.4"
val zioVersion = "1.0.0-RC18-1"
val zioInteropCatsVersion = "2.0.0.0-RC11"

Expand Down Expand Up @@ -58,6 +60,8 @@ lazy val root = project
finch,
http4s,
akkaHttp,
play_27,
play_28,
catsInteropJVM,
catsInteropJS,
monixInterop,
Expand Down Expand Up @@ -160,6 +164,28 @@ lazy val http4s = project
)
.dependsOn(coreJVM)

lazy val play_27 =
project
.in(file("play"))
.settings(commonSettings)
.dependsOn(coreJVM)
.settings(
name := s"caliban-play-27",
libraryDependencies += "com.typesafe.play" %% "play" % play27Version,
target := { file(baseDirectory.value.toString + s"/.play-27/target") }
)

lazy val play_28 =
project
.in(file("play"))
.settings(commonSettings)
.dependsOn(coreJVM)
.settings(
name := s"caliban-play-28",
libraryDependencies += "com.typesafe.play" %% "play" % play28Version,
target := { file(baseDirectory.value.toString + s"/.play-28/target") }
)

lazy val akkaHttp = project
.in(file("akka-http"))
.settings(name := "caliban-akka-http")
Expand Down Expand Up @@ -218,10 +244,11 @@ lazy val examples = project
.settings(skip in publish := true)
.settings(
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client" %% "async-http-client-backend-zio" % sttpVersion
"com.softwaremill.sttp.client" %% "async-http-client-backend-zio" % sttpVersion,
"com.typesafe.play" %% "play-akka-http-server" % play28Version
)
)
.dependsOn(akkaHttp, http4s, catsInteropJVM, finch, monixInterop, clientJVM)
.dependsOn(akkaHttp, http4s, catsInteropJVM, finch, monixInterop, clientJVM, play_28)

lazy val benchmarks = project
.in(file("benchmarks"))
Expand Down
27 changes: 11 additions & 16 deletions core/src/main/scala/caliban/CalibanError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,37 +65,32 @@ private object ErrorCirce {
case CalibanError.ParsingError(msg, locationInfo, _, extensions) =>
Json
.obj(
"message" -> s"Parsing Error: $msg".asJson,
"locations" -> Some(locationInfo).collect {
case Some(li) => Json.arr(locationToJson(li))
}.asJson,
"message" -> s"Parsing Error: $msg".asJson,
"locations" -> locationInfo.fold(Json.Null)(li => Json.arr(locationToJson(li))),
"extensions" -> (extensions: Option[ResponseValue]).asJson.dropNullValues
)
.dropNullValues
case CalibanError.ValidationError(msg, _, locationInfo, extensions) =>
Json
.obj(
"message" -> msg.asJson,
"locations" -> Some(locationInfo).collect {
case Some(li) => Json.arr(locationToJson(li))
}.asJson,
"message" -> msg.asJson,
"locations" -> locationInfo.fold(Json.Null)(li => Json.arr(locationToJson(li))),
"extensions" -> (extensions: Option[ResponseValue]).asJson.dropNullValues
)
.dropNullValues
case CalibanError.ExecutionError(msg, path, locationInfo, _, extensions) =>
Json
.obj(
"message" -> msg.asJson,
"message" -> msg.asJson,
"locations" -> locationInfo.fold(Json.Null)(li => Json.arr(locationToJson(li))),
"message" -> msg.asJson,
"locations" -> Some(locationInfo).collect {
case Some(li) => Json.arr(locationToJson(li))
}.asJson,
"path" -> Some(path).collect {
case p if p.nonEmpty =>
Json.fromValues(p.map {
case Left(value) => value.asJson
case Right(value) => value.asJson
})
}.asJson,
"path" -> (path match {
case path if path.isEmpty => Json.Null
case path => Json.fromValues(path.map(_.fold(_.asJson, _.asJson)))
}),
"extensions" -> (extensions: Option[ResponseValue]).asJson.dropNullValues
)
.dropNullValues
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/scala/caliban/GraphQLResponse.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ private object GraphQLResponseCirce {
.instance[GraphQLResponse[Any]] {
case GraphQLResponse(data, Nil, None) => Json.obj("data" -> data.asJson)
case GraphQLResponse(data, Nil, Some(extensions)) =>
Json.obj("data" -> data.asJson, "extensions" -> extensions.asInstanceOf[ResponseValue].asJson)
Json.obj("data" -> data.asJson, "extensions" -> (extensions: ResponseValue).asJson)
case GraphQLResponse(data, errors, None) =>
Json.obj("data" -> data.asJson, "errors" -> Json.fromValues(errors.map(handleError)))
case GraphQLResponse(data, errors, Some(extensions)) =>
Json.obj(
"data" -> data.asJson,
"errors" -> Json.fromValues(errors.map(handleError)),
"extensions" -> extensions.asInstanceOf[ResponseValue].asJson
"extensions" -> (extensions: ResponseValue).asJson
)
}

Expand Down
2 changes: 2 additions & 0 deletions examples/src/main/resources/application.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Used by the Play example
play.http.secret.key = "changeme"
97 changes: 97 additions & 0 deletions examples/src/main/scala/caliban/play/ExampleApp.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package caliban.play

import caliban.ExampleData.{Character, CharacterArgs, CharactersArgs, Role, sampleCharacters}
import caliban.GraphQL.graphQL
import caliban.schema.Annotations.{GQLDeprecated, GQLDescription}
import caliban.schema.GenericSchema
import caliban.wrappers.ApolloTracing.apolloTracing
import caliban.wrappers.Wrappers.{maxDepth, maxFields, printSlowQueries, timeout}
import caliban.{CalibanController, ExampleService, GraphQL, GraphQLRequest, RootResolver}
import play.api.mvc.{Action, DefaultControllerComponents}
import play.api.routing.Router
import play.api.routing.sird._
import play.api.{ApplicationLoader, BuiltInComponents, BuiltInComponentsFromContext, Environment, NoHttpFiltersComponents}
import play.core.server.{AkkaHttpServer, ServerConfig}
import zio.clock.Clock
import zio.console.Console
import zio.duration._
import zio.stream.ZStream
import zio.{Runtime, URIO}

import scala.io.StdIn.readLine
import scala.language.postfixOps

object ExampleApp extends GenericSchema[Console with Clock] {

implicit val runtime = Runtime.unsafeFromLayer(Console.live ++ Clock.live)

implicit val roleSchema = gen[Role]
implicit val characterSchema = gen[Character]
implicit val characterArgsSchema = gen[CharacterArgs]
implicit val charactersArgsSchema = gen[CharactersArgs]

case class Queries(
@GQLDescription("Return all characters from a given origin")
characters: CharactersArgs => URIO[Console, List[Character]],
@GQLDeprecated("Use `characters`")
character: CharacterArgs => URIO[Console, Option[Character]]
)
case class Mutations(deleteCharacter: CharacterArgs => URIO[Console, Boolean])
case class Subscriptions(characterDeleted: ZStream[Console, Nothing, String])

def makeApi(service: ExampleService): GraphQL[Console with Clock] =
graphQL(
RootResolver(
Queries(
args => service.getCharacters(args.origin),
args => service.findCharacter(args.name)
),
Mutations(args => service.deleteCharacter(args.name)),
Subscriptions(service.deletedEvents)
)
) @@
maxFields(200) @@ // query analyzer that limit query fields
maxDepth(30) @@ // query analyzer that limit query depth
timeout(5 seconds) @@ // wrapper that fails slow queries
printSlowQueries(500 millis) @@ // wrapper that logs slow queries
apolloTracing // wrapper for https://github.com/apollographql/apollo-tracing

val service = runtime.unsafeRun(ExampleService.make(sampleCharacters))
val interpreter = runtime.unsafeRun(makeApi(service).interpreter)

def main(args: Array[String]): Unit = {

val context = ApplicationLoader.Context.create(Environment.simple())

val components: BuiltInComponents = new BuiltInComponentsFromContext(context) with NoHttpFiltersComponents {

val controller = new CalibanController(
DefaultControllerComponents(
defaultActionBuilder,
playBodyParsers,
messagesApi,
langs,
fileMimeTypes,
executionContext
)
)

val graphQLAction: Action[GraphQLRequest] = controller.action(runtime, interpreter)

override def router: Router = Router.from { case POST(p"/api/graphql") => graphQLAction }
}

val server = AkkaHttpServer.fromApplication(
components.application,
ServerConfig(
port = Some(8088),
address = "127.0.0.1"
)
)

println("Server started, press enter to exit ...")
readLine()
server.stop()
}

}
31 changes: 31 additions & 0 deletions play/src/main/scala/caliban/CalibanController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package caliban

import caliban.PlayJson._
import caliban.Value.NullValue
import javax.inject.Inject
import play.api.mvc.{ AbstractController, Action, ControllerComponents }
import zio.{ Exit, Runtime, ZIO }

import scala.concurrent.{ Future, Promise }

class CalibanController @Inject() (cc: ControllerComponents) extends AbstractController(cc) {

private def unsafeRunToFuture[R, E <: Throwable, A](zio: ZIO[R, E, A])(runtime: Runtime[R]): Future[A] = {
val p = Promise[A]()
runtime.unsafeRunAsync(zio) {
case Exit.Success(value) => p.success(value)
case Exit.Failure(cause) => p.failure(cause.squashTrace)
}
p.future
}

def action[R, E](runtime: Runtime[R], interpreter: GraphQLInterpreter[R, E]): Action[GraphQLRequest] =
Action.async(parse.json[GraphQLRequest]) { req =>
unsafeRunToFuture(
interpreter
.execute(req.body.query, req.body.operationName, req.body.variables.getOrElse(Map.empty))
.catchAllCause(cause => ZIO.succeed(GraphQLResponse(NullValue, cause.defects)))
.map(Ok(_))
)(runtime)
}
}
103 changes: 103 additions & 0 deletions play/src/main/scala/caliban/PlayJson.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package caliban

import caliban.Value._
import caliban.parsing.adt.LocationInfo
import play.api.http.Writeable
import play.api.libs.json.Writes._
import play.api.libs.json._

object PlayJson {

//// JSON Reads
private def jsonToInputValue(json: JsValue): InputValue = json match {
case JsNull => NullValue
case boolean: JsBoolean => BooleanValue(boolean.value)
case JsNumber(value) =>
if (value.isValidInt) IntValue(value.toInt)
else if (value.isValidLong) IntValue(value.toLong)
else if (value.isWhole) IntValue(value.toBigInt)
else FloatValue(value)
case JsString(value) => StringValue(value)
case JsArray(array) =>
InputValue.ListValue(array.toList.map(jsonToInputValue))
case JsObject(obj) =>
val builder = Predef.Map.newBuilder[String, InputValue]
builder.sizeHint(obj)
obj.foreach { case (k, v) => builder += (k -> jsonToInputValue(v)) }
InputValue.ObjectValue(builder.result())
}

implicit val readsInputValue: Reads[InputValue] =
json => JsSuccess(jsonToInputValue(json))

implicit val readsGraphQLRequest: Reads[GraphQLRequest] =
Json.reads

//// JSON Writes

// ResponseValue
private def responseValueToJson(response: ResponseValue): JsValue = response match {
case v: Value => valueToJson(v)
case ResponseValue.ListValue(values) => JsArray(values.map(responseValueToJson))
case ResponseValue.ObjectValue(fields) => JsObject(fields.map { case (k, v) => k -> responseValueToJson(v) })
case s: ResponseValue.StreamValue => JsString(s.toString)
}

private def valueToJson(value: Value): JsValue = value match {
case Value.NullValue => JsNull
case value: IntValue => JsNumber(BigDecimal(value.toBigInt.bigInteger))
case value: FloatValue => JsNumber(value.toBigDecimal)
case StringValue(value) => JsString(value)
case BooleanValue(value) => JsBoolean(value)
case EnumValue(value) => JsString(value)
}

implicit val writesResponseValue: Writes[ResponseValue] = responseValueToJson _

private def handleError(err: Any): JsValue = err match {
case ce: CalibanError => errorValueEncoder.writes(ce)
case _ => Json.obj("message" -> JsString(err.toString))
}

// CalibanError
private def encodeFields(li: Option[LocationInfo], message: String, extensions: Option[ResponseValue]) =
Json.obj("message" -> message, "extensions" -> extensions) ++
li.fold(Json.obj())(li => Json.obj("locations" -> Json.arr(Json.obj("line" -> li.line, "column" -> li.column))))

val errorValueEncoder: Writes[CalibanError] = {
case CalibanError.ParsingError(msg, locationInfo, _, extensions) =>
encodeFields(locationInfo, s"Parsing Error: $msg", extensions)

case CalibanError.ValidationError(msg, _, locationInfo, extensions) =>
encodeFields(locationInfo, msg, extensions)

case CalibanError.ExecutionError(msg, path, locationInfo, _, extensions) =>
val paths =
if (path.isEmpty) Nil
else
List(
"path" -> Json
.arr(path.map(_.fold(Json.toJsFieldJsValueWrapper[String], Json.toJsFieldJsValueWrapper[Int])): _*)
)

encodeFields(locationInfo, msg, extensions) ++ JsObject(paths)
}

// GraphQLResponse
implicit def writesGraphQLResponse[E]: Writes[GraphQLResponse[E]] = {
case GraphQLResponse(data, errors, maybeExtensions) =>
Json.obj(
"data" -> responseValueToJson(data),
"errors" -> (errors match {
case Nil => Option.empty[JsValue]
case _ => Some(JsArray(errors.map(handleError)))
}),
"extensions" -> maybeExtensions.map(responseValueToJson)
)
}

//// HTTP writable
implicit def writableGraphQLResponse[E]: Writeable[GraphQLResponse[E]] =
Writeable.writeableOf_JsValue.map(writesGraphQLResponse.writes)

}