Ruby 生成コードガイド
このドキュメントを読む前に、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.proto
と src/bar/baz.proto
のファイルを読み込み、build/gen/foo_pb.rb
と build/gen/bar/baz_pb.rb
の2つの出力ファイルを生成します。コンパイラは必要に応じてディレクトリ 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)
: このメッセージのバイナリ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
フィールドを設定するたびに、その値は宣言されたフィールドの型に対して型チェックされます。値が間違った型である(または範囲外である)場合、例外がスローされます。
単数フィールド
単一のプリミティブフィールド(数値、文字列、ブール値)の場合、フィールドに割り当てる値は正しい型であり、適切な範囲内である必要があります。
- 数値型: 値は
Fixnum
、Bignum
、または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?