Skip to content

Commit

Permalink
Disable feature by default with an overridable flag
Browse files Browse the repository at this point in the history
  • Loading branch information
XiNiHa committed Apr 8, 2024
1 parent 8662d6f commit 09d4d12
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 46 deletions.
11 changes: 10 additions & 1 deletion core/src/main/scala-2/caliban/schema/SchemaDerivation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ import scala.language.experimental.macros

trait CommonSchemaDerivation[R] {

/**
* Enables the `SemanticNonNull` feature on derivation.
* It is currently disabled by default since it is not yet stable.
*
* Override this method and return `true` to enable the feature.
*/
def enableSemanticNonNull: Boolean = false

/**
* 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 @@ -96,7 +104,8 @@ trait CommonSchemaDerivation[R] {
p.annotations.collectFirst { case GQLDeprecated(reason) => reason },
Option(
p.annotations.collect { case GQLDirective(dir) => dir }.toList ++ {
if (isOptional && p.typeclass.semanticNonNull) Some(Directive("semanticNonNull"))
if (enableSemanticNonNull && isOptional && p.typeclass.semanticNonNull)
Some(Directive("semanticNonNull"))
else None
}
).filter(_.nonEmpty)
Expand Down
5 changes: 3 additions & 2 deletions core/src/main/scala-3/caliban/schema/DerivationUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ 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),
Expand All @@ -142,7 +143,7 @@ private object DerivationUtils {
deprecatedReason,
Option(
getDirectives(fieldAnnotations) ++ {
if (isOptional && schema.semanticNonNull) Some(Directive("semanticNonNull"))
if (enableSemanticNonNull && isOptional && schema.semanticNonNull) Some(Directive("semanticNonNull"))
else None
}
).filter(_.nonEmpty)
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
14 changes: 12 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,14 @@ object PrintDerived {
trait CommonSchemaDerivation {
export DerivationUtils.customizeInputTypeName

/**
* Enables the `SemanticNonNull` feature on derivation.
* It is currently disabled by default since it is not yet stable.
*
* Override this method and return `true` to enable the feature.
*/
def enableSemanticNonNull: Boolean = false

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 +103,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,
enableSemanticNonNull
)
case _ if Macros.hasAnnotation[A, GQLValueType] =>
new ValueTypeSchema[R, A](
Expand All @@ -109,7 +118,8 @@ trait CommonSchemaDerivation {
Macros.fieldsFromMethods[R, A],
MagnoliaMacro.typeInfo[A],
MagnoliaMacro.anns[A],
MagnoliaMacro.paramAnns[A].toMap
MagnoliaMacro.paramAnns[A].toMap,
enableSemanticNonNull
)(using summonInline[ClassTag[A]])
}

