/Rekord

Type-safe records in Java, to be used instead of POJOs, Java beans, maps or value objects.

Primary LanguageJava

Rekords in Java   Build Status

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.

Why?

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.

Rekord to the Rescue

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?

What else?

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:

  • builders
  • matchers
  • validation
  • serialization
  • transformations
  • equals and hashCode
  • toString

Builders

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.

Matchers

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))));

Validation

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

Transformation

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:

Serialization

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 by Rekord::toString to create a string representation of a rekord
  • MapSerializer, which converts a Rekord into a Map<String, Object>
  • DomXmlSerializer, which converts a Rekord into a Document object. It's demonstrated above
  • JacksonSerializer, which converts a Rekord into JSON, and can either return a String or write it directly to a Writer

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.

Installation

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.

Why "Rekord"?

I was in Germany, at SoCraTes 2013, when I named it. So I thought I'd make the name a little more German. ;-)

Credits

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.