SangriaでGraphQLのinterfaceを扱うには

[sangria-graphql/sangria](https://github.com/sangria-graphql/sangria/)を使ってGraphQLなAPIを実装する時にinterfaceをどうやって使うか、という話。  
interface自体は特に難しい話ではないが、地味に動かなくて困ったので残しておく。

## まとめ

- interfaceの実装自体は`InterfaceType`を使うだけ
- `Field.fieldType`に`InterfaceType`を与えるだけだとSchemeがエラーになる
- 解決策として
  - `Schema.additionalTypes`あるいは`InterfaceType.withPossibleTypes`でそのinterfaceを実装した`ObjectType`を与える
  - `UnionType`を使う

## interfaceの実装としてのInterfaceType

GraphQLの公式ページ: [Schemas and Types | GraphQL](https://graphql.org/learn/schema/#interfaces)
いわゆるinterface的な型/機能がGraphQLにも定義されている。

## SangriaでInterfaceTypeを使ったスキーマを実装する

まずはScalaでinterface的なtraitを使ったコードを用意する。

```scala
trait Animal {
  def id: String
  def name: String
}
case class Dog(id: String, name: String, kind: String) extends Animal
case class Cat(id: String, name: String, color: Color) extends Animal

sealed abstract class Color(val rgb: String)
object Color {
  case object White extends Color("#FFFFFF")
  case object Black extends Color("#000000")
  case object Brown extends Color("#A52A2A")
}
```

`Animal`なinterfaceを`Dog`と`Cat`がimplementsしていてそれぞれ独自のフィールドも持っている。
これらに対するGraphQLスキーマを実装すると以下のようになる。

```scala
import sangria.macros.derive
import sangria.schema._

lazy val animalInterface: InterfaceType[Unit, Animal] = InterfaceType[Unit, Animal](
  "Animal",
  "animal interface",
  fields[Unit, Animal](
    Field("id", StringType, resolve = ctx => ctx.value.id),
    Field("name", StringType, resolve = ctx => ctx.value.name)
  )
)

lazy val dogType = derive.deriveObjectType[Unit, Dog](
  derive.Interfaces[Unit, Dog](animalInterface)
)

implicit lazy val colorEnum = derive.deriveEnumType[Color]()

lazy val catType = derive.deriveObjectType[Unit, Cat](
  derive.Interfaces[Unit, Cat](animalInterface)
)
```

Schemeを`schema.renderPretty`すると以下のようになる。

```
interface Animal {
  id: String!
  name: String!
}

type Cat implements Animal {
  id: String!
  name: String!
  color: Color!
}

enum Color {
  White
  Black
  Brown
}

type Dog implements Animal {
  id: String!
  name: String!
  kind: String!
}                            
```

## interfaceを返却するディレクティブを定義する

先程定義した`animalInterface`を返却する`Field`を持つQueryを実装する。  
`Ctx`は`Unit`で、`all`に対して固定で`Dog`と`Cat`のListを返し、その`Field`の型は`ListType(animalInterface)`にしてある。

```scala
lazy val animalQuery: ObjectType[Unit, Unit] = {
  ObjectType.apply(
    "AnimalQuery",
    fields[Unit, Unit](
      Field("all", ListType(animalInterface), resolve = { _ =>
        Dog("dog-1", "alice", "golden") ::
          Dog("dog-2", "bob", "Chihuahua") ::
          Cat("cat-1", "charlie", Color.Brown) :: Nil
      })
    )
  )
}

val schema = Schema(animalQuery)
```

`Dog`も`Cat`も`dotType`, `catType`で`animalInterface`をimplementsしているので動きそうに見える。

しかし動かして見るとエラーになってしまう。

> sangria.schema.SchemaValidationException: Schema does not pass validation. Violations:
> Interface 'Animal' must be implemented by at least one object type.

エラーメッセージの通りで、`animalInterface: InterfaceType[Unit, Animal]`をimplementsした`ObjectType`が無いということ。

## これの解決策はいくつかある

- Schema.additionalTypesを使う
- InterfaceType.withPossibleTypesを使う
- UnionTypeを使う

## Schema.additionalTypesを使う

`dogType`と`catType`は確実に`InterfaceType[Unit, Animal]`をimplementsしているが、それが`Schema`に伝わっていないのが原因。

それを解決するために`Schema.additionalTypes`というフィールドがある。
ソースコードはこのあたり：
[sangria/Schema.scala](https://github.com/sangria-graphql/sangria/blob/63362722b9c1acad5c5a06a539636e8f37f2788b/src/main/scala/sangria/schema/Schema.scala#L725)

つまり、以下のように書くだけで良い。

```scala
Schema(animalQuery, additionalTypes = dogType :: catType :: Nil)
```

これで以下のようなクエリを記述することが出来るようになる。

```
query {
  all {
    id
    name
    ... on Dog {
      kind 
    } 
    ... on Cat {
      color
    }
  }
}
```

## InterfaceType.withPossibleTypesを使う

SchemaにInterfaceを実装している`ObjectType`を伝えるもう一つの方法。
`InterfaceType.withPossibleTypes`を利用すると`InterfaceType`のインスタンス自体にそれを実装している型をもたせることが可能。

`animalInterface`の実装全体は以下のようになる。

```scala
lazy val animalInterface: InterfaceType[Unit, Animal] = InterfaceType[Unit, Animal](
  "Animal",
  "animal interface",
  fields[Unit, Animal](
    Field("id", StringType, resolve = ctx => ctx.value.id),
    Field("name", StringType, resolve = ctx => ctx.value.name)
  )
).withPossibleTypes(() => List(dogType, catType))

// SchemaにadditionalTypeを与える必要はない。
Schema(animalQuery)
```

なお、`withPossibleTypes`には2つAPIがあって可変長引数を与える方のAPIだとStackOverflowErrorが発生してしまった。

```scala
// StackOverflowError!
.withPossibleTypes(dogType, catType)
```

## UnionTypeを使う

クエリの返り値としてinterfaceを返すのを諦めてUnionTypeを用いる方法。

今回だと`dogType`と`catType`の`UnionType`を実装すれば良い。

```scala
val animalUnionType = UnionType[Unit](
  "AnimalUnion",
  types = dogType :: catType :: Nil
)
```

一応クエリの方も載せておくと、`Field.fieldType`にUnionTypeを使うだけ。
それ以外は変えていない。

```scala
lazy val animalQuery: ObjectType[Unit, Unit] = {
  ObjectType.apply(
    "AnimalQuery",
    fields[Unit, Unit](
      Field("all", ListType(animalUnionType), resolve = { _ =>
        Dog("dog-1", "alice", "golden") ::
          Dog("dog-2", "bob", "Chihuahua") ::
          Cat("cat-1", "charlie", Color.Brown) :: Nil
      })
    )
  )
}

val schema = Schema(animalQuery)
```

しかし、こうするとクエリの幅が狭くなってしまうし、interfaceを使っている利点はなくなってしまう。

```
query {
  all {
    id # ←ここがエラーになる
    ... on Dog {
      kind 
    } 
    ... on Cat {
      color
    }
  }
}
```

## 番外編

バリデーションエラーになるならバリデーションをやめてしまえばいい、というのもある。  
つまり、interfaceに対して実装が見つからないのを許容するということ。

`Schema`には`validationRules`というフィールドがあるので、そこで今回邪魔になるルールを除去してみる。

```scala
 Schema(animalQuery,
        validationRules = List(
          DefaultValuesValidationRule,
          InterfaceImplementationValidationRule,
          // InterfaceMustHaveImplementationValidationRule,
          SubscriptionFieldsValidationRule,
          SchemaValidationRule.defaultFullSchemaTraversalValidationRule
        ))
```

これでスキーマのバリデーションはパスするが、実際に動かしてみるとエラーになる。

> sangria.execution.UndefinedConcreteTypeError: Can't find appropriate subtype of an interface type 'Animal' for value of class
> 'net.petitviolet.prac.graphql.sample.UnionInterfaceSampleSchema$Cat' at path 'all[2]'. Possible types: none. Got value: Cat(cat-1,charlie,Brown). 

`animalInterface`だけだと`dogType`と`catType`の情報が入っていないので、レスポンスを返却する時にsangriaが判断できずにエラーになってしまう。