Skip to content

Commit

Permalink
Move circe codecs to the core module
Browse files Browse the repository at this point in the history
  • Loading branch information
rtimush committed Nov 29, 2019
1 parent a347181 commit 5d8ae9e
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 65 deletions.
16 changes: 8 additions & 8 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,14 @@ lazy val core = crossProject(JSPlatform, JVMPlatform)
.settings(
testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")),
libraryDependencies ++= Seq(
"com.lihaoyi" %%% "fastparse" % "2.1.3",
"com.propensive" %%% "magnolia" % "0.12.2",
"com.propensive" %%% "mercator" % "0.2.1",
"dev.zio" %%% "zio" % "1.0.0-RC17",
"dev.zio" %%% "zio-streams" % "1.0.0-RC17",
"dev.zio" %%% "zio-test" % "1.0.0-RC17" % "test",
"dev.zio" %%% "zio-test-sbt" % "1.0.0-RC17" % "test",
"com.lihaoyi" %%% "fastparse" % "2.1.3",
"com.propensive" %%% "magnolia" % "0.12.2",
"com.propensive" %%% "mercator" % "0.2.1",
"dev.zio" %%% "zio" % "1.0.0-RC17",
"dev.zio" %%% "zio-streams" % "1.0.0-RC17",
"dev.zio" %%% "zio-test" % "1.0.0-RC17" % "test",
"dev.zio" %%% "zio-test-sbt" % "1.0.0-RC17" % "test",
"io.circe" %%% "circe-derivation" % "0.12.0-M7" % Optional,
compilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1")
)
)
Expand Down Expand Up @@ -103,7 +104,6 @@ lazy val http4s = project
"org.http4s" %% "http4s-circe" % "0.21.0-M5",
"org.http4s" %% "http4s-blaze-server" % "0.21.0-M5",
"io.circe" %% "circe-parser" % "0.12.3",
"io.circe" %% "circe-derivation" % "0.12.0-M7",
compilerPlugin(
("org.typelevel" %% "kind-projector" % "0.11.0")
.cross(CrossVersion.full)
Expand Down
25 changes: 25 additions & 0 deletions core/src/main/scala/caliban/GraphQLRequest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package caliban

import caliban.interop.circe._

import scala.language.higherKinds

/**
* Represents a GraphQL request, containing a query, an operation name and a map of variables.
*/
case class GraphQLRequest(
query: String,
operationName: Option[String],
variables: Option[Map[String, InputValue]]
)

object GraphQLRequest {
implicit def circeDecoder[F[_]: IsCirceDecoder]: F[GraphQLRequest] =
GraphQLRequestCirce.graphQLRequestDecoder.asInstanceOf[F[GraphQLRequest]]
}

private object GraphQLRequestCirce {
import io.circe._
import io.circe.derivation._
val graphQLRequestDecoder: Decoder[GraphQLRequest] = deriveDecoder[GraphQLRequest]
}
22 changes: 22 additions & 0 deletions core/src/main/scala/caliban/GraphQLResponse.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
package caliban

import caliban.interop.circe._

import scala.language.higherKinds

/**
* Represents the result of a GraphQL query, containing a data object and a list of errors.
*/
case class GraphQLResponse[+E](data: ResponseValue, errors: List[E])

object GraphQLResponse {
implicit def circeEncoder[F[_]: IsCirceEncoder, E]: F[GraphQLResponse[E]] =
GraphQLResponceCirce.graphQLResponseEncoder.asInstanceOf[F[GraphQLResponse[E]]]
}

private object GraphQLResponceCirce {
import io.circe._
import io.circe.syntax._
val graphQLResponseEncoder: Encoder[GraphQLResponse[CalibanError]] = Encoder
.instance[GraphQLResponse[CalibanError]](
response =>
Json.obj(
"data" -> response.data.asJson,
"errors" -> Json.fromValues(response.errors.map(err => Json.fromString(err.toString)))
)
)
}
52 changes: 51 additions & 1 deletion core/src/main/scala/caliban/Value.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package caliban

import scala.util.Try
import caliban.Value._
import caliban.interop.circe._
import zio.stream.Stream

import scala.language.higherKinds
import scala.util.Try

sealed trait InputValue
sealed trait ResponseValue
sealed trait Value extends InputValue with ResponseValue
Expand Down Expand Up @@ -89,6 +93,9 @@ object InputValue {
case class ListValue(values: List[InputValue]) extends InputValue
case class ObjectValue(fields: Map[String, InputValue]) extends InputValue
case class VariableValue(name: String) extends InputValue

implicit def circeDecoder[F[_]: IsCirceDecoder]: F[InputValue] =
ValueCirce.inputValueDecoder.asInstanceOf[F[InputValue]]
}

object ResponseValue {
Expand All @@ -102,4 +109,47 @@ object ResponseValue {
case class StreamValue(stream: Stream[Throwable, ResponseValue]) extends ResponseValue {
override def toString: String = "<stream>"
}

implicit def circeEncoder[F[_]](implicit ev: IsCirceEncoder[F]): F[ResponseValue] =
ValueCirce.responseValueEncoder.asInstanceOf[F[ResponseValue]]
}

private object ValueCirce {
import io.circe._
private def jsonToValue(json: Json): InputValue =
json.fold(
NullValue,
BooleanValue,
number =>
number.toBigInt.map(IntValue.apply) orElse
number.toBigDecimal.map(FloatValue.apply) getOrElse
FloatValue(number.toDouble),
StringValue,
array => InputValue.ListValue(array.toList.map(jsonToValue)),
obj => InputValue.ObjectValue(obj.toMap.map { case (k, v) => k -> jsonToValue(v) })
)
val inputValueDecoder: Decoder[InputValue] = Decoder.instance(hcursor => Right(jsonToValue(hcursor.value)))
val responseValueEncoder: Encoder[ResponseValue] = Encoder
.instance[ResponseValue]({
case NullValue => Json.Null
case v: IntValue =>
v match {
case IntValue.IntNumber(value) => Json.fromInt(value)
case IntValue.LongNumber(value) => Json.fromLong(value)
case IntValue.BigIntNumber(value) => Json.fromBigInt(value)
}
case v: FloatValue =>
v match {
case FloatValue.FloatNumber(value) => Json.fromFloatOrNull(value)
case FloatValue.DoubleNumber(value) => Json.fromDoubleOrNull(value)
case FloatValue.BigDecimalNumber(value) => Json.fromBigDecimal(value)
}
case StringValue(value) => Json.fromString(value)
case BooleanValue(value) => Json.fromBoolean(value)
case EnumValue(value) => Json.fromString(value)
case ResponseValue.ListValue(values) => Json.arr(values.map(responseValueEncoder.apply): _*)
case ResponseValue.ObjectValue(fields) =>
Json.obj(fields.map { case (k, v) => k -> responseValueEncoder.apply(v) }: _*)
case s: ResponseValue.StreamValue => Json.fromString(s.toString)
})
}
23 changes: 23 additions & 0 deletions core/src/main/scala/caliban/interop/circe/circe.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package caliban.interop.circe

import io.circe._

import scala.language.higherKinds

/**
* This class is an implementation of the pattern described in https://blog.7mind.io/no-more-orphans.html
* It makes it possible to mark circe dependency as optional and keep Encoders defined in the companion object.
*/
private[caliban] trait IsCirceEncoder[F[_]]
private[caliban] object IsCirceEncoder {
implicit val isCirceEncoder: IsCirceEncoder[Encoder] = null
}

/**
* This class is an implementation of the pattern described in https://blog.7mind.io/no-more-orphans.html
* It makes it possible to mark circe dependency as optional and keep Decoders defined in the companion object.
*/
private[caliban] trait IsCirceDecoder[F[_]]
private[caliban] object IsCirceDecoder {
implicit val isCirceDecoder: IsCirceDecoder[Decoder] = null
}
21 changes: 21 additions & 0 deletions core/src/test/scala/caliban/GraphQLRequestSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package caliban

import io.circe._
import zio.test.Assertion._
import zio.test._

object GraphQLRequestSpec
extends DefaultRunnableSpec(
suite("GraphQLRequestSpec")(
test("can be parsed from JSON") {
val request = Json
.obj("query" -> Json.fromString("{}"), "operationName" -> Json.fromString("op"), "variables" -> Json.obj())
assert(
request.as[GraphQLRequest],
isRight(
equalTo(GraphQLRequest(query = "{}", operationName = Some("op"), variables = Some(Map.empty)))
)
)
}
)
)
20 changes: 20 additions & 0 deletions core/src/test/scala/caliban/GraphQLResponseSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package caliban

import caliban.Value._
import io.circe._
import io.circe.syntax._
import zio.test.Assertion._
import zio.test._

object GraphQLResponseSpec
extends DefaultRunnableSpec(
suite("GraphQLResponseSpec")(
test("can be converted to JSON") {
val response = GraphQLResponse(StringValue("data"), Nil)
assert(
response.asJson,
equalTo(Json.obj("data" -> Json.fromString("data"), "errors" -> Json.arr()))
)
}
)
)
59 changes: 3 additions & 56 deletions http4s/src/main/scala/caliban/Http4sAdapter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import cats.effect.Effect
import cats.effect.syntax.all._
import cats.~>
import fs2.{ Pipe, Stream }
import io.circe.derivation.deriveDecoder
import io.circe._
import io.circe.parser._
import io.circe.syntax._
Expand All @@ -17,68 +16,16 @@ import org.http4s.dsl.Http4sDsl
import org.http4s.server.websocket.WebSocketBuilder
import org.http4s.websocket.WebSocketFrame
import org.http4s.websocket.WebSocketFrame.Text
import zio.interop.catz._
import zio._
import zio.interop.catz._

object Http4sAdapter {

case class GraphQLRequest(query: String, operationName: Option[String], variables: Option[Json] = None)

implicit val queryDecoder: Decoder[GraphQLRequest] = deriveDecoder[GraphQLRequest]

private def responseToJson(responseValue: ResponseValue): Json =
responseValue match {
case NullValue => Json.Null
case v: IntValue =>
v match {
case IntValue.IntNumber(value) => Json.fromInt(value)
case IntValue.LongNumber(value) => Json.fromLong(value)
case IntValue.BigIntNumber(value) => Json.fromBigInt(value)
}
case v: FloatValue =>
v match {
case FloatValue.FloatNumber(value) => Json.fromFloatOrNull(value)
case FloatValue.DoubleNumber(value) => Json.fromDoubleOrNull(value)
case FloatValue.BigDecimalNumber(value) => Json.fromBigDecimal(value)
}
case StringValue(value) => Json.fromString(value)
case BooleanValue(value) => Json.fromBoolean(value)
case EnumValue(value) => Json.fromString(value)
case ListValue(values) => Json.arr(values.map(responseToJson): _*)
case ObjectValue(fields) => Json.obj(fields.map { case (k, v) => k -> responseToJson(v) }: _*)
case s: StreamValue => Json.fromString(s.toString)
}

implicit def responseEncoder[E]: Encoder[GraphQLResponse[E]] =
(response: GraphQLResponse[E]) =>
Json.obj(
"data" -> responseToJson(response.data),
"errors" -> Json.fromValues(response.errors.map(err => Json.fromString(err.toString)))
)

private def jsonToValue(json: Json): InputValue =
json.fold(
NullValue,
BooleanValue,
number =>
number.toBigInt.map(IntValue.apply) orElse
number.toBigDecimal.map(FloatValue.apply) getOrElse
FloatValue(number.toDouble),
StringValue,
array => InputValue.ListValue(array.toList.map(jsonToValue)),
obj => InputValue.ObjectValue(obj.toMap.map { case (k, v) => k -> jsonToValue(v) })
)

private def jsonToVariables(json: Json): Map[String, InputValue] = jsonToValue(json) match {
case InputValue.ObjectValue(fields) => fields
case _ => Map()
}

private def execute[R, Q, M, S, E](
interpreter: GraphQL[R, Q, M, S, E],
query: GraphQLRequest
): URIO[R, GraphQLResponse[E]] =
interpreter.execute(query.query, query.operationName, query.variables.map(jsonToVariables).getOrElse(Map()))
interpreter.execute(query.query, query.operationName, query.variables.getOrElse(Map()))

def makeRestService[R, Q, M, S, E](interpreter: GraphQL[R, Q, M, S, E]): HttpRoutes[RIO[R, *]] = {
object dsl extends Http4sDsl[RIO[R, *]]
Expand Down Expand Up @@ -137,7 +84,7 @@ object Http4sAdapter {
case Some(query) =>
val operationName = payload.downField("operationName").success.flatMap(_.value.asString)
(for {
result <- execute(interpreter, GraphQLRequest(query, operationName))
result <- execute(interpreter, GraphQLRequest(query, operationName, None))
_ <- result.data match {
case ObjectValue((fieldName, StreamValue(stream)) :: Nil) =>
stream.foreach { item =>
Expand Down

0 comments on commit 5d8ae9e

Please sign in to comment.