Ruby-generierter Code-Leitfaden
Sie sollten die Sprachleitfäden für proto2, proto3 oder Editionen lesen, bevor Sie dieses Dokument lesen.
Compiler-Aufruf
Der Protokollpuffer-Compiler erzeugt Ruby-Ausgabe, wenn er mit dem Kommandozeilenflag --ruby_out= aufgerufen wird. Der Parameter für die Option --ruby_out= ist das Verzeichnis, in dem Sie möchten, dass der Compiler Ihre Ruby-Ausgabe schreibt. Der Compiler erstellt eine .rb-Datei für jede .proto-Eingabedatei. Die Namen der Ausgabedateien werden berechnet, indem der Name der .proto-Datei genommen und zwei Änderungen vorgenommen werden.
- Die Erweiterung (
.proto) wird durch_pb.rbersetzt. - Der Proto-Pfad (angegeben mit dem Kommandozeilenflag
--proto_path=oder-I) wird durch den Ausgabepfad (angegeben mit dem Flag--ruby_out=) ersetzt.
Nehmen wir also zum Beispiel an, Sie rufen den Compiler wie folgt auf:
protoc --proto_path=src --ruby_out=build/gen src/foo.proto src/bar/baz.proto
Der Compiler liest die Dateien src/foo.proto und src/bar/baz.proto und erzeugt zwei Ausgabedateien: build/gen/foo_pb.rb und build/gen/bar/baz_pb.rb. Der Compiler erstellt das Verzeichnis build/gen/bar automatisch, falls erforderlich, aber er erstellt nicht build oder build/gen; diese müssen bereits existieren.
Pakete (Packages)
Der im .proto-File definierte Paketname wird verwendet, um eine Modulstruktur für die generierten Nachrichten zu erstellen. Gegeben sei eine Datei wie
package foo_bar.baz;
message MyMessage {}
Der Protokollcompiler generiert eine Ausgabennachricht mit dem Namen FooBar::Baz::MyMessage.
Enthält die .proto-Datei jedoch die Option ruby_package, wie hier
option ruby_package = "Foo::Bar";
Dann hat die generierte Ausgabe Vorrang für die Option ruby_package und generiert Foo::Bar::MyMessage.
Nachrichten
Angesichts einer einfachen Nachrichten Deklaration:
message Foo {}
Der Protocol Buffer Compiler generiert eine Klasse namens Foo. Die generierte Klasse erbt von der Ruby-Klasse Object (Protos haben keine gemeinsame Basisklasse). Im Gegensatz zu C++ und Java wird der Ruby-generierte Code nicht von der Option optimize_for in der .proto-Datei beeinflusst; effektiv ist aller Ruby-Code auf Code-Größe optimiert.
Sie sollten nicht Ihre eigenen Foo-Unterklassen erstellen. Generierte Klassen sind nicht für die Unterklassenbildung konzipiert und können zu Problemen mit "fragilen Basisklassen" führen.
Ruby-Nachrichtenklassen definieren Accessoren für jedes Feld und bieten außerdem die folgenden Standardmethoden.
Message#dup,Message#clone: Führt eine flache Kopie dieser Nachricht durch und gibt die neue Kopie zurück.Message#==: Führt einen tiefen Gleichheitsvergleich zwischen zwei Nachrichten durch.Message#hash: Berechnet einen flachen Hash des Nachrichtenwertes.Message#to_hash,Message#to_h: Konvertiert das Objekt in ein RubyHash-Objekt. Nur die oberste Nachricht wird konvertiert.Message#inspect: Gibt einen menschenlesbaren String zurück, der diese Nachricht repräsentiert.Message#[],Message#[]=: Ruft ein Feld anhand seines String-Namens ab oder setzt es. Zukünftig wird dies wahrscheinlich auch zum Abrufen/Setzen von Erweiterungen verwendet.
Die Nachrichtenklassen definieren außerdem die folgenden Methoden als statisch. (Im Allgemeinen bevorzugen wir statische Methoden, da reguläre Methoden mit Feldnamen, die Sie in Ihrer .proto-Datei definiert haben, kollidieren können.)
Message.decode(str): Dekodiert einen binären protobuf für diese Nachricht und gibt ihn in einer neuen Instanz zurück.Message.encode(proto): Serialisiert ein Nachrichtenobjekt dieser Klasse in einen binären String.Message.decode_json(str): Dekodiert einen JSON-Text-String für diese Nachricht und gibt ihn in einer neuen Instanz zurück.Message.encode_json(proto): Serialisiert ein Nachrichtenobjekt dieser Klasse in einen JSON-Text-String.Message.descriptor: Gibt dasGoogle::Protobuf::Descriptor-Objekt für diese Nachricht zurück.
Wenn Sie eine Nachricht erstellen, können Sie Felder bequem im Konstruktor initialisieren. Hier ist ein Beispiel für die Erstellung und Verwendung einer Nachricht.
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
Verschachtelte Typen
Eine Nachricht kann innerhalb einer anderen Nachricht deklariert werden. Zum Beispiel:
message Foo {
message Bar { }
}
In diesem Fall wird die Klasse Bar als Klasse innerhalb von Foo deklariert, so dass Sie auf sie als Foo::Bar verweisen können.
Felder
Für jedes Feld in einem Nachrichtentyp gibt es Accessor-Methoden zum Setzen und Abrufen des Feldes. Bei einem Feld foo können Sie also schreiben.
message.foo = get_value()
print message.foo
Immer wenn Sie ein Feld setzen, wird der Wert auf den deklarierten Typ dieses Feldes überprüft. Wenn der Wert den falschen Typ hat (oder außerhalb des Bereichs liegt), wird eine Ausnahme ausgelöst.
Singuläre Felder
Bei singulären primitiven Feldern (Zahlen, Strings und Booleans) sollte der Wert, den Sie dem Feld zuweisen, vom richtigen Typ sein und im entsprechenden Bereich liegen.
- Zahltypen: Der Wert sollte ein
Fixnum,BignumoderFloatsein. Der zugewiesene Wert muss exakt im Zieltyp darstellbar sein. Das Zuweisen von1.0zu einem int32-Feld ist also in Ordnung, aber das Zuweisen von1.2nicht. - Boolesche Felder: Der Wert muss
trueoderfalsesein. Andere Werte werden nicht implizit in true/false konvertiert. - Bytes-Felder: Der zugewiesene Wert muss ein
String-Objekt sein. Die Protobuf-Bibliothek dupliziert den String, konvertiert ihn in die Kodierung ASCII-8BIT und friert ihn ein. - String-Felder: Der zugewiesene Wert muss ein
String-Objekt sein. Die Protobuf-Bibliothek dupliziert den String, konvertiert ihn in die UTF-8-Kodierung und friert ihn ein.
Es werden keine automatischen Aufrufe von #to_s, #to_i usw. zur automatischen Konvertierung vorgenommen. Sie sollten die Werte bei Bedarf zuerst selbst konvertieren.
Präsenzprüfung
Die explizite Feldpräsenz wird durch das Feature field_presence (in Editionen), das Schlüsselwort optional (in proto2/proto3) und den Feldtyp (Nachrichten- und Oneof-Felder haben immer eine explizite Präsenz) bestimmt. Wenn ein Feld Präsenz hat, können Sie prüfen, ob das Feld in einer Nachricht gesetzt ist, indem Sie eine generierte Methode has_...? aufrufen. Das Setzen eines beliebigen Wertes – auch des Standardwertes – markiert das Feld als vorhanden. Felder können durch Aufrufen einer anderen generierten Methode clear_... gelöscht werden.
Zum Beispiel für eine Nachricht MyMessage mit einem int32-Feld foo.
message MyMessage {
int32 foo = 1;
}
Die Präsenz von foo kann wie folgt überprüft werden.
m = MyMessage.new
raise if m.has_foo?
m.foo = 0
raise unless m.has_foo?
m.clear_foo
raise if m.has_foo?
Singuläre Nachrichtenfelder
Unter-Nachrichtenfelder haben immer Präsenz, unabhängig davon, ob sie als optional markiert sind. Ungesetzte Unter-Nachrichtenfelder geben nil zurück, sodass Sie immer erkennen können, ob die Nachricht explizit gesetzt wurde oder nicht. Um ein Unter-Nachrichtenfeld zu löschen, setzen Sie seinen Wert explizit auf nil.
if message.submessage_field.nil?
puts "Submessage field is unset."
else
message.submessage_field = nil
puts "Cleared submessage field."
end
Zusätzlich zum Vergleichen und Zuweisen von nil haben generierte Nachrichten has_...- und clear_...-Methoden, die sich genauso verhalten wie bei Basistypen.
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
Wenn Sie eine Unter-Nachricht zuweisen, muss es sich um ein generiertes Nachrichtenobjekt des richtigen Typs handeln.
Es ist möglich, Nachrichtenzyklen zu erstellen, wenn Sie Unter-Nachrichten zuweisen. Zum Beispiel.
// foo.proto
message RecursiveMessage {
RecursiveMessage submessage = 1;
}
# test.rb
require 'foo'
message = RecursiveMessage.new
message.submessage = message
Wenn Sie versuchen, dies zu serialisieren, erkennt die Bibliothek den Zyklus und kann ihn nicht serialisieren.
Wiederholte Felder
Wiederholte Felder werden mit einer benutzerdefinierten Klasse Google::Protobuf::RepeatedField dargestellt. Diese Klasse verhält sich wie ein Ruby Array und integriert Enumerable. Im Gegensatz zu einem regulären Ruby-Array wird RepeatedField mit einem bestimmten Typ konstruiert und erwartet, dass alle Array-Elemente den richtigen Typ haben. Die Typen und Bereiche werden genau wie bei Nachrichtenfeldern geprüft.
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?
Für wiederholte Felder, die Nachrichten enthalten, unterstützt der Konstruktor von Google::Protobuf::RepeatedField eine Variante mit drei Argumenten: :message, die Klasse der Unter-Nachricht und die zu setzenden Werte.
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
Der RepeatedField-Typ unterstützt alle Methoden eines regulären Ruby Array. Sie können ihn mit repeated_field.to_a in ein reguläres Ruby Array konvertieren.
Im Gegensatz zu singulären Feldern werden für wiederholte Felder niemals has_...?-Methoden generiert.
Map-Felder
Map-Felder werden mit einer speziellen Klasse dargestellt, die sich wie ein Ruby Hash verhält (Google::Protobuf::Map). Im Gegensatz zu einem regulären Ruby Hash wird Map mit einem bestimmten Typ für Schlüssel und Wert konstruiert und erwartet, dass alle Schlüssel und Werte des Maps den richtigen Typ haben. Die Typen und Bereiche werden genau wie bei Nachrichtenfeldern und RepeatedField-Elementen geprüft.
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
Aufzählungen (Enums)
Da Ruby keine nativen Enums hat, erstellen wir für jedes Enum ein Modul mit Konstanten zur Definition der Werte. Gegeben sei die .proto-Datei.
message Foo {
enum SomeEnum {
VALUE_A = 0;
VALUE_B = 5;
VALUE_C = 1234;
}
SomeEnum bar = 1;
}
Sie können Enum-Werte wie folgt referenzieren.
print Foo::SomeEnum::VALUE_A # => 0
message.bar = Foo::SomeEnum::VALUE_A
Sie können einem Enum-Feld entweder eine Zahl oder ein Symbol zuweisen. Wenn Sie den Wert zurücklesen, ist es ein Symbol, wenn der Enum-Wert bekannt ist, oder eine Zahl, wenn er nicht bekannt ist.
Bei OPEN-Enums, die proto3 verwendet, kann jedem Ganzzahlwert ein Enum zugewiesen werden, auch wenn dieser Wert nicht im Enum definiert ist.
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
Ein Enum-Modul definiert außerdem die folgenden Hilfsmethoden.
Foo::SomeEnum.lookup(number): Sucht die angegebene Zahl und gibt ihren Namen zurück odernil, wenn keiner gefunden wurde. Wenn mehr als ein Name diese Zahl hat, wird der erste zurückgegeben, der definiert wurde.Foo::SomeEnum.resolve(symbol): Gibt die Zahl für diesen Enum-Namen zurück odernil, wenn keiner gefunden wurde.Foo::SomeEnum.descriptor: Gibt den Deskriptor für dieses Enum zurück.
Oneof
Gegeben sei eine Nachricht mit einem Oneof.
message Foo {
oneof test_oneof {
string name = 1;
int32 serial_number = 2;
}
}
Die Ruby-Klasse, die Foo entspricht, hat Member namens name und serial_number mit Accessor-Methoden wie reguläre Felder. Im Gegensatz zu regulären Feldern kann jedoch zu einem Zeitpunkt höchstens eines der Felder in einem Oneof gesetzt sein, sodass das Setzen eines Feldes die anderen löscht.
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
Bei proto2-Nachrichten haben Oneof-Mitglieder auch individuelle has_...?-Methoden.
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?