4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Protocol Buffers / Ruby Generated Code(和訳)

Posted at

このページは Protocol Buffers 公式リファレンス Ruby Generated Codeの和訳です。原文はCreative Commons Attribution 4.0 Licenseで公開されており、ソースコードはApache Licenseで公開されています。この訳文もそれにならいます。

Ruby Generated Code

このページでは、プロトコルバッファコンパイラが任意のプロトコル定義に対して生成するメッセージオブジェクトのAPIについて説明します。 このドキュメントを読む前に、proto3言語ガイドを読むことをお勧めします。

現時点ではまだproto3だけしかサポートされていません。proto2のサポートも計画されていますが、まだ利用できません1

Rubyのプロトコルコンパイラはメッセージスキーマを定義するDSLからRubyのソースファイルを生成します。DSLは引き続き変更される可能性があります(特にproto2サポートなどの機能の追加時)が、 このガイドでは、DSLではなく生成されたメッセージのAPIのみを説明します。

コンパイラ呼び出し

プロトコルバッファコンパイラは--ruby_out=というコマンドラインフラグをつけて実行することでRubyのコードを生成します。--ruby_out=オプションにはコンパイラにRubyコードを出力させたいディレクトリを直接指定します。コンパイラはそれぞれの.protoファイルの入力に対して、.rbファイルを作成します。出力ファイルの名前は.protoファイルの名前から取られますが、二点違いがあります。

  • 拡張子 (.proto)は_pb.rbで置き換えられます。
  • (--proto_path=-Iのコマンドラインオプションで指定された)protoのパスは、(--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を自動的に作成しますが、buildまたはbuild/genは作成しません。それらはすでに作成済みである必要があります。

パッケージ

.protoファイルで定義されたパッケージ名は生成されたメッセージ型のモジュール構造を生成するために使われます。

このようなファイルがある場合、

package foo_bar.baz;

message MyMessage {}

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

メッセージ型

このようなシンプルなメッセージ宣言に対して、

message Foo {}

プロトコルバッファコンパイラはFooという名前のクラスを生成します。生成されたクラスはObjectクラスを継承しています(protoで共通の基底クラスはありません)。C++やJavaとは違って、Rubyが生成したコードは.protoファイルのoptimize_forオプションの影響を受けません。実際のところ、Rubyコードの最適化対象は常にコードサイズとなっています

Fooサブクラスを作成すべきではありません。生成されたクラスはサブクラス用に設計されていないため、「脆弱な基底クラス」の問題2を引き起こす可能性があります。

Rubyのメッセージクラスは各フィールドに対するアクセサを定義します。また、次にあげる標準メソッドを提供します。

  • Message#dup, Message#clone: このメッセージのシャローコピーを行い、新しく作られたコピーを返します。
  • Message#==: 2つのメッセージの完全な等価比較を行います。
  • Message#hash: メッセージの値のシャローハッシュ値を計算します。
  • Message#to_hash, Message#to_h: メッセージオブジェクトをRubyのHashオブジェクトに変換します。最上位のメッセージだけが変換されます。
  • Message#inspect: このメッセージを表す可読性のある表現の文字列を返します。
  • Message#[], Message#[]=: 文字列の名前でフィールドを取得または設定します。将来的には、これはおそらくget/set拡張にも使用されるでしょう。

このメッセージクラスはまた次のような静的メソッドも定義します。(通常のメソッドだと.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 => 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として参照できます。

フィールド

メッセージ型の各フィールドには、フィールドに対してget/setするアクセサメソッドがあります。したがって、fooフィールドを指定すると、次のように記述できます。

message.foo = get_value()
print message.foo

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

単数フィールド (proto3)

単数のプリミティブなフィールド(数値、文字列、真偽値)の場合、セットする値はそのフィールドへの正しい型であるべきであり、適切な範囲内でなくてはなりません。

  • 数値: 値はFixnumBignumもしくはFloatであるべきです。 セットする値はそのフィールドで正確に表現できる型である必要があります。なのでint32のフィールドに1.0を入れても構いませんが、1.2は入れられません。
  • 真偽型フィールド: 値はtrueもしくはfalseでなくてはなりません。 他の値の場合、暗黙的にtrue/falseに変換されることはありません。
  • バイト配列フィールド: セットする値はStringオブジェクトである必要があります。 プロトコルバッファライブラリは文字列を複製し、ASCII-8BITエンコーディングに変換し、freezeします。
  • 文字列フィールド: セットする値はStringオブジェクトである必要があります。 プロトコルバッファライブラリは文字列を複製し、UTF8エンコーディングに変換し、freezeします。

自動変換を実行してくれるようおな、自動の#to_s, #to_iなどの呼び出しはありません。もし必要であれば、自分で値を変換する必要があります

proto3は、単数非メッセージフィールドが明示的に設定されているかどうか、確認する方法を提供しないため、戻り値が0 / false / ""である場合は、その値がどこかで設定されたかデフォルト値のままということになります。

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

フィールドの型がメッセージの場合、未設定であればフィールドはnilを返すため、メッセージが明示的に設定されたかどうかをいつでも確認できます。 値を明示的にnilに設定して、そのフィールドをクリアすることもできます。

if message.submessage_field.nil?
  puts "submessage フィールドは未設定です"
else
  message.submessage_field = nil
  puts "submessageフィールドはクリアされました"
end

メッセージをセットするには、正しい型で生成されたメッセージオブジェクトである必要があります。
メッセージをセットする際に、メッセージを循環させることもできます。

例です。

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

# test.rb

require 'foo'

message = RecursiveSubmessage.new
message.submessage = message

もしこれをシリアライズすると、ライブラリは循環を検知して、シリアライズを失敗させます。

Repeatedフィールド

RepeatedフィールドはGoogle::Protobuf::RepeatedFieldのカスタムクラスを使って表現されます。このクラスはRubyのArrayのように振る舞い、Enumerableモジュールをmix-inしています。通常のRuby配列とは異なり、RepeatedFieldは特定の型から初期化され、配列のメンバーは全て正しい型でなくてはなりません。型と範囲はメッセージフィールドと同様にチェックされます。

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

# TypeErrorが発生
int_repeatedfield[2] = "not an int32"

# RangeErrorが発生
int_repeatedfield[2] = 2**33

message.int32_repeated_field = int_repeatedfield

# 許可されない。通常のRuby配列は型を強制できない
message.int32_repeated_field = [1, 2, 3, 4]

# これは通る。様子が安全な型の配列にコピーされるため
message.int32_repeated_field += [1, 2, 3, 4]

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

map フィールド

mapフィールドは、RubyのHashクラスのように動作する特別なクラス(Google::Protobuf::Map)を使って表されます。 通常のHashとは異なり、Mapは決まった型のキーと値で初期化され、Mapのすべてのキーと値は正しい型でなくてはなりません。 型と範囲は、メッセージクラスのフィールドやRepeatedFieldの要素と同様にチェックされます。

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

# mapに要素がない場合、nilを返します
print int_string_map[5]

# 値は文字列でなければいけないのでTypeErrorが発生します
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

# RangeError: 列挙型フィールドに対する未知のシンボル
message.bar = :UNDEFINED_VALUE

# 列挙型の値でswitchさせるのが便利です
case message.bar
when :VALUE_A
  # ...
when :VALUE_B
  # ...
when :VALUE_C
  # ...
else
  # ...
end

Enumモジュールは、次のユーティリティメソッドも定義します。

  • Enum#lookup(number): 数値からラベルを探して返します。もし存在しなければnilを返します。数値に
  • 相当するラベルが複数ある場合、最初に定義されたラベルを返します。
  • Enum#resolve(symbol): ラベルから数値を返します。存在しない場合はnilを返します。
  • Enum#descriptor: この列挙型のdescrptorを返します。

oneof

次のようなoneofを持ったメッセージ型があるとします。

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

Fooに対応するRubyのクラスには、通常のフィールドと同じようなアクセサメソッドを持つnameserial_numberというメンバー変数が含まれます。 ただし、通常のフィールドとは異なり、一度に1つまでしか、oneofのフィールドにには設定できません。そのため、あるフィールドに値を入れると他のフィールドはクリアされます。

message = Foo.new

# フィールドにはデフォルト値が入っている
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

# serial_numberにセットするとnameがクリアされる
message.serial_number = 2716057
raise unless message.name == ""
raise unless message.test_oneof == :serial_number

# serial_numberにnilを入れるとoneofフィールドがクリアされる
message.serial_number = nil
raise unless message.test_oneof == nil
  1. 一部ではあるが3.7.0でリリースされた。https://github.com/protocolbuffers/protobuf/releases/tag/v3.7.0

  2. https://qiita.com/pizyumi/items/15c4e97da3608935e51e

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?