Add SemanticNonNull support#2180
Conversation
ee81b99 to
faa4a2c
Compare
|
I haven't looked into this PR in depth, but is this similar to this annotation? |
|
Nope, it's about having 3 types of nullability instead of two - NonNull, SemanticNonNull, and Nullable. The directive you linked only (and correctly, even with the PR) overrides the nullability to be NonNull, while this PR separates out a new state, "it's NonNull unless it errors". |
Hmm, good question. We have a Maybe adding a method in |
695dac6 to
09d4d12
Compare
ghostdogpr
left a comment
There was a problem hiding this comment.
Looks okay to me.
@kyri-petrou can you check too?
|
@XiNiHa thank you very much for your contribution! I'm trying to understand a bit the spec around
The spec linked here that comes from Apollo seems to use a field directive to add the
Now this is the part that really confuses me. This RFC proposes an extension to the type system, for SemanticallyNonNull to be represented via syntax (e.g., @XiNiHa could you please clarify this for me, which spec is it that this PR is implementing? |
What I meant for the implementation was to add the
Fully implementing the spec (with the syntax addition) requires much more work for both Caliban and the ecosystem, and AFAIK none of the ecosystem members have implemented the syntax part. Instead, ecosystem members like Relay, Grats, and Apollo Kotlin, are experimenting with only the semantics, by using the directives instead of a separate syntax. Maybe this document from Grats would be more helpful in understanding the surroundings. |
I'm a little bit worried about tying the implementation of a field-specific directive to the Schema / type system, as I fear it'll probably going to bite us down the line in some way. There is already some work underway to be able to do Schema transformations, and I feel this might add another dimension to the things we'll need to cater for. I need to think this through a bit more, but instead of adding In derivation, we can then decide whether we'll add the |
|
Sounds great. I'll adjust the implementation to reflect what you've proposed! |
2d04c44 to
a0f82da
Compare
|
@kyri-petrou Done! |
kyri-petrou
left a comment
There was a problem hiding this comment.
Reviewed from my phone, but I want to test it a bit more on some services at $WORK later just to make sure we're not introducing any derivation regressions
| ): Schema[R1, ZStream[R1, Nothing, A]] = | ||
| new Schema[R1, ZStream[R1, Nothing, A]] { | ||
| override def optional: Boolean = false | ||
| override def canFail: Boolean = ev.canFail |
There was a problem hiding this comment.
Shouldn't this be false?
There was a problem hiding this comment.
IMO if A is a non-effectful faillible type, these should also be faillible. However since we have none of them, it'll always be false anyways. But semantically I see this more correct.
| ): 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 |
There was a problem hiding this comment.
Similarly, shouldn't this be false?
| 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 |
There was a problem hiding this comment.
Yeah, shouldn't this one be false?
a84bbfe to
85e67f1
Compare
There was a problem hiding this comment.
Looks great overall, although I just tested this PR locally with a service at $WORK (Scala 3), and it seems that there is a bug where top-level query and mutation fields that can fail are derived as non-null. For some reason, this doesn't seem to affect any other fields - just top-level ones
Can you please try and reproduce the issue in a test suite and fix the source of it?
Schema rendered with v2.6.0 vs this PR:
| /** | ||
| * Directive used to mark a field as semantically non-nullable. | ||
| */ | ||
| val SemanticNonNull = Directive("semanticNonNull") |
There was a problem hiding this comment.
Perhaps this is better placed in caliban.parsing.adt.Directive as we have other directives defined in there
There was a problem hiding this comment.
Since those are all name strings instead of actual directive instances, I'm worried that adding this causes some confusion 🤔
| * | ||
| * Override this method and return `true` to enable the feature. | ||
| */ | ||
| def enableSemanticNonNull: Boolean = false |
There was a problem hiding this comment.
May I recommend we use a DerivationConfig case class instead? I can see us wanting to add more derivation customization options in the future
|
By the way before sending you off down some rabbithole, this might be just a rendering issue. So I'd check that first! |
d8d3da7 to
7d8e546
Compare
I was not able to reproduce this ;( |
|
Ok I managed to track down the source of the issue. It seems that I had a custom schema defined in the application like this: given [A](using ev: Schema[R, A]): Schema[R, FieldOps => A] with {
override def arguments: List[__InputValue] = ev.arguments
override def optional: Boolean = ev.optional
def toType(isInput: Boolean, isSubscription: Boolean): __Type = ev.toType_(isInput, isSubscription)
def resolve(value: FieldOps => A): Step[R] = MetadataFunctionStep { f =>
val fops = new FieldOps(f)
ev.resolve(value(fops))
}
}Since now the underlying While this is not a common use-case, we can't assume that other users aren't doing the same thing. @ghostdogpr open to suggestions on naming, but I suggest we do something along these lines (although I'm kind of inclined towards just making it final and let it break source compatibility) @deprecatedOverriding("this method will be made final. Override canFail and nullable instead", "2.6.1")
def optional: Boolean = canFail || nullable
def canFail: Boolean = false
def nullable: Boolean = false // Happy for other name suggestionsThen the majority of the code should remain the same and use |
|
@kyri-petrou applied your suggestion! (I named it |
| val hasNullableAnn = p.annotations.contains(GQLNullable()) | ||
| val hasNonNullAnn = p.annotations.contains(GQLNonNullable()) | ||
| !hasNonNullAnn && (hasNullableAnn || p.typeclass.optional) | ||
| (!hasNonNullAnn && (hasNullableAnn || p.typeclass.nullable), hasNullableAnn || hasNonNullAnn) |
There was a problem hiding this comment.
I think this should be optional?
| (!hasNonNullAnn && (hasNullableAnn || p.typeclass.nullable), hasNullableAnn || hasNonNullAnn) | |
| (!hasNonNullAnn && (hasNullableAnn || p.typeclass.optional), hasNullableAnn || hasNonNullAnn) |
There was a problem hiding this comment.
I don't think the code was wrong, but I agree that was somewhat confusing to read. I've updated the logic to represent it better what I've meant.
| val hasNullableAnn = fieldAnnotations.contains(GQLNullable()) | ||
| val hasNonNullAnn = fieldAnnotations.contains(GQLNonNullable()) | ||
| !hasNonNullAnn && (hasNullableAnn || schema.optional) | ||
| (!hasNonNullAnn && (hasNullableAnn || schema.nullable), hasNullableAnn || hasNonNullAnn) |
|
I disabled Mima on the main branch so if you rebase that will solve that issue. |
kyri-petrou
left a comment
There was a problem hiding this comment.
@XiNiHa could you please add a test for the use-case that I previously posted? I think the bug is still there at the moment
kyri-petrou
left a comment
There was a problem hiding this comment.
Thanks for putting up with all my requests and delays in reviews! Happy to have this merged once CI is passing
|
Looks like CI is finally passing 🎉 |
|
Thanks for adding this! Would you be able to submit a PR for adding a few docs about it? |
Adds
SemanticNonNullsupport for every effectful types.Currently, the support is implemented to add
@semanticNonNulldirectives to fields that are semantically non-null. However, as the spec evolves and merges, it's very likely to have a separate syntax to annotate the semantic nullability of a field.The implementation is very clear for Caliban since we know all the possibilities of errors and nulls.
@semanticNonNullis only applied when 1. the resolver is faillible 2. the field is optional 3. the result type is not optional.Although the spec also supports more detailed configuration of semantic nullability for list fields, it was not applicable to Caliban's case since as it currently doesn't resolve error inside stream to
null. (actually it feels a bit odd considering the decision of having effects withE <: Throwableas nullable)While I think that the feature should be blocked by default with a feature flag, I have no idea how that would be implemented. Any ideas?