// # Flapi (v2.0)
// ### _A fluent API generator for Java_
/**
* Flapi is a Java DSL for generating fluent API's built on method chaining.
* Using the tool you can create your own DSL's in Java, supporting
* practices like Language Oriented Programming and enabling higher-order
* abstractions in your projects. And because they are type safe, any
* reasonable code autocompleter should be able to provide you with instant
* coding hints as you type. Method docs are also available, and most IDE's
* can display these for you inline.
*
*
* ### At Build Time
*
* In general, the user must first define a `Descriptor` which details how
* the API should operate. Once constructed, the descriptor is then used to
* generate a set of Java source files. These files are comprised of a few
* runtime classes and generated interfaces. The generated sources should
* be included in your project, perhaps as a module dependency.
*
* There are two build plugins availble, one for
* [Maven](https://github.com/UnquietCode/Flapi/wiki/Maven-Build-Plugin)
* and one for [Gradle](https://github.com/UnquietCode/Flapi/wiki/Gradle-Build-Plugin)
* which allow you to regenerate your descriptor as part of your build.
*
*
* ### At Run Time
*
* When a new instance of your builder is constructed, a single JDK dynamic
* proxy object is configured. Starting with the top-level type, each method
* in your fluent API is invoked by the user on this proxy, which in turn
* calls the relevant methods in your API's runtime implementation. (more on
* this later)
*/
EmailGenerator.compose(new EmailHelperImpl())
.sender("HAL9000@gmail.com")
.addRecipient("dave@unquietcode.com")
.subject("Just what do you think you're doing, Dave?")
.body("I know that you and Frank were planning to disconnect me, " +
"and I'm afraid that's something I cannot allow to happen...")
.send();
/**
* ### Configuring
*
* Several methods are available on the `Flapi` object for setting
* global configuration options.
*/
Flapi
// Set the Java source version for the generated code. Some features
// are only enabled when the source version is set high enough. As of
// version 2.0 the default is set at JDK 8.
.setJDKVersion(SourceVersion version)
// Get the current source version used for generating code.
.getJDKVersion()
// Set whether Flapi should write out the runtime classes with the
// rest of the generated code. Use this option to enable a fixed,
// zero-dependency version of your builder.
.shouldOutputRuntime(boolean value)
// Whether or not Flapi is currently outputting the runtime classes
// with the generated code.
.shouldOutputRuntime()
/**
* ## Descriptors
*
* The top level object in Flapi is the `Descriptor`. This contains all of the
* information needed to generate the source code. You can start creating a
* new Descriptor using `Flapi.builder()`...`.build()`, making sure to call
* all of the required methods in between. After the call to build you will
* have a descriptor which is ready to be generated.
*
* There are two ways to create a descriptor, via the fluent builder
* or by using [annotations]().
*/
Descriptor builder = Flapi.builder()
.setPackage("unquietcode.tools.flapi.examples.email.builder")
.setStartingMethodName("compose")
.setDescriptorName("Email")
.addMethod("subject(String subject)").atMost(1)
.addMethod("addRecipient(String emailAddress)").atLeast(1)
.addMethod("sender(String emailAddress)").exactly(1)
.addMethod("body(String text)").atMost(1)
.addMethod("send()").last(EmailMessage.class)
.build();
builder.writeToStream(System.out);
/**
* ### DescriptorBuilder Methods
*
* Of the methods which you can call while constructing a new
* `Descriptor` object, some are required in order to have a valid
* descriptor in the end. In general, Flapi will throw an exception if
* it detects an inconsistency. Where possible, the API has been made
* "self-driving" and intuitive via autocomplete and javadocs.
*/
// Start building a new Descriptor. Zero or more `ExecutionListener`
// instances can be provided. (check out `MethodLogger`, which is
// helpful when debugging an error)
Descriptor descriptor = Flapi.builder(ExecutionListener...listeners)
// Set the package for all generated classes. (**required**)
.setPackage(String package)
// Set the starting method name. (optional, default is `create()`)
.setStartingMethodName(String name)
// Set the name of the descriptor. These will yield names like
// `ZapBuilder` and `ZapGenerator` in the generated classes. (**required**)
.setDescriptorName(String descriptor)
// Set the return type for the entire descriptor. (optional,
// default is `void`)
.setReturnType(Class<?> class)
// As above, except the type can be specified without
// creating a compile-time dependency on the class.
.setReturnType(String class)
// Generate class names which are condensed at the expense of
// being mangled and not as readable by humans. This is
// useful if you have a complicated descriptor which creates
// class names too long to be compiled. (optional, disabled
// by default)
.enableCondensedClassNames()
// Provide a custom name generator instance, to be consulted
// when creating the generated source code. (optional,
// default implementation is the `DefaultNameGenerator`)
//
// The `DefaultNameGenerator` implementation will leave names
// as they are. A more compact form can be achieved by using
// the `TinyNameGenerator`, which will try to shorten names
// as much as possible. For more consistent naming the
// `HashedNameGenerator` can be used, which makes use of the
// MD5 hashing algorithm.
.useCustomNameGenerator(NameGenerator generator)
// Disable the printing of timestamps in the generated source
// code. This will eliminate changes between successive executions
// so long as the same version of the tool is used each time.
// (optional, disabled by default)
.disableTimestamps()
// Complete the finished descriptor, returning a new
// `Descriptor` object. (**required**)
.build()
/**
* ### Descriptor Methods
*
* After you finish creating a new `Descriptor`, there are a
* few methods which can be called on it in order to
* generate the source files and write them out.
*/
// Write the generated source code to a stream.
.writeToStream(OutputStream stream);
// Write the generated source code to a directory.
.writeToFolder(String folder);
// Write each individual file to a different stream.
// The iterator should provide a potentially unlimited
// number of streams for use.
.writeToStreams(Iterator<OutputStream> streams)
/**
* ## Blocks
*
* Blocks are the basic unit of state in Flapi, and correlate with the
* interfaces generated for your API. As a user invokes methods on your
* builder, they are transitioned from state to state by way of Java's
* type system.
*
* You can define blocks at the `Descriptor` level, or nested inside
* each other. When using your builder at runtime you are moving in
* to and out of blocks until you finally 'escape' thetop block or
* return a value from the current block.
*
* A block is comprised of methods, and these roughly correspond
* to the methods found in the generated interfaces. Methods will
* be discussed in just a moment.
*/
.startBlock("SomeBlock", "beginSomething()").any()
.addMethod("someMethod()").last()
.addMethod("someOtherMethod(java.io.File file)").last()
.startBlock("Nested", "nestedBlock()").last()
.addMethod("anotherMethod(String string)").atMost(1)
.addMethod("finish()").last()
.endBlock()
.endBlock()
/**
* ### BlockBuilder Methods
*
* A new block is started with `startBlock(...)` and must
* end with a call to `endBlock()`. There should
* be at least one method in every block which is marked as a
* terminal method (via `last(...)`). There is one exception to
* this rule, which is when all of your methods are disappearing
* (via `atMost(...)`). In that case, a warning is printed and
* the last method available in each state is marked as terminal.
*/
// Start a new block. This can be called either from the top level
// descriptor (which is actually just a special kind of block) or
// from within an existing block.
.startBlock(String blockName, String methodSignature)
// Start a new anonymous block. The names of these blocks will be
// generated, and so they cannot be referenced.
.startBlock(String methodSignature)
// End the current block. Returns a `MethodBuilder` to
// configure the invocation in the parent. (**required**)
.endBlock()
// Add a new method to the block. If no return type is provided
// then `void` is inferred.
.addMethod("someMethod()").any()
.addMethod("String someOtherMethod()").atLeast(1)
// Method parameters which are within the `java.lang` and `java.util` package are inferred.
// Other types must be fully qualified.
.addMethod("someMethod(String name)").any()
.addMethod("someOtherMethod(java.io.File file)").atMost(1)
/**
* ### Enum Selectors
*
* Enum Selectors take an enum with a series of values and fans them
* out into block with methods of the same name. This avoids importing
* enums while still making use of them.
*
* In the example, the method `test()` will be added to the current
* block, the use of which looks like this:
* ```java
* .test().One()
* .test().TWO()
* .test().three()
* ```
*/
public enum TestEnum {
One, TWO, three
}
.addEnumSelector(TestEnum.class, "test()").any()
// Add a method which starts a new block whose methods are
// comprised of every enum in the provided enum class.
// Returns a `MethodBuilder` which can be used to configure
// the method's invocation in the parent block.
.addEnumSelector(Class<?> enumClass, String methodSignature)
/**
* ### Block References
*
* It is possible to make use of an already declared block by
* using a 'block reference'. This allows you to add a method which
* returns the block type without having to redefine it in your
* descriptor. (This technique can also be used recursively to
* return a new instance of the current block.)
*
* (When a block is declared, a name can be provided which uniquely
* identifies it within the scope of your descriptor. A block can
* also be declared anonymously, in which case it cannot be
* referenced.)
*/
// Add a method which starts a new block, but by referencing it
// instead of defining it. Returns a `MethodBuilder` to
// configure the call.
.addBlockReference(String blockName, String methodSignature)
/**
* ### Block Mixins
*
* Similar to references, it is possible to combine the contents of
* a previously declared block with the current one by using a
* 'block mixin'. The mixin can be in the form of a string naming
* another block in the descriptor, or a class containing Flapi
* method annotations (see the [Annotations](#annotations) section
* below). Mixins are applied late, so it is possible to use
* forward references to blocks which have not yet been decalared.
*
* (When a block is declared, a name can be provided which uniquely
* identifies it within the scope of your descriptor. A block can
* also be declared anonymously, in which case it cannot be
* referenced.)
*/
// mixin the contents of another named block
.addMixin(String blockName)
// mixin the contents of an annotated interface or class
.addMixin(Class<?> blockType)
/**
* ## Methods
*
* Methods in Flapi roughly correspond to the methods in your
* generated interfaces. Methods have rules which define how many
* times they can be called, when they become visible or invisible,
* etc. In Flapi a method becomes _invisible_ when it has been
* called the maximum number of times, or if another method
* in the same group causes it to disappear. Similarly, trigger
* methods allow a method to become _visible_ only after another
* method is called.
*
* 'Visible' here means listed as a method in an interface which
* the user of our descriptor will interact with. If a user
* attempts to invoke a method which is invisible, the method will
* not be a member of the current class and a compile error
* will occur! When using autocomplete it is very clear that the
* method is no longer available to be invoked.
*/
.addMethod("unlimited()").any()
.addMethod("once()").atMost(1)
.addMethod("terminal()").last()
.addMethod("terminalWithValue()").last(Integer.class)
/**
* ### MethodBuilder Methods
*
* A descriptor method is defined with a preset number of times
* which it can be, or should be, invoked. Setting the _quantifier_
* for a method is one way to adjust its behavior. A method may
* be marked `any()`, `atMost(...)`, `atLeast(...)`,
* `between(...)`, `exactly(...)`, or `last(...)`. Each of these
* are terminal methods in the `MethodBuilder` classes, making
* them effectively required.
*
* **Every block must have at least one terminal method**, and
* this can be set via `last(...)`. This states that the
* method should exit the current block, either by returning
* a value (if a return type is provided on the method or
* the block), returning the parent block, or returning
* `void` if no return type is available.
*/
/**
* ### Method Quantifiers
*/
// Specifies that the method can be called at most _x_ times.
// After that amount, the method will no longer be available
// to be called.
.atMost(int x)
// The method must be called at least _x_ times.
.atLeast(int x)
// The method must be called between _x_ and _y_ times.
.between(int x, int y)
// The method must be called exactly _x_ times.
.exactly(int x)
// The method can be called any number of times.
.any()
// The method can be called once, and will return
// the block's return type after being called. Every
// block should have a last method so that there is
// a way to exit it (think of a state machine stuck
// in an infinite loop).
.last()
// The method, in addition to being last, will also
// return the specified type. This overrides any
// return type set for the block.
.last(Class<?> class)
// As above, except the type can be specified without
// creating a compile-time dependency on the class.
.last(String class)
/**
* ### Method Groups
*
* Methods can be grouped together, and this causes
* them to influence each others' behaviors. Using
* groups allows for complicated "A xor B" types of
* workflows.
*/
// Members of the same group will become invisible
// as soon as this method does so.
.atMost(int x, int group)
// The method will only become visible after at
// least one member of the group has been called.
.after(int group)
// Members of the same group will become invisible
// after this method is called for the first time.
.any(int group)
/**
* ### Block Chains
*
* Sometimes you want a method to pass through a series
* of blocks before returning. This can be accomplished
* by adding a Block Chain to any method. The chain may
* contain any number of block references, named blocks,
* and anonymous blocks.
*
* Before the target of any method is reached (be it the current
* block, a new block, the parent block, or a terminal value),
* it first passes through its block chain, which is empty by
* default. The order in which the chain is defined in the
* builder is the same as what the user will experience later
* when interacting with your API.
*
* ```
* Block chains are NOT preserved when you reference an
* existing block. That is, the block chain is part of
* the method invocation which returns the block, not
* the block itself.
* ```
*/
.addMethod("method()")
.addBlockChain()
.addBlockReference("SomeBlock")
.end()
.any()
.startBlock("block()")
.addBlockChain()
.addBlockReference("SomeBlock")
.end()
.any()
.addMethod("done()").last()
.endBlock()
// Add a block chain to the method. Returns a `BlockChainBuilder`
// which can be used to define the sequence.
.addBlockChain()
// reference an existing block
.addBlockReference(String blockName)
// new named block
.startBlock(String blockName)
...
.endBlock()
// new anonymous block
.startBlock()
...
.endBlock()
// finish defining the block chain
.end()
/**
* ### Method Documentation
*
* When declaring a method, it is possible to specify a
* documentation string to include on the method in the
* generated interfaces. Users will be able to look up
* the documentation in their IDE for these methods. As
* well, it is possible to run the generated source
* through the javadocs tool to generate API docs.
*
* The documentation methods are available on the
* `MethodBuilder` class, and allow for either a single
* line or a multi-line declaration. It is also possible
* to mark a method with the `@Deprecated` annotation.
*/
.addMethod("someMethod()")
.withDocumentation("Single line of documentation.")
.last()
.addMethod("someOtherMethod()")
.withDocumentation()
.addContent("First line.")
.addContent("Second line.")
.finish()
.last()
// Add a single line of documentation to the method.
.withDocumentation(String text)
// Add multiple lines of documentation to the method.
.withDocumentation()
// Add another line of content.
.addContent(String text)
// Finish defining the documentation.
.finish()
// Marks the method as `@Deprecated`. Also adds
// a `@deprecated` tag to the javadocs.
.markAsDeprecated(String reason)
/**
* ## Annotations
*
* Starting in version 0.6 it is possible to describe a
* descriptor based on a given interface or class, in addition
* to the existing fluent builder. This method essentially replaces
* the generated helpers with helpers that you provide, using
* annotations to describe the intended flow of execution
* through your methods.
*/
@Block(name="CustomizedName")
public interface MyHelper {
@AtLeast(2)
void doSomething();
@Between(minInc=1, maxInc=2)
void doSomethingElse();
@Last
@Documented("the last method you'll ever need")
String end();
void skipped();
}
Descriptor descriptor = Flapi.create(MyHelper.class).build();
// Create a new descriptor by introspecting the class,
// first for Flapi annotations, and then as a generic bean.
Descriptor descriptor = Flapi.create(Class class)
// Specifies that the method can be called at most _x_ times.
// After that amount, the method will no longer be available
// to be called.
@AtMost(int value)
// Members of the same group will become invisible
// as soon as this method does so.
@AtMost(int value, int group)
// The method must be called at least _x_ times.
@AtLeast(int value)
// The method must be called between _x_ and _y_ times.
@Between(int minin, int maxInc)
// The method must be called exactly _x_ times.
@Exactly(int value)
// The method can be called any number of times.
@Any()
// Members of the same group will become invisible
// after this method is called for the first time.
@Any(int group)
// The method can be called once, and will return
// the method's return type after being called.
@Last()
// The method will only become visible after at
// least one member of the group has been called.
@After(int group)
// Provide documentation for the method.
@Documented(String[] value)
// Marks a method has being the target of an enum
// selector. The method must accept zero arguments,
// and return a Consumer instance which accepts the
// enum value.
@EnumSelector
// Provide an alternate name to use for the interface
// or class's corresponding generated name, instead of
// the default (`XyzBuilder`).
@Block(String name)
/**
* ### @BlockChain Parameters
*
* A method may specify a block chain by annotating any number
* of parameters with `@BlockChain`. The parameter **must** be of
* type `AtomicReference`.
*
* The helper will be introspected like the current type, and the
* chain will be constructed moving from the leftmost parameter
* towards the rightmost.
*/
interface Alpha {
@Last
void alpha();
}
interface Beta {
@Last
void beta();
}
interface MyHelper {
@Last
String startBlock(
int paramA, @BlockChain AtomicReference<Alpha> helperA,
int paramB, @BlockChain AtomicReference<Beta> helperB
);
}
// Marks a method parameter as a container for another block's helper.
// The parameter **must** be of type `AtomicReference`.
@BlockChain
/**
* ## Thanks!
* Visit the project page [at GitHub](https://github.com/UnquietCode/Flapi)
* for more information.
*/