Skip to content

Commit

Permalink
Use Either instead of ZIO for methods that don't require it (#2292)
Browse files Browse the repository at this point in the history
* Use `Either` instead of `ZIO` for methods that don't require it

* Cleanup ValidationBenchmark

* Change the return type of `interpreter` to Exit
  • Loading branch information
kyri-petrou authored Jun 18, 2024
1 parent 668d2bd commit c0ac37c
Show file tree
Hide file tree
Showing 18 changed files with 120 additions and 170 deletions.
2 changes: 1 addition & 1 deletion benchmarks/src/main/scala/caliban/ParserBenchmark.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class ParserBenchmark {

@Benchmark
def runCaliban(): Document =
Parser.parseQueryEither(fullIntrospectionQuery).fold(throw _, identity)
Parser.parseQuery(fullIntrospectionQuery).fold(throw _, identity)

@Benchmark
def runSangria(): sangria.ast.Document =
Expand Down
101 changes: 40 additions & 61 deletions benchmarks/src/main/scala/caliban/validation/ValidationBenchmark.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import caliban._
import caliban.execution.NestedZQueryBenchmarkSchema
import caliban.introspection.Introspector
import caliban.parsing.{ Parser, VariablesCoercer }
import caliban.schema.{ RootSchema, RootType }
import org.openjdk.jmh.annotations.{ Scope, _ }
import zio._
import caliban.schema.RootType
import org.openjdk.jmh.annotations._

import java.util.concurrent.TimeUnit

Expand All @@ -18,17 +17,16 @@ import java.util.concurrent.TimeUnit
@Fork(2)
class ValidationBenchmark {

private val runtime = Runtime.default
def run[A](either: Either[Throwable, A]): A = either.fold(throw _, identity)

def run[A](zio: Task[A]): A = Unsafe.unsafe(implicit u => runtime.unsafe.run(zio).getOrThrow())
def toSchema[R](graphQL: GraphQL[R]): IO[CalibanError, RootType] =
graphQL.validateRootSchema.map { schema =>
def toRootType[R](graphQL: GraphQL[R]): RootType =
run(graphQL.validateRootSchema.map { schema =>
RootType(
schema.query.opType,
schema.mutation.map(_.opType),
schema.subscription.map(_.opType)
)
}
})

import NestedZQueryBenchmarkSchema._

Expand All @@ -38,118 +36,99 @@ class ValidationBenchmark {
val parsedDeepWithArgsQuery = run(Parser.parseQuery(deepWithArgsQuery))
val parsedIntrospectionQuery = run(Parser.parseQuery(ComplexQueryBenchmark.fullIntrospectionQuery))

val simpleType = run(
toSchema(graphQL[Any, SimpleRoot, Unit, Unit](RootResolver(NestedZQueryBenchmarkSchema.simple100Elements)))
)
val simpleType =
toRootType(graphQL[Any, SimpleRoot, Unit, Unit](RootResolver(NestedZQueryBenchmarkSchema.simple100Elements)))

val multifieldType =
run(
toSchema(
graphQL[Any, MultifieldRoot, Unit, Unit](
RootResolver(NestedZQueryBenchmarkSchema.multifield100Elements)
)
toRootType(
graphQL[Any, MultifieldRoot, Unit, Unit](
RootResolver(NestedZQueryBenchmarkSchema.multifield100Elements)
)
)

val deepType =
run(
toSchema(
graphQL[Any, DeepRoot, Unit, Unit](
RootResolver[DeepRoot](NestedZQueryBenchmarkSchema.deep100Elements)
)
toRootType(
graphQL[Any, DeepRoot, Unit, Unit](
RootResolver[DeepRoot](NestedZQueryBenchmarkSchema.deep100Elements)
)
)

val deepWithArgsType =
run(
toSchema(
graphQL[Any, DeepWithArgsRoot, Unit, Unit](
RootResolver[DeepWithArgsRoot](NestedZQueryBenchmarkSchema.deepWithArgs100Elements)
)
toRootType(
graphQL[Any, DeepWithArgsRoot, Unit, Unit](
RootResolver[DeepWithArgsRoot](NestedZQueryBenchmarkSchema.deepWithArgs100Elements)
)
)

@Benchmark
def simple(): Any = {
val io = Validator.validate(parsedSimpleQuery, simpleType)
run(io)
}
def simple(): Any =
run(Validator.validateAll(parsedSimpleQuery, simpleType))

@Benchmark
def multifield(): Any = {
val io = Validator.validate(parsedMultifieldQuery, multifieldType)
run(io)
}
def multifield(): Any =
run(Validator.validateAll(parsedMultifieldQuery, multifieldType))

@Benchmark
def deep(): Any = {
val io = Validator.validate(parsedDeepQuery, deepType)
run(io)
}
def deep(): Any =
run(Validator.validateAll(parsedDeepQuery, deepType))

@Benchmark
def variableCoercer(): Any = {
val io = VariablesCoercer.coerceVariables(deepArgs100Elements, parsedDeepWithArgsQuery, deepWithArgsType, false)
run(io)
}
def variableCoercer(): Any =
run(VariablesCoercer.coerceVariables(deepArgs100Elements, parsedDeepWithArgsQuery, deepWithArgsType, false))

@Benchmark
def introspection(): Any = {
val io =
Validator.validate(parsedIntrospectionQuery, Introspector.introspectionRootType)
run(io)
}
def introspection(): Any =
run(Validator.validateAll(parsedIntrospectionQuery, Introspector.introspectionRootType))

@Benchmark
def fieldCreationSimple(): Any =
Validator
.prepareEither(
run(
Validator.prepare(
parsedSimpleQuery,
simpleType,
None,
Map.empty,
skipValidation = true,
validations = Nil
)
.fold(throw _, identity)
)

@Benchmark
def fieldCreationMultifield(): Any =
Validator
.prepareEither(
run(
Validator.prepare(
parsedMultifieldQuery,
multifieldType,
None,
Map.empty,
skipValidation = true,
validations = Nil
)
.fold(throw _, identity)
)

@Benchmark
def fieldCreationDeep(): Any =
Validator
.prepareEither(
def fieldCreationDeep(): Any =
run(
Validator.prepare(
parsedDeepQuery,
deepType,
None,
Map.empty,
skipValidation = true,
validations = Nil
)
.fold(throw _, identity)

)
@Benchmark
def fieldCreationIntrospection(): Any =
Validator
.prepareEither(
run(
Validator.prepare(
parsedIntrospectionQuery,
Introspector.introspectionRootType,
None,
Map.empty,
skipValidation = true,
validations = Nil
)
.fold(throw _, identity)
)

}
5 changes: 3 additions & 2 deletions benchmarks/src/test/scala/caliban/CalibanSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ object CalibanSpec extends ZIOSpecDefault {
assertTrue(Caliban.run(io).errors.isEmpty)
},
test("Parser") {
val io = Parser.parseQuery(fullIntrospectionQuery)
assertTrue(Caliban.run(io).definitions.nonEmpty)
Parser.parseQuery(fullIntrospectionQuery).map { doc =>
assertTrue(doc.definitions.nonEmpty)
}
}
)
}
25 changes: 14 additions & 11 deletions core/src/main/scala/caliban/GraphQL.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import caliban.validation.Validator
import caliban.wrappers.Wrapper
import caliban.wrappers.Wrapper._
import zio.stacktracer.TracingImplicits.disableAutoTrace
import zio.{ IO, Trace, URIO, ZIO }
import zio.{ Exit, IO, Trace, URIO, Unsafe, ZIO }

/**
* A `GraphQL[-R]` represents a GraphQL API whose execution requires a ZIO environment of type `R`.
Expand All @@ -31,8 +31,8 @@ trait GraphQL[-R] { self =>
protected val features: Set[Feature]
protected val transformer: Transformer[R]

private[caliban] def validateRootSchema(implicit trace: Trace): IO[ValidationError, RootSchema[R]] =
ZIO.fromEither(Validator.validateSchemaEither(schemaBuilder))
private[caliban] def validateRootSchema: Either[ValidationError, RootSchema[R]] =
Validator.validateSchema(schemaBuilder)

/**
* Returns a string that renders the API types into the GraphQL SDL.
Expand Down Expand Up @@ -63,16 +63,16 @@ trait GraphQL[-R] { self =>
*
* @see [[interpreterEither]] for a variant that returns an Either instead, making it easier to embed in non-ZIO applications
*/
final def interpreter(implicit trace: Trace): IO[ValidationError, GraphQLInterpreter[R, CalibanError]] =
ZIO.fromEither(interpreterEither)
final def interpreter: Exit[ValidationError, GraphQLInterpreter[R, CalibanError]] =
Exit.fromEither(interpreterEither)

/**
* Impure variant of [[interpreterEither]] which throws schema validation errors. Useful for cases that the
* schema is known to be valid or when we're not planning on handling the error (e.g., during app startup)
*/
@throws[CalibanError.ValidationError]("if the schema is invalid")
final def interpreterUnsafe: GraphQLInterpreter[R, CalibanError] =
interpreterEither.fold(throw _, identity)
Unsafe.unsafe(interpreter.getOrThrowFiberFailure()(_))

/**
* Creates an interpreter from your API. A GraphQLInterpreter is a wrapper around your API that allows
Expand All @@ -83,7 +83,7 @@ trait GraphQL[-R] { self =>
* @see [[interpreterUnsafe]] of an unsafe variant that will throw validation errors
*/
final lazy val interpreterEither: Either[ValidationError, GraphQLInterpreter[R, CalibanError]] =
Validator.validateSchemaEither(schemaBuilder).map { schema =>
validateRootSchema.map { schema =>
new GraphQLInterpreter[R, CalibanError] {
private val rootType =
RootType(
Expand All @@ -98,9 +98,12 @@ trait GraphQL[-R] { self =>
private val introWrappers = wrappers.collect { case w: IntrospectionWrapper[R] => w }
private lazy val introspectionRootSchema: RootSchema[R] = Introspector.introspect(rootType, introWrappers)

private def parseZIO(query: String)(implicit trace: Trace): IO[CalibanError.ParsingError, Document] =
ZIO.fromEither(Parser.parseQuery(query))

override def check(query: String)(implicit trace: Trace): IO[CalibanError, Unit] =
for {
document <- Parser.parseQuery(query)
document <- parseZIO(query)
intro = Introspector.isIntrospection(document)
typeToValidate = if (intro) Introspector.introspectionRootType else rootType
_ <- Validator.validate(document, typeToValidate)
Expand All @@ -113,7 +116,7 @@ trait GraphQL[-R] { self =>
case (overallWrappers, parsingWrappers, validationWrappers, executionWrappers, fieldWrappers, _) =>
wrap((request: GraphQLRequest) =>
(for {
doc <- wrap(Parser.parseQuery)(parsingWrappers, request.query.getOrElse(""))
doc <- wrap(parseZIO)(parsingWrappers, request.query.getOrElse(""))
coercedVars <- coerceVariables(doc, request.variables.getOrElse(Map.empty))
executionReq <- wrap(validation(request.operationName, coercedVars))(validationWrappers, doc)
result <- wrap(execution(schemaToExecute(doc), fieldWrappers))(executionWrappers, executionReq)
Expand All @@ -128,7 +131,7 @@ trait GraphQL[-R] { self =>
if (doc.isIntrospection && !config.enableIntrospection)
ZIO.fail(CalibanError.ValidationError("Introspection is disabled", ""))
else
VariablesCoercer.coerceVariablesEither(variables, doc, typeToValidate(doc), config.skipValidation) match {
VariablesCoercer.coerceVariables(variables, doc, typeToValidate(doc), config.skipValidation) match {
case Right(value) => ZIO.succeed(value)
case Left(error) => ZIO.fail(error)
}
Expand All @@ -139,7 +142,7 @@ trait GraphQL[-R] { self =>
coercedVars: Map[String, InputValue]
)(doc: Document)(implicit trace: Trace): IO[ValidationError, ExecutionRequest] =
Configurator.ref.getWith { config =>
Validator.prepareEither(
Validator.prepare(
doc,
typeToValidate(doc),
operationName,
Expand Down
13 changes: 1 addition & 12 deletions core/src/main/scala/caliban/parsing/Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,18 @@ import caliban.CalibanError.ParsingError
import caliban.InputValue
import caliban.parsing.adt._
import fastparse._
import zio.stacktracer.TracingImplicits.disableAutoTrace
import zio.{ IO, Trace, ZIO }

import scala.util.Try
import scala.util.control.NonFatal

object Parser {
import caliban.parsing.parsers.Parsers._

/**
* Parses the given string into a [[caliban.parsing.adt.Document]] object or fails with a [[caliban.CalibanError.ParsingError]].
*
* @see [[parseQueryEither]] for a version that returns an `Either` instead of an `IO`.
*/
def parseQuery(query: String)(implicit trace: Trace): IO[ParsingError, Document] = ZIO.fromEither {
parseQueryEither(query)
}

/**
* Parses the given string into a [[caliban.parsing.adt.Document]] object or returns a [[caliban.CalibanError.ParsingError]]
* if the string is not a valid GraphQL document.
*/
def parseQueryEither(query: String): Either[ParsingError, Document] = {
def parseQuery(query: String): Either[ParsingError, Document] = {
val sm = SourceMapper(query)
try
parse(query, document(_)) match {
Expand Down
10 changes: 1 addition & 9 deletions core/src/main/scala/caliban/parsing/VariablesCoercer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ object VariablesCoercer {
doc: Document,
rootType: RootType,
skipValidation: Boolean
)(implicit trace: Trace): IO[ValidationError, GraphQLRequest] =
): Either[ValidationError, GraphQLRequest] =
coerceVariables(req.variables.getOrElse(Map.empty), doc, rootType, skipValidation)
.map(m => req.copy(variables = Some(m)))

Expand All @@ -31,14 +31,6 @@ object VariablesCoercer {
doc: Document,
rootType: RootType,
skipValidation: Boolean
)(implicit trace: Trace): IO[ValidationError, Map[String, InputValue]] =
ZIO.fromEither(coerceVariablesEither(variables, doc, rootType, skipValidation))

def coerceVariablesEither(
variables: Map[String, InputValue],
doc: Document,
rootType: RootType,
skipValidation: Boolean
): Either[ValidationError, Map[String, InputValue]] = {
// Scala 2's compiler loves inferring `ZPure.succeed` as ZPure[Nothing, Nothing, Any, R, E, A] so we help it out
type F[+A] = EReader[Any, ValidationError, A]
Expand Down
Loading

0 comments on commit c0ac37c

Please sign in to comment.