graphql-deviseのmount_graphql_devise_forの処理を追う

## これは何か

mount_graphql_devise_for の処理がブラックボックスでgraphql_deviseの理解が進まないため、理解を進めるために詳細な実装を追ったときのメモです。

## 前提

- [graphql-devise](https://github.com/graphql-devise/graphql_devise) v0.18.2
- まだ、マイナーバージョンのgemで内部の構造は結構な頻度で変わっている模様で、これは2022/04/27時点の情報です。
- 間違っている箇所などあるかもしれません。間違いありましたら、優しくご指摘いただけると嬉しいです。

## mount_graphql_devise_forの処理を追う

graphql-deviseで`bundle exec rails generate graphql_devise:install`を実行して`routes.rb`を見ると下記のコードが定義される。今回はこの処理が何をしているのかを追っていきたいと思います。

```ruby:routes.rb
mount_graphql_devise_for 'User', at: 'graphql_auth'
```

`mount_graphql_devise_for`は`ActionDispatch::Routing::Mapper`に定義されているメソッドです。

```ruby
# https://github.com/graphql-devise/graphql_devise/blob/v0.18.2/lib/graphql_devise/rails/routes.rb

module ActionDispatch::Routing
  class Mapper
    def mount_graphql_devise_for(resource, options = {})
      clean_options = GraphqlDevise::ResourceLoader.new(resource, options, true).call(
        GraphqlDevise::Types::QueryType,
        GraphqlDevise::Types::MutationType
      )

      post clean_options.at, to: 'graphql_devise/graphql#auth'
      get  clean_options.at, to: 'graphql_devise/graphql#auth'
    end
  end
end
```

`mount_graphql_devise_for 'User', at: 'graphql_auth'`が実行されると、`mount_graphql_devise_for`の引数には次のように値が渡されていました。

- `resource`
    - => `"User"`
- `options`
    - => `{:at=>"graphql_auth"}`

これらの引数は`GraphqlDevise::ResourceLoader.new`で引き渡されているので`ResourceLoader#initialize`の処理を見てみます。

```ruby
# https://github.com/graphql-devise/graphql_devise/blob/v0.18.2/lib/graphql_devise/resource_loader.rb

module GraphqlDevise
  class ResourceLoader
    def initialize(resource, options = {}, routing = false)
      @resource           = resource
      @options            = options
      @routing            = routing
      @default_operations = GraphqlDevise::DefaultOperations::MUTATIONS.merge(GraphqlDevise::DefaultOperations::QUERIES)
    end
    
    略
```

`GraphqlDevise::ResourceLoader.new(resource, options, true)`した後のインスタンス変数には下記の値が入っていました。

- `@resource`
    - => `"User"`
- `@options`
    - => `{:at=>"graphql_auth"}`
- `@routing`
    - => `true`

`@default_operations`には下記のように`login`、`logout`のように各actionに対応するクラス名などの情報がhashで入っていました。

```ruby
=> {:login=>{:klass=>GraphqlDevise::Mutations::Login, :authenticatable=>true},
 :logout=>{:klass=>GraphqlDevise::Mutations::Logout, :authenticatable=>true},
 :sign_up=>{:klass=>GraphqlDevise::Mutations::SignUp, :authenticatable=>true, :deprecation_reason=>"use register instead"},
 :register=>{:klass=>GraphqlDevise::Mutations::Register, :authenticatable=>true},
 :update_password=>
  {:klass=>GraphqlDevise::Mutations::UpdatePassword,
   :authenticatable=>true,
   :deprecation_reason=>"use update_password_with_token instead"},
 :update_password_with_token=>{:klass=>GraphqlDevise::Mutations::UpdatePasswordWithToken, :authenticatable=>true},
 :send_password_reset=>
  {:klass=>GraphqlDevise::Mutations::SendPasswordReset,
   :authenticatable=>false,
   :deprecation_reason=>"use send_password_reset_with_token instead"},
 :send_password_reset_with_token=>{:klass=>GraphqlDevise::Mutations::SendPasswordResetWithToken, :authenticatable=>false},
 :resend_confirmation=>
  {:klass=>GraphqlDevise::Mutations::ResendConfirmation,
   :authenticatable=>false,
   :deprecation_reason=>"use resend_confirmation_with_token instead"},
 :resend_confirmation_with_token=>{:klass=>GraphqlDevise::Mutations::ResendConfirmationWithToken, :authenticatable=>false},
 :confirm_registration_with_token=>{:klass=>GraphqlDevise::Mutations::ConfirmRegistrationWithToken, :authenticatable=>true},
 :confirm_account=>
  {:klass=>GraphqlDevise::Resolvers::ConfirmAccount,
   :deprecation_reason=>"use the new confirmation flow as it does not require this query anymore"},
 :check_password_token=>
  {:klass=>GraphqlDevise::Resolvers::CheckPasswordToken,
   :deprecation_reason=>"use the new password reset flow as it does not require this query anymore"}}
```

`GraphqlDevise::ResourceLoader`のインスタンスが作られた後は、`call(GraphqlDevise::Types::QueryType, GraphqlDevise::Types::MutationType)`しているので`call`メソッドの処理を追ってみます。

```ruby
# https://github.com/graphql-devise/graphql_devise/blob/v0.18.2/lib/graphql_devise/resource_loader.rb

    def call(query, mutation)
      # clean_options responds to all keys defined in GraphqlDevise::MountMethod::SUPPORTED_OPTIONS
      clean_options = GraphqlDevise::MountMethod::OptionSanitizer.new(@options).call!

      model = if @resource.is_a?(String)
        ActiveSupport::Deprecation.warn(<<-DEPRECATION.strip_heredoc, caller)
          Providing a String as the model you want to mount is deprecated and will be removed in a future version of
          this gem. Please use the actual model constant instead.
          EXAMPLE
          GraphqlDevise::ResourceLoader.new(User) # instead of GraphqlDevise::ResourceLoader.new('User')
          mount_graphql_devise_for User # instead of mount_graphql_devise_for 'User'
        DEPRECATION
        @resource.constantize
      else
        @resource
      end

      # Necesary when mounting a resource via route file as Devise forces the reloading of routes
      return clean_options if GraphqlDevise.resource_mounted?(model) && @routing

      validate_options!(clean_options)

      authenticatable_type = clean_options.authenticatable_type.presence ||
                             "Types::#{@resource}Type".safe_constantize ||
                             GraphqlDevise::Types::AuthenticatableType

      prepared_mutations = prepare_mutations(model, clean_options, authenticatable_type)

      if prepared_mutations.any? && mutation.blank?
        raise GraphqlDevise::Error, 'You need to provide a mutation type unless all mutations are skipped'
      end

      prepared_mutations.each do |action, prepared_mutation|
        mutation.field(action, mutation: prepared_mutation, authenticate: false)
      end

      prepared_resolvers = prepare_resolvers(model, clean_options, authenticatable_type)

      if prepared_resolvers.any? && query.blank?
        raise GraphqlDevise::Error, 'You need to provide a query type unless all queries are skipped'
      end

      prepared_resolvers.each do |action, resolver|
        query.field(action, resolver: resolver, authenticate: false)
      end

      GraphqlDevise.add_mapping(GraphqlDevise.to_mapping_name(@resource).to_sym, @resource)
      GraphqlDevise.mount_resource(model) if @routing

      clean_options
    end

    略
```

`GraphqlDevise::MountMethod::OptionSanitizer.new(@options).call!`ではサニタイズ処理がごにょごにょされていました。ここはあまり重要な箇所ではなさそうなので深くは追わないことにしました。

`clean_options`には、下記のようにStructのインスタンスが入るみたいです。

```ruby
GraphqlDevise::MountMethod::OptionSanitizer.new(@options).call!
=> #<struct
 at="graphql_auth",
 operations={},
 only=[],
 skip=[],
 additional_queries={},
 additional_mutations={},
 authenticatable_type=nil>
```

`if @resource.is_a?(String)`はtrueを返すので、trueのときの処理が実行されます。

`ActiveSupport::Deprecation.warn`は実行されると、下記のように警告文を出力するみたいです。

```ruby
=> "DEPRECATION WARNING: Providing a String as the model you want to mount is deprecated and will be removed in a future version of\nthis gem. Please use the actual model constant instead.\nEXAMPLE\nGraphqlDevise::ResourceLoader.new(User) # instead of GraphqlDevise::ResourceLoader.new('User')\nmount_graphql_devise_for User # instead of mount_graphql_devise_for 'User'\n (called from block in <main> at /api/config/routes.rb:7)"
```

`mount_graphql_devise_for 'User', at: 'graphql_auth'`としている箇所の`'User'`は文字列ではなく、定数を使ってくださいというものでした。

ですので、`model`には最終的に`User(定数)`が入ります。

次の`if GraphqlDevise.resource_mounted?(model) && @routing`はfalseになります。

```ruby
      # Necesary when mounting a resource via route file as Devise forces the reloading of routes
      return clean_options if GraphqlDevise.resource_mounted?(model) && @routing
```

`validate_options!(clean_options)`では`clean_options`の検証を行っているみたいです。

`authenticatable_type`は`Types::UserType`が返ってきました。

次の処理は`GraphqlDevise::Types::MutationType`にfieldを追加しているようでした。

```ruby
      prepared_mutations = prepare_mutations(model, clean_options, authenticatable_type)

      if prepared_mutations.any? && mutation.blank?
        raise GraphqlDevise::Error, 'You need to provide a mutation type unless all mutations are skipped'
      end

      prepared_mutations.each do |action, prepared_mutation|
        mutation.field(action, mutation: prepared_mutation, authenticate: false)
      end
```

`prepared_mutations`には次のようなhashが入ってきて、hashをeachして各actionを`mutation.field`で追加されていました。

```ruby
=> {:user_login=>#<Class:0x0000561b0fdcdf40>,
 :user_logout=>#<Class:0x0000561b0fe82738>,
 :user_sign_up=>#<Class:0x0000561b0fef7100>,
 :user_register=>#<Class:0x0000561b0ffc2b98>,
 :user_update_password=>#<Class:0x0000561b100b8368>,
 :user_update_password_with_token=>#<Class:0x0000561b101a1310>,
 :user_send_password_reset=>#<Class:0x0000561b10276650>,
 :user_send_password_reset_with_token=>#<Class:0x0000561b102e4dd0>,
 :user_resend_confirmation=>#<Class:0x0000561b103515c0>,
 :user_resend_confirmation_with_token=>#<Class:0x0000561b1031f9a8>,
 :user_confirm_registration_with_token=>#<Class:0x0000561b103b27a8>}
```

念のため確かめてみました。追加されています。

```ruby
GraphqlDevise::Types::MutationType.fields
=> {"userLogin"=>#<GraphQL::Schema::Field Mutation.userLogin(...): UserLoginPayload>,
 "userLogout"=>#<GraphQL::Schema::Field Mutation.userLogout: UserLogoutPayload>,
 "userSignUp"=>#<GraphQL::Schema::Field Mutation.userSignUp(...): UserSignUpPayload>,
 "userRegister"=>#<GraphQL::Schema::Field Mutation.userRegister(...): UserRegisterPayload>,
 "userUpdatePassword"=>#<GraphQL::Schema::Field Mutation.userUpdatePassword(...): UserUpdatePasswordPayload>,
 "userUpdatePasswordWithToken"=>
  #<GraphQL::Schema::Field Mutation.userUpdatePasswordWithToken(...): UserUpdatePasswordWithTokenPayload>,
 "userSendPasswordReset"=>#<GraphQL::Schema::Field Mutation.userSendPasswordReset(...): UserSendPasswordResetPayload>,
 "userSendPasswordResetWithToken"=>
  #<GraphQL::Schema::Field Mutation.userSendPasswordResetWithToken(...): UserSendPasswordResetWithTokenPayload>,
 "userResendConfirmation"=>#<GraphQL::Schema::Field Mutation.userResendConfirmation(...): UserResendConfirmationPayload>,
 "userResendConfirmationWithToken"=>
  #<GraphQL::Schema::Field Mutation.userResendConfirmationWithToken(...): UserResendConfirmationWithTokenPayload>,
 "userConfirmRegistrationWithToken"=>
  #<GraphQL::Schema::Field Mutation.userConfirmRegistrationWithToken(...): UserConfirmRegistrationWithTokenPayload>}
```

次の処理も同じような感じですが、今度は`GraphqlDevise::Types::QueryType`にfieldを追加しているようでした。

```ruby
      prepared_resolvers = prepare_resolvers(model, clean_options, authenticatable_type)

      if prepared_resolvers.any? && query.blank?
        raise GraphqlDevise::Error, 'You need to provide a query type unless all queries are skipped'
      end

      prepared_resolvers.each do |action, resolver|
        query.field(action, resolver: resolver, authenticate: false)
      end
```

`prepared_resolvers`には次のようなhashが入ってきて、hashをeachして各actionを`query.field`で追加されていました。

```ruby
=> {:user_confirm_account=>#<Class:0x0000561b107ea550>, :user_check_password_token=>#<Class:0x0000561b108945f0>}
```

念のため確かめてみました。追加されています。

```ruby
GraphqlDevise::Types::QueryType.fields
=> {"userConfirmAccount"=>#<GraphQL::Schema::Field Query.userConfirmAccount(...): User!>,
 "userCheckPasswordToken"=>#<GraphQL::Schema::Field Query.userCheckPasswordToken(...): User!>}
```

`GraphqlDevise.add_mapping(GraphqlDevise.to_mapping_name(@resource).to_sym, @resource)`では内部的に`Devise.add_mapping`が実行されていました。

```ruby
# https://github.com/graphql-devise/graphql_devise/blob/v0.18.2/lib/graphql_devise.rb

  def self.add_mapping(mapping_name, resource)
    return if Devise.mappings.key?(mapping_name.to_sym)

    Devise.add_mapping(
      mapping_name.to_s.pluralize.to_sym,
      module: :devise, class_name: resource.to_s
    )
  end
```

`Devise.add_mapping`は下記のようなメソッドでした。

- `Devise::Mapping.new`で主にやっていること
    - mappingオブジェクトを返す([参考](https://github.com/heartcombo/devise/blob/main/lib/devise/mapping.rb#L4))
- `h.define_helpers`で主にやっていること
    - `authenticate_{mapping}!`や`current_{mapping}`、や`{mapping}_signed_in?`などのヘルパーメソッドをmappingに基づいて作成している([参考](https://github.com/heartcombo/devise/blob/f8d1ea90bc328012f178b8a6616a89b73f2546a4/lib/devise/controllers/helpers.rb#L112))

```ruby
# https://github.com/heartcombo/devise/blob/main/lib/devise.rb#L353

  # Small method that adds a mapping to Devise.
  def self.add_mapping(resource, options)
    mapping = Devise::Mapping.new(resource, options)
    @@mappings[mapping.name] = mapping
    @@default_scope ||= mapping.name
    @@helpers.each { |h| h.define_helpers(mapping) }
    mapping
  end
```

`GraphqlDevise.mount_resource(model) if @routing`はあまり重要ではなさそうな処理なのでスルーします。

`GraphqlDevise::ResourceLoader.new(resource, options, true).call`は最終的にclean_optionsを返すので、`mount_graphql_devise_for` の中の下記のルーティングは

```
post clean_options.at, to: 'graphql_devise/graphql#auth'
get  clean_options.at, to: 'graphql_devise/graphql#auth'
```

から、最終的に下記になります。

```
post 'graphql_auth', to: 'graphql_devise/graphql#auth'
get  'graphql_auth', to: 'graphql_devise/graphql#auth'
```

そして、`graphql_devise/graphql#auth`は  `GraphqlDevise::GraphqlController#auth`に対応するようでした。

## mount_graphql_devise_forの処理まとめ

- 内部で`GraphqlDevise::ResourceLoader`を呼び出しており、主に次のことをやっていました
    - `GraphqlDevise::Types::MutationType`に各actionのfieldを追加
    - `GraphqlDevise::Types::QueryType`に各actionのfieldを追加
    - `Devise.add_mapping`を呼び出して`authenticate_{mapping}!`や`current_{mapping}`、や`{mapping}_signed_in?`などのヘルパーメソッドを作成
- ルーティングの定義をしていました
    - `post 'graphql_auth', to: 'graphql_devise/graphql#auth'`
    - `get  'graphql_auth', to: 'graphql_devise/graphql#auth'`
    - `graphql_devise/graphql#auth`は  `GraphqlDevise::GraphqlController#auth`に対応