Skip to content

Commit

Permalink
Validation option (#3)
Browse files Browse the repository at this point in the history
* Add options to generator params

* Add validate method option
  • Loading branch information
junkfactory authored Aug 3, 2024
1 parent f2ef10c commit 0839006
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 65 deletions.
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@

# An opinionated Java Inner Builder Generator

This is a simple Java Inner Builder Generator IntelliJ plugin that generates a
This is an opinionated but simple Java Inner Builder Generator IntelliJ plugin that generates a
builder for a given class. The builder is an inner class of the class it is building.

Based from [InnerBuilder](https://github.com/analytically/innerbuilder) with stripped down features.

Generates a builder class for a given class with the following features:

1. Generates builder method for final fields that are not initialized and/or static fields
2. Generates static `builder()` method inside the parent class
3. Uses field names as setters in the builder

Optional features:

1. Generates `toBuilder()` method to convert the object to a builder
2. Generates `validate()` method to validate the fields before building the object

<!-- Plugin description end -->

```java
Expand All @@ -25,7 +37,6 @@ public class Person {
return new Builder();
}


public static final class Builder {
private int age;
private String lastName;
Expand All @@ -43,14 +54,18 @@ public class Person {
return this;
}

private void validate() {
}

public Person build() {
validate();
return new Person(this);
}
}
}
```

Supports Java record classes and automatically detects the class visibility
Supports Java record classes

```java
record Address(String street, String city, String state, String country) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.junkfactory.innerbuilder;

import com.github.junkfactory.innerbuilder.ui.JavaInnerBuilderOption;
import com.intellij.codeInsight.generation.PsiFieldMember;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
Expand All @@ -11,7 +12,6 @@
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand All @@ -34,31 +34,47 @@ class BuilderClassGenerator extends AbstractGenerator {

@Override
public void run() {
var selectedFields = generatorParams.selectedFields();
//builder constructor
var builderConstructor = generateBuilderConstructor();
addMethod(builderClass, null, builderConstructor, false);

//build setters
var selectedFields = generatorParams.psi().selectedFields();
var fieldMembers = new ArrayList<PsiFieldMember>();
PsiElement lastAddedField = null;
for (var fieldMember : selectedFields) {
lastAddedField = findOrCreateField(builderClass, fieldMember, lastAddedField);
fieldMembers.add(fieldMember);
}
//builder constructor
var builderConstructor = generateBuilderConstructor();
addMethod(builderClass, null, builderConstructor, false);

// builder methods
PsiElement lastAddedElement = null;
for (var member : fieldMembers) {
var setterMethod = generateBuilderSetter(builderType, member);
lastAddedElement = addMethod(builderClass, lastAddedElement, setterMethod, false);
}

//build validate method
var options = generatorParams.options();
if (options.contains(JavaInnerBuilderOption.WITH_VALIDATE_METHOD)) {
var validateMethod = generateValidateMethod();
addMethod(builderClass, lastAddedElement, validateMethod, false);
}

// builder.build() method
var buildMethod = generateBuildMethod(targetClass, selectedFields);
addMethod(builderClass, lastAddedElement, buildMethod, targetClass.isRecord());
var buildMethod = generateBuildMethod();
addMethod(builderClass, null, buildMethod, targetClass.isRecord());
}

private PsiMethod generateValidateMethod() {
var psiElementFactory = generatorParams.psi().factory();
var voidType = psiElementFactory.createPrimitiveType("void");
var validateMethod = psiElementFactory.createMethod("validate", voidType);
PsiUtil.setModifierProperty(validateMethod, PsiModifier.PRIVATE, true);
return validateMethod;
}

private PsiMethod generateBuilderConstructor() {
var builderConstructor = generatorParams.psiElementFactory().createConstructor(BUILDER_CLASS_NAME);
var builderConstructor = generatorParams.psi().factory().createConstructor(BUILDER_CLASS_NAME);
PsiUtil.setModifierProperty(builderConstructor, PsiModifier.PRIVATE, true);
return builderConstructor;
}
Expand All @@ -70,7 +86,7 @@ private PsiMethod generateBuilderSetter(final PsiType builderType, final PsiFiel
var fieldName = Utils.hasOneLetterPrefix(field.getName()) ?
Character.toLowerCase(field.getName().charAt(1)) + field.getName().substring(2) : field.getName();

var psiElementFactory = generatorParams.psiElementFactory();
var psiElementFactory = generatorParams.psi().factory();
var setterMethod = psiElementFactory.createMethod(fieldName, builderType);

setterMethod.getModifierList().setModifierProperty(PsiModifier.PUBLIC, true);
Expand All @@ -86,8 +102,8 @@ private PsiMethod generateBuilderSetter(final PsiType builderType, final PsiFiel
return setterMethod;
}

private PsiMethod generateBuildMethod(final PsiClass targetClass, final List<PsiFieldMember> selectedFields) {
var psiElementFactory = generatorParams.psiElementFactory();
private PsiMethod generateBuildMethod() {
var psiElementFactory = generatorParams.psi().factory();
var targetClassType = psiElementFactory.createType(targetClass);
var buildMethod = psiElementFactory.createMethod("build", targetClassType);

Expand All @@ -98,9 +114,14 @@ private PsiMethod generateBuildMethod(final PsiClass targetClass, final List<Psi
.ifPresent(modifier -> PsiUtil.setModifierProperty(buildMethod, modifier, true));

var buildMethodBody = Objects.requireNonNull(buildMethod.getBody());
if (generatorParams.options().contains(JavaInnerBuilderOption.WITH_VALIDATE_METHOD)) {
var validateCall = psiElementFactory.createStatementFromText("validate();", buildMethod);
buildMethodBody.add(validateCall);
}

final PsiStatement returnStatement;
if (targetClass.isRecord()) {
var recordParameters = selectedFields.stream()
var recordParameters = generatorParams.psi().selectedFields().stream()
.map(m -> m.getElement().getName())
.collect(Collectors.joining(", "));
returnStatement = psiElementFactory.createStatementFromText(String.format(
Expand All @@ -109,6 +130,7 @@ private PsiMethod generateBuildMethod(final PsiClass targetClass, final List<Psi
returnStatement = psiElementFactory.createStatementFromText(String.format(
"return new %s(this);", targetClass.getName()), buildMethod);
}

buildMethodBody.add(returnStatement);
return buildMethod;
}
Expand All @@ -119,12 +141,11 @@ private PsiElement findOrCreateField(final PsiClass builderClass, final PsiField
var fieldName = field.getName();
var fieldType = field.getType();
var existingField = builderClass.findFieldByName(fieldName, false);
if (existingField == null ||
Utils.areTypesPresentableNotEqual(existingField.getType(), fieldType)) {
if (existingField == null || Utils.areTypesPresentableNotEqual(existingField.getType(), fieldType)) {
if (existingField != null) {
existingField.delete();
}
var newField = generatorParams.psiElementFactory().createField(fieldName, fieldType);
var newField = generatorParams.psi().factory().createField(fieldName, fieldType);
if (last != null) {
return builderClass.addAfter(newField, last);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,51 @@
package com.github.junkfactory.innerbuilder;

import com.intellij.codeInsight.generation.PsiFieldMember;
import com.github.junkfactory.innerbuilder.ui.JavaInnerBuilderOption;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElementFactory;
import com.intellij.psi.PsiFile;

import java.util.List;
import java.util.Set;

record GeneratorParams(Project project,
PsiFile file,
Editor editor,
List<PsiFieldMember> selectedFields,
PsiElementFactory psiElementFactory) {
PsiParams psi,
Set<JavaInnerBuilderOption> options) {

public static Builder builder() {
return new Builder();
}

public static final class Builder {
private Project project;
private Editor editor;
private PsiParams psi;
private Set<JavaInnerBuilderOption> options;

private Builder() {
}

public Builder project(Project project) {
this.project = project;
return this;
}

public Builder editor(Editor editor) {
this.editor = editor;
return this;
}

public Builder psi(PsiParams psi) {
this.psi = psi;
return this;
}

public Builder options(Set<JavaInnerBuilderOption> options) {
this.options = options;
return this;
}

GeneratorParams build() {
return new GeneratorParams(project, editor, psi, options);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ class InnerBuilderGenerator extends AbstractGenerator {

@Override
public void run() {
var file = generatorParams.file();
var file = generatorParams.psi().file();
var targetClass = Utils.getStaticOrTopLevelClass(file, generatorParams.editor());
if (targetClass == null) {
if (targetClass == null || BUILDER_CLASS_NAME.equals(targetClass.getName())) {
return;
}
var psiElementFactory = generatorParams.psiElementFactory();
var psiElementFactory = generatorParams.psi().factory();
var builderClass = findOrCreateBuilderClass(targetClass);
var builderType = psiElementFactory.createTypeFromText(BUILDER_CLASS_NAME, null);

Expand All @@ -42,9 +42,9 @@ public void run() {
addMethod(targetClass, null, newBuilderMethod, false);

// toBuilder method
var options = JavaInnerBuilderOption.currentOptions();
if (options.contains(JavaInnerBuilderOption.TO_BUILDER)) {
var toBuilderMethod = generateToBuilderMethod(builderType, generatorParams.selectedFields());
var options = generatorParams.options();
if (options.contains(JavaInnerBuilderOption.WITH_TO_BUILDER_METHOD)) {
var toBuilderMethod = generateToBuilderMethod(builderType, generatorParams.psi().selectedFields());
addMethod(targetClass, null, toBuilderMethod, true);
}

Expand All @@ -57,7 +57,7 @@ public void run() {

private PsiMethod generateToBuilderMethod(final PsiType builderType,
final Collection<PsiFieldMember> fields) {
var psiElementFactory = generatorParams.psiElementFactory();
var psiElementFactory = generatorParams.psi().factory();
var toBuilderMethod = psiElementFactory.createMethod(TO_BUILDER_NAME, builderType);
PsiUtil.setModifierProperty(toBuilderMethod, PsiModifier.PUBLIC, true);
var toBuilderBody = Objects.requireNonNull(toBuilderMethod.getBody());
Expand All @@ -74,14 +74,14 @@ private void addCopyBody(final Collection<PsiFieldMember> fields, final PsiMetho
var methodBody = Objects.requireNonNull(method.getBody());
for (final PsiFieldMember member : fields) {
var field = member.getElement();
var assignStatement = generatorParams.psiElementFactory().createStatementFromText(String.format(
var assignStatement = generatorParams.psi().factory().createStatementFromText(String.format(
"%s%2$s = this.%3$s;", "builder.", field.getName(), field.getName()), method);
methodBody.add(assignStatement);
}
}

private PsiMethod generateStaticBuilderMethod(final PsiType builderType) {
var psiElementFactory = generatorParams.psiElementFactory();
var psiElementFactory = generatorParams.psi().factory();
var newBuilderMethod = psiElementFactory.createMethod(BUILDER_METHOD_NAME, builderType);
PsiUtil.setModifierProperty(newBuilderMethod, PsiModifier.STATIC, true);
PsiUtil.setModifierProperty(newBuilderMethod, PsiModifier.PUBLIC, true);
Expand All @@ -94,15 +94,15 @@ private PsiMethod generateStaticBuilderMethod(final PsiType builderType) {
}

private PsiMethod generateConstructor(final PsiClass targetClass, final PsiType builderType) {
var psiElementFactory = generatorParams.psiElementFactory();
var psiElementFactory = generatorParams.psi().factory();
var constructor = psiElementFactory.createConstructor(Objects.requireNonNull(targetClass.getName()));
constructor.getModifierList().setModifierProperty(PsiModifier.PRIVATE, true);

var builderParameter = psiElementFactory.createParameter(BUILDER_METHOD_NAME, builderType);
constructor.getParameterList().add(builderParameter);

var constructorBody = Objects.requireNonNull(constructor.getBody());
for (var member : generatorParams.selectedFields()) {
for (var member : generatorParams.psi().selectedFields()) {
var field = member.getElement();
var setterPrototype = PropertyUtilBase.generateSetterPrototype(field);
var setter = targetClass.findMethodBySignature(setterPrototype, true);
Expand Down Expand Up @@ -143,7 +143,7 @@ private PsiClass findOrCreateBuilderClass(final PsiClass targetClass) {

@NotNull
private PsiClass createBuilderClass(final PsiClass targetClass) {
var builderClass = (PsiClass) targetClass.add(generatorParams.psiElementFactory()
var builderClass = (PsiClass) targetClass.add(generatorParams.psi().factory()
.createClass(BUILDER_CLASS_NAME));
PsiUtil.setModifierProperty(builderClass, PsiModifier.STATIC, true);
PsiUtil.setModifierProperty(builderClass, PsiModifier.FINAL, true);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.github.junkfactory.innerbuilder;

import com.github.junkfactory.innerbuilder.ui.JavaInnerBuilderOption;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.lang.LanguageCodeInsightActionHandler;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Editor;
Expand All @@ -12,6 +14,9 @@
import com.intellij.psi.PsiJavaFile;
import org.jetbrains.annotations.NotNull;

import java.util.EnumSet;
import java.util.Set;

import static com.github.junkfactory.innerbuilder.FieldCollector.collectFields;
import static com.github.junkfactory.innerbuilder.ui.JavaInnerBuilderOptionSelector.selectFieldsAndOptions;

Expand Down Expand Up @@ -64,11 +69,39 @@ public void invoke(@NotNull final Project project, @NotNull final Editor editor,
if (selectedFields.isEmpty()) {
return;
}
var generatorParams = new GeneratorParams(project, file, editor, selectedFields,
JavaPsiFacade.getElementFactory(project));
var psiParams = PsiParams.builder()
.file(file)
.selectedFields(selectedFields)
.factory(JavaPsiFacade.getElementFactory(project))
.build();
var generatorParams = GeneratorParams.builder()
.project(project)
.editor(editor)
.psi(psiParams)
.options(currentOptions())
.build();
var builderGenerator = new InnerBuilderGenerator(generatorParams);
ApplicationManager.getApplication().runWriteAction(builderGenerator);
}
}

private Set<JavaInnerBuilderOption> currentOptions() {
final var options = EnumSet.noneOf(JavaInnerBuilderOption.class);
final var propertiesComponent = PropertiesComponent.getInstance();
for (var option : JavaInnerBuilderOption.values()) {

if (Boolean.TRUE.equals(option.isBooleanProperty())) {
final boolean currentSetting = propertiesComponent.getBoolean(option.getProperty(), false);
if (currentSetting) {
options.add(option);
}
} else {
String currentValue = String.valueOf(propertiesComponent.getValue(option.getProperty()));
if (currentValue != null) {
JavaInnerBuilderOption.findValue(currentValue).ifPresent(options::add);
}
}
}
return options;
}
}
Loading

0 comments on commit 0839006

Please sign in to comment.