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

Type name validation #274

Merged
merged 3 commits into from
Mar 11, 2020
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
121 changes: 58 additions & 63 deletions core/src/main/scala/caliban/GraphQL.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import caliban.execution.Executor
import caliban.introspection.Introspector
import caliban.parsing.Parser
import caliban.parsing.adt.OperationType
import caliban.schema.RootSchema.Operation
import caliban.schema._
import caliban.validation.Validator
import caliban.wrappers.Wrapper
Expand All @@ -21,63 +20,21 @@ import zio.{ IO, URIO }
*/
trait GraphQL[-R] { self =>

protected val schema: RootSchema[R]
protected val schemaBuilder: RootSchemaBuilder[R]
protected val wrappers: List[Wrapper[R]]

private lazy val rootType: RootType =
RootType(schema.query.opType, schema.mutation.map(_.opType), schema.subscription.map(_.opType))
private lazy val introspectionRootSchema: RootSchema[Any] = Introspector.introspect(rootType)
private lazy val introspectionRootType: RootType = RootType(introspectionRootSchema.query.opType, None, None)

private final def execute(
query: String,
operationName: Option[String],
variables: Map[String, InputValue],
skipValidation: Boolean
): URIO[R, GraphQLResponse[CalibanError]] = decompose(wrappers).flatMap {
case (overallWrappers, parsingWrappers, validationWrappers, executionWrappers, fieldWrappers) =>
wrap((for {
doc <- wrap(Parser.parseQuery(query))(parsingWrappers, query)
intro = Introspector.isIntrospection(doc)
typeToValidate = if (intro) introspectionRootType else rootType
schemaToExecute = if (intro) introspectionRootSchema else schema
validate = Validator.prepare(doc, typeToValidate, schemaToExecute, operationName, variables, skipValidation)
request <- wrap(validate)(validationWrappers, doc)
op = request.operationType match {
case OperationType.Query => schemaToExecute.query
case OperationType.Mutation => schemaToExecute.mutation.getOrElse(schemaToExecute.query)
case OperationType.Subscription => schemaToExecute.subscription.getOrElse(schemaToExecute.query)
}
execute = Executor.executeRequest(request, op.plan, variables, fieldWrappers)
result <- wrap(execute)(executionWrappers, request)
} yield result).catchAll(Executor.fail))(overallWrappers, query)
}

/**
* Parses and validates the provided query against this API.
* @param query a string containing the GraphQL query.
* @return an effect that either fails with a [[CalibanError]] or succeeds with `Unit`
*/
final def check(query: String): IO[CalibanError, Unit] =
for {
document <- Parser.parseQuery(query)
intro = Introspector.isIntrospection(document)
typeToValidate = if (intro) introspectionRootType else rootType
_ <- Validator.validate(document, typeToValidate)
} yield ()

/**
* Returns a string that renders the API types into the GraphQL format.
*/
final def render: String =
s"""schema {
|${schema.query.opType.name.fold("")(n => s" query: $n\n")}${schema.mutation
|${schemaBuilder.query.flatMap(_.opType.name).fold("")(n => s" query: $n\n")}${schemaBuilder.mutation
.flatMap(_.opType.name)
.fold("")(n => s" mutation: $n\n")}${schema.subscription
.fold("")(n => s" mutation: $n\n")}${schemaBuilder.subscription
.flatMap(_.opType.name)
.fold("")(n => s" subscription: $n\n")}}
|
|${renderTypes(rootType.types)}""".stripMargin
|${renderTypes(schemaBuilder.types)}""".stripMargin

/**
* Creates an interpreter from your API. A GraphQLInterpreter is a wrapper around your API that allows
Expand All @@ -86,10 +43,48 @@ trait GraphQL[-R] { self =>
*/
final def interpreter: IO[ValidationError, GraphQLInterpreter[R, CalibanError]] =
Validator
.validateSchema(rootType)
.as {
(query: String, operationName: Option[String], variables: Map[String, InputValue], skipValidation: Boolean) =>
self.execute(query, operationName, variables, skipValidation)
.validateSchema(schemaBuilder)
.map { schema =>
lazy val rootType =
RootType(schema.query.opType, schema.mutation.map(_.opType), schema.subscription.map(_.opType))
lazy val introspectionRootSchema: RootSchema[Any] = Introspector.introspect(rootType)
lazy val introspectionRootType: RootType = RootType(introspectionRootSchema.query.opType, None, None)

new GraphQLInterpreter[R, CalibanError] {
override def check(query: String): IO[CalibanError, Unit] =
for {
document <- Parser.parseQuery(query)
intro = Introspector.isIntrospection(document)
typeToValidate = if (intro) introspectionRootType else rootType
_ <- Validator.validate(document, typeToValidate)
} yield ()

override def execute(
query: String,
operationName: Option[String],
variables: Map[String, InputValue],
skipValidation: Boolean
): URIO[R, GraphQLResponse[CalibanError]] =
decompose(wrappers).flatMap {
case (overallWrappers, parsingWrappers, validationWrappers, executionWrappers, fieldWrappers) =>
wrap((for {
doc <- wrap(Parser.parseQuery(query))(parsingWrappers, query)
intro = Introspector.isIntrospection(doc)
typeToValidate = if (intro) introspectionRootType else rootType
schemaToExecute = if (intro) introspectionRootSchema else schema
validate = Validator
.prepare(doc, typeToValidate, schemaToExecute, operationName, variables, skipValidation)
request <- wrap(validate)(validationWrappers, doc)
op = request.operationType match {
case OperationType.Query => schemaToExecute.query
case OperationType.Mutation => schemaToExecute.mutation.getOrElse(schemaToExecute.query)
case OperationType.Subscription => schemaToExecute.subscription.getOrElse(schemaToExecute.query)
}
execute = Executor.executeRequest(request, op.plan, variables, fieldWrappers)
result <- wrap(execute)(executionWrappers, request)
} yield result).catchAll(Executor.fail))(overallWrappers, query)
}
}
}

/**
Expand All @@ -100,8 +95,8 @@ trait GraphQL[-R] { self =>
*/
final def withWrapper[R2 <: R](wrapper: Wrapper[R2]): GraphQL[R2] =
new GraphQL[R2] {
override val schema: RootSchema[R2] = self.schema
override val wrappers: List[Wrapper[R2]] = wrapper :: self.wrappers
override val schemaBuilder: RootSchemaBuilder[R2] = self.schemaBuilder
override val wrappers: List[Wrapper[R2]] = wrapper :: self.wrappers
}

/**
Expand All @@ -117,7 +112,7 @@ trait GraphQL[-R] { self =>
*/
final def combine[R1 <: R](that: GraphQL[R1]): GraphQL[R1] =
new GraphQL[R1] {
override val schema: RootSchema[R1] = self.schema |+| that.schema
override val schemaBuilder: RootSchemaBuilder[R1] = self.schemaBuilder |+| that.schemaBuilder
override protected val wrappers: List[Wrapper[R1]] = self.wrappers ++ that.wrappers
}

Expand All @@ -138,15 +133,15 @@ trait GraphQL[-R] { self =>
mutationsName: Option[String] = None,
subscriptionsName: Option[String] = None
): GraphQL[R] = new GraphQL[R] {
override protected val schema: RootSchema[R] = self.schema.copy(
query = queriesName.fold(self.schema.query)(name =>
self.schema.query.copy(opType = self.schema.query.opType.copy(name = Some(name)))
override protected val schemaBuilder: RootSchemaBuilder[R] = self.schemaBuilder.copy(
query = queriesName.fold(self.schemaBuilder.query)(name =>
self.schemaBuilder.query.map(m => m.copy(opType = m.opType.copy(name = Some(name))))
),
mutation = mutationsName.fold(self.schema.mutation)(name =>
self.schema.mutation.map(m => m.copy(opType = m.opType.copy(name = Some(name))))
mutation = mutationsName.fold(self.schemaBuilder.mutation)(name =>
self.schemaBuilder.mutation.map(m => m.copy(opType = m.opType.copy(name = Some(name))))
),
subscription = subscriptionsName.fold(self.schema.subscription)(name =>
self.schema.subscription.map(m => m.copy(opType = m.opType.copy(name = Some(name))))
subscription = subscriptionsName.fold(self.schemaBuilder.subscription)(name =>
self.schemaBuilder.subscription.map(m => m.copy(opType = m.opType.copy(name = Some(name))))
)
)
override protected val wrappers: List[Wrapper[R]] = self.wrappers
Expand All @@ -166,8 +161,8 @@ object GraphQL {
mutationSchema: Schema[R, M],
subscriptionSchema: Schema[R, S]
): GraphQL[R] = new GraphQL[R] {
val schema: RootSchema[R] = RootSchema(
Operation(querySchema.toType(), querySchema.resolve(resolver.queryResolver)),
val schemaBuilder: RootSchemaBuilder[R] = RootSchemaBuilder(
resolver.queryResolver.map(r => Operation(querySchema.toType(), querySchema.resolve(r))),
resolver.mutationResolver.map(r => Operation(mutationSchema.toType(), mutationSchema.resolve(r))),
resolver.subscriptionResolver.map(r => Operation(subscriptionSchema.toType(), subscriptionSchema.resolve(r)))
)
Expand Down
22 changes: 18 additions & 4 deletions core/src/main/scala/caliban/GraphQLInterpreter.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package caliban

import caliban.Value.NullValue
import zio.{ Has, NeedsEnv, Tagged, URIO, ZEnv, ZLayer }
import zio.{ Has, IO, NeedsEnv, Tagged, URIO, ZEnv, ZLayer }

/**
* A `GraphQLInterpreter[-R, +E]` represents a GraphQL interpreter whose execution requires
Expand All @@ -12,6 +12,13 @@ import zio.{ Has, NeedsEnv, Tagged, URIO, ZEnv, ZLayer }
*/
trait GraphQLInterpreter[-R, +E] { self =>

/**
* Parses and validates the provided query against this API.
* @param query a string containing the GraphQL query.
* @return an effect that either fails with a [[CalibanError]] or succeeds with `Unit`
*/
def check(query: String): IO[CalibanError, Unit]

/**
* Parses, validates and finally runs the provided query against this interpreter.
* @param query a string containing the GraphQL query.
Expand Down Expand Up @@ -74,9 +81,16 @@ trait GraphQLInterpreter[-R, +E] { self =>
*/
final def wrapExecutionWith[R2, E2](
f: URIO[R, GraphQLResponse[E]] => URIO[R2, GraphQLResponse[E2]]
): GraphQLInterpreter[R2, E2] =
(query: String, operationName: Option[String], variables: Map[String, InputValue], skipValidation: Boolean) =>
f(self.execute(query, operationName, variables, skipValidation))
): GraphQLInterpreter[R2, E2] = new GraphQLInterpreter[R2, E2] {
override def check(query: String): IO[CalibanError, Unit] = self.check(query)
override def execute(
query: String,
operationName: Option[String],
variables: Map[String, InputValue],
skipValidation: Boolean
): URIO[R2, GraphQLResponse[E2]] = f(self.execute(query, operationName, variables, skipValidation))
}

}

object GraphQLInterpreter {
Expand Down
71 changes: 35 additions & 36 deletions core/src/main/scala/caliban/Rendering.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,47 +18,46 @@ object Rendering {
case __TypeKind.OBJECT => 8
}

private implicit val renderOrdering: Ordering[(String, __Type)] = Ordering.by(o => (o._2.kind, o._2.name))
private implicit val renderOrdering: Ordering[__Type] = Ordering.by(o => (o.kind, o.name.getOrElse("")))

/**
* Returns a string that renders the provided types into the GraphQL format.
*/
def renderTypes(types: Map[String, __Type]): String =
types.toList
def renderTypes(types: List[__Type]): String =
types
.sorted(renderOrdering)
.flatMap {
case (_, t) =>
t.kind match {
case __TypeKind.SCALAR => t.name.flatMap(name => if (isBuiltinScalar(name)) None else Some(s"scalar $name"))
case __TypeKind.NON_NULL => None
case __TypeKind.LIST => None
case __TypeKind.UNION =>
val renderedTypes: String =
t.possibleTypes
.fold(List.empty[String])(_.flatMap(_.name))
.mkString(" | ")
Some(
s"""${renderDescription(t.description)}${renderKind(t.kind)} ${renderTypeName(t)} = $renderedTypes"""
)
case _ =>
val renderedDirectives: String = renderDirectives(t.directives)
val renderedFields: String = t
.fields(__DeprecatedArgs())
.fold(List.empty[String])(_.map(renderField))
.mkString("\n ")
val renderedInputFields: String = t.inputFields
.fold(List.empty[String])(_.map(renderInputValue))
.mkString("\n ")
val renderedEnumValues = t
.enumValues(__DeprecatedArgs())
.fold(List.empty[String])(_.map(renderEnumValue))
.mkString("\n ")
Some(
s"""${renderDescription(t.description)}${renderKind(t.kind)} ${renderTypeName(t)}${renderInterfaces(t)}$renderedDirectives {
| $renderedFields$renderedInputFields$renderedEnumValues
|}""".stripMargin
)
}
.flatMap { t =>
t.kind match {
case __TypeKind.SCALAR => t.name.flatMap(name => if (isBuiltinScalar(name)) None else Some(s"scalar $name"))
case __TypeKind.NON_NULL => None
case __TypeKind.LIST => None
case __TypeKind.UNION =>
val renderedTypes: String =
t.possibleTypes
.fold(List.empty[String])(_.flatMap(_.name))
.mkString(" | ")
Some(
s"""${renderDescription(t.description)}${renderKind(t.kind)} ${renderTypeName(t)} = $renderedTypes"""
)
case _ =>
val renderedDirectives: String = renderDirectives(t.directives)
val renderedFields: String = t
.fields(__DeprecatedArgs())
.fold(List.empty[String])(_.map(renderField))
.mkString("\n ")
val renderedInputFields: String = t.inputFields
.fold(List.empty[String])(_.map(renderInputValue))
.mkString("\n ")
val renderedEnumValues = t
.enumValues(__DeprecatedArgs())
.fold(List.empty[String])(_.map(renderEnumValue))
.mkString("\n ")
Some(
s"""${renderDescription(t.description)}${renderKind(t.kind)} ${renderTypeName(t)}${renderInterfaces(t)}$renderedDirectives {
| $renderedFields$renderedInputFields$renderedEnumValues
|}""".stripMargin
)
}
}
.mkString("\n\n")

Expand Down
10 changes: 4 additions & 6 deletions core/src/main/scala/caliban/RootResolver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ package caliban
* A `root resolver` contains resolvers for the 3 types of operations allowed in GraphQL: queries, mutations and subscriptions.
*
* A `resolver` is a simple value of the case class describing the API.
*
* It's mandatory to have a query resolver, the 2 others are optional.
*/
case class RootResolver[+Query, +Mutation, +Subscription](
queryResolver: Query,
queryResolver: Option[Query],
mutationResolver: Option[Mutation],
subscriptionResolver: Option[Subscription]
)
Expand All @@ -19,13 +17,13 @@ object RootResolver {
* Constructs a [[RootResolver]] with only a query resolver.
*/
def apply[Query](queryResolver: Query): RootResolver[Query, Unit, Unit] =
RootResolver(queryResolver, Option.empty[Unit], Option.empty[Unit])
RootResolver(Some(queryResolver), Option.empty[Unit], Option.empty[Unit])

/**
* Constructs a [[RootResolver]] with a query resolver and a mutation resolver.
*/
def apply[Query, Mutation](queryResolver: Query, mutationResolver: Mutation): RootResolver[Query, Mutation, Unit] =
RootResolver(queryResolver, Some(mutationResolver), Option.empty[Unit])
RootResolver(Some(queryResolver), Some(mutationResolver), Option.empty[Unit])

/**
* Constructs a [[RootResolver]] with a query resolver, a mutation resolver and a subscription resolver.
Expand All @@ -35,5 +33,5 @@ object RootResolver {
mutationResolver: Mutation,
subscriptionResolver: Subscription
): RootResolver[Query, Mutation, Subscription] =
RootResolver(queryResolver, Some(mutationResolver), Some(subscriptionResolver))
RootResolver(Some(queryResolver), Some(mutationResolver), Some(subscriptionResolver))
}
3 changes: 1 addition & 2 deletions core/src/main/scala/caliban/introspection/Introspector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import caliban.introspection.adt._
import caliban.parsing.adt.Definition.ExecutableDefinition.OperationDefinition
import caliban.parsing.adt.Document
import caliban.parsing.adt.Selection.Field
import caliban.schema.RootSchema.Operation
import caliban.schema.{ RootSchema, RootType, Schema, Types }
import caliban.schema.{ Operation, RootSchema, RootType, Schema, Types }

object Introspector {

Expand Down
19 changes: 9 additions & 10 deletions core/src/main/scala/caliban/introspection/adt/__Type.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,20 @@ case class __Type(
enumValues: __DeprecatedArgs => Option[List[__EnumValue]] = _ => None,
inputFields: Option[List[__InputValue]] = None,
ofType: Option[__Type] = None,
directives: Option[List[Directive]] = None
directives: Option[List[Directive]] = None,
origin: Option[String] = None
) {
def |+|(that: __Type): __Type = __Type(
kind,
(name ++ that.name).reduceOption((_, b) => b),
(description ++ that.description).reduceOption((_, b) => b),
args =>
(fields(args) ++ that.fields(args)).reduceOption((a, b) => a.filterNot(f => b.exists(_.name == f.name)) ++ b),
() => (interfaces() ++ that.interfaces()).reduceOption((a, b) => a.filterNot(t => b.exists(_.name == t.name)) ++ b),
(possibleTypes ++ that.possibleTypes).reduceOption((a, b) => a.filterNot(t => b.exists(_.name == t.name)) ++ b),
args =>
(enumValues(args) ++ that.enumValues(args))
.reduceOption((a, b) => a.filterNot(v => b.exists(_.name == v.name)) ++ b),
(inputFields ++ that.inputFields).reduceOption((a, b) => a.filterNot(t => b.exists(_.name == t.name)) ++ b),
args => (fields(args) ++ that.fields(args)).reduceOption(_ ++ _),
() => (interfaces() ++ that.interfaces()).reduceOption(_ ++ _),
(possibleTypes ++ that.possibleTypes).reduceOption(_ ++ _),
args => (enumValues(args) ++ that.enumValues(args)).reduceOption(_ ++ _),
(inputFields ++ that.inputFields).reduceOption(_ ++ _),
(ofType ++ that.ofType).reduceOption(_ |+| _),
(directives ++ that.directives).reduceOption(_ ++ _)
(directives ++ that.directives).reduceOption(_ ++ _),
(origin ++ that.origin).reduceOption((_, b) => b)
)
}
8 changes: 8 additions & 0 deletions core/src/main/scala/caliban/schema/Operation.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package caliban.schema

import caliban.introspection.adt.__Type

case class Operation[-R](opType: __Type, plan: Step[R]) {
def |+|[R1 <: R](that: Operation[R1]): Operation[R1] =
Operation(opType |+| that.opType, Step.mergeRootSteps(plan, that.plan))
}
Loading