UWAGA: witryna została wycofana. Po 31 stycznia 2023 roku witryna zostanie wyłączona, a ruch będzie kierowany do nowej witryny na https://protobuf.dev. Do tego czasu aktualizacje będą dotyczyć tylko protobuf.dev.

Kod wygenerowany przez Ruby

Zadbaj o dobrą organizację dzięki kolekcji Zapisuj i kategoryzuj treści zgodnie ze swoimi preferencjami.

Ta strona zawiera opis interfejsu API obiektów komunikatów generowanych przez kompilator bufora protokołu w przypadku danej definicji protokołu. Zanim przeczytasz ten dokument, przeczytaj przewodniki po językach proto2 i proto3.

Kompilator protokołu Ruby emituje pliki źródłowe Ruby, które korzystają z DSL do definiowania schematu wiadomości. Jednak DSL nadal może ulec zmianie. W tym przewodniku opisujemy tylko interfejs API wygenerowanych wiadomości, a nie DSL.

Wywołanie kompilatora

Kompilator bufora protokołu generuje wynik Ruby po wywołaniu flagą wiersza poleceń --ruby_out=. Parametr opcji --ruby_out= to katalog, w którym kompilator ma zapisać dane wyjściowe w języku Ruby. Kompilator tworzy plik .rb na każdy wpisany plik .proto. Nazwy plików wyjściowych są obliczane na podstawie nazwy pliku .proto i dwóch zmian:

  • Rozszerzenie (.proto) zostanie zastąpione elementem _pb.rb.
  • Ścieżka proto (oznaczona flagą --proto_path= lub -I) jest zastępowana ścieżką wyjściową (oznaczoną flagą --ruby_out=).

Załóżmy na przykład, że wywołujesz kompilator w ten sposób:

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

Kompilator odczyta pliki src/foo.proto i src/bar/baz.proto, a potem wygeneruje 2 pliki wyjściowe: build/gen/foo_pb.rb i build/gen/bar/baz_pb.rb. Kompilator automatycznie utworzy katalog build/gen/bar, ale nie utworzy build ani build/gen – muszą one już istnieć.

Pakiety

Nazwa pakietu zdefiniowana w pliku .proto służy do wygenerowania struktury modułu dla wygenerowanych wiadomości. Podany plik ma postać:

package foo_bar.baz;

message MyMessage {}

Kompilator protokołu generuje komunikat wyjściowy o nazwie FooBar::Baz::MyMessage.

Wiadomości

Oto prosta deklaracja wiadomości:

message Foo {}

Kompilator bufora protokołu generuje klasę o nazwie Foo. Wygenerowana klasa pochodzi z klasy Object Ruby (proto nie mają wspólnej klasy podstawowej). W przeciwieństwie do C++ i Javy wygenerowany kod Ruby nie ma wpływu na opcję optimize_for w pliku .proto. W rzeczywistości cały kod Ruby jest zoptymalizowany pod kątem rozmiaru kodu.

Nie twórz własnych podkategorii Foo. Wygenerowane zajęcia nie są przeznaczone do stosowania podklasy i mogą prowadzić do problemów z klasą podstawową.

Klasy wiadomości Ruby definiują metody dostępu dla każdego pola. Są też dostępne te metody standardowe:

  • Message#dup, Message#clone: wykonuje płytką kopię tej wiadomości i zwraca nową kopię.
  • Message#==: przeprowadza porównywalnie 2 wiadomości.
  • Message#hash: oblicza płytki hasz wartości wiadomości.
  • Message#to_hash, Message#to_h: konwertuje obiekt na obiekt rubinowy Hash. Przekształcana jest tylko wiadomość najwyższego poziomu.
  • Message#inspect: zwraca ciąg czytelny dla człowieka.
  • Message#[], Message#[]=: pobiera lub ustawia pole według nazwy ciągu. W przyszłości prawdopodobnie będzie też służyć do pobierania i konfigurowania rozszerzeń.

Klasy komunikatów określają też te metody jako statyczne. Ogólnie preferujemy metody statyczne, bo standardowe metody mogą kolidować z nazwami pól określonymi przez Ciebie w pliku .proto.

  • Message.decode(str): dekoduje plik binarny protokołu dla tej wiadomości i zwraca ją w nowej instancji.
  • Message.encode(proto): serializuje obiekt wiadomości tej klasy do ciągu binarnego.
  • Message.decode_json(str): dekoduje ciąg tekstowy JSON dla tej wiadomości i zwraca go w nowej instancji.
  • Message.encode_json(proto): zserializuje obiekt wiadomości tej klasy do ciągu tekstowego JSON.
  • Message.descriptor: zwraca obiekt Google::Protobuf::Descriptor tej wiadomości.

Podczas tworzenia wiadomości możesz w łatwy sposób zainicjować pola w konstruktorze. Oto przykład tworzenia i używania wiadomości:

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

Typy zagnieżdżone

Daną wiadomość można zadeklarować w innej wiadomości. Na przykład: message Foo { message Bar { } }

W tym przypadku klasa Bar jest deklarowana jako klasa wewnątrz klasy Foo, więc możesz nazywać ją Foo::Bar.

Pola

Dla każdego pola w typie wiadomości dostępne są metody dostępu, które można ustawić i pobrać. Biorąc pod uwagę pole foo, możesz napisać:

message.foo = get_value()
print message.foo

Za każdym razem, gdy ustawisz pole, wartość jest sprawdzana pod kątem zadeklarowanego typu tego pola. Jeśli wartość jest nieprawidłowego typu lub jest poza zakresem, zostanie zgłoszony wyjątek.

Pola pojedyncze

