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 support for @oneOf
inputs
#1846
base: series/2.x
Are you sure you want to change the base?
Conversation
builders.view | ||
.map(_.build(input)) | ||
.find(_.isRight) | ||
.getOrElse(Left(ExecutionError(s"Invalid oneOf input $fields for trait ${ctx.typeName.short}}"))) |
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.
Open to suggestions on how we can make this more efficient. I can't figure out a way to extract the field argument here unfortunately :(
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.
Isn't there a possibility that some types can successfully be parsed as another type of the oneof?
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.
We get the subclass name though right? I think we could recapitalize the type name Dog => dog
as the default strategy, and allow people to override that explicitly with an annotation
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.
Isn't there a possibility that some types can successfully be parsed as another type of the oneof?
@ghostdogpr No because we ensure that the argument names of the leaf cases are unique
I think we could recapitalize the type name Dog => dog as the default strategy, and allow people to override that explicitly with an annotation
@paulpdaniels I think this could work, but just to be on the same page. Let's say we want to implement this:
input BarInput {
a: String!
b: Int!
}
input FooInput @oneOf {
stringArg: String
barArg: BarInput
}
Currently, we can do it as:
case class Bar(a: String, b: Int)
sealed trait Foo
object Foo:
case class ArgA(stringArg: String) extends Foo
case class ArgB(barArg: Bar) extends Foo
With your recommendation @paulpdaniels, we need to do the following:
sealed trait Foo
object Foo:
@GQLValueType
@GQLOneOfFieldName("stringArg") // Otherwise it'll be named `argA`
case class ArgA(stringArg: String) extends Foo
@GQLOneOfFieldName("barArg")
case class Bar(a: String, b: Int) extends Foo
// Alternatively:
@GQLInputName("BarInput")
case class BarArg(a: String, b: Int) extends Foo
If this is what you meant, I like it. It reuses the existing annotations, and prevents having to create an extra case class as in the case of case class ArgB(barArg: Bar) extends Foo
in my first example
EDIT: At first I used the GQLName
annotation, but on second thought, we might want to introduce another annotation as I think if we use @GQLName
to rename the field it'll cause confusion since we normally use it to rename the type. Open to suggestion for a better annotation name :)
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.
GQLName
is already used for renaming fields though
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.
Annoyingly, this would be the easiest way to define a schema / arg builder for a @oneOf
input, but then it's really annoying to extract the argument that was passed:
@GQLOneOfInput
case class Foo(
stringArg: Option[String],
barArg: Option[Bar]
)
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.
Couldn't we use __Type#origin
to find during validation that 2 fields come from the same object and fail in that case?
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.
Unfortunately no. For Scalars, we don't set a value for the origin field. I actually tried setting it manually only for oneOf input arguments and it didn't work either, it caused issues with the validateClashingTypes
validation.
The only way I could figure out how to propagate the type info was to add a field on __InputValue
and exclude that field from any schema generation: https://github.com/ghostdogpr/caliban/pull/1846/files#diff-7fd5a1e4c9e85d511a507e4a9d0ddb63b131a5be6decb8feb3f95d0c00e5f31cR16
It's a bit of a hack unfortunately, but it works. Other than binary compatibility (which we're breaking with this PR anyways), do we see any other issues with doing this?
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.
Seems okay to me, I don't mind that hack so much if things stay simple on the user side.
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 also rewrote the ArgBuilder
s for OneOf inputs to use fallback orElse
. Under the hood it's still the same as iterating through them I suppose but at least this feels more Caliban-y 😅
core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala
Outdated
Show resolved
Hide resolved
# Conflicts: # core/src/test/scala/caliban/RenderingSpec.scala
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 very nice!
core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala
Outdated
Show resolved
Hide resolved
builders.view | ||
.map(_.build(input)) | ||
.find(_.isRight) | ||
.getOrElse(Left(ExecutionError(s"Invalid oneOf input $fields for trait ${ctx.typeName.short}}"))) |
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.
Isn't there a possibility that some types can successfully be parsed as another type of the oneof?
# Conflicts: # core/src/main/scala-2/caliban/schema/ArgBuilderDerivation.scala # core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala # core/src/main/scala/caliban/rendering/DocumentRenderer.scala # core/src/main/scala/caliban/validation/Validator.scala # core/src/test/scala/caliban/RenderingSpec.scala
# Conflicts: # build.sbt # core/src/main/scala-2/caliban/schema/SchemaDerivation.scala # core/src/main/scala-3/caliban/schema/SchemaDerivation.scala # core/src/main/scala-3/caliban/schema/macros/Macros.scala # core/src/main/scala/caliban/introspection/Introspector.scala # core/src/main/scala/caliban/introspection/adt/__InputValue.scala # core/src/main/scala/caliban/introspection/adt/__Type.scala # core/src/main/scala/caliban/schema/Schema.scala # core/src/main/scala/caliban/validation/Validator.scala # core/src/test/scala/caliban/introspection/IntrospectionSpec.scala
VERY early draft for adding support for the
@oneOf
inputs, just to discuss whether we're on the right track. See current version of the RFC hereSome of the assumptions I made:
oneOf
. I think that makes sense since the spec on@oneOf
is very strict, so it's very unlikely that these sealed traits will be used for any return types@oneOf
via macros. I think it's nicer UX if we give compile-time errors instead of runtime ones for invalid types. However, in Scala 2 we don't control the derivation so we have to enforce these checks during runtime. I understand if we want to unify the validation, so I'm happy to remove the macros@oneOf
if they have only 1 field (kind of defeats the purpose?)TODO:
@oneOf
inputs inValidator
(both against the Schema & user inputs)@oneOf
inputs are a mix of input objects and Scalars