Sangriaで日付のカスタムScalar型を定義する

[Sangria](https://github.com/sangria-graphql/sangria) で以下のようなカスタム Scalar 型を定義してみました。

* 日付用の型で GraphQL 上は文字列として表現し、内部的に ```java.time.OffsetDateTime``` を使う

# はじめに

[前回](https://qiita.com/fits/items/79a4d0f86fb0e4050fa7)と同様に、以下の環境で実行しました。

##### build.sbt
```scala
ThisBuild / scalaVersion := "3.2.1"

libraryDependencies ++= Seq(
  "org.sangria-graphql" %% "sangria" % "3.4.1",
  "org.sangria-graphql" %% "sangria-circe" % "1.3.2"
)
```

結果を JSON 文字列化するため ```sangria-circe``` を使っています。

# 日付型の実装

カスタム Scalar 型は ```ScalarType``` を使って以下の変換処理を実装する事で定義できます。

* coerceOutput（内部データを GraphQL 上の表現へ変換） ```(T, Set[MarshallerCapability]) => Any```
* coerceUserInput（変数利用時の GraphQL 上の値を内部データへ変換）```Any => Either[Violation, T]```
* coerceInput（GraphQL 上の値を内部データへ変換） ```ast.Value => Either[Violation, T]```

```coerceUserInput``` と ```coerceInput``` の違いは引数の型となっており、前者は変数利用の際に、後者は GraphQL へ組み込まれた値を処理する際にそれぞれ使用されるようです。

##### Date 型の実装例
```scala
// エラー定義
case object DateCoercionViolation extends ValueCoercionViolation("invalid date")

// OffsetDateTime への変換処理
val parseDate = (d: String) =>
  try
    Right(OffsetDateTime.parse(d, DateTimeFormatter.ISO_DATE_TIME))
  catch
    case _ => Left(DateCoercionViolation)

// Date 型の定義
val DateType = ScalarType[OffsetDateTime](
  "Date",
  // 内部データを GraphQL 上の表現へ変換
  coerceOutput = (d, _) => d.format(DateTimeFormatter.ISO_DATE_TIME),
  // GraphQL 上の値を内部データへ変換（変数利用時）
  coerceUserInput = {
    case s: String => parseDate(s)
    case _ => Left(DateCoercionViolation)
  },
  // GraphQL 上の値を内部データへ変換
  coerceInput = {
    case sangria.ast.StringValue(s, _, _, _, _) => parseDate(s)
    case _ => Left(DateCoercionViolation)
  }
)
```

次の Query を使って Date 型を動作確認してみます。

##### Query 定義
```scala
val QueryType = ObjectType("Query", fields[Unit, Unit](
  Field("now", DateType, resolve = _ => OffsetDateTime.now()),
  Field(
    "addDays", 
    DateType,
    arguments = List(Argument("date", DateType), Argument("n", IntType)),
    resolve = ctx => ctx.args.arg[OffsetDateTime]("date").plusDays(ctx.args.arg[Int]("n"))
  )
))
```

ちなみに、GraphQL で表現すると下記のようになると思います。

```graphql
scalar Date

type Query {
    now: Date!
    addDays(date: Date!, n: Int!): Date!
}
```

## 動作確認1

now の結果はこのようになります。

##### 処理1
```scala
val schema = Schema(QueryType)

given ExecutionContext = ExecutionContext.global

val r1 = Executor.execute(schema, graphql"{ now }")

r1.foreach(println(_))
```

##### 結果1
```json
{
  "data" : {
    "now" : "2023-01-07T21:19:03.003669+09:00"
  }
}
```

## 動作確認2

```addDays``` の結果です。
日付の値を GraphQL クエリへ組み込んでいるので ```coerceInput``` で処理されます。

##### 処理2
```scala
val r2 = Executor.execute(schema, graphql"""{ addDays(date: "2023-02-10T15:10:00+09:00", n: 2) }""")

r2.foreach(println(_))
```

##### 結果2
```json
{
  "data" : {
    "addDays" : "2023-02-12T15:10:00+09:00"
  }
}
```

## 動作確認3

日付のフォーマットを変えてみます。

##### 処理3
```scala
val r3 = Executor.execute(schema, graphql"""{ addDays(date: "2023-03-10T17:10:00Z", n: 3) }""")

r3.foreach(println(_))
```

##### 結果3
```json
{
  "data" : {
    "addDays" : "2023-03-13T17:10:00Z"
  }
}
```

## 動作確認4

日付のフォーマットに問題がある（ISO_DATE_TIME でパースできない）場合の結果です。

##### 処理4
```scala
val r4 = Executor.execute(schema, graphql"""{ addDays(date: "2023-04-10 06:10:00", n: 4) }""")

r4.recover {
  case e: ErrorWithResolver => e.resolveError
}.foreach(println(_))
```

##### 結果4
```json
{
  "data" : null,
  "errors" : [
    {
      "message" : "Expected type 'Date!', found '\"2023-04-10 06:10:00\"'. invalid date (line 1, column 17):\n{ addDays(date: \"2023-04-10 06:10:00\", n: 4) }\n                ^",
      "locations" : [
        {
          "line" : 1,
          "column" : 17
        }
      ]
    }
  ]
}
```

## 動作確認5

変数利用時の結果です。
この場合は ```coerceUserInput``` で処理されます。

##### 処理5
```scala
val q = graphql"""
  query ($$d: Date!) {
    addDays(date: $$d, n: 5)      
  }
"""

val vs = InputUnmarshaller.mapVars("d" -> "2023-05-10T17:30:00+09:00")

val r5 = Executor.execute(schema, q, variables = vs)

r5.foreach(println(_))
```

##### 結果5
```json
{
  "data" : {
    "addDays" : "2023-05-15T17:30:00+09:00"
  }
}
```

今回のサンプルコードは https://github.com/fits/try_samples/tree/master/blog/20230107/sangria_custom_scalar