OBSERVAÇÃO:este site foi descontinuado. O site será desativado após 31 de janeiro de 2023, e o tráfego será redirecionado para o novo site em https://protobuf.dev. Enquanto isso, as atualizações serão feitas apenas para protobuf.dev.

Código gerado pelo Ruby

Mantenha tudo organizado com as coleções Salve e categorize o conteúdo com base nas suas preferências.

Esta página descreve a API de objetos de mensagem que o compilador de buffer de protocolo gera para qualquer definição de protocolo. Leia os guias de idiomas do arquivo proto2 ou proto3 antes de ler este documento.

O compilador de protocolos do Ruby emite arquivos de origem Ruby que usam uma DSL para definir o esquema da mensagem. No entanto, a DSL ainda está sujeita a mudanças. Neste guia, descrevemos apenas a API das mensagens geradas, e não a DSL.

Invocação do compilador

O compilador de buffer de protocolo produz a saída do Ruby quando invocado com a sinalização de linha de comando --ruby_out=. O parâmetro para a opção --ruby_out= é o diretório em que você quer que o compilador grave a saída do Ruby. O compilador cria um arquivo .rb para cada entrada de arquivo .proto. Os nomes dos arquivos de saída são calculados com base no nome do arquivo .proto e fazendo duas mudanças:

  • A extensão (.proto) foi substituída por _pb.rb.
  • O caminho do proto (especificado com a sinalização de linha de comando --proto_path= ou -I) é substituído pelo caminho de saída (especificado com a sinalização --ruby_out=).

Então, por exemplo, digamos que você invoque o compilador da seguinte maneira:

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

O compilador lerá os arquivos src/foo.proto e src/bar/baz.proto e produzirá dois arquivos de saída: build/gen/foo_pb.rb e build/gen/bar/baz_pb.rb. O compilador criará automaticamente o diretório build/gen/bar, se necessário, mas não criará build ou build/gen. Eles já devem existir.

Entrega de pacotes

O nome do pacote definido no arquivo .proto é usado para gerar uma estrutura de módulos para as mensagens geradas. Dados de um arquivo como este:

package foo_bar.baz;

message MyMessage {}

O compilador do protocolo gera uma mensagem de saída com o nome FooBar::Baz::MyMessage.

Mensagens

Se a declaração for simples:

message Foo {}

O compilador de buffer de protocolo gera uma classe chamada Foo. A classe gerada é derivada da classe Object do Ruby (os protos não têm uma classe base comum). Ao contrário do C++ e Java, o código gerado pelo Ruby não é afetado pela opção optimize_for no arquivo .proto. Na verdade, todo o código do Ruby é otimizado para o tamanho do código.

Não crie suas próprias subclasses Foo. As classes geradas não são projetadas para criar subclasses e podem gerar problemas de "classe base frágil".

As classes de mensagem do Ruby definem os acessadores para cada campo, além de fornecer os seguintes métodos padrão:

  • Message#dup, Message#clone: realiza uma cópia superficial desta mensagem e retorna a nova cópia.
  • Message#==: realiza uma comparação de igualdade profunda entre duas mensagens.
  • Message#hash: calcula um hash superficial do valor da mensagem.
  • Message#to_hash, Message#to_h: converte o objeto em um objeto Hash ruby. Apenas a mensagem de nível superior é convertida.
  • Message#inspect: retorna uma string legível que representa essa mensagem.
  • Message#[], Message#[]=: recebe ou define um campo por nome de string. No futuro, isso provavelmente também será usado para obter/definir extensões.

As classes de mensagem também definem os seguintes métodos como estáticos. Em geral, preferimos métodos estáticos, já que os métodos regulares podem entrar em conflito com os nomes de campos definidos no arquivo .proto.

  • Message.decode(str): decodifica um protobuf binário para essa mensagem e o retorna em uma nova instância.
  • Message.encode(proto): serializa um objeto de mensagem dessa classe para uma string binária.
  • Message.decode_json(str): decodifica uma string de texto JSON para essa mensagem e a retorna em uma nova instância.
  • Message.encode_json(proto): serializa um objeto de mensagem dessa classe para uma string de texto JSON.
  • Message.descriptor: retorna o objeto Google::Protobuf::Descriptor desta mensagem.

Ao criar uma mensagem, você pode inicializar campos no construtor. Veja um exemplo de como criar e usar uma mensagem:

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

Tipos aninhados

Uma mensagem pode ser declarada dentro de outra. Por exemplo: message Foo { message Bar { } }

Nesse caso, a classe Bar é declarada como dentro do Foo. Portanto, você pode se referir a ela como Foo::Bar.

Campos

Para cada campo em um tipo de mensagem, há métodos do acessador para definir e receber o campo. Dessa forma, considerando um campo foo, é possível escrever:

message.foo = get_value()
print message.foo

