GraphQL-RubyのEnum classes are never instantiated and their methods are never called.とは何か

GraphQL-RubyのEnumを翻訳するにあたり、Enum classes are never instantiated and their methods are never called.に疑問を持ったのでソースコードを読むことにしました。

version

graphq-ruby: 1.9.5
ruby: 2.6.5

実装は、GraphQL::Schema::Enumにあります。GraphQL-Rubyは、既に巨大なOSSであるためテストから概要を確認しました。

enumという変数は、spec/supportに定義されておりGraphQL::Schema::Enumを継承しています。

テスト項目から、GraphQL::Schema::Enumを継承したクラス自体にdescriptionを定義できることやブロックでvalueを追加することがわかりました。

describe GraphQL::Schema::Enum do
  let(:enum) { Jazz::Family }

  ~~~~
  describe "type info" do
    it "tells about the definition" do
      assert_equal "Family", enum.graphql_name
      assert_equal 29, enum.description.length
      assert_equal 7, enum.values.size
    end

    it "inherits values and description" do
      new_enum = Class.new(enum) do
        value :Nonsense
        value :PERCUSSION, "new description"
      end

      # Description was inherited
      assert_equal 29, new_enum.description.length
      # values were inherited without modifying the parent
      assert_equal 7, enum.values.size
      assert_equal 8, new_enum.values.size
      perc_value = new_enum.values["PERCUSSION"]
      assert_equal "new description", perc_value.description
    end
    
    it "accepts a block" do
      assert_equal "Neither here nor there, really", enum.values["KEYS"].description
    end
    ~~~~
  end
  
  ~~~~
end

つまり、valueは以下のように複数の定義方法があるということです。

class Types::BaseEnum < GraphQL::Schema::Enum
end

class Types::MediaCategory < Types::BaseEnum
  description "Groups of media category"
  value "AUDIO", "An audio file, such as music or spoken word"
  value :TEXT, "Written words"
  value "IMAGE" do
    "A still image, such as a photo or graphic"
  end
  value "VIDEO", value: :video
end

利用方法がわかったので、実際のソースコードを追っていきます。

GraphQL::Schema::Enumには、クラスメソッドしか存在していません。

では、各valueの値はどこで定義されているのでしょうか?コードを見れば、明らかですがvalueメソッドが正解です。

def value(*args, **kwargs, &block)
  kwargs[:owner] = self
  value = enum_value_class.new(*args, **kwargs, &block)
  own_values[value.graphql_name] = value
  nil
end

このメソッドで重要な行は、value = enum_value_class.new(*args, **kwargs, &block)です。

enum_value_classは、superclass <= GraphQL::Schema::Enumがfalseになるまで再帰的に繰り返します。

ここで、GraphQL::Schema::Enumが定義された時にenum_value_class(GraphQL::Schema::EnumValue)を実行しているためGraphQL::Schema::Enumに対してenum_value_classを実行した結果はGraphQL::Schema::EnumValueです。

よって、value = enum_value_class.new(*args, **kwargs, &block)GraphQL::Schema::EnumValueのinitializeメソッドを呼び出します。

def enum_value_class(new_enum_value_class = nil)
  if new_enum_value_class
    @enum_value_class = new_enum_value_class
  end
  @enum_value_class || (superclass <= GraphQL::Schema::Enum ? superclass.enum_value_class : nil)
end

GraphQL::Schema::EnumValueは、とても単純でオブジェクトの初期値をセットするだけです。

to_graphqlメソッドは、クラスベース以前の.define-styleと互換性を保つために用意されているので気にする必要はありません。

def value(*args, **kwargs, &block)
  kwargs[:owner] = self
  value = enum_value_class.new(*args, **kwargs, &block)
  ## ここから
  own_values[value.graphql_name] = value
  nil
end

最後に、privateメソッドであるown_valuesを呼び出しGraphQL::Schema::EnumValueのオブジェクトを@own_valuesに代入します。

private

def own_values
  @own_values ||= {}
end

以上からvalueメソッドがGraphQL::Schema::EnumValueのオブジェクトを@own_valuesに代入していることがわかりました。

さて、テスト項目に振り返るとvaluesメソッドを呼び出していることがわかります。

it "tells about the definition" do
  assert_equal "Family", enum.graphql_name
  assert_equal 29, enum.description.length
  assert_equal 7, enum.values.size
end

valuesメソッドは、先ほど説明した@own_valuesを返しているだけということがわかります。

def values
  inherited_values = superclass <= GraphQL::Schema::Enum ? superclass.values : {}
  # Local values take precedence over inherited ones
  inherited_values.merge(own_values)
end

これで、以下のコードが内部で何をしているかわかりました。

class Types::BaseEnum < GraphQL::Schema::Enum
end

class Types::MediaCategory < Types::BaseEnum
  description "Groups of media category"
  value "AUDIO", "An audio file, such as music or spoken word"
  value :TEXT, "Written words"
  value "IMAGE" do
    "A still image, such as a photo or graphic"
  end
  value "VIDEO", value: :video
end

最後に

ソースコード読解を通して以下について大まかな把握ができたので、まとめます。

GraphQL-RubyのEnum classes are never instantiated and their methods are never called.とは何か

最初に説明した通り、GraphQL::Schema::Enumはインスタンスメソッドを持ちません。また、クラスを定義した時にenum_value_class(GraphQL::Schema::EnumValue)を呼び出すことでDSL風にEnumValueの設定できるようにしています。これらは、全てクラスメソッドで完結しているため仮にインスタンスメソッドを自前に実装したとしても呼び出されることがないことを示唆しています。

憶測ではありますが、Enumは他のオブジェクトから影響を受けるべきではありません。よって他のオブジェクトによって汚染されやすいインスタンスメソッドでは、クラスメソッドのみで完結するようにしているのではないかと思いました。

初めてのGraphQL ―Webサービスを作って学ぶ新世代API

初めてのGraphQL ―Webサービスを作って学ぶ新世代API

  • 作者:Eve Porcello,Alex Banks
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2019/11/13
  • メディア: 単行本(ソフトカバー)

メタプログラミングRuby 第2版

メタプログラミングRuby 第2版

  • 作者:Paolo Perrotta
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2015/10/10
  • メディア: 大型本