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

Move circe codecs to the core module #102

Merged
merged 1 commit into from
Dec 1, 2019
Merged
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
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",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we reduce the dependency to circe-core as I suggested in the other comment, this is good. But otherwise, I think we should keep that one so that people who use caliban-http4s don't need to add the dependency to circe-derivation themselves.

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._
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are using circe-derivation only for this single (and simple) type, how about deriving it manually and only depend on circe-core?
Because not everyone uses circe-derivation, some people use circe-generic, others circe-magnolia. That way we would keep the dependency minimal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think codecs created by circe-derivation macros depend on the circe core only, so people won't need circe-derivation as a dependency. Although, to be on the safe side I can indeed implement it manually.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, you must be right! All good then.

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