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

Fragment code generation is broken for union types #76

Open
muuki88 opened this issue Aug 26, 2019 · 3 comments
Open

Fragment code generation is broken for union types #76

muuki88 opened this issue Aug 26, 2019 · 3 comments
Labels

Comments

@muuki88
Copy link
Owner

muuki88 commented Aug 26, 2019

The code generation for fragments produced invalid / not compilable scala code.

Example schema and query

A simple schema with a union type

union Animal = Cat | Dog
type Cat {
  catName: String!
}
type Dog {
  dogName: String!
}

Query {
  animals: [Animal!]!
}

The animal fragments

fragment AnimalName on Animal {
  ...DogName
  ...CatName
}

fragment DogName on Dog {
  name
}

fragment CatName on Cat {
  name
}

And use the fragment

# import fragements/animalName.fragment.graphql
query AllAnimals {
  animals {
    ...AnimalName
  }
}

Expected code

The fragment and query should generate the following types in the Interfaces.scala

trait AnimalName
trait DogName extends AnimalName {
   def name: String
}
trait CatName extends AnimalName {
  def name: String
}

And the query object code should use these types

object GetAnimals {
   object GetAnimals extends GraphQLQuery {
      val document: sangria.ast.Document = graphql"""..."""
      case class Variables()
      case  class Data(animals: List[AnimalName])
      case class DogName(name: String) extends DogName
      case class CatName(name: String) extends CatName
   }
}

Complexities

  • We need to make sure that we reference the correct trait in the generated query code. In the Example DogName is defined in the Interfaces.scala and in the GetAnimals$GetAnimals object
  • The circe code generation needs to put the decoder / encoder above the data encoder so it can find it during derivation
@muuki88 muuki88 added the bug label Aug 26, 2019
muuki88 added a commit that referenced this issue Nov 5, 2019
@ellenlight
Copy link

Hello @muuki88! Thank you for writing this excellent plugin! I am using it to generate the client code for a project and I am running into some problems related to this issue.

Reproducing the issue

This issue occurs with the Apollo codegen style.
The schema is

union Animal = Cat | Dog
type Cat {
  catName: String!
}
type Dog {
  dogName: String!
  favoriteToys: [Toy!]!
}

type Toy {
	toyName: String!
	boughtAt: Store!
}

type Store {
	storeName: String!
}

type Query {
  animals: [Animal!]!
}

The query is

query AllAnimals {
  animals {
    ...AnimalName
  }
}

fragment AnimalName on Animal {
	__typename
   ...DogName
   ...CatName
 }

 fragment DogName on Dog {
   dogName
   favoriteToys {
   		...ToyName
   }
 }

 fragment ToyName on Toy {
 	toyName
 	boughtAt {
 		...StoreName
 	}
 }

 fragment StoreName on Store {
 	storeName
 }

 fragment CatName on Cat {
   catName
 }

The compile error is

[error] web/target/scala-2.13/src_managed/main/sbt-graphql/Pets.scala:48:86: type FavoriteToys is not a member of object graphql.codegen.Pets.AllAnimals.Animals
[error]       case class Dog(__typename: String, dogName: String, favoriteToys: List[Animals.FavoriteToys]) extends Animals

The problem is that the generated code should be

Dog(__typename: String, dogName: String, favoriteToys: List[Dog.FavoriteToys])

but instead it is

Dog(__typename: String, dogName: String, favoriteToys: List[Animals.FavoriteToys])

Here is the full generated code:

Click to expand!

