Skip to content

Commit

Permalink
Add semanticNonNull support
Browse files Browse the repository at this point in the history
  • Loading branch information
XiNiHa committed Apr 5, 2024
1 parent 7ddba86 commit faa4a2c
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 6 deletions.
7 changes: 6 additions & 1 deletion core/src/main/scala-2/caliban/schema/SchemaDerivation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,12 @@ trait CommonSchemaDerivation[R] {
else p.typeclass.toType_(isInput, isSubscription).nonNull,
p.annotations.collectFirst { case GQLDeprecated(_) => () }.isDefined,
p.annotations.collectFirst { case GQLDeprecated(reason) => reason },
Option(p.annotations.collect { case GQLDirective(dir) => dir }.toList).filter(_.nonEmpty)
Option(
p.annotations.collect { case GQLDirective(dir) => dir }.toList ++ {
if (isOptional && p.typeclass.semanticallyNonNull) Some(Directive("semanticNonNull"))
else None
}
).filter(_.nonEmpty)
)
}
.toList,
Expand Down
7 changes: 6 additions & 1 deletion core/src/main/scala-3/caliban/schema/DerivationUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,12 @@ private object DerivationUtils {
else schema.toType_(isInput, isSubscription).nonNull,
deprecatedReason.isDefined,
deprecatedReason,
Option(getDirectives(fieldAnnotations)).filter(_.nonEmpty)
Option(
getDirectives(fieldAnnotations) ++ {
if (isOptional && schema.semanticallyNonNull) Some(Directive("semanticNonNull"))
else None
}
).filter(_.nonEmpty)
)
},
getDirectives(annotations),
Expand Down
25 changes: 23 additions & 2 deletions core/src/main/scala/caliban/schema/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ trait Schema[-R, T] { self =>
*/
def optional: Boolean = false

/**
* Defines if the type is considered semantically nullable or not.
*/
def semanticallyNonNull: Boolean = false

/**
* Defined the arguments of the given type. Should be empty except for `Function`.
*/
Expand Down Expand Up @@ -504,6 +509,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema {
): Schema[R0, ZIO[R1, E, A]] =
new Schema[R0, ZIO[R1, E, A]] {
override def optional: Boolean = true
override def semanticallyNonNull: Boolean = !ev.optional
override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription)
override def resolve(value: ZIO[R1, E, A]): Step[R0] = QueryStep(ZQuery.fromZIONow(value.map(ev.resolve)))
}
Expand All @@ -512,6 +518,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema {
): Schema[R0, ZIO[R1, E, A]] =
new Schema[R0, ZIO[R1, E, A]] {
override def optional: Boolean = true
override def semanticallyNonNull: Boolean = !ev.optional
override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription)
override def resolve(value: ZIO[R1, E, A]): Step[R0] = QueryStep(
ZQuery.fromZIONow(value.mapBoth(convertError, ev.resolve))
Expand All @@ -530,6 +537,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema {
): Schema[R0, ZQuery[R1, E, A]] =
new Schema[R0, ZQuery[R1, E, A]] {
override def optional: Boolean = true
override def semanticallyNonNull: Boolean = !ev.optional
override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription)
override def resolve(value: ZQuery[R1, E, A]): Step[R0] = QueryStep(value.map(ev.resolve))
}
Expand All @@ -538,6 +546,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema {
): Schema[R0, ZQuery[R1, E, A]] =
new Schema[R0, ZQuery[R1, E, A]] {
override def optional: Boolean = true
override def semanticallyNonNull: Boolean = !ev.optional
override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription)
override def resolve(value: ZQuery[R1, E, A]): Step[R0] = QueryStep(value.mapBoth(convertError, ev.resolve))
}
Expand All @@ -557,6 +566,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema {
): Schema[R0, ZStream[R1, E, A]] =
new Schema[R0, ZStream[R1, E, A]] {
override def optional: Boolean = true
override def semanticallyNonNull: Boolean = !ev.optional
override def toType(isInput: Boolean, isSubscription: Boolean): __Type = {
val t = ev.toType_(isInput, isSubscription)
if (isSubscription) t else (if (ev.optional) t else t.nonNull).list
Expand All @@ -568,6 +578,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema {
): Schema[R0, ZStream[R1, E, A]] =
new Schema[R0, ZStream[R1, E, A]] {
override def optional: Boolean = true
override def semanticallyNonNull: Boolean = !ev.optional
override def toType(isInput: Boolean, isSubscription: Boolean): __Type = {
val t = ev.toType_(isInput, isSubscription)
if (isSubscription) t else (if (ev.optional) t else t.nonNull).list
Expand Down Expand Up @@ -703,7 +714,12 @@ abstract class PartiallyAppliedFieldBase[V](name: String, description: Option[St
else ev.toType_(ft.isInput, ft.isSubscription).nonNull,
isDeprecated = Directives.isDeprecated(directives),
deprecationReason = Directives.deprecationReason(directives),
directives = Some(directives.filter(_.name != "deprecated")).filter(_.nonEmpty)
directives = Some(
directives.filter(_.name != "deprecated") ++ {
if (ev.optional && ev.semanticallyNonNull) Some(Directive("semanticNonNull"))
else None
}
).filter(_.nonEmpty)
)
}

Expand Down Expand Up @@ -735,7 +751,12 @@ case class PartiallyAppliedFieldWithArgs[V, A](name: String, description: Option
else ev1.toType_(fa.isInput, fa.isSubscription).nonNull,
isDeprecated = Directives.isDeprecated(directives),
deprecationReason = Directives.deprecationReason(directives),
directives = Some(directives.filter(_.name != "deprecated")).filter(_.nonEmpty)
directives = Some(
directives.filter(_.name != "deprecated") ++ {
if (ev1.optional && ev1.semanticallyNonNull) Some(Directive("semanticNonNull"))
else None
}
).filter(_.nonEmpty)
),
(v: V) => ev1.resolve(fn(v))
)
Expand Down
27 changes: 25 additions & 2 deletions core/src/test/scala/caliban/schema/SchemaSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package caliban.schema

import caliban.Value.StringValue
import caliban._
import caliban.introspection.adt.{ __DeprecatedArgs, __Type, __TypeKind }
import caliban.introspection.adt.{ __DeprecatedArgs, __Field, __Type, __TypeKind }
import caliban.parsing.adt.Directive
import caliban.schema.Annotations._
import caliban.schema.ArgBuilder.auto._
Expand All @@ -26,11 +26,33 @@ object SchemaSpec extends ZIOSpecDefault {
isSome(hasField[__Type, __TypeKind]("kind", _.kind, equalTo(__TypeKind.SCALAR)))
)
},
test("effectful field as semanticNonNull") {
assert(introspect[EffectfulFieldSchema].fields(__DeprecatedArgs()).toList.flatten.headOption)(
isSome(
hasField[__Field, Option[List[Directive]]](
"directives",
_.directives,
isSome(contains((Directive("semanticNonNull"))))
)
)
)
},
test("effectful field as non-nullable") {
assert(introspect[EffectfulFieldSchema].fields(__DeprecatedArgs()).toList.flatten.apply(1)._type)(
hasField[__Type, __TypeKind]("kind", _.kind, equalTo(__TypeKind.NON_NULL))
)
},
test("optional effectful field") {
assert(introspect[OptionalEffectfulFieldSchema].fields(__DeprecatedArgs()).toList.flatten.headOption)(
isSome(
hasField[__Field, Option[List[Directive]]](
"directives",
_.directives.map(_.filter(_.name == "semanticNonNull")).filter(_.nonEmpty),
isNone
)
)
)
},
test("infallible effectful field") {
assert(introspect[InfallibleFieldSchema].fields(__DeprecatedArgs()).toList.flatten.headOption.map(_._type))(
isSome(hasField[__Type, __TypeKind]("kind", _.kind, equalTo(__TypeKind.NON_NULL)))
Expand Down Expand Up @@ -300,7 +322,7 @@ object SchemaSpec extends ZIOSpecDefault {
|}
|
|type EnvironmentSchema {
| test: Int
| test: Int @semanticNonNull
| box: Box!
|}
|
Expand Down Expand Up @@ -386,6 +408,7 @@ object SchemaSpec extends ZIOSpecDefault {
)

case class EffectfulFieldSchema(q: Task[Int], @GQLNonNullable qAnnotated: Task[Int])
case class OptionalEffectfulFieldSchema(q: Task[Option[String]], @GQLNonNullable qAnnotated: Task[Option[String]])
case class InfallibleFieldSchema(q: UIO[Int], @GQLNullable qAnnotated: UIO[Int])
case class FutureFieldSchema(q: Future[Int])
case class IDSchema(id: UUID)
Expand Down

0 comments on commit faa4a2c

Please sign in to comment.