Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ Maven project. The notable capabilities are:
proto3: proto2 `required` fields are enforced by the builder chain, while
proto3 (which has no required fields) generates all-optional builders with
presence-aware `hasXxx()` accessors (only for message fields and explicit
`optional` fields).
`optional` fields). `oneof` groups get a `getXxxCase()` discriminator and a
`clearXxx()` for the whole group, so the selected member is inspectable and
resettable through the builder chain.
- **Context engineering** (`code/context`) — a fast, regex-based finder that
builds the tree of classes used by a given class, plus a `ProjectTreeBuilder`
that scans a whole Java project into a tree of folders, files and
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,12 @@ presence — every singular field in proto2, but in proto3 only message fields a
those declared with the explicit `optional` keyword (implicit-presence proto3
scalars, which have no `hasXxx()`, are left alone).

A `oneof` group additionally gets a `getXxxCase()` accessor returning protobuf's
generated `XxxCase` enum, so you can tell which member is set, plus a
`clearXxx()` that resets the whole group — both reachable through the fluent
builder chain. The synthetic oneofs that back proto3 `optional` fields are not
treated as groups, so no spurious case accessor is generated for them.

## gRPC example

An end-to-end gRPC example combining standard protobuf/gRPC code generation with the compile-time-safe builder generation from this project.
Expand Down
8 changes: 8 additions & 0 deletions code/protogen-maven-plugin-test/src/main/proto/test4.proto
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,11 @@ message Team {
Profile lead = 1;
repeated string members = 2;
}

