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
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ public record ArmoredCore(
```

The following builder class will be generated for the above:
```java
<details>
<summary>Generated Class, (click to expand)</summary>
<pre content="java">
/** Builder class for {@link ArmoredCore} */
public class ArmoredCoreBuilder {
private String coreName = "Steel Haze";
Expand Down Expand Up @@ -105,5 +107,29 @@ public class ArmoredCoreBuilder {
this.ap = ap;
return this;
}
}
</pre>
</details>

## Default Values
Using `@DefaultValue` we can directly write the code to set the default value in the generated builder. This allows us to directly write a value or use static methods to set the default builder state.
```java
@RecordBuilder
public record Defaults(
@DefaultValue("List.of(1,2,3)") List<Integer> list,
@DefaultValue("24") int num,
@DefaultValue("\"string val\"") String str,
@DefaultValue("CustomClass.createDefault()") CustomClass custom) {}
```

This will generate:
```java
public class DefaultsBuilder {
private List<Integer> list = List.of(1,2,3);
private int num = 24;
private String str = "string val";
private CustomClass custom = CustomClass.createDefault();

...the rest of the builder
}
```
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
package io.avaje.recordbuilder.internal;

import static io.avaje.recordbuilder.internal.APContext.elements;
import static io.avaje.recordbuilder.internal.Templates.classTemplate;
import static java.util.stream.Collectors.joining;

import java.text.MessageFormat;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import javax.lang.model.element.RecordComponentElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;

// TODO better name?
public class ClassBodyBuilder {

static String createClassStart(TypeElement type, String typeParams, boolean isImported) {
private ClassBodyBuilder() {}

static String createClassStart(
TypeElement type, String typeParams, boolean isImported, String packageName) {

final var components = type.getRecordComponents();
final var packageName =
elements().getPackageOf(type).getQualifiedName().toString()
+ (isImported ? ".builder" : "");

final var shortName = type.getSimpleName().toString();
if (type.getEnclosingElement() instanceof TypeElement) {
isImported = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,20 +190,6 @@ private static AnnotationMirror getMirror(Element target) {
return null;
}

private static <T> T getValue(
Map<String, AnnotationValue> memberValues,
Map<String, AnnotationValue> defaults,
String name,
Class<T> clazz) {
AnnotationValue av = memberValues.get(name);
if (av == null) av = defaults.get(name);
if (av == null) {
return null;
}
if (clazz.isInstance(av.getValue())) return clazz.cast(av.getValue());
return null;
}

private static <T> List<T> getArrayValues(
Map<String, AnnotationValue> memberValues,
Map<String, AnnotationValue> defaults,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Name;
import javax.lang.model.element.RecordComponentElement;
import javax.lang.model.element.TypeElement;
Expand All @@ -48,22 +49,18 @@ public synchronized void init(ProcessingEnvironment env) {

@Override
public boolean process(Set<? extends TypeElement> tes, RoundEnvironment roundEnv) {

final var globalTypeInitializers =
roundEnv.getElementsAnnotatedWith(typeElement(GlobalPrism.PRISM_TYPE)).stream()
.map(GlobalPrism::getInstanceOn)
.collect(toMap(s -> s.type().toString(), GlobalPrism::value));

InitMap.putAll(globalTypeInitializers);
APContext.setProjectModuleElement(tes, roundEnv);
for (final TypeElement type :
ElementFilter.typesIn(
roundEnv.getElementsAnnotatedWith(typeElement(RecordBuilderPrism.PRISM_TYPE)))) {
if (type.getRecordComponents().isEmpty()) {
for (final TypeElement type : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(typeElement(RecordBuilderPrism.PRISM_TYPE)))) {
if (type.getKind() != ElementKind.RECORD) {
logError(type, "Builders can only be generated for record classes");
continue;
}

readElement(type);
}

Expand All @@ -76,7 +73,6 @@ public boolean process(Set<? extends TypeElement> tes, RoundEnvironment roundEnv

if (roundEnv.processingOver()) {
try (var reader = getModuleInfoReader()) {

ModuleReader.read(reader);
} catch (IOException e) {
// Can't read module, it's whatever
Expand All @@ -90,73 +86,93 @@ private void readElement(TypeElement type) {
}

private void readElement(TypeElement type, boolean isImported) {

final var components = type.getRecordComponents();
final var packageElement = elements().getPackageOf(type);
var unnamed = Utils.isInUnnamedPackage(isImported, packageElement);
final var packageName =
elements().getPackageOf(type).getQualifiedName().toString()
+ (isImported ? ".builder" : "");
unnamed
? ""
: packageElement.getQualifiedName().toString() + (isImported ? ".builder" : "");
final var shortName = type.getSimpleName().toString();

try (var writer =
new Append(createSourceFile(packageName + "." + shortName + "Builder").openWriter())) {
new Append(
createSourceFile((unnamed ? "" : packageName + ".") + shortName + "Builder")
.openWriter())) {

var typeParams =
type.getTypeParameters().stream()
.map(Object::toString)
.collect(joining(", "))
.transform(s -> s.isEmpty() ? s : "<" + s + ">");
writer.append(ClassBodyBuilder.createClassStart(type, typeParams, isImported));
writer.append(ClassBodyBuilder.createClassStart(type, typeParams, isImported, packageName));
final var writeGetters = RecordBuilderPrism.getInstanceOn(type).getters();
methods(writer, typeParams, shortName, components, writeGetters);
methods(new WriteContext(writer, typeParams, shortName, components, writeGetters));
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
}

private void methods(
Append writer,
String typeParams,
String shortName,
List<? extends RecordComponentElement> components,
Boolean writeGetters) {
private record WriteContext(
Append writer,
String typeParams,
String shortName,
List<? extends RecordComponentElement> components,
Boolean writeGetters
) {
boolean getters() {
return Boolean.TRUE.equals(writeGetters);
}

boolean getters = Boolean.TRUE.equals(writeGetters);
void append(String content) {
writer.append(content);
}
}

for (final var element : components) {
private void methods(WriteContext ctx) {
for (final var element : ctx.components()) {
final var type = UType.parse(element.asType());
writer.append(methodSetter(element.getSimpleName(), type.shortType(), shortName, typeParams));
if (getters) {
writer.append(
methodGetter(
element.getSimpleName(),
type.shortType().transform(ProcessorUtils::trimAnnotations),
shortName));
ctx.append(methodSetter(element.getSimpleName(), type.shortType(), ctx.shortName(), ctx.typeParams()));
if (ctx.getters()) {
writeGetter(ctx, element, type);
}

if (APContext.isAssignable(type.mainType(), "java.util.Collection")) {

String param0ShortType = type.param0().shortType();
Name simpleName = element.getSimpleName();
writer.append(
methodAdd(
simpleName.toString(), type.shortType(), shortName, param0ShortType, typeParams));
writeAdd(ctx, element, type);
}

if (APContext.isAssignable(type.mainType(), "java.util.Map")) {

String param0ShortType = type.param0().shortType();
String param1ShortType = type.param1().shortType();
Name simpleName = element.getSimpleName();
writer.append(
methodPut(
simpleName.toString(),
type.shortType().transform(ProcessorUtils::trimAnnotations),
shortName,
param0ShortType,
param1ShortType,
typeParams));
writePut(ctx, element, type);
}
}
writer.append("}");
ctx.append("}");
}

private static void writePut(WriteContext ctx, RecordComponentElement element, UType type) {
String param0ShortType = type.param0().shortType();
String param1ShortType = type.param1().shortType();
Name simpleName = element.getSimpleName();
ctx.append(
methodPut(
simpleName.toString(),
type.shortType().transform(ProcessorUtils::trimAnnotations),
ctx.shortName(),
param0ShortType,
param1ShortType,
ctx.typeParams()));
}

private static void writeAdd(WriteContext ctx, RecordComponentElement element, UType type) {
String param0ShortType = type.param0().shortType();
Name simpleName = element.getSimpleName();
ctx.append(
methodAdd(
simpleName.toString(), type.shortType(), ctx.shortName(), param0ShortType, ctx.typeParams()));
}

private static void writeGetter(WriteContext ctx, RecordComponentElement element, UType type) {
ctx.append(
methodGetter(
element.getSimpleName(),
type.shortType().transform(ProcessorUtils::trimAnnotations),
ctx.shortName()));
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.avaje.recordbuilder.internal;

import java.text.MessageFormat;
import java.util.function.Consumer;

public class Templates {
private Templates() {}
Expand All @@ -20,20 +19,20 @@ static String classTemplate(

return MessageFormat.format(
"""
package {0};
{0}

{1}

/** Builder class for '{'@link {2}'}' */
@Generated("avaje-record-builder")
public class {2}Builder{8} '{'
{3}
private {2}Builder() '{'
'}'

private {2}Builder({4}) '{'
{5}
private {2}Builder() '{'
'}'
"""
+ constructor(constructor)
+ """

/**
* Return a new builder with all fields set to default Java values
Expand All @@ -56,7 +55,7 @@ public class {2}Builder{8} '{'
return new {2}{10}({7});
'}'
""",
packageName,
packageName.isBlank() ? "" : "package " + packageName + ";",
imports,
shortName,
fields,
Expand All @@ -69,6 +68,18 @@ public class {2}Builder{8} '{'
typeParams);
}

private static String constructor(String constructor) {

return constructor.isBlank()
? ""
: """

private {2}Builder({4}) '{'
{5}
'}'
""";
}

static String methodSetter(
CharSequence componentName, String type, String shortName, String typeParams) {
return MessageFormat.format(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
package io.avaje.recordbuilder.internal;

import javax.lang.model.element.PackageElement;
import javax.lang.model.util.ElementFilter;

final class Utils {
private Utils() {}

static boolean isInUnnamedPackage(boolean isImported, final PackageElement packageElement) {
var module = APContext.getProjectModuleElement();

return packageElement.isUnnamed()
|| isImported
&& module.isUnnamed()
&& ElementFilter.packagesIn(module.getEnclosedElements()).stream()
.allMatch(PackageElement::isUnnamed);
}
}
4 changes: 4 additions & 0 deletions blackbox-test-records/src/main/java/Unnamed.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// import io.avaje.recordbuilder.RecordBuilder;
// delete the module-info and uncomment to test
// @RecordBuilder(getters = true)
// public record Unnamed(String line1, String line2, String city, String country) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.avaje.recordbuilder.defaults;

public class CustomClass {

private CustomClass() {}

public static CustomClass createDefault() {
return new CustomClass();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.avaje.recordbuilder.defaults;

import java.util.List;

import io.avaje.recordbuilder.DefaultValue;
import io.avaje.recordbuilder.RecordBuilder;

@RecordBuilder
public record Defaults(
@DefaultValue("List.of(1,2,3)") List<Integer> list,
@DefaultValue("24") int num,
@DefaultValue("\"string val\"") String str,
@DefaultValue("CustomClass.createDefault()") CustomClass custom) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.avaje.recordbuilder.test;

import io.avaje.recordbuilder.RecordBuilder;

@RecordBuilder
public record EmptyRecord() {
// nothing to see here
}