package graphql.codegen
import io.circe.{ Decoder, Encoder }
import io.circe.generic.semiauto.{ deriveDecoder, deriveEncoder }
import sangria.macros._
import types._
object Pets {
  object AllAnimals extends GraphQLQuery {
    val document: sangria.ast.Document = graphql"""query AllAnimals {
  animals {
    ...AnimalName
  }
}

fragment DogName on Dog {
  dogName
  favoriteToys {
    ...ToyName
  }
}
fragment CatName on Cat {
  catName
}
fragment StoreName on Store {
  storeName
}
fragment ToyName on Toy {
  toyName
  boughtAt {
    ...StoreName
  }
}
fragment AnimalName on Animal {
  __typename
  ...DogName
  ...CatName
}"""
    case class Variables()
    object Variables { implicit val jsonEncoder: Encoder[Variables] = deriveEncoder[Variables] }
    case class Data(animals: List[Animals])
    object Data { implicit val jsonDecoder: Decoder[Data] = deriveDecoder[Data] }
    sealed trait Animals { def __typename: String }
    object Animals {
      case class Cat(__typename: String, catName: String) extends Animals
      object Cat {
        implicit val jsonDecoder: Decoder[Cat] = deriveDecoder[Cat]
        implicit val jsonEncoder: Encoder[Cat] = deriveEncoder[Cat]
      }
      case class Dog(__typename: String, dogName: String, favoriteToys: List[Animals.FavoriteToys]) extends Animals
      object Dog {
        case class FavoriteToys(toyName: String, boughtAt: FavoriteToys.BoughtAt) extends ToyName
        object FavoriteToys {
          implicit val jsonDecoder: Decoder[FavoriteToys] = deriveDecoder[FavoriteToys]
          implicit val jsonEncoder: Encoder[FavoriteToys] = deriveEncoder[FavoriteToys]
          case class BoughtAt(storeName: String) extends StoreName
          object BoughtAt {
            implicit val jsonDecoder: Decoder[BoughtAt] = deriveDecoder[BoughtAt]
            implicit val jsonEncoder: Encoder[BoughtAt] = deriveEncoder[BoughtAt]
          }
        }
        implicit val jsonDecoder: Decoder[Dog] = deriveDecoder[Dog]
        implicit val jsonEncoder: Encoder[Dog] = deriveEncoder[Dog]
      }
      implicit val jsonDecoder: Decoder[Animals] = for (typeDiscriminator <- Decoder[String].prepare(_.downField("__typename")); value <- typeDiscriminator match {
        case "Cat" =>
          Decoder[Cat]
        case "Dog" =>
          Decoder[Dog]
        case other =>
          Decoder.failedWithMessage("invalid type: " + other)
      }) yield value
    }
  }
}

I believe the problem also occurs in the unit tests here: https://github.com/muuki88/sbt-graphql/compare/issue-76/fix-fragment-union-codegen#diff-045de0b210c8e206e71c3ab7f73d3bf9b84f88653bdf74bb2c2cfb949b7a75f0R9
I believe it should be

case class Article(id: ID, __typename: String, title: String, status: ArticleStatus, author: Search.Article.Author) extends Search

instead?

Possible Short-Term Workarounds

This issue does not occur with the Sangria codegen style. The generated code is

Click to expand!
package graphql.codegen
object GraphQLCodegen {
  case class AllAnimals(animals: List[GraphQLCodegen.AllAnimals.Animals])
  object AllAnimals {
    case class AllAnimalsVariables()
    sealed trait Animals
    object Animals {
      case class Cat(__typename: String, catName: String) extends GraphQLCodegen.AllAnimals.Animals
      case class Dog(__typename: String, dogName: String, favoriteToys: List[GraphQLCodegen.AllAnimals.Animals.Dog.FavoriteToys]) extends GraphQLCodegen.AllAnimals.Animals
      object Dog {
        case class FavoriteToys(toyName: String, boughtAt: GraphQLCodegen.AllAnimals.Animals.Dog.FavoriteToys.BoughtAt)
        object FavoriteToys { case class BoughtAt(storeName: String) }
      }
    }
  }
}

which compiles successfully. However, I was wondering whether you could provide an example of generating the request body with the Sangria style since there is no Sangria Document generated?

The other possible workaround is to write the problematic class by hand using the codeGen directive, but I would prefer to use the Sangria style instead if possible.

Questions

  1. What is causing this compile error?
  2. As a temporary work-around, I'd like to use the Sangria codegen style, but am confused about how to generate the request body?

Thanks!

@muuki88
Copy link
Owner Author

muuki88 commented Mar 24, 2021

Hi @ellenlight

Thanks for your detailed and comprehensive issue report ☺️

TL;DR codeGen is the way to go at the moment.

Sangria Style

The sangria style code generation was the initial implementation from Jonas from another plugin.
My focus is the Apollo Code generation and I'm playing with the though to remove the sangria style
as it adds a bit of confusion and unnecessary complexity for the plugin.

What is causing the compilation error

Union types and fragments are quite tricky to get right in the code generation. I spent several hours to find a way to implement this, but without success so far. Any help is welcome as this project is purely maintained in my freetime (and the one of others I assume) 🤗

This is also the reason codeGen was introduced. To have a simple and stable workaround for all code gen related issues.

@ellenlight
Copy link

Hi @ellenlight

Thanks for your detailed and comprehensive issue report ☺️

TL;DR codeGen is the way to go at the moment.

Sangria Style

The sangria style code generation was the initial implementation from Jonas from another plugin.
My focus is the Apollo Code generation and I'm playing with the though to remove the sangria style
as it adds a bit of confusion and unnecessary complexity for the plugin.

What is causing the compilation error

Union types and fragments are quite tricky to get right in the code generation. I spent several hours to find a way to implement this, but without success so far. Any help is welcome as this project is purely maintained in my freetime (and the one of others I assume) 🤗

This is also the reason codeGen was introduced. To have a simple and stable workaround for all code gen related issues.

Sounds good and thank you for the quick reply!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants