Fluent API code generator is a fairly complex generator that can generate fluent API facade in front of an already existing class.
You can read the following articles to get to know what fluent api is:
FluentInterface from Martin Fowler
The Java Fluent API Designer Crash Course
Fluent API in Java is a technique that results readable method call chaining. Example:
CreateSql.select("column1","column2").from("tableName")
.where("column1 = 'value'")
This can be reached implementing the methods select
, from
and where
returning an instance of the the class CreateSql
.
(Note: select
is static.)
This is simple and straightforward.
However this is far from real fluent API.
This implementation will not prevent someone write
// WRONG!!!
CreateSql.select("column1","column2")
.from("tableName").from("anotherTable")
which is simply wrong. To prevent this you have to define extra interfaces as depicted in the articles The Java Fluent API Designer Crash Course
This fluent API code generator will generate those extra interfaces automatically based on the definition of the grammar of the fluent API.
To use the fluent API generator you have to create three code parts.
-
One is the builder class that has the methods with the appropriate names and arguments. The methods may or may not be chained in this "builder", it is up to you. You have to annotate the class using the
Geci()
annotation to ensure that the generator will process this class. The annotation string should contain the parameterdefinedBy
to specify the fluent API grammar. We will describe this a few lines below. The class should also have aneditor-fold
segment to hold the generated code. -
You should write the test code that creates the fluent API code. This is the same as for any other generator. Note that you only need one test method even if you have many classes to fluentize.
-
Finally you need a method that defines the grammar of the fluent API. You need a separate one for each fluentized class as it is not likely that they share the same grammar. The name of this method is defined by the
definedBy
parameter of the class annotation.
A good example is the JavaSource
class that is in the Java::Geci tools module.
It starts with the following lines:
@Geci("fluent definedBy='javax0.geci.buildfluent.TestBuildFluentForSourceBuilder::sourceBuilderGrammar'")
public class JavaSource implements AutoCloseable {
As you can see the mnemonic of the generator is fluent
and the parameter definedBy
specifies the method in the syntax of a method reference.
This is not really a method reference as it is inside a string, but the syntax follows that of the method references.
(You can also use #
or a simple dot .
to separate the name of the class and the name of the method.)
Also note that in this specification you have to specify the class with a fully qualified name.
An alternative possibility to use the syntax
parameter and provide the syntax of the fluent API directly and not through a builder method.
Note that certain features cannot be described using the syntax parameter.
An example is to make the API AutoClosable
.
@Geci("fluent syntax='a|b|c d'")
public class JavaSource {
The editor fold
//<editor-fold id="fluent" desc="fluent API interfaces and classes">
//</editor-fold>
will hold the generated code after the generator runs.
You can define an id
for the editor fold in the annotation.
The default is the mnemonic of the generator.
It is usually okay, you are not likely to generate more than one fluent API into one single class.
Most probably they would also collide with each other.
The test code that generates the fluent API is the following:
@Test
public void testSourceBuilderGeneratedApiIsGood() throws Exception {
if (new Geci().source("../javageci-tools/src/main/java", "./javageci-tools/src/main/java").register(new Fluent()).generate()) {
Assertions.fail(Geci.FAILED);
}
The framework will try to open the ../tools/src/main/java
directory first and in case it can not be found then it goes on to open the ./tools/src/main/java
directory to discover the source files.
If you use standard maven directory structure you can use the Source.maven()
static method to specify the directories and to ease the readability of the test.
The code creates a new instance of the generator and starts the generation invoking generate()
.
In case the generated code differs from the one that was already in the file the return value of generate()
is true
and then the test fails: the code was modified, it has to be committed into the repository and compiled and tested again.
No manual code modification is needed.
This is also standard for all the generators.
The method that defines the fluent API is in the method sourceBuilderGrammar()
:
public static FluentBuilder sourceBuilderGrammar() {
var source = FluentBuilder.from(JavaSource.class).start("builder").fluentType("Builder").implement("AutoCloseable").exclude("close");
var statement = source.oneOf("comment", "statement", "write", "write_r", "write_l", "newline", "open");
var methodStatement = source.oneOf(statement, source.oneOf("returnStatement()", "returnStatement(String,Object[])"));
var ifStatement = source.one("ifStatement").zeroOrMore(statement).optional(source.one("elseStatement").zeroOrMore(statement));
var whileStatement = source.one("whileStatement").zeroOrMore(statement);
var forStatement = source.one("forStatement").zeroOrMore(statement);
var methodDeclaration = source.one("method").optional("modifiers").optional("returnType").optional("exceptions").oneOf("noArgs", "args");
var method = source.name("MethodBody").one(methodDeclaration).zeroOrMore(methodStatement);
var grammar = source.zeroOrMore(source.oneOf(statement, ifStatement, whileStatement, forStatement, method)).one("toString");
return grammar;
}
It is recommended that you place this method in the test class. There are different reasons for it:
- It is close to the generating code, it eases maintenance and readability.
- The Java::Geci core module is used by this code and this module is probably not used in anywhere else in the code.
Placing this code in the test class the dependency scope for the core module can remain
test
(note that the code generating test already needs this dependency). - This method is not needed during production run time, this is a test support code.
The method has to build a FluentBuilder
object and to do that it uses the fluent API of the FluentBuilder
.
Thus we have a fluent API to defined our own fluent API.
To create the fluent API grammar you can use the following (fluent) methods.
Whenever an argument is a String
that identifies a method the name of the method can be used.
If there are more than one methods in the class with the same name then the signature of the method should be used to identify the actual methods.
Other methods can still be referred only by name.
The signature of the method is the name of the method and the argument types between (
and )
comma separated.
If the type is a Java JDK class (package starts with java.
) then the package can be omitted.
For example you can write String
instead of java.lang.String
.
In other cases the fully qualified domain name has to be used (dot separated and not $
even if the type is an inner class).
Define the name of the start method.
The start method is a public static
method that can be used to instantiate the builder.
When you fluentize a class MyClass
and call start("builder")
then you will start a fluent API use with MyClass.builder()
.
The start method does not have any parameter in the current implementation.
method
is the name of the start method.
Define interfaces that all other interfaces in the fluent interface should implement.
This can be typically AutoCloseable
when some API uses the structure of the try-with-resources command to follow the built structures in the generating Java code.
The parameter interfaces
is the names of the interfaces to be implemented comma separated.
This string will be inserted into the list of the interfaces that stands after the extends
or implements
keyword.
This is a complimentary method that is equivalent to call implement("AutoCloseable")
.
Define the top-level interface name that will start the fluent API.
Other names are generated automatically unless defined by the method name(String)
.
The parameter type
is the name of the interface top-level interface.
Exclude a method from the fluent interface.
If a method is excluded it can not be used in the definition of the fluent api and it will not be part of the interfaces and the wrapper class.
The caller may exclude more than one method from the fluent API with subsequent calls to exclude(String)
.
The parameter method
is the name or signature of the method to be excluded.
Starting with version 1.0.1 there is no need to exclude certain methods. After this version only those methods get into the interface and into the Wrapper class that are explicitly referenced in the fluent API definition.
Define the method that clones the current instance of the class that is fluentized. Such a method usually creates a new instance and copies all the fields to the new instance so that fluent building can go on from that instance and all previous instances can be used in case they are needed to build something different.
The parameter method
is the name of the cloner method.
The method should return a new instance of the class and should have no parameters.
The method may be called zero or one time in the fluent API at the defined point.
The parameter method
is the name of the method.
For more information see the note in the documentation of the class FluentBuilder
.
The sub expression may be called zero or one times in the fluent API at the defined point.
The parameter sub
is the fluent api structure used in the expression.
The method may be called one or more time in the fluent API at the defined point.
The parameter method
is the name of the method.
For more information see the note in the documentation of the class FluentBuilder
.
The sub expression may be called one or more times in the fluent API at the defined point.
The parameter sub
is the fluent api structure used in the expression.
The method may be called zero or more time in the fluent API at the defined point.
The parameter method
is the name of the method.
For more information see the note in the documentation of the class FluentBuilder
.
The sub expression may be called zero or more times in the fluent API at the defined point.
The parameter sub
is the fluent api structure used in the expression.
The fluent API using code may call one of the methods at this point.
The parameter methods
is the names of the methods.
For more information see the note in the documentation of the class FluentBuilder
.
The fluent API using code may call one of the sub structures at this point.
The parameter subs
is the sub structures from which one may be selected by the caller.
The method can be called exactly once at the point.
The parameter method
is the name of the method.
For more information see the note in the documentation of the class FluentBuilder
.
The sub structure can be called exactly once at the point.
The parameter sub
is substructure.
The structure at the very point has to use the name as the interface name.
The parameter interfaceName
is the name of the interface to use at this point of the structure.
Where the name is not defined the fluent api builder generates interface names automatically.
The syntax can be defined using a complex string in addition to fluent API calls.
The fluent API calls and the call of the method syntax()
can also be mixed together with some limits.
In the the unit tests there is a syntax test string sample that looks like the following:
"kw(String) ( noParameters | parameters | parameter+ )? regex* usage help executor build"
This syntax definition says that the fluent API generatedwhen used has to call the method kw
first, the one of the methods noParameters
, parameters
or parameter
.
When the last one is used it can be called one or more times, however all these alternative calls can also be just skipped.
After that the method regex
can be called zero or more times.
After that the usage
, help
, executor
and build
methods have to be invoked and they are mandatory.
The rules are intuitive and simple.
- A word means a method call.
- Methods that should be called one after the other are written one after the other separated by space.
- Methods are defined the same way as in other calls, (e.g.: as the argument to method
one()
) with the name and with optional signature. - Something enclosed between '(' and ')' characters is a substructure.
- Alternatives are enclosed between '(' and ')' and the elements are separated using '|'.
- Anything followed by a '?' is optional.
- Anything followed by a '+' is one or more times.
- Anything followed by a '*' is zero or more times.
Call to syntax()
can be mixed with other calls.
For example the syntax does not provide any means to define interface name like the call to the method name()
.
If you need that and still want to use the syntax instead of method chain you can use the following expression:
klass.syntax("kw(String) ( noParameters | parameters | parameter+ )? regex* usage help executor")
.name("SpecialName")
.syntax("build");
In the last call you could just call one("build")
and it finally would result the same structure.