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

Add SemanticNonNull support #2180

Merged
merged 12 commits into from
May 12, 2024
Merged
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