From 306b5cbb84c0f9d05d0b6a3f2f8c86190cf37abb Mon Sep 17 00:00:00 2001 From: XiNiHa Date: Fri, 5 Apr 2024 18:34:34 +0900 Subject: [PATCH 01/12] Add semanticNonNull support --- .../caliban/schema/SchemaDerivation.scala | 7 ++++- .../caliban/schema/DerivationUtils.scala | 7 ++++- .../main/scala/caliban/schema/Schema.scala | 25 +++++++++++++++-- .../scala/caliban/schema/SchemaSpec.scala | 27 +++++++++++++++++-- 4 files changed, 60 insertions(+), 6 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index bac5dbf92..d08a3528d 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -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.semanticNonNull) Some(Directive("semanticNonNull")) + else None + } + ).filter(_.nonEmpty) ) } .toList, diff --git a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala index a1fd4fe88..2dfea6c34 100644 --- a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala +++ b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala @@ -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.semanticNonNull) Some(Directive("semanticNonNull")) + else None + } + ).filter(_.nonEmpty) ) }, getDirectives(annotations), diff --git a/core/src/main/scala/caliban/schema/Schema.scala b/core/src/main/scala/caliban/schema/Schema.scala index 8193ad0c9..5df9c687c 100644 --- a/core/src/main/scala/caliban/schema/Schema.scala +++ b/core/src/main/scala/caliban/schema/Schema.scala @@ -77,6 +77,11 @@ trait Schema[-R, T] { self => */ def optional: Boolean = false + /** + * Defines if the type is considered semantically nullable or not. + */ + def semanticNonNull: Boolean = false + /** * Defined the arguments of the given type. Should be empty except for `Function`. */ @@ -506,6 +511,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 semanticNonNull: 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))) } @@ -514,6 +520,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 semanticNonNull: 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)) @@ -532,6 +539,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 semanticNonNull: 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)) } @@ -540,6 +548,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 semanticNonNull: 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)) } @@ -559,6 +568,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 semanticNonNull: 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 @@ -570,6 +580,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 semanticNonNull: 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 @@ -705,7 +716,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.semanticNonNull) Some(Directive("semanticNonNull")) + else None + } + ).filter(_.nonEmpty) ) } @@ -737,7 +753,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.semanticNonNull) Some(Directive("semanticNonNull")) + else None + } + ).filter(_.nonEmpty) ), (v: V) => ev1.resolve(fn(v)) ) diff --git a/core/src/test/scala/caliban/schema/SchemaSpec.scala b/core/src/test/scala/caliban/schema/SchemaSpec.scala index 8fb204034..6dacca93a 100644 --- a/core/src/test/scala/caliban/schema/SchemaSpec.scala +++ b/core/src/test/scala/caliban/schema/SchemaSpec.scala @@ -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._ @@ -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))) @@ -300,7 +322,7 @@ object SchemaSpec extends ZIOSpecDefault { |} | |type EnvironmentSchema { - | test: Int + | test: Int @semanticNonNull | box: Box! |} | @@ -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) From 1abee63b61f5b699fbb860ad97bb7cb27159fea4 Mon Sep 17 00:00:00 2001 From: XiNiHa Date: Tue, 9 Apr 2024 00:30:43 +0900 Subject: [PATCH 02/12] 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 5df9c687c..5391f1045 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 @@ -698,7 +701,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) @@ -718,30 +726,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( @@ -755,7 +776,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] +} From b77a0599136da95be384cb5be15c6f4c0ad1a927 Mon Sep 17 00:00:00 2001 From: XiNiHa Date: Tue, 9 Apr 2024 20:44:50 +0900 Subject: [PATCH 03/12] Adjust schema definition to contain canFail instead of semanticNonNull --- .../caliban/schema/SchemaDerivation.scala | 11 ++-- .../caliban/schema/DerivationUtils.scala | 12 ++-- .../main/scala/caliban/schema/Schema.scala | 56 +++++++++++-------- .../caliban/interop/cats/CatsInterop.scala | 1 + .../caliban/interop/fs2/Fs2Interop.scala | 2 + .../caliban/interop/monix/MonixInterop.scala | 1 + .../scala/caliban/interop/tapir/package.scala | 2 +- 7 files changed, 50 insertions(+), 35 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index 7a1fe41dd..95ef702c7 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -69,7 +69,7 @@ trait CommonSchemaDerivation[R] { getName(p), getDescription(p), () => - if (p.typeclass.optional) p.typeclass.toType_(isInput, isSubscription) + if (p.typeclass.optional || p.typeclass.canFail) p.typeclass.toType_(isInput, isSubscription) else p.typeclass.toType_(isInput, isSubscription).nonNull, p.annotations.collectFirst { case GQLDefault(v) => v }, p.annotations.collectFirst { case GQLDeprecated(_) => () }.isDefined, @@ -88,23 +88,24 @@ trait CommonSchemaDerivation[R] { ctx.parameters .filterNot(_.annotations.exists(_ == GQLExcluded())) .map { p => - val isOptional = { + val (isNullable, isNullabilityForced) = { val hasNullableAnn = p.annotations.contains(GQLNullable()) val hasNonNullAnn = p.annotations.contains(GQLNonNullable()) - !hasNonNullAnn && (hasNullableAnn || p.typeclass.optional) + (!hasNonNullAnn && (hasNullableAnn || p.typeclass.optional), hasNullableAnn || hasNonNullAnn) } Types.makeField( getName(p), getDescription(p), p.typeclass.arguments, () => - if (isOptional) p.typeclass.toType_(isInput, isSubscription) + if (isNullable || (!isNullabilityForced && p.typeclass.canFail)) + 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 ++ { - if (enableSemanticNonNull && isOptional && p.typeclass.semanticNonNull) + if (enableSemanticNonNull && !isNullable && p.typeclass.canFail) Some(Directive("semanticNonNull")) else None } diff --git a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala index 22d192a41..487024cd8 100644 --- a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala +++ b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala @@ -105,7 +105,7 @@ private object DerivationUtils { name, getDescription(fieldAnnotations), () => - if (schema.optional) schema.toType_(isInput, isSubscription) + if (schema.optional || schema.canFail) schema.toType_(isInput, isSubscription) else schema.toType_(isInput, isSubscription).nonNull, getDefaultValue(fieldAnnotations), getDeprecatedReason(fieldAnnotations).isDefined, @@ -126,24 +126,24 @@ private object DerivationUtils { Some(getName(annotations, info)), getDescription(annotations), fields.map { (name, fieldAnnotations, schema) => - val deprecatedReason = getDeprecatedReason(fieldAnnotations) - val isOptional = { + val deprecatedReason = getDeprecatedReason(fieldAnnotations) + val (isNullable, isNullabilityForced) = { val hasNullableAnn = fieldAnnotations.contains(GQLNullable()) val hasNonNullAnn = fieldAnnotations.contains(GQLNonNullable()) - !hasNonNullAnn && (hasNullableAnn || schema.optional) + (!hasNonNullAnn && (hasNullableAnn || schema.optional), hasNullableAnn || hasNonNullAnn) } Types.makeField( name, getDescription(fieldAnnotations), schema.arguments, () => - if (isOptional) schema.toType_(isInput, isSubscription) + if (isNullable || (!isNullabilityForced && schema.canFail)) schema.toType_(isInput, isSubscription) else schema.toType_(isInput, isSubscription).nonNull, deprecatedReason.isDefined, deprecatedReason, Option( getDirectives(fieldAnnotations) ++ { - if (enableSemanticNonNull && isOptional && schema.semanticNonNull) Some(Directive("semanticNonNull")) + if (enableSemanticNonNull && !isNullable && schema.canFail) Some(Directive("semanticNonNull")) else None } ).filter(_.nonEmpty) diff --git a/core/src/main/scala/caliban/schema/Schema.scala b/core/src/main/scala/caliban/schema/Schema.scala index 5391f1045..f556e078b 100644 --- a/core/src/main/scala/caliban/schema/Schema.scala +++ b/core/src/main/scala/caliban/schema/Schema.scala @@ -78,9 +78,9 @@ trait Schema[-R, T] { self => def optional: Boolean = false /** - * Defines if the type is considered semantically nullable or not. + * Defines if the type can fail during resolution. */ - def semanticNonNull: Boolean = false + def canFail: Boolean = false /** * Defined the arguments of the given type. Should be empty except for `Function`. @@ -96,6 +96,7 @@ trait Schema[-R, T] { self => */ def contramap[A](f: A => T): Schema[R, A] = new Schema[R, A] { override def optional: Boolean = self.optional + override def canFail: Boolean = self.canFail override def arguments: List[__InputValue] = self.arguments override def toType(isInput: Boolean, isSubscription: Boolean): __Type = self.toType_(isInput, isSubscription) override def resolve(value: A): Step[R] = self.resolve(f(value)) @@ -108,6 +109,7 @@ trait Schema[-R, T] { self => */ def rename(name: String, inputName: Option[String] = None): Schema[R, T] = new Schema[R, T] { override def optional: Boolean = self.optional + override def canFail: Boolean = self.canFail override def arguments: List[__InputValue] = self.arguments override def toType(isInput: Boolean, isSubscription: Boolean): __Type = { val tpe = self.toType_(isInput, isSubscription) @@ -362,7 +364,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { implicit def listSchema[R0, A](implicit ev: Schema[R0, A]): Schema[R0, List[A]] = new Schema[R0, List[A]] { override def toType(isInput: Boolean, isSubscription: Boolean): __Type = { val t = ev.toType_(isInput, isSubscription) - (if (ev.optional) t else t.nonNull).list + (if (ev.optional || ev.canFail) t else t.nonNull).list } override def resolve(value: List[A]): Step[R0] = ListStep(value.map(ev.resolve)) @@ -376,6 +378,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { implicit def functionUnitSchema[R0, A](implicit ev: Schema[R0, A]): Schema[R0, () => A] = new Schema[R0, () => A] { override def optional: Boolean = ev.optional + override def canFail: Boolean = ev.canFail override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription) override def resolve(value: () => A): Step[R0] = FunctionStep(_ => ev.resolve(value())) } @@ -383,6 +386,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { new Schema[R0, Field => A] { override def arguments: List[__InputValue] = ev.arguments override def optional: Boolean = ev.optional + override def canFail: Boolean = ev.canFail override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription) override def resolve(value: Field => A): Step[R0] = MetadataFunctionStep(field => ev.resolve(value(field))) } @@ -398,11 +402,13 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { implicit val leftSchema: Schema[RA, A] = new Schema[RA, A] { override def optional: Boolean = true + override def canFail: Boolean = evA.canFail override def toType(isInput: Boolean, isSubscription: Boolean): __Type = evA.toType_(isInput, isSubscription) override def resolve(value: A): Step[RA] = evA.resolve(value) } implicit val rightSchema: Schema[RB, B] = new Schema[RB, B] { override def optional: Boolean = true + override def canFail: Boolean = evB.canFail override def toType(isInput: Boolean, isSubscription: Boolean): __Type = evB.toType_(isInput, isSubscription) override def resolve(value: B): Step[RB] = evB.resolve(value) } @@ -470,7 +476,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { __InputValue( unwrappedArgumentName, None, - () => if (ev1.optional) inputType else inputType.nonNull, + () => if (ev1.optional || ev1.canFail) inputType else inputType.nonNull, None ) ) @@ -478,6 +484,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { } override def optional: Boolean = ev2.optional + override def canFail: Boolean = ev2.canFail override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev2.toType_(isInput, isSubscription) override def resolve(f: A => B): Step[RB] = @@ -506,6 +513,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { implicit def infallibleEffectSchema[R0, R1 >: R0, R2 >: R0, A](implicit ev: Schema[R2, A]): Schema[R0, URIO[R1, A]] = new Schema[R0, URIO[R1, A]] { override def optional: Boolean = ev.optional + override def canFail: Boolean = ev.canFail override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription) override def resolve(value: URIO[R1, A]): Step[R0] = QueryStep(ZQuery.fromZIONow(value.map(ev.resolve))) } @@ -513,8 +521,8 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ev: Schema[R2, A] ): Schema[R0, ZIO[R1, E, A]] = new Schema[R0, ZIO[R1, E, A]] { - override def optional: Boolean = true - override def semanticNonNull: Boolean = !ev.optional + override def optional: Boolean = ev.optional + override def canFail: Boolean = true 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))) } @@ -522,8 +530,8 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ev: Schema[R2, A] ): Schema[R0, ZIO[R1, E, A]] = new Schema[R0, ZIO[R1, E, A]] { - override def optional: Boolean = true - override def semanticNonNull: Boolean = !ev.optional + override def optional: Boolean = ev.optional + override def canFail: Boolean = true 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)) @@ -534,6 +542,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ): Schema[R0, ZQuery[R1, Nothing, A]] = new Schema[R0, ZQuery[R1, Nothing, A]] { override def optional: Boolean = ev.optional + override def canFail: Boolean = ev.canFail override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription) override def resolve(value: ZQuery[R1, Nothing, A]): Step[R0] = QueryStep(value.map(ev.resolve)) } @@ -541,8 +550,8 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ev: Schema[R2, A] ): Schema[R0, ZQuery[R1, E, A]] = new Schema[R0, ZQuery[R1, E, A]] { - override def optional: Boolean = true - override def semanticNonNull: Boolean = !ev.optional + override def optional: Boolean = ev.optional + override def canFail: Boolean = true 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)) } @@ -550,8 +559,8 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ev: Schema[R2, A] ): Schema[R0, ZQuery[R1, E, A]] = new Schema[R0, ZQuery[R1, E, A]] { - override def optional: Boolean = true - override def semanticNonNull: Boolean = !ev.optional + override def optional: Boolean = ev.optional + override def canFail: Boolean = true 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)) } @@ -560,9 +569,10 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ): Schema[R1, ZStream[R1, Nothing, A]] = new Schema[R1, ZStream[R1, Nothing, A]] { override def optional: Boolean = false + override def canFail: Boolean = ev.canFail 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 + if (isSubscription) t else (if (ev.optional || ev.canFail) t else t.nonNull).list } override def resolve(value: ZStream[R1, Nothing, A]): Step[R1] = StreamStep(value.map(ev.resolve)) } @@ -570,11 +580,11 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ev: Schema[R2, A] ): Schema[R0, ZStream[R1, E, A]] = new Schema[R0, ZStream[R1, E, A]] { - override def optional: Boolean = true - override def semanticNonNull: Boolean = !ev.optional + override def optional: Boolean = ev.optional + override def canFail: Boolean = true 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 + if (isSubscription) t else (if (ev.optional || ev.canFail) t else t.nonNull).list } override def resolve(value: ZStream[R1, E, A]): Step[R0] = StreamStep(value.map(ev.resolve)) } @@ -582,11 +592,11 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ev: Schema[R2, A] ): Schema[R0, ZStream[R1, E, A]] = new Schema[R0, ZStream[R1, E, A]] { - override def optional: Boolean = true - override def semanticNonNull: Boolean = !ev.optional + override def optional: Boolean = ev.optional + override def canFail: Boolean = true 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 + if (isSubscription) t else (if (ev.optional || ev.canFail) t else t.nonNull).list } override def resolve(value: ZStream[R1, E, A]): Step[R0] = StreamStep(value.mapBoth(convertError, ev.resolve)) } @@ -720,13 +730,13 @@ abstract class PartiallyAppliedFieldBase[V]( description, _ => Nil, () => - if (ev.optional) ev.toType_(ft.isInput, ft.isSubscription) + if (ev.optional || ev.canFail) ev.toType_(ft.isInput, ft.isSubscription) else ev.toType_(ft.isInput, ft.isSubscription).nonNull, isDeprecated = Directives.isDeprecated(directives), deprecationReason = Directives.deprecationReason(directives), directives = Some( directives.filter(_.name != "deprecated") ++ { - if (enableSemanticNonNull && ev.optional && ev.semanticNonNull) Some(Directive("semanticNonNull")) + if (enableSemanticNonNull && !ev.optional && ev.canFail) Some(Directive("semanticNonNull")) else None } ).filter(_.nonEmpty) @@ -770,13 +780,13 @@ case class PartiallyAppliedFieldWithArgs[V, A]( description, ev1.arguments, () => - if (ev1.optional) ev1.toType_(fa.isInput, fa.isSubscription) + if (ev1.optional || ev1.canFail) ev1.toType_(fa.isInput, fa.isSubscription) else ev1.toType_(fa.isInput, fa.isSubscription).nonNull, isDeprecated = Directives.isDeprecated(directives), deprecationReason = Directives.deprecationReason(directives), directives = Some( directives.filter(_.name != "deprecated") ++ { - if (enableSemanticNonNull && ev1.optional && ev1.semanticNonNull) Some(Directive("semanticNonNull")) + if (enableSemanticNonNull && !ev1.optional && ev1.canFail) Some(Directive("semanticNonNull")) else None } ).filter(_.nonEmpty) diff --git a/interop/cats/src/main/scala/caliban/interop/cats/CatsInterop.scala b/interop/cats/src/main/scala/caliban/interop/cats/CatsInterop.scala index b355eef9a..f387813a6 100644 --- a/interop/cats/src/main/scala/caliban/interop/cats/CatsInterop.scala +++ b/interop/cats/src/main/scala/caliban/interop/cats/CatsInterop.scala @@ -192,6 +192,7 @@ object CatsInterop { override def optional: Boolean = ev.optional + override def canFail: Boolean = true override def resolve(value: F[A]): Step[R] = QueryStep(ZQuery.fromZIO(interop.fromEffect(value).map(ev.resolve))) diff --git a/interop/cats/src/main/scala/caliban/interop/fs2/Fs2Interop.scala b/interop/cats/src/main/scala/caliban/interop/fs2/Fs2Interop.scala index 181cf247f..5c6e8f0c9 100644 --- a/interop/cats/src/main/scala/caliban/interop/fs2/Fs2Interop.scala +++ b/interop/cats/src/main/scala/caliban/interop/fs2/Fs2Interop.scala @@ -15,6 +15,7 @@ object Fs2Interop { ev.toType_(isInput, isSubscription) override def optional: Boolean = ev.optional + override def canFail: Boolean = true override def resolve(value: Stream[RIO[R, *], A]): Step[R] = ev.resolve(value.toZStream()) @@ -29,6 +30,7 @@ object Fs2Interop { ev.toType_(isInput, isSubscription) override def optional: Boolean = ev.optional + override def canFail: Boolean = true override def resolve(value: Stream[F, A]): Step[R] = ev.resolve(value.translate(interop.fromEffectK)) diff --git a/interop/monix/src/main/scala/caliban/interop/monix/MonixInterop.scala b/interop/monix/src/main/scala/caliban/interop/monix/MonixInterop.scala index e06864d65..daf648929 100644 --- a/interop/monix/src/main/scala/caliban/interop/monix/MonixInterop.scala +++ b/interop/monix/src/main/scala/caliban/interop/monix/MonixInterop.scala @@ -61,6 +61,7 @@ object MonixInterop { new Schema[R, MonixTask[A]] { override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription) override def optional: Boolean = ev.optional + override def canFail: Boolean = true override def resolve(value: MonixTask[A]): Step[R] = QueryStep(ZQuery.fromZIO(value.to[Task].map(ev.resolve))) } diff --git a/interop/tapir/src/main/scala/caliban/interop/tapir/package.scala b/interop/tapir/src/main/scala/caliban/interop/tapir/package.scala index 39cfb621b..c33f1b86a 100644 --- a/interop/tapir/src/main/scala/caliban/interop/tapir/package.scala +++ b/interop/tapir/src/main/scala/caliban/interop/tapir/package.scala @@ -107,7 +107,7 @@ package object tapir { Types.makeField( extractPath(serverEndpoint.endpoint.info.name, serverEndpoint.endpoint.input), serverEndpoint.endpoint.info.description, - getArgs(inputSchema.toType_(isInput = true), inputSchema.optional), + getArgs(inputSchema.toType_(isInput = true), inputSchema.optional || inputSchema.canFail), () => if (serverEndpoint.endpoint.errorOutput == EndpointOutput.Void[E]()) outputSchema.toType_().nonNull From 4c515dce56c1a215c6fa2cf1c7ff1733b61c9eda Mon Sep 17 00:00:00 2001 From: XiNiHa Date: Tue, 23 Apr 2024 20:24:19 +0900 Subject: [PATCH 04/12] No semanticNonNull transform on hand-written schema --- .../main/scala/caliban/schema/Schema.scala | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/core/src/main/scala/caliban/schema/Schema.scala b/core/src/main/scala/caliban/schema/Schema.scala index f556e078b..03c551ffc 100644 --- a/core/src/main/scala/caliban/schema/Schema.scala +++ b/core/src/main/scala/caliban/schema/Schema.scala @@ -258,9 +258,8 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { name: String, description: Option[String] = None, directives: List[Directive] = List.empty, - enableSemanticNonNull: Boolean = false ): PartiallyAppliedField[V] = - PartiallyAppliedField[V](name, description, directives, enableSemanticNonNull) + PartiallyAppliedField[V](name, description, directives) /** * Manually defines a lazy field from a name, a description, some directives and a resolver. @@ -269,9 +268,8 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { name: String, description: Option[String] = None, directives: List[Directive] = List.empty, - enableSemanticNonNull: Boolean = false ): PartiallyAppliedFieldLazy[V] = - PartiallyAppliedFieldLazy[V](name, description, directives, enableSemanticNonNull) + PartiallyAppliedFieldLazy[V](name, description, directives) /** * Manually defines a field with arguments from a name, a description, some directives and a resolver. @@ -280,9 +278,8 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { name: String, description: Option[String] = None, directives: List[Directive] = Nil, - enableSemanticNonNull: Boolean = false ): PartiallyAppliedFieldWithArgs[V, A] = - PartiallyAppliedFieldWithArgs[V, A](name, description, directives, enableSemanticNonNull) + PartiallyAppliedFieldWithArgs[V, A](name, description, directives) /** * Creates a new hand-rolled schema. For normal usage use the derived schemas, this is primarily for schemas @@ -714,8 +711,7 @@ case class FieldAttributes(isInput: Boolean, isSubscription: Boolean) abstract class PartiallyAppliedFieldBase[V]( name: String, description: Option[String], - directives: List[Directive], - enableSemanticNonNull: Boolean + directives: List[Directive] ) { 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) @@ -735,10 +731,7 @@ abstract class PartiallyAppliedFieldBase[V]( isDeprecated = Directives.isDeprecated(directives), deprecationReason = Directives.deprecationReason(directives), directives = Some( - directives.filter(_.name != "deprecated") ++ { - if (enableSemanticNonNull && !ev.optional && ev.canFail) Some(Directive("semanticNonNull")) - else None - } + directives.filter(_.name != "deprecated") ).filter(_.nonEmpty) ) } @@ -746,9 +739,8 @@ abstract class PartiallyAppliedFieldBase[V]( case class PartiallyAppliedField[V]( name: String, description: Option[String], - directives: List[Directive], - enableSemanticNonNull: Boolean -) extends PartiallyAppliedFieldBase[V](name, description, directives, enableSemanticNonNull) { + directives: List[Directive] +) extends PartiallyAppliedFieldBase[V](name, description, directives) { def either[R, V1]( fn: V => Either[V1, Step[R]] )(implicit ev: Schema[R, V1], ft: FieldAttributes): (__Field, V => Step[R]) = @@ -758,9 +750,8 @@ case class PartiallyAppliedField[V]( case class PartiallyAppliedFieldLazy[V]( name: String, description: Option[String], - directives: List[Directive], - enableSemanticNonNull: Boolean -) extends PartiallyAppliedFieldBase[V](name, description, directives, enableSemanticNonNull) { + directives: List[Directive] +) extends PartiallyAppliedFieldBase[V](name, description, directives) { def either[R, V1]( fn: V => Either[V1, Step[R]] )(implicit ev: Schema[R, V1], ft: FieldAttributes): (__Field, V => Step[R]) = @@ -770,8 +761,7 @@ case class PartiallyAppliedFieldLazy[V]( case class PartiallyAppliedFieldWithArgs[V, A]( name: String, description: Option[String], - directives: List[Directive], - enableSemanticNonNull: Boolean + directives: List[Directive] ) { def apply[R, V1](fn: V => (A => V1))(implicit ev1: Schema[R, A => V1], fa: FieldAttributes): (__Field, V => Step[R]) = ( @@ -785,10 +775,7 @@ case class PartiallyAppliedFieldWithArgs[V, A]( isDeprecated = Directives.isDeprecated(directives), deprecationReason = Directives.deprecationReason(directives), directives = Some( - directives.filter(_.name != "deprecated") ++ { - if (enableSemanticNonNull && !ev1.optional && ev1.canFail) Some(Directive("semanticNonNull")) - else None - } + directives.filter(_.name != "deprecated") ).filter(_.nonEmpty) ), (v: V) => ev1.resolve(fn(v)) From a63acce2af4342e19e999e7ccae115b0d6444f34 Mon Sep 17 00:00:00 2001 From: XiNiHa Date: Tue, 23 Apr 2024 20:33:39 +0900 Subject: [PATCH 05/12] Reuse semanticNonNull directive definition across files --- .../src/main/scala-2/caliban/schema/SchemaDerivation.scala | 2 +- core/src/main/scala-3/caliban/schema/DerivationUtils.scala | 2 +- core/src/main/scala/caliban/schema/Schema.scala | 6 +++--- core/src/main/scala/caliban/schema/SchemaUtils.scala | 7 +++++++ .../scala/caliban/schema/SemanticNonNullSchemaSpec.scala | 4 ++-- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index 95ef702c7..eabecc03e 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -106,7 +106,7 @@ trait CommonSchemaDerivation[R] { Option( p.annotations.collect { case GQLDirective(dir) => dir }.toList ++ { if (enableSemanticNonNull && !isNullable && p.typeclass.canFail) - Some(Directive("semanticNonNull")) + Some(SchemaUtils.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 487024cd8..040b327b6 100644 --- a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala +++ b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala @@ -143,7 +143,7 @@ private object DerivationUtils { deprecatedReason, Option( getDirectives(fieldAnnotations) ++ { - if (enableSemanticNonNull && !isNullable && schema.canFail) Some(Directive("semanticNonNull")) + if (enableSemanticNonNull && !isNullable && schema.canFail) Some(SchemaUtils.SemanticNonNull) else None } ).filter(_.nonEmpty) diff --git a/core/src/main/scala/caliban/schema/Schema.scala b/core/src/main/scala/caliban/schema/Schema.scala index 03c551ffc..a8669174a 100644 --- a/core/src/main/scala/caliban/schema/Schema.scala +++ b/core/src/main/scala/caliban/schema/Schema.scala @@ -257,7 +257,7 @@ 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 ): PartiallyAppliedField[V] = PartiallyAppliedField[V](name, description, directives) @@ -267,7 +267,7 @@ 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 ): PartiallyAppliedFieldLazy[V] = PartiallyAppliedFieldLazy[V](name, description, directives) @@ -277,7 +277,7 @@ 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 ): PartiallyAppliedFieldWithArgs[V, A] = PartiallyAppliedFieldWithArgs[V, A](name, description, directives) diff --git a/core/src/main/scala/caliban/schema/SchemaUtils.scala b/core/src/main/scala/caliban/schema/SchemaUtils.scala index de0e571aa..65d40e368 100644 --- a/core/src/main/scala/caliban/schema/SchemaUtils.scala +++ b/core/src/main/scala/caliban/schema/SchemaUtils.scala @@ -4,8 +4,15 @@ import caliban.ResponseValue.ObjectValue import caliban.Value.{ EnumValue, NullValue, StringValue } import caliban.introspection.adt.{ __DeprecatedArgs, __Field, __Type } import caliban.schema.Step.MetadataFunctionStep +import caliban.parsing.adt.Directive private[schema] object SchemaUtils { + + /** + * Directive used to mark a field as semantically non-nullable. + */ + val SemanticNonNull = Directive("semanticNonNull") + private val fakeField = Some( List( diff --git a/core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala b/core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala index 3e89533f8..f65237c43 100644 --- a/core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala +++ b/core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala @@ -21,7 +21,7 @@ object SemanticNonNullSchemaSpec extends ZIOSpecDefault { hasField[__Field, Option[List[Directive]]]( "directives", _.directives, - isSome(contains((Directive("semanticNonNull")))) + isSome(contains(SchemaUtils.SemanticNonNull)) ) ) ) @@ -31,7 +31,7 @@ object SemanticNonNullSchemaSpec extends ZIOSpecDefault { isSome( hasField[__Field, Option[List[Directive]]]( "directives", - _.directives.map(_.filter(_.name == "semanticNonNull")).filter(_.nonEmpty), + _.directives.map(_.filter(_ == SchemaUtils.SemanticNonNull)).filter(_.nonEmpty), isNone ) ) From 4107b2ee5c7539e83fbf0bea7535975a9484ccde Mon Sep 17 00:00:00 2001 From: XiNiHa Date: Wed, 24 Apr 2024 21:47:31 +0900 Subject: [PATCH 06/12] Wrap enableSemanticNonNull option with a config object --- .../caliban/schema/SchemaDerivation.scala | 17 ++++++++++++----- .../caliban/schema/SchemaDerivation.scala | 19 +++++++++++++------ .../schema/SemanticNonNullSchemaSpec.scala | 2 +- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index eabecc03e..b7526f83a 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -11,13 +11,20 @@ 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 + ) + /** - * Enables the `SemanticNonNull` feature on derivation. - * It is currently disabled by default since it is not yet stable. + * Returns a configuration object that can be used to customize the derivation behavior. * - * Override this method and return `true` to enable the feature. + * Override this method to customize the configuration. */ - def enableSemanticNonNull: Boolean = false + def config: DerivationConfig = DerivationConfig() /** * Default naming logic for input types. @@ -105,7 +112,7 @@ trait CommonSchemaDerivation[R] { p.annotations.collectFirst { case GQLDeprecated(reason) => reason }, Option( p.annotations.collect { case GQLDirective(dir) => dir }.toList ++ { - if (enableSemanticNonNull && !isNullable && p.typeclass.canFail) + if (config.enableSemanticNonNull && !isNullable && p.typeclass.canFail) Some(SchemaUtils.SemanticNonNull) else None } diff --git a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala index 8efb28385..4262eb5f2 100644 --- a/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-3/caliban/schema/SchemaDerivation.scala @@ -23,13 +23,20 @@ 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 + ) + /** - * Enables the `SemanticNonNull` feature on derivation. - * It is currently disabled by default since it is not yet stable. + * Returns a configuration object that can be used to customize the derivation behavior. * - * Override this method and return `true` to enable the feature. + * Override this method to customize the configuration. */ - def enableSemanticNonNull: Boolean = false + def config: DerivationConfig = DerivationConfig() inline def recurseSum[R, P, Label, A <: Tuple]( inline types: List[(String, __Type, List[Any])] = Nil, @@ -104,7 +111,7 @@ trait CommonSchemaDerivation { 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, - enableSemanticNonNull + config.enableSemanticNonNull ) case _ if Macros.hasAnnotation[A, GQLValueType] => new ValueTypeSchema[R, A]( @@ -119,7 +126,7 @@ trait CommonSchemaDerivation { MagnoliaMacro.typeInfo[A], MagnoliaMacro.anns[A], MagnoliaMacro.paramAnns[A].toMap, - enableSemanticNonNull + config.enableSemanticNonNull )(using summonInline[ClassTag[A]]) } diff --git a/core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala b/core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala index f65237c43..a1c5400f4 100644 --- a/core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala +++ b/core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala @@ -9,7 +9,7 @@ import zio.test.Assertion._ import zio.test._ object SemanticNonNullSchema extends SchemaDerivation[Any] { - override def enableSemanticNonNull: Boolean = true + override def config = DerivationConfig(enableSemanticNonNull = true) } object SemanticNonNullSchemaSpec extends ZIOSpecDefault { From fddcc3ba8c94ad45214b2687527eabb25b0b210e Mon Sep 17 00:00:00 2001 From: XiNiHa Date: Thu, 25 Apr 2024 19:58:25 +0900 Subject: [PATCH 07/12] Revert changing the semantic of Schema.optional by introducing Schema.nullable --- .../caliban/schema/SchemaDerivation.scala | 4 +- .../caliban/schema/DerivationUtils.scala | 4 +- .../main/scala/caliban/schema/Schema.scala | 59 +++++++++++-------- core/src/test/scala/caliban/TriState.scala | 2 +- .../federation/FederationSupport.scala | 2 +- .../caliban/interop/cats/CatsInterop.scala | 4 +- .../caliban/interop/fs2/Fs2Interop.scala | 4 +- .../caliban/interop/monix/MonixInterop.scala | 6 +- .../scala/caliban/interop/tapir/package.scala | 2 +- 9 files changed, 47 insertions(+), 40 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index b7526f83a..a138a556b 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -76,7 +76,7 @@ trait CommonSchemaDerivation[R] { getName(p), getDescription(p), () => - if (p.typeclass.optional || p.typeclass.canFail) p.typeclass.toType_(isInput, isSubscription) + if (p.typeclass.optional) p.typeclass.toType_(isInput, isSubscription) else p.typeclass.toType_(isInput, isSubscription).nonNull, p.annotations.collectFirst { case GQLDefault(v) => v }, p.annotations.collectFirst { case GQLDeprecated(_) => () }.isDefined, @@ -98,7 +98,7 @@ trait CommonSchemaDerivation[R] { val (isNullable, isNullabilityForced) = { val hasNullableAnn = p.annotations.contains(GQLNullable()) val hasNonNullAnn = p.annotations.contains(GQLNonNullable()) - (!hasNonNullAnn && (hasNullableAnn || p.typeclass.optional), hasNullableAnn || hasNonNullAnn) + (!hasNonNullAnn && (hasNullableAnn || p.typeclass.nullable), hasNullableAnn || hasNonNullAnn) } Types.makeField( getName(p), diff --git a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala index 040b327b6..853c26ca2 100644 --- a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala +++ b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala @@ -105,7 +105,7 @@ private object DerivationUtils { name, getDescription(fieldAnnotations), () => - if (schema.optional || schema.canFail) schema.toType_(isInput, isSubscription) + if (schema.optional) schema.toType_(isInput, isSubscription) else schema.toType_(isInput, isSubscription).nonNull, getDefaultValue(fieldAnnotations), getDeprecatedReason(fieldAnnotations).isDefined, @@ -130,7 +130,7 @@ private object DerivationUtils { val (isNullable, isNullabilityForced) = { val hasNullableAnn = fieldAnnotations.contains(GQLNullable()) val hasNonNullAnn = fieldAnnotations.contains(GQLNonNullable()) - (!hasNonNullAnn && (hasNullableAnn || schema.optional), hasNullableAnn || hasNonNullAnn) + (!hasNonNullAnn && (hasNullableAnn || schema.nullable), hasNullableAnn || hasNonNullAnn) } Types.makeField( name, diff --git a/core/src/main/scala/caliban/schema/Schema.scala b/core/src/main/scala/caliban/schema/Schema.scala index a8669174a..404604e2a 100644 --- a/core/src/main/scala/caliban/schema/Schema.scala +++ b/core/src/main/scala/caliban/schema/Schema.scala @@ -73,9 +73,16 @@ trait Schema[-R, T] { self => def resolve(value: T): Step[R] /** - * Defines if the type is considered optional or non-null. Should be false except for `Option`. + * Defines if the type can resolve to null or not. + * It is true if the type is nullable or if it can fail. */ - def optional: Boolean = false + @deprecatedOverriding("this method will be made final. Override canFail and nullable instead", "2.6.1") + def optional: Boolean = canFail || nullable + + /** + * Defines if the underlying type represents a nullable value. Should be false except for `Option`. + */ + def nullable: Boolean = false /** * Defines if the type can fail during resolution. @@ -95,7 +102,7 @@ trait Schema[-R, T] { self => * @param f a function from `A` to `T`. */ def contramap[A](f: A => T): Schema[R, A] = new Schema[R, A] { - override def optional: Boolean = self.optional + override def nullable: Boolean = self.nullable override def canFail: Boolean = self.canFail override def arguments: List[__InputValue] = self.arguments override def toType(isInput: Boolean, isSubscription: Boolean): __Type = self.toType_(isInput, isSubscription) @@ -108,7 +115,7 @@ trait Schema[-R, T] { self => * @param inputName new name for the type when it's an input type (by default "Input" is added after the name) */ def rename(name: String, inputName: Option[String] = None): Schema[R, T] = new Schema[R, T] { - override def optional: Boolean = self.optional + override def nullable: Boolean = self.nullable override def canFail: Boolean = self.canFail override def arguments: List[__InputValue] = self.arguments override def toType(isInput: Boolean, isSubscription: Boolean): __Type = { @@ -349,7 +356,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { Schema.stringSchema.contramap(Cursor[Base64Cursor].encode) implicit def optionSchema[R0, A](implicit ev: Schema[R0, A]): Schema[R0, Option[A]] = new Schema[R0, Option[A]] { - override def optional: Boolean = true + override def nullable: Boolean = true override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription) override def resolve(value: Option[A]): Step[R0] = @@ -361,7 +368,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { implicit def listSchema[R0, A](implicit ev: Schema[R0, A]): Schema[R0, List[A]] = new Schema[R0, List[A]] { override def toType(isInput: Boolean, isSubscription: Boolean): __Type = { val t = ev.toType_(isInput, isSubscription) - (if (ev.optional || ev.canFail) t else t.nonNull).list + (if (ev.nullable || ev.canFail) t else t.nonNull).list } override def resolve(value: List[A]): Step[R0] = ListStep(value.map(ev.resolve)) @@ -374,7 +381,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { listSchema[R0, A].contramap(_.toList) implicit def functionUnitSchema[R0, A](implicit ev: Schema[R0, A]): Schema[R0, () => A] = new Schema[R0, () => A] { - override def optional: Boolean = ev.optional + override def nullable: Boolean = ev.nullable override def canFail: Boolean = ev.canFail override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription) override def resolve(value: () => A): Step[R0] = FunctionStep(_ => ev.resolve(value())) @@ -382,7 +389,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { implicit def metadataFunctionSchema[R0, A](implicit ev: Schema[R0, A]): Schema[R0, Field => A] = new Schema[R0, Field => A] { override def arguments: List[__InputValue] = ev.arguments - override def optional: Boolean = ev.optional + override def nullable: Boolean = ev.nullable override def canFail: Boolean = ev.canFail override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription) override def resolve(value: Field => A): Step[R0] = MetadataFunctionStep(field => ev.resolve(value(field))) @@ -398,13 +405,13 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { val description: String = s"Either $typeAName or $typeBName" implicit val leftSchema: Schema[RA, A] = new Schema[RA, A] { - override def optional: Boolean = true + override def nullable: Boolean = true override def canFail: Boolean = evA.canFail override def toType(isInput: Boolean, isSubscription: Boolean): __Type = evA.toType_(isInput, isSubscription) override def resolve(value: A): Step[RA] = evA.resolve(value) } implicit val rightSchema: Schema[RB, B] = new Schema[RB, B] { - override def optional: Boolean = true + override def nullable: Boolean = true override def canFail: Boolean = evB.canFail override def toType(isInput: Boolean, isSubscription: Boolean): __Type = evB.toType_(isInput, isSubscription) override def resolve(value: B): Step[RB] = evB.resolve(value) @@ -473,14 +480,14 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { __InputValue( unwrappedArgumentName, None, - () => if (ev1.optional || ev1.canFail) inputType else inputType.nonNull, + () => if (ev1.nullable || ev1.canFail) inputType else inputType.nonNull, None ) ) ) } - override def optional: Boolean = ev2.optional + override def nullable: Boolean = ev2.nullable override def canFail: Boolean = ev2.canFail override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev2.toType_(isInput, isSubscription) @@ -509,7 +516,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { effectSchema[R0, R0, R0, Throwable, A].contramap[Future[A]](future => ZIO.fromFuture(_ => future)(Trace.empty)) implicit def infallibleEffectSchema[R0, R1 >: R0, R2 >: R0, A](implicit ev: Schema[R2, A]): Schema[R0, URIO[R1, A]] = new Schema[R0, URIO[R1, A]] { - override def optional: Boolean = ev.optional + override def nullable: Boolean = ev.nullable override def canFail: Boolean = ev.canFail override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription) override def resolve(value: URIO[R1, A]): Step[R0] = QueryStep(ZQuery.fromZIONow(value.map(ev.resolve))) @@ -518,7 +525,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ev: Schema[R2, A] ): Schema[R0, ZIO[R1, E, A]] = new Schema[R0, ZIO[R1, E, A]] { - override def optional: Boolean = ev.optional + override def nullable: Boolean = ev.nullable override def canFail: Boolean = true 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))) @@ -527,7 +534,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ev: Schema[R2, A] ): Schema[R0, ZIO[R1, E, A]] = new Schema[R0, ZIO[R1, E, A]] { - override def optional: Boolean = ev.optional + override def nullable: Boolean = ev.nullable override def canFail: Boolean = true override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription) override def resolve(value: ZIO[R1, E, A]): Step[R0] = QueryStep( @@ -538,7 +545,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ev: Schema[R2, A] ): Schema[R0, ZQuery[R1, Nothing, A]] = new Schema[R0, ZQuery[R1, Nothing, A]] { - override def optional: Boolean = ev.optional + override def nullable: Boolean = ev.nullable override def canFail: Boolean = ev.canFail override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription) override def resolve(value: ZQuery[R1, Nothing, A]): Step[R0] = QueryStep(value.map(ev.resolve)) @@ -547,7 +554,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ev: Schema[R2, A] ): Schema[R0, ZQuery[R1, E, A]] = new Schema[R0, ZQuery[R1, E, A]] { - override def optional: Boolean = ev.optional + override def nullable: Boolean = ev.nullable override def canFail: Boolean = true 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)) @@ -556,7 +563,7 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ev: Schema[R2, A] ): Schema[R0, ZQuery[R1, E, A]] = new Schema[R0, ZQuery[R1, E, A]] { - override def optional: Boolean = ev.optional + override def nullable: Boolean = ev.nullable override def canFail: Boolean = true 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)) @@ -565,11 +572,11 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ev: Schema[R2, A] ): Schema[R1, ZStream[R1, Nothing, A]] = new Schema[R1, ZStream[R1, Nothing, A]] { - override def optional: Boolean = false + override def nullable: Boolean = false override def canFail: Boolean = ev.canFail override def toType(isInput: Boolean, isSubscription: Boolean): __Type = { val t = ev.toType_(isInput, isSubscription) - if (isSubscription) t else (if (ev.optional || ev.canFail) t else t.nonNull).list + if (isSubscription) t else (if (ev.nullable || ev.canFail) t else t.nonNull).list } override def resolve(value: ZStream[R1, Nothing, A]): Step[R1] = StreamStep(value.map(ev.resolve)) } @@ -577,11 +584,11 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ev: Schema[R2, A] ): Schema[R0, ZStream[R1, E, A]] = new Schema[R0, ZStream[R1, E, A]] { - override def optional: Boolean = ev.optional + override def nullable: Boolean = ev.nullable override def canFail: Boolean = true override def toType(isInput: Boolean, isSubscription: Boolean): __Type = { val t = ev.toType_(isInput, isSubscription) - if (isSubscription) t else (if (ev.optional || ev.canFail) t else t.nonNull).list + if (isSubscription) t else (if (ev.nullable || ev.canFail) t else t.nonNull).list } override def resolve(value: ZStream[R1, E, A]): Step[R0] = StreamStep(value.map(ev.resolve)) } @@ -589,11 +596,11 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema { ev: Schema[R2, A] ): Schema[R0, ZStream[R1, E, A]] = new Schema[R0, ZStream[R1, E, A]] { - override def optional: Boolean = ev.optional + override def nullable: Boolean = ev.nullable override def canFail: Boolean = true override def toType(isInput: Boolean, isSubscription: Boolean): __Type = { val t = ev.toType_(isInput, isSubscription) - if (isSubscription) t else (if (ev.optional || ev.canFail) t else t.nonNull).list + if (isSubscription) t else (if (ev.nullable || ev.canFail) t else t.nonNull).list } override def resolve(value: ZStream[R1, E, A]): Step[R0] = StreamStep(value.mapBoth(convertError, ev.resolve)) } @@ -726,7 +733,7 @@ abstract class PartiallyAppliedFieldBase[V]( description, _ => Nil, () => - if (ev.optional || ev.canFail) ev.toType_(ft.isInput, ft.isSubscription) + if (ev.nullable || ev.canFail) ev.toType_(ft.isInput, ft.isSubscription) else ev.toType_(ft.isInput, ft.isSubscription).nonNull, isDeprecated = Directives.isDeprecated(directives), deprecationReason = Directives.deprecationReason(directives), @@ -770,7 +777,7 @@ case class PartiallyAppliedFieldWithArgs[V, A]( description, ev1.arguments, () => - if (ev1.optional || ev1.canFail) ev1.toType_(fa.isInput, fa.isSubscription) + if (ev1.nullable || ev1.canFail) ev1.toType_(fa.isInput, fa.isSubscription) else ev1.toType_(fa.isInput, fa.isSubscription).nonNull, isDeprecated = Directives.isDeprecated(directives), deprecationReason = Directives.deprecationReason(directives), diff --git a/core/src/test/scala/caliban/TriState.scala b/core/src/test/scala/caliban/TriState.scala index 2e9646587..f6ae618f0 100644 --- a/core/src/test/scala/caliban/TriState.scala +++ b/core/src/test/scala/caliban/TriState.scala @@ -15,7 +15,7 @@ object TriState { def schemaCustom[R, A](undefined: PureStep)(implicit ev: Schema[R, A]): Schema[R, TriState[A]] = new Schema[R, TriState[A]] { - override val optional = true + override val nullable = true override def toType(isInput: Boolean, isSubscription: Boolean) = ev.toType_(isInput, isSubscription) diff --git a/federation/src/main/scala/caliban/federation/FederationSupport.scala b/federation/src/main/scala/caliban/federation/FederationSupport.scala index 5eef75a02..e4d695f94 100644 --- a/federation/src/main/scala/caliban/federation/FederationSupport.scala +++ b/federation/src/main/scala/caliban/federation/FederationSupport.scala @@ -60,7 +60,7 @@ abstract class FederationSupport( import genericSchema.auto._ implicit val entitySchema: Schema[R, _Entity] = new Schema[R, _Entity] { - override def optional: Boolean = true + override def nullable: Boolean = true override def toType(isInput: Boolean, isSubscription: Boolean): __Type = __Type( __TypeKind.UNION, diff --git a/interop/cats/src/main/scala/caliban/interop/cats/CatsInterop.scala b/interop/cats/src/main/scala/caliban/interop/cats/CatsInterop.scala index f387813a6..488347988 100644 --- a/interop/cats/src/main/scala/caliban/interop/cats/CatsInterop.scala +++ b/interop/cats/src/main/scala/caliban/interop/cats/CatsInterop.scala @@ -190,8 +190,8 @@ object CatsInterop { override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription) - override def optional: Boolean = - ev.optional + override def nullable: Boolean = + ev.nullable override def canFail: Boolean = true override def resolve(value: F[A]): Step[R] = diff --git a/interop/cats/src/main/scala/caliban/interop/fs2/Fs2Interop.scala b/interop/cats/src/main/scala/caliban/interop/fs2/Fs2Interop.scala index 5c6e8f0c9..c2a3239f2 100644 --- a/interop/cats/src/main/scala/caliban/interop/fs2/Fs2Interop.scala +++ b/interop/cats/src/main/scala/caliban/interop/fs2/Fs2Interop.scala @@ -14,7 +14,7 @@ object Fs2Interop { override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription) - override def optional: Boolean = ev.optional + override def nullable: Boolean = ev.nullable override def canFail: Boolean = true override def resolve(value: Stream[RIO[R, *], A]): Step[R] = @@ -29,7 +29,7 @@ object Fs2Interop { override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription) - override def optional: Boolean = ev.optional + override def nullable: Boolean = ev.nullable override def canFail: Boolean = true override def resolve(value: Stream[F, A]): Step[R] = diff --git a/interop/monix/src/main/scala/caliban/interop/monix/MonixInterop.scala b/interop/monix/src/main/scala/caliban/interop/monix/MonixInterop.scala index daf648929..c29e38e8e 100644 --- a/interop/monix/src/main/scala/caliban/interop/monix/MonixInterop.scala +++ b/interop/monix/src/main/scala/caliban/interop/monix/MonixInterop.scala @@ -60,7 +60,7 @@ object MonixInterop { def taskSchema[R, A](implicit ev: Schema[R, A], ev2: ConcurrentEffect[MonixTask]): Schema[R, MonixTask[A]] = new Schema[R, MonixTask[A]] { override def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription) - override def optional: Boolean = ev.optional + override def nullable: Boolean = ev.nullable override def canFail: Boolean = true override def resolve(value: MonixTask[A]): Step[R] = QueryStep(ZQuery.fromZIO(value.to[Task].map(ev.resolve))) @@ -70,10 +70,10 @@ object MonixInterop { queueSize: Int )(implicit ev: Schema[R, A], ev2: ConcurrentEffect[MonixTask]): Schema[R, Observable[A]] = new Schema[R, Observable[A]] { - override def optional: Boolean = true + override def nullable: Boolean = true 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 + if (isSubscription) t else (if (ev.nullable) t else t.nonNull).list } override def resolve(value: Observable[A]): Step[R] = StreamStep( diff --git a/interop/tapir/src/main/scala/caliban/interop/tapir/package.scala b/interop/tapir/src/main/scala/caliban/interop/tapir/package.scala index c33f1b86a..39cfb621b 100644 --- a/interop/tapir/src/main/scala/caliban/interop/tapir/package.scala +++ b/interop/tapir/src/main/scala/caliban/interop/tapir/package.scala @@ -107,7 +107,7 @@ package object tapir { Types.makeField( extractPath(serverEndpoint.endpoint.info.name, serverEndpoint.endpoint.input), serverEndpoint.endpoint.info.description, - getArgs(inputSchema.toType_(isInput = true), inputSchema.optional || inputSchema.canFail), + getArgs(inputSchema.toType_(isInput = true), inputSchema.optional), () => if (serverEndpoint.endpoint.errorOutput == EndpointOutput.Void[E]()) outputSchema.toType_().nonNull From 6fd85ce08bfb57fb89f0ed329a6c7ec0fe35124e Mon Sep 17 00:00:00 2001 From: XiNiHa Date: Sat, 11 May 2024 16:02:47 +0900 Subject: [PATCH 08/12] Change deprecation warning version to 2.7.0 --- core/src/main/scala/caliban/schema/Schema.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/caliban/schema/Schema.scala b/core/src/main/scala/caliban/schema/Schema.scala index 404604e2a..24657497f 100644 --- a/core/src/main/scala/caliban/schema/Schema.scala +++ b/core/src/main/scala/caliban/schema/Schema.scala @@ -76,7 +76,7 @@ trait Schema[-R, T] { self => * Defines if the type can resolve to null or not. * It is true if the type is nullable or if it can fail. */ - @deprecatedOverriding("this method will be made final. Override canFail and nullable instead", "2.6.1") + @deprecatedOverriding("this method will be made final. Override canFail and nullable instead", "2.7.0") def optional: Boolean = canFail || nullable /** From 82662cbcb8e4b60be270595e6844c97e1e7a7948 Mon Sep 17 00:00:00 2001 From: XiNiHa Date: Sat, 11 May 2024 18:08:52 +0900 Subject: [PATCH 09/12] Clear nullability determination logic --- .../scala-2/caliban/schema/SchemaDerivation.scala | 13 ++++++++----- .../scala-3/caliban/schema/DerivationUtils.scala | 14 +++++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index a138a556b..ef87048eb 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -95,24 +95,27 @@ trait CommonSchemaDerivation[R] { ctx.parameters .filterNot(_.annotations.exists(_ == GQLExcluded())) .map { p => - val (isNullable, isNullabilityForced) = { + val (isNullable, isSemanticNonNull) = { val hasNullableAnn = p.annotations.contains(GQLNullable()) val hasNonNullAnn = p.annotations.contains(GQLNonNullable()) - (!hasNonNullAnn && (hasNullableAnn || p.typeclass.nullable), hasNullableAnn || hasNonNullAnn) + + if (hasNonNullAnn) (false, false) + else if (hasNullableAnn || p.typeclass.nullable) (true, false) + else if (p.typeclass.canFail) (true, true) + else (false, false) } Types.makeField( getName(p), getDescription(p), p.typeclass.arguments, () => - if (isNullable || (!isNullabilityForced && p.typeclass.canFail)) - 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 ++ { - if (config.enableSemanticNonNull && !isNullable && p.typeclass.canFail) + if (config.enableSemanticNonNull && isSemanticNonNull) Some(SchemaUtils.SemanticNonNull) else None } diff --git a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala index 853c26ca2..538df7242 100644 --- a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala +++ b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala @@ -126,24 +126,28 @@ private object DerivationUtils { Some(getName(annotations, info)), getDescription(annotations), fields.map { (name, fieldAnnotations, schema) => - val deprecatedReason = getDeprecatedReason(fieldAnnotations) - val (isNullable, isNullabilityForced) = { + val deprecatedReason = getDeprecatedReason(fieldAnnotations) + val (isNullable, isSemanticNonNull) = { val hasNullableAnn = fieldAnnotations.contains(GQLNullable()) val hasNonNullAnn = fieldAnnotations.contains(GQLNonNullable()) - (!hasNonNullAnn && (hasNullableAnn || schema.nullable), hasNullableAnn || hasNonNullAnn) + + if (hasNonNullAnn) (false, false) + else if (hasNullableAnn || schema.nullable) (true, false) + else if (schema.canFail) (true, true) + else (false, false) } Types.makeField( name, getDescription(fieldAnnotations), schema.arguments, () => - if (isNullable || (!isNullabilityForced && schema.canFail)) schema.toType_(isInput, isSubscription) + if (isNullable) schema.toType_(isInput, isSubscription) else schema.toType_(isInput, isSubscription).nonNull, deprecatedReason.isDefined, deprecatedReason, Option( getDirectives(fieldAnnotations) ++ { - if (enableSemanticNonNull && !isNullable && schema.canFail) Some(SchemaUtils.SemanticNonNull) + if (enableSemanticNonNull && isSemanticNonNull) Some(SchemaUtils.SemanticNonNull) else None } ).filter(_.nonEmpty) From 61f3c6d668c8e45c161aec28e0f6f20148b4ac3b Mon Sep 17 00:00:00 2001 From: XiNiHa Date: Sat, 11 May 2024 19:02:45 +0900 Subject: [PATCH 10/12] Add test to ensure semantic of .optional for custom schemas --- .../scala/caliban/schema/OptionalSpec.scala | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 core/src/test/scala/caliban/schema/OptionalSpec.scala diff --git a/core/src/test/scala/caliban/schema/OptionalSpec.scala b/core/src/test/scala/caliban/schema/OptionalSpec.scala new file mode 100644 index 000000000..f04d2108e --- /dev/null +++ b/core/src/test/scala/caliban/schema/OptionalSpec.scala @@ -0,0 +1,57 @@ +package caliban.schema + +import caliban.{ graphQL, RootResolver } +import zio._ +import zio.test._ + +object OptionalSpec extends ZIOSpecDefault { + import caliban.schema.Schema._ + + override def spec = suite("OptionalSpec")( + test("Semantic of Schema.optional is kept consistent across various nullable/canFail combinations") { + val expected = + """schema { + | query: Query + |} + | + |type Query { + | a: String! + | b: String + | c: String + | d: String + |}""".stripMargin + + implicit def wrapperSchema[A](implicit ev: Schema[Any, A]): Schema[Any, Wrapper[A]] = + new Schema[Any, Wrapper[A]] { + @annotation.nowarn + override def optional = ev.optional + def toType(isInput: Boolean, isSubscription: Boolean) = ev.toType_(isInput, isSubscription) + def resolve(value: Wrapper[A]): Step[Any] = + ev.resolve(value.value) + } + + implicit def querySchema: Schema[Any, Query] = Schema.gen[Any, Query] + + val resolver = RootResolver( + Query( + Wrapper("a"), + Wrapper(Some("b")), + Wrapper(ZIO.succeed("c")), + Wrapper(ZIO.succeed(Some("d"))) + ) + ) + val gql = graphQL(resolver) + + assertTrue(gql.render == expected) + } + ) + + case class Wrapper[A](value: A) + + case class Query( + a: Wrapper[String], + b: Wrapper[Option[String]], + c: Wrapper[Task[String]], + d: Wrapper[Task[Option[String]]] + ) +} From 066a821501d2ffcb26ca54582b032a01c0d6c60a Mon Sep 17 00:00:00 2001 From: XiNiHa Date: Sat, 11 May 2024 19:06:33 +0900 Subject: [PATCH 11/12] Fix not keeping semantics of .optional on custom schema --- core/src/main/scala-2/caliban/schema/SchemaDerivation.scala | 4 ++-- core/src/main/scala-3/caliban/schema/DerivationUtils.scala | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala index ef87048eb..a5c595613 100644 --- a/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala +++ b/core/src/main/scala-2/caliban/schema/SchemaDerivation.scala @@ -100,8 +100,8 @@ trait CommonSchemaDerivation[R] { val hasNonNullAnn = p.annotations.contains(GQLNonNullable()) if (hasNonNullAnn) (false, false) - else if (hasNullableAnn || p.typeclass.nullable) (true, false) - else if (p.typeclass.canFail) (true, true) + else if (hasNullableAnn) (true, false) + else if (p.typeclass.optional) (true, !p.typeclass.nullable) else (false, false) } Types.makeField( diff --git a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala index 538df7242..b5204610e 100644 --- a/core/src/main/scala-3/caliban/schema/DerivationUtils.scala +++ b/core/src/main/scala-3/caliban/schema/DerivationUtils.scala @@ -132,8 +132,8 @@ private object DerivationUtils { val hasNonNullAnn = fieldAnnotations.contains(GQLNonNullable()) if (hasNonNullAnn) (false, false) - else if (hasNullableAnn || schema.nullable) (true, false) - else if (schema.canFail) (true, true) + else if (hasNullableAnn) (true, false) + else if (schema.optional) (true, !schema.nullable) else (false, false) } Types.makeField( From 7aea83bfbbf6f4919f365a2ea30507875292ac18 Mon Sep 17 00:00:00 2001 From: XiNiHa Date: Sat, 11 May 2024 23:41:03 +0900 Subject: [PATCH 12/12] Add supplementary tests to make things sure --- .../schema/SemanticNonNullSchemaSpec.scala | 97 +++++++++++++++---- 1 file changed, 78 insertions(+), 19 deletions(-) diff --git a/core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala b/core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala index a1c5400f4..b368ae9c1 100644 --- a/core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala +++ b/core/src/test/scala/caliban/schema/SemanticNonNullSchemaSpec.scala @@ -1,7 +1,7 @@ package caliban.schema import caliban._ -import caliban.introspection.adt.{ __DeprecatedArgs, __Field } +import caliban.introspection.adt.{ __DeprecatedArgs, __Field, __Type, __TypeKind } import caliban.parsing.adt.Directive import caliban.schema.Annotations._ import zio._ @@ -16,34 +16,93 @@ 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(SchemaUtils.SemanticNonNull)) - ) + val field = effectfulFieldObjectSchema.toType_().fields(__DeprecatedArgs()).toList.flatten.apply(0) + assert(field)( + hasField[__Field, Option[List[Directive]]]( + "directives", + _.directives, + isSome(contains(SchemaUtils.SemanticNonNull)) ) ) + assert(field._type)( + hasField[__Type, __TypeKind]("kind", _.kind, equalTo(__TypeKind.SCALAR)) + ) + }, + test("effectful field as non-nullable") { + val field = effectfulFieldObjectSchema.toType_().fields(__DeprecatedArgs()).toList.flatten.apply(1) + assert(field)( + hasField[__Field, Option[List[Directive]]]( + "directives", + _.directives.map(_.filter(_ == SchemaUtils.SemanticNonNull)).filter(_.nonEmpty), + isNone + ) + ) + assert(field._type)( + hasField[__Type, __TypeKind]("kind", _.kind, equalTo(__TypeKind.NON_NULL)) + ) + }, + test("nullable effectful field") { + val field = nullableEffectfulFieldObjectSchema.toType_().fields(__DeprecatedArgs()).toList.flatten.apply(0) + assert(field)( + hasField[__Field, Option[List[Directive]]]( + "directives", + _.directives.map(_.filter(_ == SchemaUtils.SemanticNonNull)).filter(_.nonEmpty), + isNone + ) + ) + assert(field._type)( + hasField[__Type, __TypeKind]("kind", _.kind, equalTo(__TypeKind.SCALAR)) + ) }, - test("optional effectful field") { - assert(optionalEffectfulFieldObjectSchema.toType_().fields(__DeprecatedArgs()).toList.flatten.headOption)( - isSome( - hasField[__Field, Option[List[Directive]]]( - "directives", - _.directives.map(_.filter(_ == SchemaUtils.SemanticNonNull)).filter(_.nonEmpty), - isNone - ) + test("nullable effectful field as non-nullable") { + val field = nullableEffectfulFieldObjectSchema.toType_().fields(__DeprecatedArgs()).toList.flatten.apply(1) + assert(field)( + hasField[__Field, Option[List[Directive]]]( + "directives", + _.directives, + isNone ) ) + assert(field._type)( + hasField[__Type, __TypeKind]("kind", _.kind, equalTo(__TypeKind.NON_NULL)) + ) + }, + test("infallible effectful field") { + val field = infallibleEffectfulFieldSchema.toType_().fields(__DeprecatedArgs()).toList.flatten.apply(0) + assert(field)( + hasField[__Field, Option[List[Directive]]]( + "directives", + _.directives, + isNone + ) + ) + assert(field._type)( + hasField[__Type, __TypeKind]("kind", _.kind, equalTo(__TypeKind.NON_NULL)) + ) + }, + test("infallible effectful field as nullable") { + val field = infallibleEffectfulFieldSchema.toType_().fields(__DeprecatedArgs()).toList.flatten.apply(1) + assert(field)( + hasField[__Field, Option[List[Directive]]]( + "directives", + _.directives, + isNone + ) + ) + assert(field._type)( + hasField[__Type, __TypeKind]("kind", _.kind, equalTo(__TypeKind.SCALAR)) + ) } ) case class EffectfulFieldObject(q: Task[Int], @GQLNonNullable qAnnotated: Task[Int]) - case class OptionalEffectfulFieldObject(q: Task[Option[String]], @GQLNonNullable qAnnotated: Task[Option[String]]) + case class NullableEffectfulFieldObject(q: Task[Option[String]], @GQLNonNullable qAnnotated: Task[Option[String]]) + case class InfallibleFieldObject(q: UIO[Int], @GQLNullable qAnnotated: UIO[Int]) implicit val effectfulFieldObjectSchema: Schema[Any, EffectfulFieldObject] = SemanticNonNullSchema.gen[Any, EffectfulFieldObject] - implicit val optionalEffectfulFieldObjectSchema: Schema[Any, OptionalEffectfulFieldObject] = - SemanticNonNullSchema.gen[Any, OptionalEffectfulFieldObject] + implicit val nullableEffectfulFieldObjectSchema: Schema[Any, NullableEffectfulFieldObject] = + SemanticNonNullSchema.gen[Any, NullableEffectfulFieldObject] + implicit val infallibleEffectfulFieldSchema: Schema[Any, InfallibleFieldObject] = + SemanticNonNullSchema.gen[Any, InfallibleFieldObject] }