Sempre que você define um campo, o valor é verificado em relação ao tipo declarado desse campo. Se o valor for do tipo errado (ou estiver fora do intervalo), será gerada uma exceção.

Singular

Para campos primitivos singulares (números, strings e booleanos), o valor atribuído ao campo precisa ser do tipo correto e estar no intervalo apropriado:

  • Tipos de número: o valor deve ser Fixnum, Bignum ou Float. O valor atribuído precisa ser representado exatamente no tipo de destino. Portanto, atribuir 1.0 a um campo int32 é aceitável, mas atribuir 1.2 não.
  • Campos booleanos: o valor precisa ser true ou false. Nenhum outro valor será convertido implicitamente em verdadeiro/falso.
  • Campos de bytes: o valor atribuído precisa ser um objeto String. A biblioteca protobuf vai duplicar a string, converter para a codificação ASCII-8BIT e congelar.
  • Campos de string: o valor atribuído precisa ser um objeto String. A biblioteca protobuf vai duplicar, converter e congelar a string para UTF-8.

Nenhuma chamada automática #to_s, #to_i etc. realizará uma conversão automática. Converta os valores primeiro, se necessário.

Verificando presença

Ao usar campos "opcionais", a presença do campo é verificada chamando um método has_...? gerado. A definição de qualquer valor, até mesmo o valor padrão, marca o campo como presente. Os campos podem ser apagados chamando outro método clear_... gerado. Por exemplo, para uma mensagem MyMessage com um campo 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?

Campos de mensagens individuais

Para submensagens, os campos não definidos retornam nil, então sempre é possível saber se a mensagem foi definida explicitamente ou não. Para limpar um campo de submensagem, defina o valor explicitamente como nil.

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

Além de comparar e atribuir nil, as mensagens geradas têm os métodos has_... e clear_..., que se comportam da mesma forma que os básicos:

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

Quando você atribui uma submensagem, ela precisa ser um objeto de mensagem gerado do tipo correto.

É possível criar ciclos de mensagem quando você atribui submensagens. Por exemplo:

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

# test.rb

require 'foo'

message = RecursiveSubmessage.new
message.submessage = message

Se você tentar serializar isso, a biblioteca detectará o ciclo e não serializará.

Campos repetidos

Os campos repetidos são representados usando uma classe personalizada Google::Protobuf::RepeatedField. Essa classe funciona como um Array Ruby e se mistura em Enumerable. Ao contrário de uma matriz Ruby comum, RepeatedField é construído com um tipo específico e espera que todos os membros da matriz tenham o tipo correto. Os tipos e intervalos são verificados como campos de mensagem.

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?
O tipo RepeatedField oferece suporte aos mesmos métodos que um Array Ruby comum. É possível convertê-lo em uma matriz Ruby regular com repeated_field.to_a.

Ao contrário dos campos singulares, os métodos has_...? nunca são gerados para campos repetidos.

Campos do mapa

Os campos do mapa são representados por uma classe especial que atua como uma Hash (Google::Protobuf::Map) Ruby. Ao contrário de um hash Ruby comum, Map é construído com um tipo específico para a chave e o valor e espera que todas as chaves e valores do mapa tenham o tipo correto. Os tipos e intervalos são verificados como campos de mensagem e elementos 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

Enumerações

Como o Ruby não tem enumerações nativas, criamos um módulo para cada enum com constantes para definir os valores. Considerando o arquivo .proto:

message Foo {
  enum SomeEnum {
    VALUE_A = 0;
    VALUE_B = 5;
    VALUE_C = 1234;
  }
  optional SomeEnum bar = 1;
}
Você pode consultar valores de enumeração da seguinte maneira:
print Foo::SomeEnum::VALUE_A  # => 0
message.bar = Foo::SomeEnum::VALUE_A

Você pode atribuir um número ou símbolo a um campo de enumeração. Ao ler o valor de volta, ele será um símbolo se o valor da enumeração for conhecido ou um número se for desconhecido. Como proto3 usa semântica de enumeração aberta, qualquer número pode ser atribuído a um campo de enumeração, mesmo que ele não tenha sido definido na enumeração.

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
Um módulo de enumeração também define os seguintes métodos utilitários:

  • Enum#lookup(number): procura o número fornecido e retorna o nome dele ou nil, se nenhum for encontrado. Se mais de um nome tiver esse número, retornará o primeiro que foi definido.
  • Enum#resolve(symbol): retorna o número para o nome da enumeração ou nil caso nenhum tenha sido encontrado.
  • Enum#descriptor: retorna o descritor para essa enumeração.

Oneof

Você recebeu uma mensagem com uma destas opções:

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

A classe Ruby correspondente a Foo terá membros com o nome name e serial_number com métodos do acessador, assim como campos comuns. No entanto, ao contrário dos campos normais, no máximo um dos campos em um campo pode ser definido por vez. Portanto, definir um campo limpará os outros.

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

Para mensagens proto2, um dos membros também tem métodos has_...? individuais:

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?