Skip to content

Commit 695dac6

Browse files
committed
Disable feature by default with an overridable flag
1 parent 8662d6f commit 695dac6

File tree

7 files changed

+109
-42
lines changed

7 files changed

+109
-42
lines changed

core/src/main/scala-2/caliban/schema/SchemaDerivation.scala

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ import scala.language.experimental.macros
1111

1212
trait CommonSchemaDerivation[R] {
1313

14+
/**
15+
* Enables the `SemanticNonNull` feature on derivation.
16+
* It is currently disabled by default since it is not yet stable.
17+
*
18+
* Override this method and return `true` to enable the feature.
19+
*/
20+
def enableSemanticNonNull: Boolean = false
21+
1422
/**
1523
* Default naming logic for input types.
1624
* 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] {
96104
p.annotations.collectFirst { case GQLDeprecated(reason) => reason },
97105
Option(
98106
p.annotations.collect { case GQLDirective(dir) => dir }.toList ++ {
99-
if (isOptional && p.typeclass.semanticNonNull) Some(Directive("semanticNonNull"))
107+
if (enableSemanticNonNull && isOptional && p.typeclass.semanticNonNull)
108+
Some(Directive("semanticNonNull"))
100109
else None
101110
}
102111
).filter(_.nonEmpty)

core/src/main/scala-3/caliban/schema/DerivationUtils.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ private object DerivationUtils {
120120
def mkObject[R](
121121
annotations: List[Any],
122122
fields: List[(String, List[Any], Schema[R, Any])],
123-
info: TypeInfo
123+
info: TypeInfo,
124+
enableSemanticNonNull: Boolean
124125
)(isInput: Boolean, isSubscription: Boolean): __Type = makeObject(
125126
Some(getName(annotations, info)),
126127
getDescription(annotations),
@@ -142,7 +143,7 @@ private object DerivationUtils {
142143
deprecatedReason,
143144
Option(
144145
getDirectives(fieldAnnotations) ++ {
145-
if (isOptional && schema.semanticNonNull) Some(Directive("semanticNonNull"))
146+
if (enableSemanticNonNull && isOptional && schema.semanticNonNull) Some(Directive("semanticNonNull"))
146147
else None
147148
}
148149
).filter(_.nonEmpty)

core/src/main/scala-3/caliban/schema/ObjectSchema.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ final private class ObjectSchema[R, A](
1212
_methodFields: => List[(String, List[Any], Schema[R, ?])],
1313
info: TypeInfo,
1414
anns: List[Any],
15-
paramAnnotations: Map[String, List[Any]]
15+
paramAnnotations: Map[String, List[Any]],
16+
enableSemanticNonNull: Boolean
1617
)(using ct: ClassTag[A])
1718
extends Schema[R, A] {
1819

@@ -48,7 +49,7 @@ final private class ObjectSchema[R, A](
4849
def toType(isInput: Boolean, isSubscription: Boolean): __Type = {
4950
val _ = resolver // Init the lazy val
5051
if (isInput) mkInputObject[R](anns, fields.map(_._1), info)(isInput, isSubscription)
51-
else mkObject[R](anns, fields.map(_._1), info)(isInput, isSubscription)
52+
else mkObject[R](anns, fields.map(_._1), info, enableSemanticNonNull)(isInput, isSubscription)
5253
}
5354

5455
def resolve(value: A): Step[R] = resolver.resolve(value)

core/src/main/scala-3/caliban/schema/SchemaDerivation.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ object PrintDerived {
2323
trait CommonSchemaDerivation {
2424
export DerivationUtils.customizeInputTypeName
2525

26+
/**
27+
* Enables the `SemanticNonNull` feature on derivation.
28+
* It is currently disabled by default since it is not yet stable.
29+
*
30+
* Override this method and return `true` to enable the feature.
31+
*/
32+
def enableSemanticNonNull: Boolean = false
33+
2634
inline def recurseSum[R, P, Label, A <: Tuple](
2735
inline types: List[(String, __Type, List[Any])] = Nil,
2836
inline schemas: List[Schema[R, Any]] = Nil

core/src/main/scala/caliban/schema/Schema.scala

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -255,29 +255,32 @@ trait GenericSchema[R] extends SchemaDerivation[R] with TemporalSchema {
255255
def field[V](
256256
name: String,
257257
description: Option[String] = None,
258-
directives: List[Directive] = List.empty
258+
directives: List[Directive] = List.empty,
259+
enableSemanticNonNull: Boolean = false
259260
): PartiallyAppliedField[V] =
260-
PartiallyAppliedField[V](name, description, directives)
261+
PartiallyAppliedField[V](name, description, directives, enableSemanticNonNull)
261262

262263
/**
263264
* Manually defines a lazy field from a name, a description, some directives and a resolver.
264265
*/
265266
def fieldLazy[V](
266267
name: String,
267268
description: Option[String] = None,
268-
directives: List[Directive] = List.empty
269+
directives: List[Directive] = List.empty,
270+
enableSemanticNonNull: Boolean = false
269271
): PartiallyAppliedFieldLazy[V] =
270-
PartiallyAppliedFieldLazy[V](name, description, directives)
272+
PartiallyAppliedFieldLazy[V](name, description, directives, enableSemanticNonNull)
271273

272274
/**
273275
* Manually defines a field with arguments from a name, a description, some directives and a resolver.
274276
*/
275277
def fieldWithArgs[V, A](
276278
name: String,
277279
description: Option[String] = None,
278-
directives: List[Directive] = Nil
280+
directives: List[Directive] = Nil,
281+
enableSemanticNonNull: Boolean = false
279282
): PartiallyAppliedFieldWithArgs[V, A] =
280-
PartiallyAppliedFieldWithArgs[V, A](name, description, directives)
283+
PartiallyAppliedFieldWithArgs[V, A](name, description, directives, enableSemanticNonNull)
281284

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

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

699-
abstract class PartiallyAppliedFieldBase[V](name: String, description: Option[String], directives: List[Directive]) {
702+
abstract class PartiallyAppliedFieldBase[V](
703+
name: String,
704+
description: Option[String],
705+
directives: List[Directive],
706+
enableSemanticNonNull: Boolean
707+
) {
700708
def apply[R, V1](fn: V => V1)(implicit ev: Schema[R, V1], ft: FieldAttributes): (__Field, V => Step[R]) =
701709
either[R, V1](v => Left(fn(v)))(ev, ft)
702710

@@ -716,30 +724,43 @@ abstract class PartiallyAppliedFieldBase[V](name: String, description: Option[St
716724
deprecationReason = Directives.deprecationReason(directives),
717725
directives = Some(
718726
directives.filter(_.name != "deprecated") ++ {
719-
if (ev.optional && ev.semanticNonNull) Some(Directive("semanticNonNull"))
727+
if (enableSemanticNonNull && ev.optional && ev.semanticNonNull) Some(Directive("semanticNonNull"))
720728
else None
721729
}
722730
).filter(_.nonEmpty)
723731
)
724732
}
725733

726-
case class PartiallyAppliedField[V](name: String, description: Option[String], directives: List[Directive])
727-
extends PartiallyAppliedFieldBase[V](name, description, directives) {
734+
case class PartiallyAppliedField[V](
735+
name: String,
736+
description: Option[String],
737+
directives: List[Directive],
738+
enableSemanticNonNull: Boolean
739+
) extends PartiallyAppliedFieldBase[V](name, description, directives, enableSemanticNonNull) {
728740
def either[R, V1](
729741
fn: V => Either[V1, Step[R]]
730742
)(implicit ev: Schema[R, V1], ft: FieldAttributes): (__Field, V => Step[R]) =
731743
(makeField, (v: V) => fn(v).fold(ev.resolve, identity))
732744
}
733745

734-
case class PartiallyAppliedFieldLazy[V](name: String, description: Option[String], directives: List[Directive])
735-
extends PartiallyAppliedFieldBase[V](name, description, directives) {
746+
case class PartiallyAppliedFieldLazy[V](
747+
name: String,
748+
description: Option[String],
749+
directives: List[Directive],
750+
enableSemanticNonNull: Boolean
751+
) extends PartiallyAppliedFieldBase[V](name, description, directives, enableSemanticNonNull) {
736752
def either[R, V1](
737753
fn: V => Either[V1, Step[R]]
738754
)(implicit ev: Schema[R, V1], ft: FieldAttributes): (__Field, V => Step[R]) =
739755
(makeField, (v: V) => FunctionStep(_ => fn(v).fold(ev.resolve, identity)))
740756
}
741757

742-
case class PartiallyAppliedFieldWithArgs[V, A](name: String, description: Option[String], directives: List[Directive]) {
758+
case class PartiallyAppliedFieldWithArgs[V, A](
759+
name: String,
760+
description: Option[String],
761+
directives: List[Directive],
762+
enableSemanticNonNull: Boolean
763+
) {
743764
def apply[R, V1](fn: V => (A => V1))(implicit ev1: Schema[R, A => V1], fa: FieldAttributes): (__Field, V => Step[R]) =
744765
(
745766
Types.makeField(
@@ -753,7 +774,7 @@ case class PartiallyAppliedFieldWithArgs[V, A](name: String, description: Option
753774
deprecationReason = Directives.deprecationReason(directives),
754775
directives = Some(
755776
directives.filter(_.name != "deprecated") ++ {
756-
if (ev1.optional && ev1.semanticNonNull) Some(Directive("semanticNonNull"))
777+
if (enableSemanticNonNull && ev1.optional && ev1.semanticNonNull) Some(Directive("semanticNonNull"))
757778
else None
758779
}
759780
).filter(_.nonEmpty)

core/src/test/scala/caliban/schema/SchemaSpec.scala

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,33 +26,11 @@ object SchemaSpec extends ZIOSpecDefault {
2626
isSome(hasField[__Type, __TypeKind]("kind", _.kind, equalTo(__TypeKind.SCALAR)))
2727
)
2828
},
29-
test("effectful field as semanticNonNull") {
30-
assert(introspect[EffectfulFieldSchema].fields(__DeprecatedArgs()).toList.flatten.headOption)(
31-
isSome(
32-
hasField[__Field, Option[List[Directive]]](
33-
"directives",
34-
_.directives,
35-
isSome(contains((Directive("semanticNonNull"))))
36-
)
37-
)
38-
)
39-
},
4029
test("effectful field as non-nullable") {
4130
assert(introspect[EffectfulFieldSchema].fields(__DeprecatedArgs()).toList.flatten.apply(1)._type)(
4231
hasField[__Type, __TypeKind]("kind", _.kind, equalTo(__TypeKind.NON_NULL))
4332
)
4433
},
45-
test("optional effectful field") {
46-
assert(introspect[OptionalEffectfulFieldSchema].fields(__DeprecatedArgs()).toList.flatten.headOption)(
47-
isSome(
48-
hasField[__Field, Option[List[Directive]]](
49-
"directives",
50-
_.directives.map(_.filter(_.name == "semanticNonNull")).filter(_.nonEmpty),
51-
isNone
52-
)
53-
)
54-
)
55-
},
5634
test("infallible effectful field") {
5735
assert(introspect[InfallibleFieldSchema].fields(__DeprecatedArgs()).toList.flatten.headOption.map(_._type))(
5836
isSome(hasField[__Type, __TypeKind]("kind", _.kind, equalTo(__TypeKind.NON_NULL)))
@@ -322,7 +300,7 @@ object SchemaSpec extends ZIOSpecDefault {
322300
|}
323301
|
324302
|type EnvironmentSchema {
325-
| test: Int @semanticNonNull
303+
| test: Int
326304
| box: Box!
327305
|}
328306
|
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package caliban.schema
2+
3+
import caliban._
4+
import caliban.introspection.adt.{ __DeprecatedArgs, __Field }
5+
import caliban.parsing.adt.Directive
6+
import caliban.schema.Annotations._
7+
import zio._
8+
import zio.test.Assertion._
9+
import zio.test._
10+
11+
object SemanticNonNullSchema extends SchemaDerivation[Any] {
12+
override def enableSemanticNonNull: Boolean = true
13+
}
14+
15+
object SemanticNonNullSchemaSpec extends ZIOSpecDefault {
16+
override def spec =
17+
suite("SemanticNonNullSchemaSpec")(
18+
test("effectful field as semanticNonNull") {
19+
assert(effectfulFieldObjectSchema.toType_().fields(__DeprecatedArgs()).toList.flatten.headOption)(
20+
isSome(
21+
hasField[__Field, Option[List[Directive]]](
22+
"directives",
23+
_.directives,
24+
isSome(contains((Directive("semanticNonNull"))))
25+
)
26+
)
27+
)
28+
},
29+
test("optional effectful field") {
30+
assert(optionalEffectfulFieldObjectSchema.toType_().fields(__DeprecatedArgs()).toList.flatten.headOption)(
31+
isSome(
32+
hasField[__Field, Option[List[Directive]]](
33+
"directives",
34+
_.directives.map(_.filter(_.name == "semanticNonNull")).filter(_.nonEmpty),
35+
isNone
36+
)
37+
)
38+
)
39+
}
40+
)
41+
42+
case class EffectfulFieldObject(q: Task[Int], @GQLNonNullable qAnnotated: Task[Int])
43+
case class OptionalEffectfulFieldObject(q: Task[Option[String]], @GQLNonNullable qAnnotated: Task[Option[String]])
44+
45+
implicit val effectfulFieldObjectSchema: Schema[Any, EffectfulFieldObject] =
46+
SemanticNonNullSchema.gen[Any, EffectfulFieldObject]
47+
implicit val optionalEffectfulFieldObjectSchema: Schema[Any, OptionalEffectfulFieldObject] =
48+
SemanticNonNullSchema.gen[Any, OptionalEffectfulFieldObject]
49+
}

0 commit comments

Comments
 (0)