Ruby で生成されたコードのガイド

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

このドキュメントを読む前に、proto2proto3、またはエディションの言語ガイドを読む必要があります。

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

コンパイラの呼び出し

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

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

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

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

コンパイラはファイル `src/foo.proto` と `src/bar/baz.proto` を読み込み、2 つの出力ファイル `build/gen/foo_pb.rb` と `build/gen/bar/baz_pb.rb` を生成します。コンパイラは必要に応じてディレクトリ `build/gen/bar` を自動的に作成しますが、`build` や `build/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)`: このメッセージのバイナリプロトバッファをデコードし、新しいインスタンスで返します。
  • `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: MyMessage::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

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

単数フィールド

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

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

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

存在チェック

明示的なフィールドの存在は、`field_presence` 機能 (エディションの場合)、`optional` キーワード (proto2/proto3 の場合)、およびフィールドの型 (メッセージと oneof フィールドは常に明示的な存在を持つ) によって決定されます。フィールドが存在する場合、生成された `has_...?` メソッドを呼び出すことで、そのフィールドがメッセージに設定されているかどうかを確認できます。デフォルト値であっても、何らかの値を設定すると、そのフィールドは存在するとしてマークされます。フィールドは、別の生成された `clear_...` メソッドを呼び出すことでクリアできます。

例えば、int32型のフィールド `foo` を持つメッセージ `MyMessage` の場合

message MyMessage {
  int32 foo = 1;
}

`foo` の存在は以下のように確認できます。

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

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

サブメッセージフィールドは、`optional` とマークされているかどうかにかかわらず、常に存在します。未設定のサブメッセージフィールドは `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?
  puts "Submessage field is unset."
else
  message.clear_submessage_field
  raise if message.has_submessage_field?
  puts "Cleared submessage field."
end

サブメッセージを割り当てる際には、正しい型の生成されたメッセージオブジェクトでなければなりません。

サブメッセージを割り当てる際に、メッセージサイクルを作成する可能性があります。例えば、

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

# test.rb
require 'foo'

message = RecursiveMessage.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` のコンストラクタは、3 つの引数を持つバリアントをサポートしています:`:message`、サブメッセージのクラス、および設定する値。

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;
  }
  SomeEnum bar = 1;
}

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

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

数値またはシンボルのいずれかを列挙型フィールドに割り当てることができます。値を読み戻すとき、列挙型値が既知の場合はシンボルとなり、そうでない場合は数値となります。

proto3 で使用される `OPEN` な列挙型では、列挙型で定義されていない値であっても、任意の整数値を列挙型に割り当てることができます。

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)`: この enum 名に対応する数値を返します。見つからない場合は `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?