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 308ebff8c..8278efd8f 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) @@ -360,7 +362,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)) @@ -374,6 +376,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())) } @@ -381,6 +384,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))) } @@ -396,11 +400,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) } @@ -468,7 +474,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 ) ) @@ -476,6 +482,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] = @@ -504,6 +511,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))) } @@ -511,8 +519,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))) } @@ -520,8 +528,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)) @@ -532,6 +540,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)) } @@ -539,8 +548,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)) } @@ -548,8 +557,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)) } @@ -558,9 +567,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)) } @@ -568,11 +578,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)) } @@ -580,11 +590,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)) } @@ -718,13 +728,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) @@ -768,13 +778,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 b7e92669b..00ef144ae 100644 --- a/interop/tapir/src/main/scala/caliban/interop/tapir/package.scala +++ b/interop/tapir/src/main/scala/caliban/interop/tapir/package.scala @@ -106,7 +106,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