Skip to content

Commit

Permalink
Introduce mechanism for specifying protobuf field numbers
Browse files Browse the repository at this point in the history
Previously, field numbers were always assigned automatically during protobuf
schema generation, based on declaration order of properties in a data class.
It is very easy to modify the data class in a way that changes field numbers
unintentionally, which introduces a breaking change to the protobuf schema.

This commit introduces the annotation `@ProtobufField` as a mechanism for
explicitly providing field numbers, as well as a `FieldNumberStrategy` enum
(used in `@ProtobufGen.fieldNumberStrategy`) to select how field numbers
should be assigned. There's a manual strategy and two automatic strategies.

Further, `@ProtobufGen.reservedFieldNumbers` and `reservedFieldNames` allow
specifying reserved field numbers / names, similarly to the protobuf language
itself. Reserved field numbers and names are verified at compile time.

The fields in the protobuf schema are generated in the field number order,
not in the property declaration order. The serialization/deserialization
code in generated converters also uses field number order. This is to make
sure that a Vert.x-generated and `protoc`-generated serializers produce
outputs that are not just mutually _compatible_, but also binary _identical_.
  • Loading branch information
Ladicek committed Oct 2, 2023
1 parent bdcbc3f commit c7dd68a
Show file tree
Hide file tree
Showing 19 changed files with 818 additions and 43 deletions.
14 changes: 14 additions & 0 deletions vertx-codegen-protobuf/src/converters/generated/dataobjects.proto
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,26 @@ message Address {
float latitude = 3;
}

message Book {
reserved 2;
reserved "title";
string name = 1;
string author = 3;
string isbn = 10;
string genre = 20;
}

enum EnumType {
A = 0;
B = 1;
C = 2;
}

message Person {
string name = 2;
int32 age = 4;
}

