Skip to content

Commit

Permalink
Allow GQLValueType to generate a scalar (#1127)
Browse files Browse the repository at this point in the history
* Allow GQLValueType to generate a scalar

* Fix typo
  • Loading branch information
ghostdogpr authored Nov 5, 2021
1 parent 52ec847 commit b942ba0
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 11 deletions.
17 changes: 12 additions & 5 deletions core/src/main/scala-2/caliban/schema/SchemaDerivation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,22 @@ trait SchemaDerivation[R] extends LowPriorityDerivedSchema {

def isValueType[T](ctx: ReadOnlyCaseClass[Typeclass, T]): Boolean =
ctx.annotations.exists {
case GQLValueType() => true
case _ => false
case GQLValueType(_) => true
case _ => false
}

def isScalarValueType[T](ctx: ReadOnlyCaseClass[Typeclass, T]): Boolean =
ctx.annotations.exists {
case GQLValueType(true) => true
case _ => false
}

def combine[T](ctx: ReadOnlyCaseClass[Typeclass, T]): Typeclass[T] = new Typeclass[T] {
override def toType(isInput: Boolean, isSubscription: Boolean): __Type =
if ((ctx.isValueClass || isValueType(ctx)) && ctx.parameters.nonEmpty)
ctx.parameters.head.typeclass.toType_(isInput, isSubscription)
else if (isInput)
if ((ctx.isValueClass || isValueType(ctx)) && ctx.parameters.nonEmpty) {
if (isScalarValueType(ctx)) makeScalar(getName(ctx), getDescription(ctx))
else ctx.parameters.head.typeclass.toType_(isInput, isSubscription)
} else if (isInput)
makeInputObject(
Some(ctx.annotations.collectFirst { case GQLInputName(suffix) => suffix }
.getOrElse(customizeInputTypeName(getName(ctx)))),
Expand Down
14 changes: 11 additions & 3 deletions core/src/main/scala-3/caliban/schema/SchemaDerivation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ trait SchemaDerivation[R] {
lazy val paramAnnotations = Macros.paramAnnotations[A].toMap
new Schema[R, A] {
def toType(isInput: Boolean, isSubscription: Boolean): __Type =
if (isValueType(annotations) && fields.nonEmpty) fields.head._3.toType_(isInput, isSubscription)
if (isValueType(annotations) && fields.nonEmpty)
if (isScalarValueType(annotations)) makeScalar(getName(annotations, info), getDescription(annotations))
else fields.head._3.toType_(isInput, isSubscription)
else if (isInput)
makeInputObject(
Some(annotations.collectFirst { case GQLInputName(suffix) => suffix }
Expand Down Expand Up @@ -209,8 +211,14 @@ trait SchemaDerivation[R] {

private def isValueType(annotations: Seq[Any]): Boolean =
annotations.exists {
case GQLValueType() => true
case _ => false
case GQLValueType(_) => true
case _ => false
}

private def isScalarValueType(annotations: Seq[Any]): Boolean =
annotations.exists {
case GQLValueType(true) => true
case _ => false
}

private def getName(annotations: Seq[Any], label: String): String =
Expand Down
6 changes: 4 additions & 2 deletions core/src/main/scala/caliban/schema/Annotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ object Annotations {
case class GQLUnion() extends StaticAnnotation

/**
* Annotation to make a union or interface redirect to a value type
* Annotation to make a case class redirect to its inner type.
* The `Schema` of the inner type will be used, unless `isScalar` is true in which case
* a scalar with the name of the parent case class will be created.
*/
case class GQLValueType() extends StaticAnnotation
case class GQLValueType(isScalar: Boolean = false) extends StaticAnnotation

/**
* Annotation to specify the default value of an input field
Expand Down
34 changes: 34 additions & 0 deletions core/src/test/scala/caliban/execution/ExecutionSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,40 @@ object ExecutionSpec extends DefaultRunnableSpec {
"""{"foos":[{"common":true,"int":42},{"common":false,"value":"hello"}]}"""
)
)
},
testM("value type not scalar") {
@GQLValueType
case class Wrapper(value: Int)
case class Queries(test: Wrapper)

val queries = Queries(Wrapper(2))

val api: GraphQL[Any] = GraphQL.graphQL(RootResolver(queries))
val interpreter = api.interpreter
val query = gqldoc("""{test}""")

assertM(interpreter.flatMap(_.execute(query)).map(_.data.toString))(
equalTo(
"""{"test":2}"""
)
)
},
testM("value type scalar") {
@GQLValueType(isScalar = true)
case class Wrapper(value: Int)
case class Queries(test: Wrapper)

val queries = Queries(Wrapper(2))

val api: GraphQL[Any] = GraphQL.graphQL(RootResolver(queries))
val interpreter = api.interpreter
val query = gqldoc("""{test}""")

assertM(interpreter.flatMap(_.execute(query)).map(_.data.toString))(
equalTo(
"""{"test":2}"""
)
)
}
)
}
18 changes: 18 additions & 0 deletions core/src/test/scala/caliban/schema/SchemaSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,24 @@ object SchemaSpec extends DefaultRunnableSpec {
)
) &&
assert(fieldNames)(hasSameElements(List("common")))
},
test("value type not scalar") {
@GQLValueType
case class Wrapper(value: Int)
case class Queries(test: Option[Wrapper])

assert(introspect[Queries].fields(__DeprecatedArgs()).toList.flatten.headOption.map(_.`type`()))(
isSome(hasField[__Type, Option[String]]("name", _.name, equalTo(Some("Int"))))
)
},
test("value type scalar") {
@GQLValueType(isScalar = true)
case class Wrapper(value: Int)
case class Queries(test: Option[Wrapper])

assert(introspect[Queries].fields(__DeprecatedArgs()).toList.flatten.headOption.map(_.`type`()))(
isSome(hasField[__Type, Option[String]]("name", _.name, equalTo(Some("Wrapper"))))
)
}
)

Expand Down
2 changes: 1 addition & 1 deletion vuepress/docs/docs/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ Caliban supports a few annotations to enrich data types:
- `@GQLDeprecated("reason")` allows deprecating a field or an enum value.
- `@GQLInterface` to force a sealed trait generating an interface instead of a union.
- `@GQLDirective(directive: Directive)` to add a directive to a field or type.
- `@GQLValueType` forces a type to behave as a value type for derivation. Meaning that caliban will ignore the outer type and take the first case class parameter as the real type.
- `@GQLValueType(isScalar: Boolean)` forces a type to behave as a value type for derivation. Meaning that caliban will ignore the outer type and take the first case class parameter as the real type. If `isScalar` is true, it will generate a scalar named after the case class (default: false).
- `@GQLDefault("defaultValue")` allows you to specify a default value for an input field using GraphQL syntax. The default value will be visible in your schema's SDL and during introspection.

## Java 8 Time types
Expand Down

0 comments on commit b942ba0

Please sign in to comment.