Ruby 生成コードガイド

プロトコルバッファコンパイラが任意のプロトコル定義に対して生成するメッセージオブジェクトのAPIについて説明します。

このドキュメントを読む前に、proto2 または proto3 の言語ガイドを読むことをお勧めします。

Ruby 用のプロトコルコンパイラは、メッセージスキーマを定義するために DSL を使用する Ruby ソースファイルを出力します。しかし、この DSL は変更される可能性があります。このガイドでは、生成されるメッセージの API のみを説明し、DSL については説明しません。

コンパイラの呼び出し

プロトコルバッファコンパイラは、--ruby_out= コマンドラインフラグを指定して呼び出されると、Ruby 出力を生成します。--ruby_out= オプションのパラメータは、コンパイラが Ruby 出力を書き込むディレクトリです。コンパイラは、入力された各 .proto ファイルに対して .rb ファイルを作成します。出力ファイルの名前は、.proto ファイルの名前から次の2つの変更を行って計算されます。

  • 拡張子 (.proto) が _pb.rb に置き換えられます。
  • プロトパス (--proto_path= または -I コマンドラインフラグで指定) が、出力パス (--ruby_out= フラグで指定) に置き換えられます。

例えば、次のようにコンパイラを呼び出すとします。

protoc --proto_path=src --ruby_out=build/gen src/foo.proto src/bar/baz.proto

コンパイラは src/foo.protosrc/bar/baz.proto のファイルを読み込み、build/gen/foo_pb.rbbuild/gen/bar/baz_pb.rb の2つの出力ファイルを生成します。コンパイラは必要に応じてディレクトリ build/gen/bar を自動的に作成しますが、buildbuild/gen は作成しません。これらはすでに存在している必要があります。

パッケージ

.proto ファイルで定義されたパッケージ名が、生成されたメッセージのモジュール構造を生成するために使用されます。次のようなファイルの場合:

package foo_bar.baz;

message MyMessage {}

プロトコルコンパイラは、FooBar::Baz::MyMessage という名前の出力メッセージを生成します。

しかし、.proto ファイルが次のように ruby_package オプションを含んでいる場合:

option ruby_package = "Foo::Bar";

その場合、生成される出力は ruby_package オプションを優先し、Foo::Bar::MyMessage を生成します。

メッセージ

次のような単純なメッセージ宣言の場合:

message Foo {}

プロトコルバッファコンパイラは、Foo というクラスを生成します。生成されたクラスは Ruby の Object クラスから派生します (プロトには共通の基底クラスはありません)。C++ や Java とは異なり、Ruby の生成コードは .proto ファイルの optimize_for オプションの影響を受けません。実際には、すべての Ruby コードはコードサイズのために最適化されます。

独自の Foo サブクラスを作成するべきではありません。生成されたクラスはサブクラス化を目的として設計されておらず、「脆い基底クラス」の問題につながる可能性があります。

Ruby のメッセージクラスは、各フィールドのアクセサを定義し、以下の標準メソッドも提供します。

  • Message#dup, Message#clone: このメッセージのシャローコピーを実行し、新しいコピーを返します。
  • Message#==: 2つのメッセージ間でディープな等価比較を実行します。
  • Message#hash: メッセージの値のシャローハッシュを計算します。
  • Message#to_hash, Message#to_h: オブジェクトを Ruby の Hash オブジェクトに変換します。トップレベルのメッセージのみが変換されます。
  • Message#inspect: このメッセージを表す人間が読める文字列を返します。
  • Message#[], Message#[]=: 文字列名でフィールドを取得または設定します。将来的には、拡張の取得/設定にも使用される可能性があります。

メッセージクラスは、以下のメソッドも静的メソッドとして定義します。(一般的に、通常のメソッドは .proto ファイルで定義したフィールド名と衝突する可能性があるため、静的メソッドを推奨しています。)

  • Message.decode(str): このメッセージのバイナリprotobufをデコードし、新しいインスタンスで返します。
  • Message.encode(proto): このクラスのメッセージオブジェクトをバイナリ文字列にシリアライズします。
  • Message.decode_json(str): このメッセージのJSONテキスト文字列をデコードし、新しいインスタンスで返します。
  • Message.encode_json(proto): このクラスのメッセージオブジェクトをJSONテキスト文字列にシリアライズします。
  • Message.descriptor: このメッセージの Google::Protobuf::Descriptor オブジェクトを返します。

メッセージを作成する際、コンストラクタでフィールドを便利に初期化できます。以下は、メッセージを構築して使用する例です。

message = MyMessage.new(:int_field => 1,
                        :string_field => "String",
                        :repeated_int_field => [1, 2, 3, 4],
                        :submessage_field => SubMessage.new(:foo => 42))