message RecursiveItem {
string id = 1;
RecursiveItem childA = 2;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package io.vertx.test.codegen.converter;

import com.google.protobuf.CodedOutputStream;
import com.google.protobuf.CodedInputStream;
import java.io.IOException;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Arrays;
import io.vertx.core.json.JsonObject;
import io.vertx.codegen.protobuf.utils.ExpandableIntArray;
import io.vertx.codegen.protobuf.converters.*;

public class BookProtoConverter {

public static void fromProto(CodedInputStream input, Book obj) throws IOException {
int tag;
while ((tag = input.readTag()) != 0) {
switch (tag) {
case 10: {
obj.setName(input.readString());
break;
}
case 26: {
obj.setAuthor(input.readString());
break;
}
case 82: {
obj.setIsbn(input.readString());
break;
}
case 162: {
obj.setGenre(input.readString());
break;
}
}
}
}

public static void toProto(Book obj, CodedOutputStream output) throws IOException {
ExpandableIntArray cache = new ExpandableIntArray(16);
BookProtoConverter.computeSize(obj, cache, 0);
BookProtoConverter.toProto(obj, output, cache, 0);
}

public static int toProto(Book obj, CodedOutputStream output, ExpandableIntArray cache, int index) throws IOException {
index = index + 1;
if (obj.getName() != null) {
output.writeString(1, obj.getName());
}
if (obj.getAuthor() != null) {
output.writeString(3, obj.getAuthor());
}
if (obj.getIsbn() != null) {
output.writeString(10, obj.getIsbn());
}
if (obj.getGenre() != null) {
output.writeString(20, obj.getGenre());
}
return index;
}

public static int computeSize(Book obj) {
ExpandableIntArray cache = new ExpandableIntArray(16);
BookProtoConverter.computeSize(obj, cache, 0);
return cache.get(0);
}

public static int computeSize(Book obj, ExpandableIntArray cache, final int baseIndex) {
int size = 0;
int index = baseIndex + 1;
if (obj.getName() != null) {
size += CodedOutputStream.computeStringSize(1, obj.getName());
}
if (obj.getAuthor() != null) {
size += CodedOutputStream.computeStringSize(3, obj.getAuthor());
}
if (obj.getIsbn() != null) {
size += CodedOutputStream.computeStringSize(10, obj.getIsbn());
}
if (obj.getGenre() != null) {
size += CodedOutputStream.computeStringSize(20, obj.getGenre());
}
cache.set(baseIndex, size);
return index;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.vertx.test.codegen.converter;

import com.google.protobuf.CodedOutputStream;
import com.google.protobuf.CodedInputStream;
import java.io.IOException;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Arrays;
import io.vertx.core.json.JsonObject;
import io.vertx.codegen.protobuf.utils.ExpandableIntArray;
import io.vertx.codegen.protobuf.converters.*;

public class PersonProtoConverter {

public static void fromProto(CodedInputStream input, Person obj) throws IOException {
int tag;
while ((tag = input.readTag()) != 0) {
switch (tag) {
case 18: {
obj.setName(input.readString());
break;
}
case 32: {
obj.setAge(input.readInt32());
break;
}
}
}
}

public static void toProto(Person obj, CodedOutputStream output) throws IOException {
ExpandableIntArray cache = new ExpandableIntArray(16);
PersonProtoConverter.computeSize(obj, cache, 0);
PersonProtoConverter.toProto(obj, output, cache, 0);
}

public static int toProto(Person obj, CodedOutputStream output, ExpandableIntArray cache, int index) throws IOException {
index = index + 1;
if (obj.getName() != null) {
output.writeString(2, obj.getName());
}
if (obj.getAge() != 0) {
output.writeInt32(4, obj.getAge());
}
return index;
}

public static int computeSize(Person obj) {
ExpandableIntArray cache = new ExpandableIntArray(16);
PersonProtoConverter.computeSize(obj, cache, 0);
return cache.get(0);
}

public static int computeSize(Person obj, ExpandableIntArray cache, final int baseIndex) {
int size = 0;
int index = baseIndex + 1;
if (obj.getName() != null) {
size += CodedOutputStream.computeStringSize(2, obj.getName());
}
if (obj.getAge() != 0) {
size += CodedOutputStream.computeInt32Size(4, obj.getAge());
}
cache.set(baseIndex, size);
return index;
}

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package io.vertx.test.codegen.converter;

import io.vertx.codegen.annotations.DataObject;
import io.vertx.codegen.protobuf.annotations.FieldNumberStrategy;
import io.vertx.codegen.protobuf.annotations.ProtobufGen;

import java.util.Objects;

@DataObject
@ProtobufGen
@ProtobufGen(fieldNumberStrategy = FieldNumberStrategy.PACKED)
public class Address {
private String name;
private Float longitude;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.vertx.test.codegen.converter;

import io.vertx.codegen.annotations.DataObject;
import io.vertx.codegen.protobuf.annotations.FieldNumberStrategy;
import io.vertx.codegen.protobuf.annotations.ProtobufField;
import io.vertx.codegen.protobuf.annotations.ProtobufGen;

import java.util.Objects;

@DataObject
@ProtobufGen(fieldNumberStrategy = FieldNumberStrategy.SEGMENTED, reservedFieldNumbers = 2, reservedFieldNames = "title")
public class Book {
private String name;
private String author;
@ProtobufField(10)
private String isbn;
@ProtobufField(20)
private String genre;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getAuthor() {
return author;
}

public void setAuthor(String author) {
this.author = author;
}

public String getIsbn() {
return isbn;
}

public void setIsbn(String isbn) {
this.isbn = isbn;
}

public String getGenre() {
return genre;
}

public void setGenre(String genre) {
this.genre = genre;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equals(name, book.name) && Objects.equals(author, book.author) && Objects.equals(isbn, book.isbn) && Objects.equals(genre, book.genre);
}

@Override
public int hashCode() {
return Objects.hash(name, author, isbn, genre);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package io.vertx.test.codegen.converter;

import io.vertx.codegen.annotations.VertxGen;
import io.vertx.codegen.protobuf.annotations.FieldNumberStrategy;
import io.vertx.codegen.protobuf.annotations.ProtobufField;
import io.vertx.codegen.protobuf.annotations.ProtobufGen;

@VertxGen
@ProtobufGen
@ProtobufGen(fieldNumberStrategy = FieldNumberStrategy.PACKED)
public enum EnumType {
A,
B,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.vertx.test.codegen.converter;

import io.vertx.codegen.annotations.DataObject;
import io.vertx.codegen.protobuf.annotations.FieldNumberStrategy;
import io.vertx.codegen.protobuf.annotations.ProtobufField;
import io.vertx.codegen.protobuf.annotations.ProtobufGen;

import java.util.Objects;

@DataObject
@ProtobufGen(fieldNumberStrategy = FieldNumberStrategy.MANUAL)
public class Person {
@ProtobufField(2)
private String name;
@ProtobufField(4)
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(name, person.name) && Objects.equals(age, person.age);
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package io.vertx.test.codegen.converter;

import io.vertx.codegen.annotations.DataObject;
import io.vertx.codegen.protobuf.annotations.FieldNumberStrategy;
import io.vertx.codegen.protobuf.annotations.ProtobufGen;

import java.util.Objects;

@DataObject
@ProtobufGen
@ProtobufGen(fieldNumberStrategy = FieldNumberStrategy.PACKED)
public class RecursiveItem {

private String id;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.vertx.test.codegen.converter;

import io.vertx.codegen.annotations.DataObject;
import io.vertx.codegen.protobuf.annotations.FieldNumberStrategy;
import io.vertx.codegen.protobuf.annotations.ProtobufGen;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
Expand All @@ -12,7 +13,7 @@
import java.util.Objects;

@DataObject
@ProtobufGen
@ProtobufGen(fieldNumberStrategy = FieldNumberStrategy.PACKED)
public class User {
private String userName;
private Integer age;
Expand Down
Loading

0 comments on commit c7dd68a

Please sign in to comment.