diff --git a/AGENTS.md b/AGENTS.md index 1fd653bd..12acfa07 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/README.md b/README.md index 8e3e39cb..f53526fc 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/code/protogen-maven-plugin-test/src/main/proto/test4.proto b/code/protogen-maven-plugin-test/src/main/proto/test4.proto index ce078bdf..60fbc4c9 100644 --- a/code/protogen-maven-plugin-test/src/main/proto/test4.proto +++ b/code/protogen-maven-plugin-test/src/main/proto/test4.proto @@ -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; + } +} diff --git a/code/protogen-maven-plugin-test/src/test/java/io/github/adamw7/tools/code/usecase/GeneretedCodeTest.java b/code/protogen-maven-plugin-test/src/test/java/io/github/adamw7/tools/code/usecase/GeneretedCodeTest.java index 5bccc3e4..271d7f32 100644 --- a/code/protogen-maven-plugin-test/src/test/java/io/github/adamw7/tools/code/usecase/GeneretedCodeTest.java +++ b/code/protogen-maven-plugin-test/src/test/java/io/github/adamw7/tools/code/usecase/GeneretedCodeTest.java @@ -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; @@ -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; @@ -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()); + } + } diff --git a/code/protogen-maven-plugin/src/main/java/io/github/adamw7/tools/code/gen/ClassInfo.java b/code/protogen-maven-plugin/src/main/java/io/github/adamw7/tools/code/gen/ClassInfo.java index c290307f..62b9706d 100644 --- a/code/protogen-maven-plugin/src/main/java/io/github/adamw7/tools/code/gen/ClassInfo.java +++ b/code/protogen-maven-plugin/src/main/java/io/github/adamw7/tools/code/gen/ClassInfo.java @@ -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 { @@ -129,6 +130,10 @@ public String getInputPkg() { public String fullName() { return Utils.getClassName(descriptor.getFullName()); } + + public List realOneofs() { + return descriptor.getRealOneofs(); + } public List getPureComplexFields() { return getPureComplexFields(descriptor); diff --git a/code/protogen-maven-plugin/src/main/java/io/github/adamw7/tools/code/gen/Implementations.java b/code/protogen-maven-plugin/src/main/java/io/github/adamw7/tools/code/gen/Implementations.java index bafb22a0..60ca4c51 100644 --- a/code/protogen-maven-plugin/src/main/java/io/github/adamw7/tools/code/gen/Implementations.java +++ b/code/protogen-maven-plugin/src/main/java/io/github/adamw7/tools/code/gen/Implementations.java @@ -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 { @@ -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; } diff --git a/code/protogen-maven-plugin/src/main/java/io/github/adamw7/tools/code/gen/Interfaces.java b/code/protogen-maven-plugin/src/main/java/io/github/adamw7/tools/code/gen/Interfaces.java index 7e2829e0..edf9b319 100644 --- a/code/protogen-maven-plugin/src/main/java/io/github/adamw7/tools/code/gen/Interfaces.java +++ b/code/protogen-maven-plugin/src/main/java/io/github/adamw7/tools/code/gen/Interfaces.java @@ -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 { @@ -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); diff --git a/code/protogen-maven-plugin/src/main/java/io/github/adamw7/tools/code/gen/Methods.java b/code/protogen-maven-plugin/src/main/java/io/github/adamw7/tools/code/gen/Methods.java index c1dbad5d..80391fcf 100644 --- a/code/protogen-maven-plugin/src/main/java/io/github/adamw7/tools/code/gen/Methods.java +++ b/code/protogen-maven-plugin/src/main/java/io/github/adamw7/tools/code/gen/Methods.java @@ -3,6 +3,7 @@ import java.util.List; import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Descriptors.OneofDescriptor; public class Methods { @@ -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) { @@ -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) {