Skip to content

Commit

Permalink
Add SemanticNonNull support (#2180)
Browse files Browse the repository at this point in the history
* Add semanticNonNull support

* Disable feature by default with an overridable flag

* Adjust schema definition to contain canFail instead of semanticNonNull

* No semanticNonNull transform on hand-written schema

* Reuse semanticNonNull directive definition across files

* Wrap enableSemanticNonNull option with a config object

* Revert changing the semantic of Schema.optional by introducing Schema.nullable

* Change deprecation warning version to 2.7.0

* Clear nullability determination logic

* Add test to ensure semantic of .optional for custom schemas

* Fix not keeping semantics of .optional on custom schema

* Add supplementary tests to make things sure
  • Loading branch information
XiNiHa authored May 12, 2024
1 parent 5b6cb3e commit 63f4d6d
Show file tree
Hide file tree
Showing 15 changed files with 337 additions and 60 deletions.
33 changes: 29 additions & 4 deletions core/src/main/scala-2/caliban/schema/SchemaDerivation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ import scala.language.experimental.macros

trait CommonSchemaDerivation[R] {

case class DerivationConfig(
/**
* Whether to enable the `SemanticNonNull` feature on derivation.
* It is currently disabled by default since it is not yet stable.
*/
enableSemanticNonNull: Boolean = false
)

/**
* Returns a configuration object that can be used to customize the derivation behavior.
*
* Override this method to customize the configuration.
*/
def config: DerivationConfig = DerivationConfig()

/**
* Default naming logic for input types.
* This is needed to avoid a name clash between a type used as an input and the same type used as an output.
Expand Down Expand Up @@ -80,21 +95,31 @@ trait CommonSchemaDerivation[R] {
ctx.parameters
.filterNot(_.annotations.exists(_ == GQLExcluded()))
.map { p =>
val isOptional = {
val (isNullable, isSemanticNonNull) = {
val hasNullableAnn = p.annotations.contains(GQLNullable())
val hasNonNullAnn = p.annotations.contains(GQLNonNullable())
!hasNonNullAnn && (hasNullableAnn || p.typeclass.optional)

if (hasNonNullAnn) (false, false)
else if (hasNullableAnn) (true, false)
else if (p.typeclass.optional) (true, !p.typeclass.nullable)
else (false, false)
}
Types.makeField(
getName(p),
getDescription(p),
p.typeclass.arguments,
() =>
if (isOptional) p.typeclass.toType_(isInput, isSubscription)
if (isNullable) p.typeclass.toType_(isInput, isSubscription)
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 (config.enableSemanticNonNull && isSemanticNonNull)
Some(SchemaUtils.SemanticNonNull)
else None
}
).filter(_.nonEmpty)
)
}
.toList,
Expand Down
22 changes: 16 additions & 6 deletions core/src/main/scala-3/caliban/schema/DerivationUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -120,27 +120,37 @@ private object DerivationUtils {
def mkObject[R](
annotations: List[Any],
fields: List[(String, List[Any], Schema[R, Any])],
info: TypeInfo
info: TypeInfo,
enableSemanticNonNull: Boolean
)(isInput: Boolean, isSubscription: Boolean): __Type = makeObject(
Some(getName(annotations, info)),
getDescription(annotations),
fields.map { (name, fieldAnnotations, schema) =>
val deprecatedReason = getDeprecatedReason(fieldAnnotations)
val isOptional = {
val deprecatedReason = getDeprecatedReason(fieldAnnotations)
val (isNullable, isSemanticNonNull) = {
val hasNullableAnn = fieldAnnotations.contains(GQLNullable())
val hasNonNullAnn = fieldAnnotations.contains(GQLNonNullable())
!hasNonNullAnn && (hasNullableAnn || schema.optional)

if (hasNonNullAnn) (false, false)
else if (hasNullableAnn) (true, false)
else if (schema.optional) (true, !schema.nullable)
else (false, false)
}
Types.makeField(
name,
getDescription(fieldAnnotations),
schema.arguments,
() =>
if (isOptional) schema.toType_(isInput, isSubscription)
if (isNullable) schema.toType_(isInput, isSubscription)
else schema.toType_(isInput, isSubscription).nonNull,
deprecatedReason.isDefined,
deprecatedReason,
Option(getDirectives(fieldAnnotations)).filter(_.nonEmpty)
Option(
getDirectives(fieldAnnotations) ++ {
if (enableSemanticNonNull && isSemanticNonNull) Some(SchemaUtils.SemanticNonNull)
else None
}
).filter(_.nonEmpty)
)
},
getDirectives(annotations),
Expand Down
5 changes: 3 additions & 2 deletions core/src/main/scala-3/caliban/schema/EnumValueSchema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import magnolia1.TypeInfo

final private class EnumValueSchema[R, A](
info: TypeInfo,
anns: List[Any]
anns: List[Any],
enableSemanticNonNull: Boolean
) extends Schema[R, A] {

def toType(isInput: Boolean, isSubscription: Boolean): __Type =
if (isInput) mkInputObject[R](anns, Nil, info)(isInput, isSubscription)
else mkObject[R](anns, Nil, info)(isInput, isSubscription)
else mkObject[R](anns, Nil, info, enableSemanticNonNull)(isInput, isSubscription)

private val step = PureStep(EnumValue(getName(anns, info)))
def resolve(value: A): Step[R] = step
Expand Down
5 changes: 3 additions & 2 deletions core/src/main/scala-3/caliban/schema/ObjectSchema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ final private class ObjectSchema[R, A](
_methodFields: => List[(String, List[Any], Schema[R, ?])],
info: TypeInfo,
anns: List[Any],
paramAnnotations: Map[String, List[Any]]
paramAnnotations: Map[String, List[Any]],
enableSemanticNonNull: Boolean
)(using ct: ClassTag[A])
extends Schema[R, A] {

Expand Down Expand Up @@ -48,7 +49,7 @@ final private class ObjectSchema[R, A](
def toType(isInput: Boolean, isSubscription: Boolean): __Type = {
val _ = resolver // Init the lazy val
if (isInput) mkInputObject[R](anns, fields.map(_._1), info)(isInput, isSubscription)
else mkObject[R](anns, fields.map(_._1), info)(isInput, isSubscription)
else mkObject[R](anns, fields.map(_._1), info, enableSemanticNonNull)(isInput, isSubscription)
}

def resolve(value: A): Step[R] = resolver.resolve(value)
Expand Down
21 changes: 19 additions & 2 deletions core/src/main/scala-3/caliban/schema/SchemaDerivation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ object PrintDerived {
trait CommonSchemaDerivation {
export DerivationUtils.customizeInputTypeName

case class DerivationConfig(
/**
* Whether to enable the `SemanticNonNull` feature on derivation.
* It is currently disabled by default since it is not yet stable.
*/
enableSemanticNonNull: Boolean = false
)

/**
* Returns a configuration object that can be used to customize the derivation behavior.
*
* Override this method to customize the configuration.
*/
def config: DerivationConfig = DerivationConfig()

inline def recurseSum[R, P, Label, A <: Tuple](
inline types: List[(String, __Type, List[Any])] = Nil,
inline schemas: List[Schema[R, Any]] = Nil
Expand Down Expand Up @@ -95,7 +110,8 @@ trait CommonSchemaDerivation {
new EnumValueSchema[R, A](
MagnoliaMacro.typeInfo[A],
// Workaround until we figure out why the macro uses the parent's annotations when the leaf is a Scala 3 enum
inline if (!MagnoliaMacro.isEnum[A]) MagnoliaMacro.anns[A] else Nil
inline if (!MagnoliaMacro.isEnum[A]) MagnoliaMacro.anns[A] else Nil,
config.enableSemanticNonNull
)
case _ if Macros.hasAnnotation[A, GQLValueType] =>
new ValueTypeSchema[R, A](
Expand All @@ -109,7 +125,8 @@ trait CommonSchemaDerivation {
Macros.fieldsFromMethods[R, A],
MagnoliaMacro.typeInfo[A],
MagnoliaMacro.anns[A],
MagnoliaMacro.paramAnns[A].toMap
MagnoliaMacro.paramAnns[A].toMap,
config.enableSemanticNonNull
)(using summonInline[ClassTag[A]])
}

Expand Down
Loading

0 comments on commit 63f4d6d

Please sign in to comment.