serialized = MyMessage.encode(message)

message2 = MyMessage.decode(serialized)
raise unless message2.int_field == 1

ネストされた型

メッセージは別のメッセージの内部で宣言できます。例:

message Foo {
  message Bar { }
}

この場合、Bar クラスは Foo の内部でクラスとして宣言されるため、Foo::Bar として参照できます。

フィールド

メッセージ型の各フィールドには、フィールドを設定および取得するためのアクセサメソッドがあります。したがって、フィールド foo がある場合、次のように記述できます。

message.foo = get_value()
print message.foo

フィールドを設定するたびに、その値は宣言されたフィールドの型に対して型チェックされます。値が間違った型である(または範囲外である)場合、例外がスローされます。

単数フィールド

単一のプリミティブフィールド(数値、文字列、ブール値)の場合、フィールドに割り当てる値は正しい型であり、適切な範囲内である必要があります。

  • 数値型: 値は FixnumBignum、または Float である必要があります。割り当てる値は、ターゲット型で正確に表現できる必要があります。したがって、int32 フィールドに 1.0 を割り当てるのは問題ありませんが、1.2 を割り当てることはできません。
  • ブール型フィールド: 値は true または false である必要があります。他の値が暗黙的に true/false に変換されることはありません。
  • バイト型フィールド: 割り当てられる値は String オブジェクトである必要があります。protobuf ライブラリは文字列を複製し、ASCII-8BIT エンコーディングに変換して凍結します。
  • 文字列型フィールド: 割り当てられる値は String オブジェクトである必要があります。protobuf ライブラリは文字列を複製し、UTF-8 エンコーディングに変換して凍結します。

自動的な変換を実行するための #to_s#to_i などの自動呼び出しは行われません。必要に応じて、まず値を自分で変換する必要があります。

存在チェック

optional フィールドを使用する場合、フィールドの有無は生成された has_...? メソッドを呼び出すことによってチェックされます。値を設定する(デフォルト値であっても)と、そのフィールドは存在するとマークされます。フィールドは、異なる生成された clear_... メソッドを呼び出すことによってクリアできます。たとえば、int32 フィールド foo を持つメッセージ MyMessage の場合:

m = MyMessage.new
raise unless !m.has_foo?
m.foo = 0
raise unless m.has_foo?
m.clear_foo
raise unless !m.has_foo?

単数メッセージフィールド

サブメッセージの場合、設定されていないフィールドは nil を返すため、メッセージが明示的に設定されたかどうかを常に判別できます。サブメッセージフィールドをクリアするには、その値を明示的に nil に設定します。

if message.submessage_field.nil?
  puts "Submessage field is unset."
else
  message.submessage_field = nil
  puts "Cleared submessage field."
end

nil の比較と割り当てに加えて、生成されたメッセージには has_... および clear_... メソッドがあり、これらは基本型と同じように動作します。

if message.has_submessage_field?
  raise unless message.submessage_field == nil
  puts "Submessage field is unset."
else
  raise unless message.submessage_field != nil
  message.clear_submessage_field
  raise unless message.submessage_field == nil
  puts "Cleared submessage field."
end

サブメッセージを割り当てる際は、正しい型の生成されたメッセージオブジェクトである必要があります。

サブメッセージを割り当てる際に、メッセージサイクルを作成することが可能です。例:

// foo.proto
message RecursiveMessage {
  RecursiveMessage submessage = 1;
}

# test.rb

require 'foo'

message = RecursiveSubmessage.new
message.submessage = message

これをシリアライズしようとすると、ライブラリはサイクルを検出し、シリアライズに失敗します。

繰り返しフィールド

繰り返しフィールドは、カスタムクラス Google::Protobuf::RepeatedField を使用して表現されます。このクラスは Ruby の Array のように動作し、Enumerable をミックスインします。通常の Ruby 配列とは異なり、RepeatedField は特定の型で構築され、配列のすべてのメンバーが正しい型であることを期待します。型と範囲は、メッセージフィールドと同じようにチェックされます。

int_repeatedfield = Google::Protobuf::RepeatedField.new(:int32, [1, 2, 3])

raise unless !int_repeatedfield.empty?

# Raises TypeError.
int_repeatedfield[2] = "not an int32"

# Raises RangeError
int_repeatedfield[2] = 2**33

message.int32_repeated_field = int_repeatedfield

# This isn't allowed; the regular Ruby array doesn't enforce types like we need.
message.int32_repeated_field = [1, 2, 3, 4]

# This is fine, since the elements are copied into the type-safe array.
message.int32_repeated_field += [1, 2, 3, 4]

# The elements can be cleared without reassigning.
int_repeatedfield.clear
raise unless int_repeatedfield.empty?

