ghostdogpr / caliban

Functional GraphQL library for Scala

Home Page:https://ghostdogpr.github.io/caliban/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Codegen: Support union types as scala 3 union types

oyvindberg opened this issue · comments

With namespacing gone after #1925, we're quite close to being able to output union types as union types.

The remaining issue is basically this:

The problem is that Scala 3 doesn't generate Mirror for union types, so we can't use typeclass derivation for it: scala/scala3#15279

That's true. But, and I'm on very thin ice here, I think we can write a macro which pattern matches on the union type, picks out all the members and summonAlls Schemas for them.

Then we need to generate some code like this:

  type SearchResult = Human | Droid | Starship

  given Schema[Any, SearchResult] = Schema.typeUnion[SearchResult]

  // which would expand to something like this
  given Schema[Any, SearchResult] with {
    val _1: Schema[Any, Human] = summon[Schema[Any, Human]]
    val _2: Schema[Any, Droid] = summon[Schema[Any, Droid]]
    val _3: Schema[Any, Starship] = summon[Schema[Any, Starship]]
    val subTypes = List(_1, _2, _3)

    def resolve(value: SearchResult): caliban.schema.Step[Any] =
      value match {
        case x: Human => _1.resolve(x)
        case x: Droid => _2.resolve(x)
        case x: Starship => _3.resolve(x)
      }

    def toType(isInput: Boolean, isSubscription: Boolean): caliban.introspection.adt.__Type =
      caliban.schema.Types.makeUnion(Some("SearchResult"), None, subTypes.map(_.toType_(isInput, isSubscription)))
  }

Originally posted by @oyvindberg in #1925 (comment)

I gave this a go some time ago and failed miserably, albeit my knowledge of macros goes as far as I can google things. Also things might have changed in the compiler since then. If you can make this work it'd be awesome :)

I think I can make it work. Progress so far:

Calling the macro like this:

  type A = Int | String | Types.QueryHeroArgs
  Foo.typeAliasSchema[A]

gives this data:

{
  starwars.generated.Foo.TypeAndSchema.apply[scala.Int]("scala.Int", caliban.schema.Schema.intSchema)
  starwars.generated.Foo.TypeAndSchema.apply[scala.Predef.String]("java.lang.String", caliban.schema.Schema.stringSchema)
  starwars.generated.Foo.TypeAndSchema.apply[starwars.generated.Types.QueryHeroArgs]("starwars.generated.Types.QueryHeroArgs", starwars.generated.Types.QueryHeroArgs.derived$SemiAuto)
}

It's obviously not in the correct shape yet, but I managed to pick apart the type union and resolve Schema for each type.

Code so far:

  case class TypeAndSchema[T](typeRef: String, schema: Schema[Any, T])

  inline def typeAliasSchema[T]: TypeAndSchema[?] = ${ mirrorFieldsImpl[T] }

  def mirrorFieldsImpl[T: Type](using Quotes): Expr[TypeAndSchema[?]] = {
    import quotes.reflect.* // Import `Tree`, `TypeRepr`, `Symbol`, `Position`, .....

    def rec[TT](using tpe: Type[TT]): List[Expr[TypeAndSchema[?]]] = TypeRepr.of(using tpe).dealias match {
      case OrType(l, r) =>
        quotes.reflect.report.warning(s"union ${l.show} | ${r.show}")
        rec(using l.asType.asInstanceOf[Type[Any]]) ++ rec(using r.asType.asInstanceOf[Type[Any]])
      case otherRepr =>
        val otherString = otherRepr.show
        val expr: Expr[TypeAndSchema[TT]] =
          Expr.summon[Schema[Any, TT]] match {
            case Some(found) =>
              '{ TypeAndSchema[TT](${ Expr(otherString) }, ${found}) }
            case None =>
              quotes.reflect.report.errorAndAbort(s"Couldn't resolve Schema[Any, $otherString]")
          }

        List(expr)
    }

    val exprs = rec[T]
    val expr = Expr.block(exprs.init, exprs.last)
    quotes.reflect.report.errorAndAbort(expr.show)
    expr
  }

this library likely contains what we need https://github.com/iRevive/union-derivation