diff --git a/README.md b/README.md index 7ee78e6..2bbdfcd 100644 --- a/README.md +++ b/README.md @@ -67,23 +67,6 @@ public class NameAndAgeBuilder { return new NameAndAge(name, age); } - @Override - public String toString() { - return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]"; - } - - @Override - public int hashCode() { - return Objects.hash(name, age); - } - - @Override - public boolean equals(Object o) { - return (this == o) || ((o instanceof NameAndAgeBuilder b) - && Objects.equals(name, b.name) - && (age == b.age)); - } - /** * Set a new value for the {@code name} record component in the builder */ @@ -113,6 +96,31 @@ public class NameAndAgeBuilder { public int age() { return age; } + + /** + * Return a stream of the record components as map entries keyed with the component name and the value as the component value + */ + public static Stream> stream(NameAndAge record) { + return Stream.of(new AbstractMap.SimpleEntry<>("name", record.name()), + new AbstractMap.SimpleEntry<>("age", record.age())); + } + + @Override + public String toString() { + return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(name, age); + } + + @Override + public boolean equals(Object o) { + return (this == o) || ((o instanceof NameAndAgeBuilder b) + && Objects.equals(name, b.name) + && (age == b.age)); + } } ``` diff --git a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderMetaData.java b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderMetaData.java index 44d79d5..8c7795f 100644 --- a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderMetaData.java +++ b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderMetaData.java @@ -68,6 +68,15 @@ public interface RecordBuilderMetaData { return "build"; } + /** + * The name to use for the method that returns the record components as a stream + * + * @return build method + */ + default String componentsMethodName() { + return "stream"; + } + /** * Return the comment to place at the top of generated files. Return null or an empty string for no comment. * diff --git a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalProcessor.java b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalProcessor.java index 53c1613..e51bfe9 100644 --- a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalProcessor.java +++ b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalProcessor.java @@ -22,10 +22,13 @@ import javax.annotation.processing.Generated; import javax.lang.model.element.Element; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; +import java.util.AbstractMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.IntStream; +import java.util.stream.Stream; class InternalProcessor { private static final AnnotationSpec generatedAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RecordBuilderProcessor.NAME).build(); @@ -55,6 +58,7 @@ class InternalProcessor { addAllArgsConstructor(); addStaticDefaultBuilderMethod(); addStaticCopyBuilderMethod(); + addStaticComponentsMethod(); addBuildMethod(); addToStringMethod(); addHashCodeMethod(); @@ -296,6 +300,40 @@ class InternalProcessor { builder.addMethod(methodSpec); } + private void addStaticComponentsMethod() { + /* + Adds a static method that converts a record instance into a stream of its component parts + + public static Stream> stream(MyRecord record) { + return Stream.of( + new AbstractMap.SimpleEntry<>("p1", record.p1()), + new AbstractMap.SimpleEntry<>("p2", record.p2()) + ); + } + */ + var codeBuilder = CodeBlock.builder().add("return $T.of(", Stream.class); + IntStream.range(0, recordComponents.size()).forEach(index -> { + if (index > 0) { + codeBuilder.add(",\n "); + } + var name = recordComponents.get(index).name(); + codeBuilder.add("new $T<>($S, record.$L())", AbstractMap.SimpleEntry.class, name, name); + }); + codeBuilder.add(")"); + var mapEntryTypeVariables = ParameterizedTypeName.get(Map.Entry.class, String.class, Object.class); + var mapEntryType = ParameterizedTypeName.get(ClassName.get(Stream.class), mapEntryTypeVariables); + var methodSpec = MethodSpec.methodBuilder(metaData.componentsMethodName()) + .addJavadoc("Return a stream of the record components as map entries keyed with the component name and the value as the component value\n") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(recordClassType.typeName(), "record") + .addAnnotation(generatedAnnotation) + .addTypeVariables(typeVariables) + .returns(mapEntryType) + .addStatement(codeBuilder.build()) + .build(); + builder.addMethod(methodSpec); + } + private void add1Field(ClassType component) { /* For a single record component, add a field similar to: