Skip to content
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

[Scala 3] Fix derivation of case objects / parameterless case classes that contain @GQLField methods #2305

Merged
merged 3 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions core/src/main/scala-3/caliban/schema/SchemaDerivation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,20 @@ trait CommonSchemaDerivation {

case m: Mirror.ProductOf[A] =>
inline erasedValue[m.MirroredElemLabels] match {
case _: EmptyTuple =>
case _: EmptyTuple if !Macros.hasFieldsFromMethods[A] =>
new EnumValueSchema[R, A](
MagnoliaMacro.typeInfo[A],
// Workaround until we figure out why the macro uses the parent's annotations when the leaf is a Scala 3 enum
inline if (!MagnoliaMacro.isEnum[A]) MagnoliaMacro.anns[A] else Nil,
config.enableSemanticNonNull
)
case _ if Macros.hasAnnotation[A, GQLValueType] =>
case _ if Macros.hasAnnotation[A, GQLValueType] =>
new ValueTypeSchema[R, A](
valueTypeSchema[R, m.MirroredElemLabels, m.MirroredElemTypes],
MagnoliaMacro.typeInfo[A],
MagnoliaMacro.anns[A]
)
case _ =>
case _ =>
new ObjectSchema[R, A](
recurseProduct[R, A, m.MirroredElemLabels, m.MirroredElemTypes]()(),
Macros.fieldsFromMethods[R, A],
Expand Down
12 changes: 12 additions & 0 deletions core/src/main/scala-3/caliban/schema/macros/Macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ object Macros {
inline def implicitExists[T]: Boolean = ${ implicitExistsImpl[T] }
inline def hasAnnotation[T, Ann]: Boolean = ${ hasAnnotationImpl[T, Ann] }

transparent inline def hasFieldsFromMethods[T]: Boolean =
${ hasFieldsFromMethodsImpl[T] }

transparent inline def fieldsFromMethods[R, T]: List[(String, List[Any], Schema[R, ?])] =
${ fieldsFromMethodsImpl[R, T] }

Expand Down Expand Up @@ -48,6 +51,15 @@ object Macros {
Expr(TypeRepr.of[P].typeSymbol.flags.is(Flags.Enum) && TypeRepr.of[T].typeSymbol.flags.is(Flags.Enum))
}

private def hasFieldsFromMethodsImpl[T: Type](using q: Quotes): Expr[Boolean] = {
import q.reflect.*
val targetSym = TypeTree.of[T].symbol
val annType = TypeRepr.of[GQLField]
val annSym = annType.typeSymbol

Expr(targetSym.declaredMethods.exists(_.getAnnotation(annSym).isDefined))
}

private def fieldsFromMethodsImpl[R: Type, T: Type](using
q: Quotes
): Expr[List[(String, List[Any], Schema[R, ?])]] = {
Expand Down
80 changes: 59 additions & 21 deletions core/src/test/scala-3/caliban/schema/Scala3DerivesSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -292,27 +292,27 @@ object Scala3DerivesSpec extends ZIOSpecDefault {

val expectedSchema =
"""schema {
query: Query
}

"Union type Payload"
union Payload2 @mydirective(arg: "value") = Foo | Bar | Baz

type Bar {
foo: Int!
}

type Baz {
bar: Int!
}

type Foo {
value: String!
}

type Query {
testQuery(isFoo: Boolean!): Payload2!
}""".stripMargin
| query: Query
|}
|
|"Union type Payload"
|union Payload2 @mydirective(arg: "value") = Foo | Bar | Baz
|
|type Bar {
| foo: Int!
|}
|
|type Baz {
| bar: Int!
|}
|
|type Foo {
| value: String!
|}
|
|type Query {
| testQuery(isFoo: Boolean!): Payload2!
|}""".stripMargin
val interpreter = gql.interpreterUnsafe

for {
Expand All @@ -325,6 +325,44 @@ type Query {
data2 == """{"testQuery":{"foo":1}}""",
gql.render == expectedSchema
)
},
test("case object with @GQLField methods") {
case object Foo derives Schema.SemiAuto {
@GQLField def fooValue: Option[String] = None
@GQLField def barValue: Int = 42
}
val rendered = graphQL(RootResolver(Foo)).render

val expected =
"""schema {
| query: Foo
|}
|
|type Foo {
| fooValue: String
| barValue: Int!
|}""".stripMargin

assertTrue(rendered == expected)
},
test("parameterless case class with @GQLField methods") {
case class Foo() derives Schema.SemiAuto {
@GQLField def fooValue: Option[String] = None
@GQLField def barValue: Int = 42
}
val rendered = graphQL(RootResolver(Foo())).render

val expected =
"""schema {
| query: Foo
|}
|
|type Foo {
| fooValue: String
| barValue: Int!
|}""".stripMargin

assertTrue(rendered == expected)
}
)
}
Expand Down