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

Fix #1352 using magnolia macros #1952

Merged
merged 3 commits into from
Oct 22, 2023
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
46 changes: 45 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ jobs:
- "~/.ivy2/cache"
- "~/.sbt"
- "~/.m2"
test213_jdk17:
kyri-petrou marked this conversation as resolved.
Show resolved Hide resolved
docker:
- image: cimg/openjdk:17.0-node
resource_class: large
steps:
- checkout
- restore_cache:
key: sbtcache
- run: sbt ++2.13.12 core/test http4s/test akkaHttp/test pekkoHttp/test play/test zioHttp/test examples/compile catsInterop/compile benchmarks/compile monixInterop/compile tapirInterop/test clientJVM/test tools/test federation/test reporting/test tracing/test
- save_cache:
key: sbtcache
paths:
- "~/.ivy2/cache"
- "~/.sbt"
- "~/.m2"
test3_jdk:
docker:
- image: cimg/openjdk:11.0-node
Expand All @@ -91,6 +106,21 @@ jobs:
- "~/.ivy2/cache"
- "~/.sbt"
- "~/.m2"
test3_jdk17:
docker:
- image: cimg/openjdk:17.0-node
resource_class: large
steps:
- checkout
- restore_cache:
key: sbtcache
- run: sbt ++3.3.1 core/test catsInterop/compile benchmarks/compile monixInterop/compile clientJVM/test clientJS/compile zioHttp/test tapirInterop/test pekkoHttp/test http4s/test federation/test tools/test reporting/test tracing/test
- save_cache:
key: sbtcache
paths:
- "~/.ivy2/cache"
- "~/.sbt"
- "~/.m2"
test213_js_native:
docker:
- image: cimg/openjdk:11.0-node
Expand Down Expand Up @@ -149,7 +179,7 @@ jobs:
- restore_cache:
key: sbtcache
- run: sbt ++2.13.12 http4s/mimaReportBinaryIssues akkaHttp/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues play/mimaReportBinaryIssues zioHttp/mimaReportBinaryIssues catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues tools/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues
- run: sbt ++3.3.0 catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues zioHttp/mimaReportBinaryIssues http4s/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues tools/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues
- run: sbt ++3.3.1 catsInterop/mimaReportBinaryIssues monixInterop/mimaReportBinaryIssues clientJVM/mimaReportBinaryIssues clientJS/mimaReportBinaryIssues clientLaminextJS/mimaReportBinaryIssues zioHttp/mimaReportBinaryIssues http4s/mimaReportBinaryIssues federation/mimaReportBinaryIssues reporting/mimaReportBinaryIssues tracing/mimaReportBinaryIssues tools/mimaReportBinaryIssues core/mimaReportBinaryIssues tapirInterop/mimaReportBinaryIssues pekkoHttp/mimaReportBinaryIssues
- save_cache:
key: sbtcache
paths:
Expand Down Expand Up @@ -239,6 +269,12 @@ workflows:
filters:
tags:
only: /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
- test213_jdk17:
requires:
- lint
filters:
tags:
only: /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
- test213_docs:
requires:
- lint
Expand All @@ -251,6 +287,12 @@ workflows:
filters:
tags:
only: /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
- test3_jdk17:
requires:
- lint
filters:
tags:
only: /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
- test_mima:
requires:
- lint
Expand All @@ -264,9 +306,11 @@ workflows:
- scripted212_jdk
- scripted3_jdk
- test213_jdk
- test213_jdk17
- test213_js_native
- test213_docs
- test3_jdk
- test3_jdk17
- test3_js_native
- test_mima
filters:
Expand Down
38 changes: 26 additions & 12 deletions core/src/main/scala-3/caliban/schema/ArgBuilderDerivation.scala
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
package caliban.schema

import caliban.CalibanError.ExecutionError
import caliban.{ CalibanError, InputValue }
import caliban.Value.*
import caliban.schema.Annotations.GQLDefault
import caliban.schema.Annotations.GQLName
import caliban.schema.Annotations.{ GQLDefault, GQLName }
import caliban.schema.macros.Macros
import caliban.{ CalibanError, InputValue }
import magnolia1.Macro as MagnoliaMacro

import scala.deriving.Mirror
import scala.compiletime.*
import scala.deriving.Mirror
import scala.util.NotGiven