W przypadku pojedynczych pól podstawowych (liczb, ciągów znaków i wartości logicznych) wartość, którą przypiszesz do pola, musi być prawidłowego typu i musi być w odpowiednim zakresie:

  • Typy liczb: wartością może być Fixnum, Bignum lub Float. Przypisana wartość musi dokładnie odpowiadać typowi wartości docelowej. Nie można więc przypisać elementu 1.0 do pola int32, ale przypisanie numeru 1.2 jest niedozwolone.
  • Pola wartości logiczne: wartość musi być true lub false. Żadne inne wartości nie zostaną bezpośrednio skonwertowane na true/false.
  • Pola bajtów: przypisana wartość musi być obiektem String. Biblioteka protobuf skopiuje ciąg znaków, przekonwertowa go na kodowanie ASCII-8BIT i zablokuje go.
  • Pola ciągu: przypisana wartość musi być obiektem String. Biblioteka protobuf skopiuje ciąg znaków, przekonwertuje go na kodowanie UTF-8 i zablokuje.

Automatyczne połączenia, np. #to_s, #to_i itp. nie będą wykonywane w celu dokonania konwersji automatycznej. W razie potrzeby musisz najpierw przekonwertować wartości.

Sprawdzam obecność

Jeśli używasz opcjonalnych pól, ich obecność jest sprawdzana przez wywołanie metody has_...?. Ustawienie dowolnej wartości – nawet domyślnej – powoduje oznaczenie pola jako obecnego. Pola można wyczyścić, wywołując inną metodę clear_.... Na przykład w przypadku wiadomości MyMessage z polem int32 foo:

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

Pojedyncze pola wiadomości

W przypadku wiadomości podrzędnych pola nieskonfigurowane będą zwracać wartość nil, dzięki czemu zawsze możesz sprawdzić, czy wiadomość została dokładnie ustawiona. Aby wyczyścić pole komunikatu podrzędnego, ustaw jego wartość na nil.

if message.submessage_field.nil?
  puts "Submessage field is unset."
else
  message.submessage_field = nil
  puts "Cleared submessage field."
end

Oprócz porównywania i przypisywania nil wygenerowane wiadomości używają metod has_... i clear_..., które działają tak samo jak w przypadku typów podstawowych:

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

Gdy przypiszesz wiadomość podrzędną, musi to być wygenerowany obiekt wiadomości odpowiedniego typu.

Przypisując wiadomości podrzędne, możesz tworzyć cykle wiadomości. Na przykład:

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

# test.rb

require 'foo'

message = RecursiveSubmessage.new
message.submessage = message

Jeśli spróbujesz dokonać serializacji, biblioteka wykryje cykl i nie powiedzie się.

Pola powtarzane

Powtórzone pola są reprezentowane przez klasę niestandardową Google::Protobuf::RepeatedField. Te zajęcia działają jak rubinowy Array i mieszają się w Enumerable. W przeciwieństwie do standardowej tablicy Ruby właściwość RepeatedField jest skonstruowana z określonym typem i oczekuje, że wszyscy jej użytkownicy będą mieli poprawny typ. Typy i zakresy są sprawdzane tak samo jak pola wiadomości.

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?
Typ RepeatedField obsługuje te same metody co zwykły kod Ruby Array. Możesz ją przekonwertować na standardową tablicę Ruby z repeated_field.to_a.

W odróżnieniu od pojedynczych pól metody has_...? nie są generowane dla pól powtarzanych.

Pola mapy

Pola mapy są reprezentowane przez specjalną klasę, która działa jak Ruby Hash (Google::Protobuf::Map). W odróżnieniu od zwykłego skrótu Ruby Map jest budowany na podstawie określonego typu klucza i wartości oraz oczekuje, że wszystkie klucze i wartości mapy mają prawidłowy typ. Typy i zakresy są sprawdzane tak samo jak pola wiadomości i elementy 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

Wyliczenia

Ponieważ Ruby nie ma wartości wyliczenia natywnego, tworzymy dla każdego wyliczenia wartość stałą, która określa wartości. Biorąc pod uwagę plik .proto:

message Foo {
  enum SomeEnum {
    VALUE_A = 0;
    VALUE_B = 5;
    VALUE_C = 1234;
  }
  optional SomeEnum bar = 1;
}
Możesz podać wartości wyliczenia w ten sposób:
print Foo::SomeEnum::VALUE_A  # => 0
message.bar = Foo::SomeEnum::VALUE_A

Do pola wyliczenia możesz przypisać liczbę lub symbol. Podczas odczytu wartości ta wartość będzie symbolem, jeśli wyliczenie będzie znane, a liczba, jeśli nie będzie znana. Ponieważ w polu proto3 stosowana jest otwarta semantyka wyliczenia, do pola wyliczenia może być przypisana dowolna liczba, nawet jeśli nie została ona zdefiniowana w wartości wyliczenia.

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
Moduł wyliczenia definiuje również te metody narzędziowe:

  • Enum#lookup(number): wyszukuje określony numer i zwraca jego nazwę. Jeśli nie znaleziono żadnej wartości, zwraca wartość nil. Jeśli więcej niż jedna nazwa ma ten numer, wyświetlana jest ta, która została zdefiniowana.
  • Enum#resolve(symbol): zwraca liczbę dla tej nazwy wyliczenia lub nil, jeśli nie znaleziono żadnej.
  • Enum#descriptor: zwraca deskryptor tego wyliczenia.

Oneof

Pojawiła się wiadomość z jednym z tych numerów:

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

Klasa Ruby odpowiadająca wartości Foo zawiera elementy name i serial_number z metodami dostępu (tak jak zwykłe pola). Jednak w przeciwieństwie do zwykłych pól, jedno z pól w jednym z nich można ustawić na raz, dlatego ustawienie jednego pola spowoduje usunięcie innych.

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

W przypadku wiadomości proto2 jeden z członków ma też indywidualne metody 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?