From 09d4d12506653883943dd12fac276658dc39bf1a Mon Sep 17 00:00:00 2001 From: XiNiHa Date: Tue, 9 Apr 2024 00:30:43 +0900 Subject: [PATCH] Disable feature by default with an overridable flag --- .../caliban/schema/SchemaDerivation.scala | 11 ++++- .../caliban/schema/DerivationUtils.scala | 5 +- .../caliban/schema/EnumValueSchema.scala | 5 +- .../scala-3/caliban/schema/ObjectSchema.scala | 5 +- .../caliban/schema/SchemaDerivation.scala | 14 +++++- .../main/scala/caliban/schema/Schema.scala | 49 +++++++++++++------ .../scala/caliban/schema/SchemaSpec.scala | 24 +-------- .../schema/SemanticNonNullSchemaSpec.scala | 49 +++++++++++++++++++ 8 files changed, 116 insertions(+), 46 deletions(-) create mode 100644 core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index d08a3528d..7a1fe41dd 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -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. @@ -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) diff --git a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala index 2dfea6c34..22d192a41 100644 --- a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala +++ b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala @@ -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), @@ -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) diff --git a/core/src/main/scala-3/caliban/schema/EnumValueSchema.scala b/core/src/main/scala-3/caliban/schema/EnumValueSchema.scala index aec6e4152..7bd6587cf 100644 --- a/core/src/main/scala-3/caliban/schema/EnumValueSchema.scala +++ b/core/src/main/scala-3/caliban/schema/EnumValueSchema.scala @@ -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 diff --git a/core/src/main/scala-3/caliban/schema/ObjectSchema.scala b/core/src/main/scala-3/caliban/schema/ObjectSchema.scala index d0afc06a1..1b5169dab 100644 --- a/core/src/main/scala-3/caliban/schema/ObjectSchema.scala +++ b/core/src/main/scala-3/caliban/schema/ObjectSchema.scala @@ -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] { @@ -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) diff --git a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala index fe2c6838e..8efb28385 100644 --- a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala @@ -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 @@ -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]( @@ -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]]) } diff --git a/core/src/main/scala/caliban/schema/Schema.scala b/core/src/main/scala/caliban/schema/Schema.scala index 224cf17fc..308ebff8c 100644 --- a/core/src/main/scala/caliban/schema/Schema.scala +++ b/core/src/main/scala/caliban/schema/Schema.scala @@ -255,9 +255,10 @@ 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. @@ -265,9 +266,10 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { 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. @@ -275,9 +277,10 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { 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 @@ -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) @@ -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( @@ -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) diff --git a/core/src/test/scala/caliban/schema/SchemaSpec.scala b/core/src/test/scala/caliban/schema/SchemaSpec.scala index 6dacca93a..925c24466 100644 --- a/core/src/test/scala/caliban/schema/SchemaSpec.scala +++ b/core/src/test/scala/caliban/schema/SchemaSpec.scala @@ -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))) @@ -322,7 +300,7 @@ object SchemaSpec extends ZIOSpecDefault { |} | |type EnvironmentSchema { - | test: Int @semanticNonNull + | test: Int | box: Box! |} | diff --git a/core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala b/core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala new file mode 100644 index 000000000..3e89533f8 --- /dev/null +++ b/core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala @@ -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] +}