メッセージを含む繰り返しフィールドの場合、Google::Protobuf::RepeatedField のコンストラクタは、:message、サブメッセージのクラス、および設定する値という3つの引数を取るバリアントをサポートしています。

first_message = MySubMessage.new(:foo => 42)
second_message = MySubMessage.new(:foo => 79)

repeated_field = Google::Protobuf::RepeatedField.new(
    :message,
    MySubMessage,
    [first_message, second_message]
)
message.sub_message_repeated_field = repeated_field

RepeatedField 型は、通常の Ruby の Array と同じすべてのメソッドをサポートしています。repeated_field.to_a を使用して、通常の Ruby Array に変換できます。

単数フィールドとは異なり、繰り返しフィールドに対して has_...? メソッドは生成されません。

マップフィールド

マップフィールドは、Ruby の Hash のように動作する特殊なクラス (Google::Protobuf::Map) を使用して表現されます。通常の Ruby ハッシュとは異なり、Map はキーと値に特定の型を指定して構築され、マップのすべてのキーと値が正しい型であることを期待します。型と範囲は、メッセージフィールドや RepeatedField の要素と同様にチェックされます。

int_string_map = Google::Protobuf::Map.new(:int32, :string)

# Returns nil; items is not in the map.
print int_string_map[5]

# Raises TypeError, value should be a string
int_string_map[11] = 200

# Ok.
int_string_map[123] = "abc"

message.int32_string_map_field = int_string_map

列挙型

Ruby にはネイティブな列挙型がないため、各列挙型に対して定数で値を定義するモジュールを作成します。.proto ファイルがある場合:

message Foo {
  enum SomeEnum {
    VALUE_A = 0;
    VALUE_B = 5;
    VALUE_C = 1234;
  }
  optional SomeEnum bar = 1;
}

列挙値は次のように参照できます。

print Foo::SomeEnum::VALUE_A  # => 0
message.bar = Foo::SomeEnum::VALUE_A

列挙型フィールドには、数値またはシンボルのいずれかを割り当てることができます。値を読み戻す際、その列挙値が既知であればシンボルになり、不明であれば数値になります。proto3 はオープンな列挙型セマンティクスを使用するため、列挙型で定義されていない場合でも、任意の数値を列挙型フィールドに割り当てることができます。

message.bar = 0
puts message.bar.inspect  # => :VALUE_A
message.bar = :VALUE_B
puts message.bar.inspect  # => :VALUE_B
message.bar = 999
puts message.bar.inspect  # => 999

# Raises: RangeError: Unknown symbol value for enum field.
message.bar = :UNDEFINED_VALUE

# Switching on an enum value is convenient.
case message.bar
when :VALUE_A
  # ...
when :VALUE_B
  # ...
when :VALUE_C
  # ...
else
  # ...
end

列挙型モジュールは、以下のユーティリティメソッドも定義します。

  • Foo::SomeEnum.lookup(number): 指定された数値を検索し、その名前を返します。見つからなかった場合は nil を返します。この数値を持つ名前が複数ある場合、最初に定義されたものを返します。
  • Foo::SomeEnum.resolve(symbol): この列挙型名の数値を返します。見つからなかった場合は nil を返します。
  • Foo::SomeEnum.descriptor: この列挙型のディスクリプタを返します。

Oneof

oneof を持つメッセージの場合:

message Foo {
  oneof test_oneof {
     string name = 1;
     int32 serial_number = 2;
  }
}

Foo に対応する Ruby クラスには、通常の フィールド と同様にアクセサメソッドを持つ name および serial_number と呼ばれるメンバーがあります。ただし、通常のフィールドとは異なり、oneof のフィールドは一度に1つしか設定できないため、1つのフィールドを設定すると他のフィールドがクリアされます。

message = Foo.new

# Fields have their defaults.
raise unless message.name == ""
raise unless message.serial_number == 0
raise unless message.test_oneof == nil

message.name = "Bender"
raise unless message.name == "Bender"
raise unless message.serial_number == 0
raise unless message.test_oneof == :name

# Setting serial_number clears name.
message.serial_number = 2716057
raise unless message.name == ""
raise unless message.test_oneof == :serial_number

# Setting serial_number to nil clears the oneof.
message.serial_number = nil
raise unless message.test_oneof == nil

proto2 メッセージの場合、oneof メンバーにも個別の has_...? メソッドがあります。

message = Foo.new

raise unless !message.has_test_oneof?
raise unless !message.has_name?
raise unless !message.has_serial_number?
raise unless !message.has_test_oneof?

message.name = "Bender"
raise unless message.has_test_oneof?
raise unless message.has_name?
raise unless !message.has_serial_number?
raise unless !message.has_test_oneof?