Ruby 代码生成指南

描述协议缓冲区编译器为任何给定的协议定义生成的消息对象的 API。

在阅读本文档之前,您应该阅读 proto2proto3 的语言指南。

Ruby 的协议编译器会发出 Ruby 源文件,这些文件使用 DSL 来定义消息模式。但是,DSL 仍然可能会发生变化。在本指南中,我们仅描述生成的消息的 API,而不描述 DSL。

编译器调用

当使用 --ruby_out= 命令行标志调用时,协议缓冲区编译器会生成 Ruby 输出。--ruby_out= 选项的参数是您希望编译器写入 Ruby 输出的目录。编译器为每个 .proto 文件输入创建一个 .rb 文件。输出文件的名称是通过获取 .proto 文件的名称并进行两项更改来计算的

  • 扩展名 (.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.protosrc/bar/baz.proto 并生成两个输出文件:build/gen/foo_pb.rbbuild/gen/bar/baz_pb.rb。如果需要,编译器将自动创建目录 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 类(protos 没有公共基类)。与 C++ 和 Java 不同,Ruby 生成的代码不受 .proto 文件中 optimize_for 选项的影响;实际上,所有 Ruby 代码都针对代码大小进行了优化。

不应创建自己的 Foo 子类。生成的类并非为子类化而设计,可能会导致“脆弱的基类”问题。

Ruby 消息类为每个字段定义了访问器,并且还提供了以下标准方法

  • Message#dup, Message#clone:对此消息执行浅拷贝并返回新副本。
  • Message#==:在两个消息之间执行深度相等比较。
  • 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

每当您设置字段时,都会根据该字段的声明类型对值进行类型检查。如果值的类型错误(或超出范围),则会引发异常。

奇异字段

对于奇异原始字段(数字、字符串和布尔值),您分配给字段的值应为正确的类型,并且必须在适当的范围内

  • 数字类型:值应为 FixnumBignumFloat。您分配的值必须在目标类型中完全可表示。因此,将 1.0 分配给 int32 字段是可以的,但分配 1.2 则不行。
  • 布尔字段:值必须为 truefalse。没有其他值会隐式转换为 true/false。
  • Bytes 字段:分配的值必须是 String 对象。protobuf 库将复制字符串,将其转换为 ASCII-8BIT 编码,并冻结它。
  • String 字段:分配的值必须是 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?

RepeatedField 类型支持与常规 Ruby Array 相同的所有方法。您可以使用 repeated_field.to_a 将其转换为常规 Ruby 数组。

与奇异字段不同,永远不会为重复字段生成 has_...? 方法。

Map 字段

Map 字段使用一个特殊的类表示,该类充当 Ruby Hash (Google::Protobuf::Map)。与常规 Ruby 哈希不同,Map 使用键和值的特定类型构造,并期望所有 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 类将具有名为 nameserial_number 的成员,以及与常规 字段 类似的访问器方法。但是,与常规字段不同,oneof 中的字段一次最多只能设置一个,因此设置一个字段将清除其他字段。

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?