A rekord is an immutable data structure of key-value pairs. Kind of like an immutable map of objects, but completely type-safe, as the keys themselves contain the type information of the value.
Duplication is difficult to exterminate in Java code. In particular, one type of structural duplication is scattered throughout our software. It looks something like this:
public class Person {
private final String firstName;
private final String lastName;
private final LocalDate dateOfBirth;
private final Address address;
public Person(String firstName, String lastName,
LocalDate dateOfBirth, Address address) {
this.firstName = firstName;
this.lastName = lastName;
this.dateOfBirth = dateOfBirth;
this.address = address;
}
public String getFirstName() {
return firstName;
}
// I can't go on. You know the rest.
}
Of course, that's not all. We then have to make a builder, a matcher for readable test cases, and everything else to support this, the dumbest of all classes.
OK, now we can use our Person
type. It's beautiful, right? It just needs some annotations to serialize to JSON, then
some JPA annotations for persistence to the database, and…
UGH.
Code like the above makes me angry. It's such a waste of space. The same thing, over and over again.
With Rekord, the above suddenly becomes a lot smaller.
public interface Person {
Key<Person, String> firstName = SimpleKey.named("first name");
Key<Person, String> lastName = SimpleKey.named("last name");
Key<Person, LocalDate> dateOfBirth = SimpleKey.named("date of birth");
Key<Person, FixedRekord<Address>> address = RekordKey.named("address");
Rekord<Person> rekord = Rekords.of(Person.class)
.accepting(firstName, lastName, dateOfBirth, address);
}
That Rekord<Person>
object is a rekord builder. You can construct new people with it. Like so:
Rekord<Person> woz = Person.rekord
.with(Person.firstName, "Steve")
.with(Person.lastName, "Wozniak")
.with(Person.dateOfBirth, LocalDate.of(1950, 8, 11))
.with(Person.address, Address.rekord
.with(Address.city, "Cupertino"));
woz
has the type Rekord<Person>
, but you can treat it basically as if it were a Person
as shown above. There's
only one real difference. Instead of:
woz.getFirstName()
You call:
woz.get(Person.firstName)
Simple, right?
Rekord is designed to be used as an alternative to classes with getters (immutable beans, if you will) so you don't have to implement a new concrete class for every value concept—instead, a single type has you covered.
For free, you also get:
Every Rekord is also a builder. Rekords themselves are immutable, so the with
method returns a new
Rekord each time. Use them, pass them around, make new rekords out of them; because they don't mutate, they're perfectly
safe.
There are matchers for the builders. You can assert that a rekord conforms to a specific specification,
just check they have specific keys, or anywhere in between. Take a look at RekordMatchers
for
more information.
Rekord<Person> steve = Person.rekord
.with(Person.firstName, "Steve")
.with(Person.lastName, "Wozniak")
.with(Person.dateOfBirth, LocalDate.of(1950, 8, 11));
assertThat(steve, is(aRekordOf(Person.class)
.with(Person.firstName, equalToIgnoringCase("steVE"))
.with(Person.lastName, containsString("Woz"))));
assertThat(steve, hasProperty(Person.dateOfBirth, lessThan(LocalDate.of(1970, 1, 1))));
The matchers play into validation. Rather than just building a rekord and using it, you can also create a
ValidatingRekord
which allows you to build a rekord up, then ensure it passes a
specification.
The same matchers you can use in your tests are used for validation.
When you fix
a validating rekord, one of two things happen. It will either return a ValidRekord
, which implements
the FixedRekord
interface, providing you the get
method (and a few others), or it will throw an
InvalidRecordException
. Because we use Hamcrest matchers, the exception should have a decent error message which
explains why the validation failed.
ValidatingRekord<Person> namedPerson = ValidatingRekord.validating(Person.rekord)
.expecting(hasProperties(Person.firstName, Person.lastName));
ValidRekord<Person> steve = namedPerson
.with(Person.lastName, "Wozniak")
.with(Person.dateOfBirth, LocalDate.of(1950, 8, 11))
.fix(); // throws InvalidRekordException
Rekord properties can be transformed on storage and on retrieval. The rekord-keys library adds a number of keys that wrap existing keys. As of the time of writing, you can:
- Specify a default value for a key with
DefaultedKey
- Apply an arbitrary (reversible) transformation with
FunctionKey
- Break a value into many values with
OneToManyKey
- Dive into rekords several layers deep with
ComposedKey
- Rename a key with
RenamedKey
Finally, rekords can be serialized. Whether you want it to be JSON, XML or just a Java map, we've got you covered. It's pretty simple. For example:
Rekord<Person> spongebob = Person.rekord
.with(Person.firstName, "Spongebob")
.with(Person.lastName, "Squarepants");
Document document = spongebob.serialize(new DomXmlSerializer());
assertThat(the(document), isSimilarTo(the(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<person>" +
" <first-name>Spongebob</first-name>" +
" <last-name>Squarepants</last-name>" +
"</person>")));
The available serializers are, at the time of writing:
StringSerializer
, which is used byRekord::toString
to create a string representation of a rekordMapSerializer
, which converts a Rekord into aMap<String, Object>
DomXmlSerializer
, which converts a Rekord into aDocument
object. It's demonstrated aboveJacksonSerializer
, which converts a Rekord into JSON, and can either return aString
or write it directly to aWriter
Note: to use JacksonSerializer
, you'll need to include rekord-jackson
as a separate dependency. This is to avoid
including the Jackson JSON Processor as a dependency of Rekord.
There's almost certainly a bunch of stuff we haven't covered. More examples can be found in the tests.
You can use Rekord v0.3 by dropping the following into your Maven pom.xml
. It's in Maven Central.
<dependency>
<groupId>com.noodlesandwich</groupId>
<artifactId>rekord</artifactId>
<version>0.3</version>
</dependency>
If you want to serialize to JSON, grab this one too:
<dependency>
<groupId>com.noodlesandwich</groupId>
<artifactId>rekord-jackson</artifactId>
<version>0.3</version>
</dependency>
If you're not using Maven, alter as appropriate for your dependency management system.
There are also individual JARs available if you don't want all of Rekord, or if you want to manually manage your dependencies. You can get them all from Maven Central.
I was in Germany, at SoCraTes 2013, when I named it. So I thought I'd make the name a little more German. ;-)
Thanks go to:
- Nat Pryce, for coming up with the idea of "key" objects in Make It Easy.
- Dominic Fox, for extending the idea by delegating to a simple map in karg.
- Quentin Spencer-Harper, for working with me on the initial implementation of this library.