-
-
Notifications
You must be signed in to change notification settings - Fork 248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
@@ -546,31 +567,34 @@ 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be false
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -522,22 +540,25 @@ 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, shouldn't this be false
?
@@ -496,22 +511,25 @@ 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
false
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, shouldn't this one be false?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, saw your comment below
a84bbfe
to
85e67f1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May I recommend we use a DerivationConfig
case class instead? I can see us wanting to add more derivation customization options in the future
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 suggestions Then 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above
I disabled Mima on the main branch so if you rebase that will solve that issue. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
SemanticNonNull
support for every effectful types.Currently, the support is implemented to add
@semanticNonNull
directives 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.
@semanticNonNull
is 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 <: Throwable
as 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?