trait CommonArgBuilderDerivation {
inline def recurse[P, Label, A <: Tuple](
inline def recurseSum[P, Label, A <: Tuple](
inline values: List[(String, List[Any], ArgBuilder[Any])] = Nil
): List[(String, List[Any], ArgBuilder[Any])] =
inline erasedValue[(Label, A)] match {
case (_: EmptyTuple, _) => values.reverse
case (_: (name *: names), _: (t *: ts)) =>
recurse[P, names, ts](
recurseSum[P, names, ts](
(
constValue[name].toString,
Macros.annotations[t], {
MagnoliaMacro.anns[t], {
if (Macros.isEnumField[P, t])
if (!Macros.implicitExists[ArgBuilder[t]]) derived[t]
else summonInline[ArgBuilder[t]]
Expand All @@ -31,18 +31,32 @@ trait CommonArgBuilderDerivation {
)
}

inline def recurseProduct[P, Label, A <: Tuple](
inline values: List[(String, ArgBuilder[Any])] = Nil
): List[(String, ArgBuilder[Any])] =
inline erasedValue[(Label, A)] match {
case (_: EmptyTuple, _) => values.reverse
case (_: (name *: names), _: (t *: ts)) =>
recurseProduct[P, names, ts](
(
constValue[name].toString,
summonInline[ArgBuilder[t]].asInstanceOf[ArgBuilder[Any]]
) :: values
)
}

inline def derived[A]: ArgBuilder[A] =
inline summonInline[Mirror.Of[A]] match {
case m: Mirror.SumOf[A] =>
makeSumArgBuilder[A](
recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](),
recurseSum[A, m.MirroredElemLabels, m.MirroredElemTypes](),
constValue[m.MirroredLabel]
)

case m: Mirror.ProductOf[A] =>
makeProductArgBuilder(
recurse[A, m.MirroredElemLabels, m.MirroredElemTypes](),
Macros.paramAnnotations[A].to(Map)
recurseProduct[A, m.MirroredElemLabels, m.MirroredElemTypes](),
MagnoliaMacro.paramAnns[A].toMap
)(m.fromProduct)
}

Expand Down Expand Up @@ -74,14 +88,14 @@ trait CommonArgBuilderDerivation {
}

private def makeProductArgBuilder[A](
_fields: => List[(String, List[Any], ArgBuilder[Any])],
_fields: => List[(String, ArgBuilder[Any])],
_annotations: => Map[String, List[Any]]
)(fromProduct: Product => A) = new ArgBuilder[A] {
private lazy val fields = _fields
private lazy val annotations = _annotations

def build(input: InputValue): Either[ExecutionError, A] =
fields.view.map { (label, _, builder) =>
fields.view.map { (label, builder) =>
input match {
case InputValue.ObjectValue(fields) =>
val labelList = annotations.get(label)
Expand Down
54 changes: 34 additions & 20 deletions core/src/main/scala-3/caliban/schema/SchemaDerivation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,37 @@ trait CommonSchemaDerivation {
case _ => s"${name}Input"
}

inline def recurse[R, P, Label, A <: Tuple](
inline values: List[(String, List[Any], Schema[R, Any], Int)] = Nil
)(inline index: Int = 0): List[(String, List[Any], Schema[R, Any], Int)] =
inline def recurseSum[R, P, Label, A <: Tuple](
inline values: List[(String, List[Any], Schema[R, Any])] = Nil
): List[(String, List[Any], Schema[R, Any])] =
inline erasedValue[(Label, A)] match {
case (_: EmptyTuple, _) => values.reverse
case (_: (name *: names), _: (t *: ts)) =>
recurse[R, P, names, ts] {
recurseSum[R, P, names, ts] {
(
constValue[name].toString,
MagnoliaMacro.anns[t], {
if (Macros.isEnumField[P, t])
if (!Macros.implicitExists[Schema[R, t]]) derived[R, t]
else summonInline[Schema[R, t]]
else summonInline[Schema[R, t]]
}.asInstanceOf[Schema[R, Any]]
) :: values
}
}

inline def recurseProduct[R, P, Label, A <: Tuple](
inline values: List[(String, Schema[R, Any], Int)] = Nil
)(inline index: Int = 0): List[(String, Schema[R, Any], Int)] =
inline erasedValue[(Label, A)] match {
case (_: EmptyTuple, _) => values.reverse
case (_: (name *: names), _: (t *: ts)) =>
recurseProduct[R, P, names, ts] {
inline if (Macros.isFieldExcluded[P, name]) values
else
(
constValue[name].toString,
Macros.annotations[t], {
if (Macros.isEnumField[P, t])
if (!Macros.implicitExists[Schema[R, t]]) derived[R, t]
else summonInline[Schema[R, t]]
else summonInline[Schema[R, t]]
}.asInstanceOf[Schema[R, Any]],
summonInline[Schema[R, t]].asInstanceOf[Schema[R, Any]],
index
) :: values
}(index + 1)
Expand All @@ -63,29 +77,29 @@ trait CommonSchemaDerivation {
inline summonInline[Mirror.Of[A]] match {
case m: Mirror.SumOf[A] =>
makeSumSchema[R, A](
recurse[R, A, m.MirroredElemLabels, m.MirroredElemTypes]()(),
recurseSum[R, A, m.MirroredElemLabels, m.MirroredElemTypes](),
MagnoliaMacro.typeInfo[A],
Macros.annotations[A]
MagnoliaMacro.anns[A]
)(m.ordinal)

case m: Mirror.ProductOf[A] =>
makeProductSchema[R, A](
recurse[R, A, m.MirroredElemLabels, m.MirroredElemTypes]()(),
recurseProduct[R, A, m.MirroredElemLabels, m.MirroredElemTypes]()(),
MagnoliaMacro.typeInfo[A],
Macros.annotations[A],
Macros.paramAnnotations[A].toMap
MagnoliaMacro.anns[A],
MagnoliaMacro.paramAnns[A].toMap
)
}

private def makeSumSchema[R, A](
_members: => List[(String, List[Any], Schema[R, Any], Int)],
_members: => List[(String, List[Any], Schema[R, Any])],
info: TypeInfo,
annotations: List[Any]
)(ordinal: A => Int): Schema[R, A] = new Schema[R, A] {

private lazy val members = _members.toVector // Vector's .apply is O(1) vs List's O(N)

private lazy val subTypes = members.map { case (label, subTypeAnnotations, schema, _) =>
private lazy val subTypes = members.map { case (label, subTypeAnnotations, schema) =>
(label, schema.toType_(), subTypeAnnotations)
}.sortBy { case (label, _, _) => label }.toList

Expand Down Expand Up @@ -119,19 +133,19 @@ trait CommonSchemaDerivation {
}

def resolve(value: A): Step[R] = {
val (label, _, schema, _) = members(ordinal(value))
val (label, _, schema) = members(ordinal(value))
if (isEnum) PureStep(EnumValue(label)) else schema.resolve(value)
}
}

private def makeProductSchema[R, A](
_fields: => List[(String, List[Any], Schema[R, Any], Int)],
_fields: => List[(String, Schema[R, Any], Int)],
info: TypeInfo,
annotations: List[Any],
paramAnnotations: Map[String, List[Any]]
): Schema[R, A] = new Schema[R, A] {

private lazy val fields = _fields.map { case (label, _, schema, index) =>
private lazy val fields = _fields.map { case (label, schema, index) =>
val fieldAnnotations = paramAnnotations.getOrElse(label, Nil)
(getName(fieldAnnotations, label), fieldAnnotations, schema, index)
}
Expand Down
34 changes: 5 additions & 29 deletions core/src/main/scala-3/caliban/schema/macros/Macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,18 @@ import scala.quoted.*
export magnolia1.TypeInfo

object Macros {
inline def annotations[T]: List[Any] = ${ annotationsImpl[T] }
inline def paramAnnotations[T]: List[(String, List[Any])] = ${ paramAnnotationsImpl[T] }
inline def isFieldExcluded[P, T]: Boolean = ${ isFieldExcludedImpl[P, T] }
inline def isEnumField[P, T]: Boolean = ${ isEnumFieldImpl[P, T] }
inline def implicitExists[T]: Boolean = ${ implicitExistsImpl[T] }

private def annotationsImpl[T: Type](using qctx: Quotes): Expr[List[Any]] = {
import qctx.reflect.*
val tpe = TypeRepr.of[T]
Expr.ofList {
tpe.typeSymbol.annotations.filter { a =>
a.tpe.typeSymbol.maybeOwner.isNoSymbol || (a.tpe.typeSymbol.owner.fullName != "scala.annotation.internal" && a.tpe.typeSymbol.owner.fullName != "jdk.internal")
}.map(_.asExpr.asInstanceOf[Expr[Any]])
}
}

private def paramAnnotationsImpl[T: Type](using qctx: Quotes): Expr[List[(String, List[Any])]] = {
import qctx.reflect.*
val tpe = TypeRepr.of[T]
Expr.ofList {
tpe.typeSymbol.primaryConstructor.paramSymss.flatten.map { field =>
Expr(field.name) -> field.annotations.filter { a =>
a.tpe.typeSymbol.maybeOwner.isNoSymbol ||
(a.tpe.typeSymbol.owner.fullName != "scala.annotation.internal" && a.tpe.typeSymbol.owner.fullName != "jdk.internal")
}.map(_.asExpr.asInstanceOf[Expr[Any]])
}.filter(_._2.nonEmpty).map((name, anns) => Expr.ofTuple(name, Expr.ofList(anns)))
}
}
inline def isFieldExcluded[P, T]: Boolean = ${ isFieldExcludedImpl[P, T] }
inline def isEnumField[P, T]: Boolean = ${ isEnumFieldImpl[P, T] }
inline def implicitExists[T]: Boolean = ${ implicitExistsImpl[T] }

/**
* Tests whether type argument [[FieldT]] in [[Parent]] is annotated with [[GQLExcluded]]
*/
private def isFieldExcludedImpl[Parent: Type, FieldT: Type](using qctx: Quotes): Expr[Boolean] = {
import qctx.reflect.*
val fieldName = Type.valueOfConstant[FieldT]
Expr(TypeRepr.of[Parent].typeSymbol.primaryConstructor.paramSymss.flatten.exists { v =>
Type.valueOfConstant[FieldT].map(_ == v.name).getOrElse(false)
fieldName.map(_ == v.name).getOrElse(false)
ghostdogpr marked this conversation as resolved.
Show resolved Hide resolved
&& v.annotations.exists(_.tpe =:= TypeRepr.of[GQLExcluded])
})
}
Expand Down
Loading