message Payment {
string reference = 1;
oneof method {
string card = 2;
string cash = 3;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.output.generated.MeasurementBuilder;
import org.output.generated.MyMessageBuilder;
import org.output.generated.OneOptionalFieldOnlyBuilder;
import org.output.generated.PaymentBuilder;
import org.output.generated.ProfileBuilder;
import org.output.generated.ServerBuilder;
import org.output.generated.TeamBuilder;
Expand All @@ -35,6 +36,7 @@
import io.github.adamw7.tools.code.test.MyMessage;
import io.github.adamw7.tools.code.test.Server;
import io.github.adamw7.tools.code.test.Wheel;
import io.github.adamw7.tools.code.test4.Payment;
import io.github.adamw7.tools.code.test4.Profile;
import io.github.adamw7.tools.code.test4.Team;

Expand Down Expand Up @@ -206,4 +208,32 @@ public void proto3MessageFieldAndRepeatedFieldBuild() {
assertTrue(team.hasLead());
}

@Test
public void proto3OneofCaseReflectsSelectedMember() {
PaymentBuilder builder = new PaymentBuilder();
assertEquals(Payment.MethodCase.METHOD_NOT_SET, builder.getMethodCase());

builder.setCard("4111");
assertEquals(Payment.MethodCase.CARD, builder.getMethodCase());
assertTrue(builder.hasCard());
assertFalse(builder.hasCash());

builder.setCash("20");
assertEquals(Payment.MethodCase.CASH, builder.getMethodCase());
assertTrue(builder.hasCash());
assertFalse(builder.hasCard());
}

@Test
public void proto3OneofClearResetsSelection() {
PaymentBuilder builder = new PaymentBuilder();
builder.setReference("order-1").setCard("4111");
assertEquals(Payment.MethodCase.CARD, builder.getMethodCase());

Payment payment = builder.clearMethod().build();
assertEquals(Payment.MethodCase.METHOD_NOT_SET, payment.getMethodCase());
assertEquals("order-1", payment.getReference());
assertFalse(payment.hasCard());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.google.protobuf.Descriptors;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Descriptors.OneofDescriptor;

public class ClassInfo {

Expand Down Expand Up @@ -129,6 +130,10 @@ public String getInputPkg() {
public String fullName() {
return Utils.getClassName(descriptor.getFullName());
}

public List<OneofDescriptor> realOneofs() {
return descriptor.getRealOneofs();
}

public List<FieldDescriptor> getPureComplexFields() {
return getPureComplexFields(descriptor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.List;

import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Descriptors.OneofDescriptor;

public class Implementations extends AbstractStatements {

Expand Down Expand Up @@ -76,7 +77,11 @@ public StringBuilder generateMethods() {
for (FieldDescriptor field : info.optional()) {
builder.append(methods.setter(field, optionalIfcName));
builder.append(methods.has("builder", field));
builder.append(methods.clear("builder", field, optionalIfcName));
builder.append(methods.clear("builder", field, optionalIfcName));
}
for (OneofDescriptor oneof : info.realOneofs()) {
builder.append(methods.oneofCaseGetter("builder", oneof));
builder.append(methods.oneofClear("builder", oneof, optionalIfcName));
}
return builder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.List;

import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Descriptors.OneofDescriptor;

public class Interfaces extends AbstractStatements {

Expand All @@ -26,11 +27,16 @@ public ClassContainer generateOptional() {

for (FieldDescriptor optionalField : info.optional()) {
builder.append(methods.declareSetter(optionalField, optionalIfcName));
builder.append(methods.declareHas(optionalField));
builder.append(methods.declareHas(optionalField));
String clearReturnType = Utils.getNextIfc(info.name(), info.nonOptional(), optionalField);
builder.append(methods.declareClear(optionalField, clearReturnType));
}


for (OneofDescriptor oneof : info.realOneofs()) {
builder.append(methods.declareOneofCaseGetter(oneof));
builder.append(methods.declareOneofClear(oneof, optionalIfcName));
}

builder.append(info.name()).append(" build();}");

return new ClassContainer(optionalIfcName, builder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.List;

import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Descriptors.OneofDescriptor;

public class Methods {

Expand Down Expand Up @@ -65,10 +66,25 @@ private boolean needsHas(FieldDescriptor field) {
}

public StringBuilder clear(String classOrBuilder, FieldDescriptor field, String returnType) {
String fieldName = Utils.toUpperCamelCase(field.getName());
return clearByName(classOrBuilder, field.getName(), returnType);
}

private StringBuilder clearByName(String classOrBuilder, String rawName, String returnType) {
String name = Utils.toUpperCamelCase(rawName);
String impl = returnType.replace(Utils.IFC_SUFFIX, Utils.IMPL_SUFFIX);
return override("public %s clear%s() {%s.clear%s();return new %s(%s);}"
.formatted(returnType, fieldName, classOrBuilder, fieldName, impl, classOrBuilder));
.formatted(returnType, name, classOrBuilder, name, impl, classOrBuilder));
}

public StringBuilder oneofCaseGetter(String classOrBuilder, OneofDescriptor oneof) {
String name = Utils.toUpperCamelCase(oneof.getName());
String caseType = "%s.%sCase".formatted(className, name);
return override("public %s get%sCase() {return %s.get%sCase();}"
.formatted(caseType, name, classOrBuilder, name));
}

public StringBuilder oneofClear(String classOrBuilder, OneofDescriptor oneof, String returnType) {
return clearByName(classOrBuilder, oneof.getName(), returnType);
}

private StringBuilder override(String body) {
Expand All @@ -93,8 +109,22 @@ public StringBuilder declareHas(FieldDescriptor field) {
}

public StringBuilder declareClear(FieldDescriptor field, String returnType) {
return declareClearByName(field.getName(), returnType);
}

private StringBuilder declareClearByName(String rawName, String returnType) {
return new StringBuilder(
"%s clear%s();".formatted(returnType, Utils.toUpperCamelCase(field.getName())));
"%s clear%s();".formatted(returnType, Utils.toUpperCamelCase(rawName)));
}

public StringBuilder declareOneofCaseGetter(OneofDescriptor oneof) {
String name = Utils.toUpperCamelCase(oneof.getName());
return new StringBuilder(
"%s.%sCase get%sCase();".formatted(className, name, name));
}

public StringBuilder declareOneofClear(OneofDescriptor oneof, String returnType) {
return declareClearByName(oneof.getName(), returnType);
}

private String generateSetter(FieldDescriptor field) {
Expand Down
Loading