Skip to content

Commit

Permalink
Make error extension section an ObjectValue for arbitrary k,v support
Browse files Browse the repository at this point in the history
  • Loading branch information
DJLemkes committed Mar 5, 2020
1 parent 50cfb87 commit d060434
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 46 deletions.
48 changes: 17 additions & 31 deletions core/src/main/scala/caliban/CalibanError.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package caliban

import caliban.CalibanError.EncodableError
import caliban.ResponseValue.ObjectValue
import caliban.interop.circe.IsCirceEncoder
import caliban.parsing.adt.LocationInfo
import io.circe.Encoder

/**
* The base type for all Caliban errors.
Expand All @@ -20,31 +19,33 @@ object CalibanError {
case class ParsingError(
msg: String,
locationInfo: Option[LocationInfo] = None,
innerThrowable: Option[Throwable] = None
innerThrowable: Option[Throwable] = None,
extensions: Option[ObjectValue] = None
) extends CalibanError {
override def toString: String = s"Parsing Error: $msg ${innerThrowable.fold("")(_.toString)}"
}

/**
* Describes an error that happened while validating a query.
*/
case class ValidationError(msg: String, explanatoryText: String, locationInfo: Option[LocationInfo] = None)
extends CalibanError {
case class ValidationError(
msg: String,
explanatoryText: String,
locationInfo: Option[LocationInfo] = None,
extensions: Option[ObjectValue] = None
) extends CalibanError {
override def toString: String = s"ValidationError Error: $msg"
}

trait EncodableError extends Throwable {
def errorCode: String
}

/**
* Describes an error that happened while executing a query.
*/
case class ExecutionError(
msg: String,
path: List[Either[String, Int]] = Nil,
locationInfo: Option[LocationInfo] = None,
innerThrowable: Option[Throwable] = None
innerThrowable: Option[Throwable] = None,
extensions: Option[ObjectValue] = None
) extends CalibanError {
override def toString: String = s"Execution Error: $msg ${innerThrowable.fold("")(_.toString)}"
}
Expand All @@ -61,35 +62,27 @@ private object ErrorCirce {
Json.obj("line" -> li.line.asJson, "column" -> li.column.asJson)

val errorValueEncoder: Encoder[CalibanError] = Encoder.instance[CalibanError] {
case CalibanError.ParsingError(msg, locationInfo, _) =>
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,
"extensions" -> Json
.obj(
"code" -> "PARSING_ERROR".asJson
)
.dropNullValues
"extensions" -> extensions.asInstanceOf[Option[ResponseValue]].asJson.dropNullValues
)
.dropNullValues
case CalibanError.ValidationError(msg, _, locationInfo) =>
case CalibanError.ValidationError(msg, _, locationInfo, extensions) =>
Json
.obj(
"message" -> msg.asJson,
"locations" -> Some(locationInfo).collect {
case Some(li) => Json.arr(locationToJson(li))
}.asJson,
"extensions" -> Json
.obj(
"code" -> "VALIDATION_ERROR".asJson
)
.dropNullValues
"extensions" -> extensions.asInstanceOf[Option[ResponseValue]].asJson.dropNullValues
)
.dropNullValues
case CalibanError.ExecutionError(msg, path, locationInfo, innerThrowable) =>
case CalibanError.ExecutionError(msg, path, locationInfo, _, extensions) =>
Json
.obj(
"message" -> msg.asJson,
Expand All @@ -103,14 +96,7 @@ private object ErrorCirce {
case Right(value) => value.asJson
})
}.asJson,
"extensions" -> Json
.obj(
"code" -> innerThrowable.collect {
case error: EncodableError => error.errorCode
case _ => "EXECUTION_ERROR"
}.asJson
)
.dropNullValues
"extensions" -> extensions.asInstanceOf[Option[ResponseValue]].asJson.dropNullValues
)
.dropNullValues
}
Expand Down
46 changes: 36 additions & 10 deletions core/src/test/scala/caliban/GraphQLResponseSpec.scala
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package caliban

import caliban.CalibanError.{ EncodableError, ExecutionError }
import caliban.CalibanError.ExecutionError
import caliban.ResponseValue.ObjectValue
import caliban.Value._
import io.circe._
import io.circe.syntax._
import zio.test.Assertion._
import zio.test._

case object TestEncodableError extends Throwable with EncodableError {
override def errorCode: String = "TEST_ERROR"
}

