Braque is a compile-time model creation framework for Java.
tl;dr here's an Android sample project
Braque aims to solve the following problems:
- It lessens the need for the
"What does a property with a value of
null
mean?" conversation that is (not) had between developers, leading to various forms of data inconsistency andNullPointerExceptions
. - As server/client communication schema evolve rapidly (think NoSQL), we need extensible object models that are strong-typed.
- For clients that denormalize their data, we need a set of tools to keep data consistent across these denormalizations without enforcing any particular scheme.
- We need to predictably serialize and deserialize dynamically typed
data into objects whose contents can be queried via inspection instead
of via "Does
name=null
mean that the person's name is null?" (see 1 above). - We need to be able to automatically update groups of objects with new annotations, methods, etc..
In your top-level build.gradle
:
buildscript {
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.0' // for android
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' // for android
classpath "net.ltgt.gradle:gradle-apt-plugin:0.8" // for java
}
}
allprojects {
repositories {
jcenter()
maven {
url "https://jongla-bin.bintray.com/java-utils"
}
}
}
And then in the build.gradle
for the projects where braque is used:
dependencies {
compile 'com.jongla:braque-core:0.0.1'
apt 'com.jongla:braque-compiler:0.0.1'
}
The easiest way to learn about Braque is to run ./gradlew koans
, which
will walk you through the koans associated with simple-braque-example
.
Make the tests in braque.koans.yours
pass. For help, check out braque.koans.mine
.
Because Braque is based on code generation, running this in an IDE may be a headache at
first depending on your IDE. To start off, use the command line for compiling
and your favorite text editor for editing.
You can also checkout the whitepapers for more information on the motivations for creating this library:
First, let's create a User
object with several potential properties.
mypackage.model.User.java
@Type(Id.class)
public interface Id {
}
mypackage.model.Id.java
@UID
@Property(String.class)
public interface Id {
}
mypackage.model.Name.java
@Property(String.class)
public interface Name {
}
mypackage.model.Age.java
@Property(Integer.class)
public interface Age {
}
mypackage.model.Friend.java
@Property(User.class)
public interface Friend {
}
Then, let's create an endpoint. Endpoints are written like REST endpoints -
they support Show
, Create
, Update
and Destroy
.
mypackage.api.UserEndpoint.java
@Create(
argument = Id.class,
properties = {
Name.class,
Age.class,
Friend.class, Name.class, $.class
}
)
@Show(
argument = Id.class,
properties = {
Name.class,
Friend.class, Name.class, $.class
}
)
public interface UserEndpoint {
}
$.class
is used as a separator to
signal that the nested Friend
object ends.
What you get out of this is when the
annotations are processed is:
UserCreateUser user = Transformer.makeUserEndpointCreate("aUserId").addName("me").commit();
boolean willBeTrue = user instanceof Name;
boolean willBeFalse = user instanceof Age;
UserCreateUserAge_Implementation stronglyTypedUser =
Transformer.take(user).removeName().addAge(43).commit();
int age = stronglyTypedUser.getAge(); // will be 43
// String name = stronglyTypedUser.getName(); // won't compile because we have removed the name
Map<String, Object> userMap = new HashMap<>();
userMap.put("userendpoint/myId/_type","User");
userMap.put("userendpoint/myId/id", "myId");
userMap.put("userendpoint/myId/name", "John");
userMap.put("userendpoint/myId/friend/_type","User");
userMap.put("userendpoint/myId/friend/id", "anotherId");
userMap.put("userendpoint/myId/friend/name", "Jane");
List<UserEndpointCreateUser> deserialized =
Deserializer.deserialize(userMap, UserEndpointCreateUser.class);
boolean willAlsoBeTrue = deserialized.get(0) instanceof Name;
boolean willAlsoBeFalse deserialized.get(0) instanceof Age;
Map<String, Object> backAgain = Serializer.serialize(deserialized.get(0));
boolean trueness = backAgain.get("userendpoint/myId/name").equals("John");
Whet your appetite? Check out simple-braque-example
to see other neat
things like inheritance of types, the Fanner
class to help with
denormalization and clojure integration for code injection via the
@Clojure
annotation. All of this is covered in the koans.
AndroidStudio is great at handling generated code via the android-apt
plugin.
IntelliJ is less friendly. There are a few apt options of which
simple-braque-example
uses one. But even this takes Project Structure
tweaking in IntelliJ to get it to recognize the correct source folders.
Fields are by default protected
so that getters and setters make sense.
However, if you want to use packages like Dagger (which you can by generating new annotations
via the @Clojure
annotation - run the koans to learn more), then fields need to be public. This can be tweaked
by setting makeObjectFieldsPublic
as true
. Check out build.gradle
of simple-braque-example
to see how this is done in apt (for now it is set to false).
Georges Braque (1882-1963), a French painter, was a master at convincingly representing the same object from various different angles within one work.
The first version of Braque was written by Mike Solomon at Jongla. The Braque Logo was created by Tomi Tuomela. Braque is Open Source under the Apache License Version 2.0. Pull requests are welcome!