Expand Down
49 changes: 35 additions & 14 deletions core/src/main/scala/caliban/schema/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -255,29 +255,32 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema {
def field[V](
name: String,
description: Option[String] = None,
directives: List[Directive] = List.empty
directives: List[Directive] = List.empty,
enableSemanticNonNull: Boolean = false
): PartiallyAppliedField[V] =
PartiallyAppliedField[V](name, description, directives)
PartiallyAppliedField[V](name, description, directives, enableSemanticNonNull)

/**
* Manually defines a lazy field from a name, a description, some directives and a resolver.
*/
def fieldLazy[V](
name: String,
description: Option[String] = None,
directives: List[Directive] = List.empty
directives: List[Directive] = List.empty,
enableSemanticNonNull: Boolean = false
): PartiallyAppliedFieldLazy[V] =
PartiallyAppliedFieldLazy[V](name, description, directives)
PartiallyAppliedFieldLazy[V](name, description, directives, enableSemanticNonNull)

/**
* Manually defines a field with arguments from a name, a description, some directives and a resolver.
*/
def fieldWithArgs[V, A](
name: String,
description: Option[String] = None,
directives: List[Directive] = Nil
directives: List[Directive] = Nil,
enableSemanticNonNull: Boolean = false
): PartiallyAppliedFieldWithArgs[V, A] =
PartiallyAppliedFieldWithArgs[V, A](name, description, directives)
PartiallyAppliedFieldWithArgs[V, A](name, description, directives, enableSemanticNonNull)

/**
* Creates a new hand-rolled schema. For normal usage use the derived schemas, this is primarily for schemas
Expand Down Expand Up @@ -696,7 +699,12 @@ trait TemporalSchema {

case class FieldAttributes(isInput: Boolean, isSubscription: Boolean)

abstract class PartiallyAppliedFieldBase[V](name: String, description: Option[String], directives: List[Directive]) {
abstract class PartiallyAppliedFieldBase[V](
name: String,
description: Option[String],
directives: List[Directive],
enableSemanticNonNull: Boolean
) {
def apply[R, V1](fn: V => V1)(implicit ev: Schema[R, V1], ft: FieldAttributes): (__Field, V => Step[R]) =
either[R, V1](v => Left(fn(v)))(ev, ft)

Expand All @@ -716,30 +724,43 @@ abstract class PartiallyAppliedFieldBase[V](name: String, description: Option[St
deprecationReason = Directives.deprecationReason(directives),
directives = Some(
directives.filter(_.name != "deprecated") ++ {
if (ev.optional && ev.semanticNonNull) Some(Directive("semanticNonNull"))
if (enableSemanticNonNull && ev.optional && ev.semanticNonNull) Some(Directive("semanticNonNull"))
else None
}
).filter(_.nonEmpty)
)
}

case class PartiallyAppliedField[V](name: String, description: Option[String], directives: List[Directive])
extends PartiallyAppliedFieldBase[V](name, description, directives) {
case class PartiallyAppliedField[V](
name: String,
description: Option[String],
directives: List[Directive],
enableSemanticNonNull: Boolean
) extends PartiallyAppliedFieldBase[V](name, description, directives, enableSemanticNonNull) {
def either[R, V1](
fn: V => Either[V1, Step[R]]
)(implicit ev: Schema[R, V1], ft: FieldAttributes): (__Field, V => Step[R]) =
(makeField, (v: V) => fn(v).fold(ev.resolve, identity))
}

case class PartiallyAppliedFieldLazy[V](name: String, description: Option[String], directives: List[Directive])
extends PartiallyAppliedFieldBase[V](name, description, directives) {
case class PartiallyAppliedFieldLazy[V](
name: String,
description: Option[String],
directives: List[Directive],
enableSemanticNonNull: Boolean
) extends PartiallyAppliedFieldBase[V](name, description, directives, enableSemanticNonNull) {
def either[R, V1](
fn: V => Either[V1, Step[R]]
)(implicit ev: Schema[R, V1], ft: FieldAttributes): (__Field, V => Step[R]) =
(makeField, (v: V) => FunctionStep(_ => fn(v).fold(ev.resolve, identity)))
}

case class PartiallyAppliedFieldWithArgs[V, A](name: String, description: Option[String], directives: List[Directive]) {
case class PartiallyAppliedFieldWithArgs[V, A](
name: String,
description: Option[String],
directives: List[Directive],
enableSemanticNonNull: Boolean
) {
def apply[R, V1](fn: V => (A => V1))(implicit ev1: Schema[R, A => V1], fa: FieldAttributes): (__Field, V => Step[R]) =
(
Types.makeField(
Expand All @@ -753,7 +774,7 @@ case class PartiallyAppliedFieldWithArgs[V, A](name: String, description: Option
deprecationReason = Directives.deprecationReason(directives),
directives = Some(
directives.filter(_.name != "deprecated") ++ {
if (ev1.optional && ev1.semanticNonNull) Some(Directive("semanticNonNull"))
if (enableSemanticNonNull && ev1.optional && ev1.semanticNonNull) Some(Directive("semanticNonNull"))
else None
}
).filter(_.nonEmpty)
Expand Down
24 changes: 1 addition & 23 deletions core/src/test/scala/caliban/schema/SchemaSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,33 +26,11 @@ 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 @@ -322,7 +300,7 @@ object SchemaSpec extends ZIOSpecDefault {
|}
|
|type EnvironmentSchema {
| test: Int @semanticNonNull
| test: Int
| box: Box!
|}
|
Expand Down
49 changes: 49 additions & 0 deletions core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package caliban.schema

import caliban._
import caliban.introspection.adt.{ __DeprecatedArgs, __Field }
import caliban.parsing.adt.Directive
import caliban.schema.Annotations._
import zio._
import zio.test.Assertion._
import zio.test._

object SemanticNonNullSchema extends SchemaDerivation[Any] {
override def enableSemanticNonNull: Boolean = true
}

object SemanticNonNullSchemaSpec extends ZIOSpecDefault {
override def spec =
suite("SemanticNonNullSchemaSpec")(
test("effectful field as semanticNonNull") {
assert(effectfulFieldObjectSchema.toType_().fields(__DeprecatedArgs()).toList.flatten.headOption)(
isSome(
hasField[__Field, Option[List[Directive]]](
"directives",
_.directives,
isSome(contains((Directive("semanticNonNull"))))
)
)
)
},
test("optional effectful field") {
assert(optionalEffectfulFieldObjectSchema.toType_().fields(__DeprecatedArgs()).toList.flatten.headOption)(
isSome(
hasField[__Field, Option[List[Directive]]](
"directives",
_.directives.map(_.filter(_.name == "semanticNonNull")).filter(_.nonEmpty),
isNone
)
)
)
}
)

case class EffectfulFieldObject(q: Task[Int], @GQLNonNullable qAnnotated: Task[Int])
case class OptionalEffectfulFieldObject(q: Task[Option[String]], @GQLNonNullable qAnnotated: Task[Option[String]])

implicit val effectfulFieldObjectSchema: Schema[Any, EffectfulFieldObject] =
SemanticNonNullSchema.gen[Any, EffectfulFieldObject]
implicit val optionalEffectfulFieldObjectSchema: Schema[Any, OptionalEffectfulFieldObject] =
SemanticNonNullSchema.gen[Any, OptionalEffectfulFieldObject]
}

0 comments on commit 09d4d12

Please sign in to comment.