Skip to content

Commit

Permalink
[Scala 3] Fix derivation of case objects / parameterless case classes…
Browse files Browse the repository at this point in the history
… that contain `@GQLField` methods (#2305)

* Fix derivation of Scala 3 case objects with `@GQLField` methods

* Cleanup

* Put tests under the correct suite
  • Loading branch information
kyri-petrou authored Jun 25, 2024
1 parent cf93e97 commit 544cdf8
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 25 deletions.
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
82 changes: 60 additions & 22 deletions core/src/test/scala-3/caliban/schema/Scala3DerivesSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ object Scala3DerivesSpec extends ZIOSpecDefault {
)
}
),
suite("methods as fields") {
suite("@GQLField annotation") {
val expectedSchema =
"""schema {
| query: Bar
Expand Down Expand Up @@ -244,6 +244,44 @@ object Scala3DerivesSpec extends ZIOSpecDefault {
assertTrue(s == """{"foo":{"value":"foo","value2":"foo2"}}""")
}
}
},
test("case object") {
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") {
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 Expand Up @@ -292,27 +330,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 Down

0 comments on commit 544cdf8

Please sign in to comment.