Add support for generating methods

Add `GeneratedMethods` and `GeneratedMethod` support classes which
can be used during code generation to generation additional methods
that will ultimately be included in a `JavaFile`.

See gh-28414
This commit is contained in:
Phillip Webb
2022-04-13 18:21:50 -07:00
parent 16456342f5
commit 55d7f7a014
7 changed files with 574 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.generate;
import java.util.function.Consumer;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.MethodSpec.Builder;
import org.springframework.util.Assert;
/**
* A generated method.
*
* @author Phillip Webb
* @since 6.0
* @see GeneratedMethods
* @see MethodGenerator
*/
public final class GeneratedMethod {
private final String name;
private MethodSpec spec;
/**
* Create a new {@link GeneratedMethod} instance with the given name. This
* constructor is package-private since names should only be generated via a
* {@link GeneratedMethods}.
* @param name the generated name
*/
GeneratedMethod(String name) {
this.name = name;
}
/**
* Return the generated name of the method.
* @return the name of the generated method
*/
public String getName() {
return this.name;
}
/**
* Return the {@link MethodSpec} for this generated method.
* @return the method spec
* @throws IllegalStateException if one of the {@code generateBy(...)}
* methods has not been called
*/
public MethodSpec getSpec() {
Assert.state(this.spec != null,
() -> String.format("Method '%s' has no method spec defined", this.name));
return this.spec;
}
/**
* Generate the method using the given consumer.
* @param builder a consumer that will accept a method spec builder and
* configure it as necessary
* @return this instance
*/
public GeneratedMethod using(Consumer<MethodSpec.Builder> builder) {
Builder builderToUse = MethodSpec.methodBuilder(this.name);
builder.accept(builderToUse);
MethodSpec spec = builderToUse.build();
assertNameHasNotBeenChanged(spec);
this.spec = spec;
return this;
}
private void assertNameHasNotBeenChanged(MethodSpec spec) {
Assert.isTrue(this.name.toString().equals(spec.name), () -> String
.format("'spec' must use the generated name \"%s\"", this.name));
}
@Override
public String toString() {
return (this.spec != null) ? this.spec.toString() : this.name.toString();
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.generate;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.springframework.javapoet.MethodSpec;
import org.springframework.util.Assert;
/**
* A managed collection of generated methods.
*
* @author Phillip Webb
* @since 6.0
* @see GeneratedMethod
*/
public class GeneratedMethods implements Iterable<GeneratedMethod>, MethodGenerator {
private final MethodNameGenerator methodNameGenerator;
private final List<GeneratedMethod> generatedMethods = new ArrayList<>();
/**
* Create a new {@link GeneratedMethods} instance backed by a new
* {@link MethodNameGenerator}.
*/
public GeneratedMethods() {
this(new MethodNameGenerator());
}
/**
* Create a new {@link GeneratedMethods} instance backed by the given
* {@link MethodNameGenerator}.
* @param methodNameGenerator the method name generator
*/
public GeneratedMethods(MethodNameGenerator methodNameGenerator) {
Assert.notNull(methodNameGenerator, "'methodNameGenerator' must not be null");
this.methodNameGenerator = methodNameGenerator;
}
@Override
public GeneratedMethod generateMethod(Object... methodNameParts) {
return add(methodNameParts);
}
/**
* Add a new {@link GeneratedMethod}. The returned instance must define the
* method spec by calling {@code using(builder -> ...)}.
* @param methodNameParts the method name parts that should be used to
* generate a unique method name
* @return the newly added {@link GeneratedMethod}
*/
public GeneratedMethod add(Object... methodNameParts) {
GeneratedMethod method = new GeneratedMethod(
this.methodNameGenerator.generateMethodName(methodNameParts));
this.generatedMethods.add(method);
return method;
}
/**
* Call the given action with each of the {@link MethodSpec MethodSpecs}
* that have been added to this collection.
* @param action the action to perform
*/
public void doWithMethodSpecs(Consumer<MethodSpec> action) {
stream().map(GeneratedMethod::getSpec).forEach(action);
}
@Override
public Iterator<GeneratedMethod> iterator() {
return this.generatedMethods.iterator();
}
/**
* Return a {@link Stream} of all the methods in this collection.
* @return a stream of {@link GeneratedMethod} instances
*/
public Stream<GeneratedMethod> stream() {
return this.generatedMethods.stream();
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.generate;
/**
* Generates new {@link GeneratedMethod} instances.
*
* @author Phillip Webb
* @since 6.0
* @see GeneratedMethods
*/
@FunctionalInterface
public interface MethodGenerator {
/**
* Generate a new {@link GeneratedMethod}. The returned instance must define
* the method spec by calling {@code using(builder -> ...)}.
* @param methodNameParts the method name parts that should be used to
* generate a unique method name
* @return the newly added {@link GeneratedMethod}
*/
GeneratedMethod generateMethod(Object... methodNameParts);
/**
* Return a new {@link MethodGenerator} instance that generates method with
* additional implicit method name parts. The final generated name will be
* of the following form:
* <p>
* <table border="1">
* <tr>
* <th>Original</th>
* <th>Updated</th>
* </tr>
* <tr>
* <td>run</td>
* <td>&lt;name&gt;Run</td>
* </tr>
* <tr>
* <td>getValue</td>
* <td>get&lt;Name&gt;Value</td>
* </tr>
* <tr>
* <td>setValue</td>
* <td>set&lt;Name&gt;Value</td>
* </tr>
* <tr>
* <td>isEnabled</td>
* <td>is&lt;Name&gt;Enabled</td>
* </tr>
* </table>
* @param nameParts the implicit name parts
* @return a new {@link MethodGenerator} instance
*/
default MethodGenerator withName(Object... nameParts) {
return new MethodGeneratorWithName(this, nameParts);
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.generate;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Internal class used to support {@link MethodGenerator#withName(Object...)}.
*
* @author Phillip Webb
* @since 6.0
*/
class MethodGeneratorWithName implements MethodGenerator {
private static final String[] PREFIXES = { "get", "set", "is" };
private final MethodGenerator methodGenerator;
private final Object[] nameParts;
MethodGeneratorWithName(MethodGenerator methodGenerator, Object[] nameParts) {
this.methodGenerator = methodGenerator;
this.nameParts = nameParts;
}
@Override
public GeneratedMethod generateMethod(Object... methodNameParts) {
return this.methodGenerator.generateMethod(generateName(methodNameParts));
}
private Object[] generateName(Object... methodNameParts) {
String joined = MethodNameGenerator.join(methodNameParts);
String prefix = getPrefix(joined);
String suffix = joined.substring(prefix.length());
Object[] result = this.nameParts;
if (StringUtils.hasLength(prefix)) {
result = ObjectUtils.addObjectToArray(result, prefix, 0);
}
if (StringUtils.hasLength(suffix)) {
result = ObjectUtils.addObjectToArray(result, suffix);
}
return result;
}
private String getPrefix(String name) {
for (String candidate : PREFIXES) {
if (name.startsWith(candidate)) {
return candidate;
}
}
return "";
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.generate;
import javax.lang.model.element.Modifier;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link GeneratedMethod}.
*
* @author Phillip Webb
*/
class GeneratedMethodTests {
private static final String NAME = "spring";
@Test
void getNameReturnsName() {
GeneratedMethod method = new GeneratedMethod(NAME);
assertThat(method.getName()).isSameAs(NAME);
}
@Test
void getSpecReturnsSpec() {
GeneratedMethod method = new GeneratedMethod(NAME);
method.using(builder -> builder.addJavadoc("Test"));
assertThat(method.getSpec().javadoc.toString()).contains("Test");
}
@Test
void getSpecReturnsSpecWhenNoSpecDefinedThrowsException() {
GeneratedMethod method = new GeneratedMethod(NAME);
assertThatIllegalStateException().isThrownBy(() -> method.getSpec())
.withMessage("Method 'spring' has no method spec defined");
}
@Test
void usingAddsSpec() {
GeneratedMethod method = new GeneratedMethod(NAME);
method.using(builder -> builder.addModifiers(Modifier.PUBLIC));
assertThat(method.getSpec().toString())
.isEqualToIgnoringNewLines("public void spring() {}");
}
@Test
void usingWhenBuilderChanagesNameThrowsException() {
GeneratedMethod method = new GeneratedMethod(NAME);
assertThatIllegalArgumentException()
.isThrownBy(() -> method.using(builder -> builder.setName("badname")))
.withMessage("'spec' must use the generated name \"spring\"");
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.generate;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.javapoet.MethodSpec;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link GeneratedMethods}.
*
* @author Phillip Webb
*/
class GeneratedMethodsTests {
private final GeneratedMethods methods = new GeneratedMethods();
@Test
void createWhenMethodNameGeneratorIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new GeneratedMethods(null))
.withMessage("'methodNameGenerator' must not be null");
}
@Test
void createWithExistingGeneratorUsesGenerator() {
MethodNameGenerator generator = new MethodNameGenerator();
generator.generateMethodName("test");
GeneratedMethods methods = new GeneratedMethods(generator);
assertThat(methods.add("test").getName()).hasToString("test1");
}
@Test
void addAddsMethod() {
this.methods.add("spring", "beans").using(this::build);
this.methods.add("spring", "context").using(this::build);
assertThat(
this.methods.stream().map(GeneratedMethod::getName).map(Object::toString))
.containsExactly("springBeans", "springContext");
}
@Test
void doWithMethodSpecsAcceptsMethodSpecs() {
this.methods.add("spring", "beans").using(this::build);
this.methods.add("spring", "context").using(this::build);
List<String> names = new ArrayList<>();
this.methods.doWithMethodSpecs(spec -> names.add(spec.name));
assertThat(names).containsExactly("springBeans", "springContext");
}
@Test
void doWithMethodSpecsWhenMethodHasNotHadSpecDefinedThrowsException() {
this.methods.add("spring");
assertThatIllegalStateException()
.isThrownBy(() -> this.methods.doWithMethodSpecs(spec -> {
})).withMessage("Method 'spring' has no method spec defined");
}
@Test
void iteratorIteratesMethods() {
this.methods.add("spring", "beans").using(this::build);
this.methods.add("spring", "context").using(this::build);
Iterator<GeneratedMethod> iterator = this.methods.iterator();
assertThat(iterator.next().getName()).hasToString("springBeans");
assertThat(iterator.next().getName()).hasToString("springContext");
assertThat(iterator.hasNext()).isFalse();
}
@Test
void streamStreamsMethods() {
this.methods.add("spring", "beans").using(this::build);
this.methods.add("spring", "context").using(this::build);
assertThat(this.methods.stream()).hasSize(2);
}
private void build(MethodSpec.Builder builder) {
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.generate;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests {@link MethodGeneratorWithName}.
*
* @author Phillip Webb
* @since 6.0
*/
class MethodGeneratorWithNameTests {
private final GeneratedMethods generatedMethods = new GeneratedMethods();
@Test
void withNameWhenGeneratingGetMethod() {
GeneratedMethod generateMethod = generatedMethods.withName("my", "bean")
.generateMethod("get", "test");
assertThat(generateMethod.getName()).hasToString("getMyBeanTest");
}
@Test
void withNameWhenGeneratingSetMethod() {
GeneratedMethod generateMethod = generatedMethods.withName("my", "bean")
.generateMethod("set", "test");
assertThat(generateMethod.getName()).hasToString("setMyBeanTest");
}
@Test
void withNameWhenGeneratingIsMethod() {
GeneratedMethod generateMethod = generatedMethods.withName("my", "bean")
.generateMethod("is", "test");
assertThat(generateMethod.getName()).hasToString("isMyBeanTest");
}
@Test
void withNameWhenGeneratingOtherMethod() {
GeneratedMethod generateMethod = generatedMethods.withName("my", "bean")
.generateMethod("test");
assertThat(generateMethod.getName()).hasToString("myBeanTest");
}
}