object GraphQLResponseSpec
extends DefaultRunnableSpec(
suite("GraphQLResponseSpec")(
Expand All @@ -21,23 +18,52 @@ object GraphQLResponseSpec
equalTo(Json.obj("data" -> Json.fromString("data")))
)
},
test("should include error only if non-empty") {
test("should include error objects for every error, including extensions") {

val errorExtensions = List(
("errorCode", StringValue("TEST_ERROR")),
("myCustomKey", StringValue("my-value"))
)

val response = GraphQLResponse(StringValue("data"),
List(ExecutionError("Resolution failed", innerThrowable = Some(TestEncodableError))))
val response = GraphQLResponse(
StringValue("data"),
List(
ExecutionError(
"Resolution failed",
extensions = Some(ObjectValue(errorExtensions))
)
)
)

assert(
response.asJson,
equalTo(
Json.obj(
"data" -> Json.fromString("data"),
"errors" -> Json.arr(
Json.obj("message" -> Json.fromString("Resolution failed"),
"extensions" -> Json.obj("code" -> TestEncodableError.errorCode.asJson))
Json.obj(
"message" -> Json.fromString("Resolution failed"),
"extensions" -> Json.obj("errorCode" -> "TEST_ERROR".asJson, "myCustomKey" -> "my-value".asJson)
)
)
)
)
)
},
test("should not include errors element when there are none") {
val response = GraphQLResponse(
StringValue("data"),
List.empty
)

assert(
response.asJson,
equalTo(
Json.obj(
"data" -> Json.fromString("data")
)
)
)
}
)
)
26 changes: 21 additions & 5 deletions examples/src/main/scala/caliban/http4s/ExampleApp.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package caliban.http4s

import caliban.CalibanError.EncodableError
import caliban.CalibanError.{ ExecutionError, ParsingError, ValidationError }
import caliban.ExampleData._
import caliban.GraphQL._
import caliban.ResponseValue.ObjectValue
import caliban.Value.StringValue
import caliban.schema.Annotations.{ GQLDeprecated, GQLDescription }
import caliban.schema.GenericSchema
import caliban.wrappers.ApolloTracing.apolloTracing
import caliban.wrappers.Wrappers._
import caliban.{ ExampleService, GraphQL, Http4sAdapter, RootResolver }
import caliban.{ CalibanError, ExampleService, GraphQL, GraphQLInterpreter, Http4sAdapter, RootResolver }
import cats.data.Kleisli
import cats.effect.Blocker
import org.http4s.StaticFile
Expand All @@ -27,7 +29,9 @@ import scala.language.postfixOps

object ExampleApp extends CatsApp with GenericSchema[Console with Clock] {

sealed trait ExampleAppEncodableError extends EncodableError
sealed trait ExampleAppEncodableError extends Throwable {
def errorCode: String
}
case object UnauthorizedError extends ExampleAppEncodableError {
override def errorCode: String = "UNAUTHORIZED"
}
Expand Down Expand Up @@ -68,13 +72,25 @@ object ExampleApp extends CatsApp with GenericSchema[Console with Clock] {
printSlowQueries(500 millis) @@ // wrapper that logs slow queries
apolloTracing // wrapper for https://github.com/apollographql/apollo-tracing

def withErrorCodeExtensions[R](
interpreter: GraphQLInterpreter[R, CalibanError]
): GraphQLInterpreter[R, CalibanError] = interpreter.mapError {
case err @ ExecutionError(_, _, _, Some(exampleError: ExampleAppEncodableError), _) =>
err.copy(extensions = Some(ObjectValue(List(("errorCode", StringValue(exampleError.errorCode))))))
case err: ValidationError =>
err.copy(extensions = Some(ObjectValue(List(("errorCode", StringValue("VALIDATION_ERROR"))))))
case err: ParsingError =>
err.copy(extensions = Some(ObjectValue(List(("errorCode", StringValue("PARSING_ERROR"))))))
}

override def run(args: List[String]): ZIO[ZEnv, Nothing, Int] =
(for {
blocker <- ZIO
.accessM[Blocking](_.blocking.blockingExecutor.map(_.asEC))
.map(Blocker.liftExecutionContext)
service <- ExampleService.make(sampleCharacters)
interpreter <- makeApi(service).interpreter
service <- ExampleService.make(sampleCharacters)
baseInterpreter <- makeApi(service).interpreter
interpreter = withErrorCodeExtensions(baseInterpreter)
_ <- BlazeServerBuilder[ExampleTask]
.bindHttp(8088, "localhost")
.withHttpApp(
Expand Down

0 comments on commit d060434

Please sign in to comment.