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

Codegen: Support union types as scala 3 union types #1926

Open
oyvindberg opened this issue Oct 6, 2023 · 3 comments
Open

Codegen: Support union types as scala 3 union types #1926

oyvindberg opened this issue Oct 6, 2023 · 3 comments
Labels
enhancement New feature or request server Issue related to caliban server tools Issue related to Caliban tools like code generation or schema comparison

Comments

@oyvindberg
Copy link
Contributor

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)

@ghostdogpr ghostdogpr added enhancement New feature or request tools Issue related to Caliban tools like code generation or schema comparison server Issue related to caliban server labels Oct 6, 2023
@kyri-petrou
Copy link
Collaborator

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 :)

@oyvindberg
Copy link
Contributor Author

oyvindberg commented Oct 6, 2023

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
  }

@oyvindberg
Copy link
Contributor Author

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

oyvindberg added a commit to oyvindberg/caliban that referenced this issue May 7, 2024
oyvindberg added a commit to oyvindberg/caliban that referenced this issue May 7, 2024
oyvindberg added a commit to oyvindberg/caliban that referenced this issue May 7, 2024
oyvindberg added a commit to oyvindberg/caliban that referenced this issue May 7, 2024
oyvindberg added a commit to oyvindberg/caliban that referenced this issue May 14, 2024
oyvindberg added a commit to oyvindberg/caliban that referenced this issue May 14, 2024
oyvindberg added a commit to oyvindberg/caliban that referenced this issue May 14, 2024
oyvindberg added a commit to oyvindberg/caliban that referenced this issue May 14, 2024
oyvindberg added a commit to oyvindberg/caliban that referenced this issue May 15, 2024
oyvindberg added a commit to oyvindberg/caliban that referenced this issue May 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request server Issue related to caliban server tools Issue related to Caliban tools like code generation or schema comparison
Projects
None yet
Development

No branches or pull requests

3 participants