graphql-rubyで入力値バリデーションをカスタマイズする

# 背景とやりたいこと

graphql-rubyにはargumentに対して[バリデーション](https://graphql-ruby.org/fields/validation.html)をかけることができるのですが、

愚直にやると以下のようなpathとlocations付きの形式でエラーが返ってきます。

参考: [https://graphql-ruby.org/errors/execution_errors#adding-errors-to-the-array](https://graphql-ruby.org/errors/execution_errors#adding-errors-to-the-array)

```json
{
  "errors": [
    {
      "message": "Can't continue with this query",
      "locations": [
        {
          "line": 2,
          "column": 10,
        }
      ],
      "path": ["user", "login"],
    }
  ]
}
```

今回、こちらの形式ではなく[extensionsを用いた形式](https://graphql-ruby.org/errors/execution_errors#customizing-error-json)で返したいと思い、どうやればできそうなのか調べてみました。

# 環境

- ruby: 2.6.6
- rails: 5.2.6
- graphql-ruby: 1.12.7
- rspec: 3.10.0
- rspec-rails: 5.0.2

# format validationを上書きしてみる

例えば以下のようなemailの形式を検証したいとします。

```app/graphql/mutations/create_user.rb
module Mutations
  class CreateUser < ::Mutations::BaseMutation
	..
	argument :email, String, required: true, validates: { format: { with: /\A([a-zA-Z0-9]+)([a-zA-Z0-9\._\-\+]*)@([a-zA-Z0-9]+)([a-zA-Z0-9\._\-]+)\z/ } }
	..

    def resolve(input)
	  ...
	end
  end
end
```

ここで[GraphQL::Schema::Validator::FormatValidator](https://graphql-ruby.org/api-doc/2.0.5/GraphQL/Schema/Validator/FormatValidator.html)を継承するCustomValidatorを作成します。

```app/graphql/custom_validators/format_validator.rb
module CustomValidators
  class FormatValidator < GraphQL::Schema::Validator::FormatValidator
    def validate(_object, context, _value)
      return unless super

      context.add_error(GraphQL::ExecutionError.new("#{validated.name}の形式が正しくありません。", extensions: { code: "invalid_argument_#{validated.name}" }))
    end
  end
end
```

superでFormatValidatorのvalidateメソッドを実行し、エラーの場合はmessageが、エラーでない場合はnilが返ってきます。

エラーの場合はcontext.add_errorでカスタムエラーを追加することでextensionsを実現できます。

つづいてカスタムバリデータを初期化時にinstallすることで有効にします。

```config/initializers/graphql.rb
GraphQL::Schema::Validator.install(:format, CustomValidators::FormatValidator)
```

バリデーションエラーが一つでもあった場合はcontext#add_errorによってcontext.errorsに格納されるので以下のようにraiseすれば良さそうです！

```app/graphql/mutations/create_user.rb
module Mutations
  class CreateUser < ::Mutations::BaseMutation
	..
	argument :email, String, required: true, validates: { format: { with: /\A([a-zA-Z0-9]+)([a-zA-Z0-9\._\-\+]*)@([a-zA-Z0-9]+)([a-zA-Z0-9\._\-]+)\z/ } }
	..

	def resolve(input)
		validate_input!
		...
	end

	def validate_input!
	  return if context.errors.empty?
		
	  # To avoid duplicated error
	  last_error = context.errors.pop
	  raise last_error
    end
  end
end
```

試すと以下のように返ってきました。

```json
{
  "data": {
    "createUser": null
  },
  "errors": [
    {
      "message": "emailの形式が正しくありません。",
      "extensions": {
        "code": "invalid_argument_email"
      }
    }
  ]
}
```

# CustomValidatorとして切り出す

上記のように汎用的なものでもいいのですが、メールアドレスや電話番号など使いまわしたいformatはCustomValidatorとして切り出した方が使い勝手がいいかもしれません。

やり方はあまり変わりません。

同じようにカスタムバリデータを作成します。

`@with_pattern` に直接正規表現を入れてあげるのが良さそうです。

```app/graphql/custom_validators/email_format_validator.rb
module CustomValidators
  class EmailFormatValidator < GraphQL::Schema::Validator::FormatValidator
    def validate(_object, context, _value)
      @with_pattern = /\A([a-zA-Z0-9]+)([a-zA-Z0-9\._\-\+]*)@([a-zA-Z0-9]+)([a-zA-Z0-9\._\-]+)\z/
      return unless super

      context.add_error(GraphQL::ExecutionError.new('メールアドレスの形式が正しくありません。', extensions: { code: 'invalid_argument_email_format' }))
    end
  end
end
```

`email_format`としてinstallします。

```config/initializers/graphql.rb
GraphQL::Schema::Validator.install(:email_format, CustomValidators::EmailFormatValidator)
```

validatesには以下のように指定します。

```app/graphql/mutations/create_user.rb
module Mutations
  class CreateUser < ::Mutations::BaseMutation
	..
    argument :email, String, required: true, validates: { email_format: {} }
	..

    def resolve(input)
	  validate_input!
	  ...
    end
		
    ...
  end
end
```

# 単体テスト

上記のEmailFormatValidatorのテストの書き方も調べました。

rspecで書いています。

```spec/graphql/custom_validators/email_format_validator_spec.rb
RSpec.describe CustomValidators::EmailFormatValidator do
  describe '#validate' do
    subject do
      test_schema.execute(
        <<~GQL
          query {
            validated(value: "#{email}")
          }
        GQL
      )
    end

    let(:query_type) do
      Class.new(GraphQL::Schema::Object) do
        graphql_name 'Query'
        field :validated, String, null: true do
          argument :value, String, required: false, validates: { email_format: {} }
        end

        def validated(value: nil)
          value
        end
      end
    end
    let(:test_schema) do
      schema = Class.new(GraphQL::Schema)
      schema.query(query_type)
      schema
    end

    context 'when valid format' do
      let(:email) { 'test@example.com' }

      it 'does not return error' do
        expect(subject['errors']).to eq(nil)
      end
    end

    context 'when invalid format' do
      let(:email) { 'example.com' }

      it 'returns error' do
        errors = subject['errors']
        expect(errors.count).to eq(1)
        expect(errors.first['extensions']['code']).to eq('invalid_argument_email_format')
      end
    end
  end
end
```

このテストは実際にgraphql-rubyの単体テストを参考に書いてみました。

[https://github.com/rmosolgo/graphql-ruby/pull/3671/files](https://github.com/rmosolgo/graphql-ruby/pull/3671/files)

[https://github.com/rmosolgo/graphql-ruby/blob/master/spec/graphql/schema/validator/validator_helpers.rb](https://github.com/rmosolgo/graphql-ruby/blob/master/spec/graphql/schema/validator/validator_helpers.rb)

# まとめ

graphql-rubyが公式で提供している割には、調べてもあまり出てこなかったので、

メンテナンスできそうなら導入するのが良さそうです。

個人的には以下のメリットがあるかなと思いました。

- 入力値バリデーションをロジックに関するバリデータから切り離せる
- add_errorして最後にまとめてraiseすることで、複数のエラーを一度に検知できる

# 参考リンク

[公式ドキュメント](https://graphql-ruby.org/fields/validation.html)
[公式issue](https://github.com/rmosolgo/graphql-ruby/pull/3671/files)