Jerk.

Jerk is a small project I started working on to help me deal with the JSON schemas used to define Open Charge Point Protocol messages. Yesterday I gave an impromptu lightning talk about what it is and why it is the way that it is. These are basically my notes for the talk, but polished up a bit.

Problem

The Open Charge Point Protocol includes 64 message types that are each divided into two schemas, one for the request and one for the response. Each of these 128 schemas must be represented by an Erlang term with a uniform and convenient API. Given the number of schemas as well as there complexity manually implementing an API for each message type would be time-consuming and error prone. This is the first requirement: translating between the schemas and Erlang terms must be automated.

Two additional requirements relate to handling different protocol versions. It must be possible to support multiple versions of the protocol concurrently. This means we need to represent multiple versions of messages with different constraint. Also, for the sake of maintainability updating the schemas for new versions should not require only minimal effort beyond installing the new schema files “somewhere.”

And lastly, the raison d’ĂȘtre for this project is that it must be impossible to construct a term with an invalid value or set an attribute to an invalid value. Such an action should fail immediately.

Solutions

I went through a couple different concepts for how to solve this problem. First, I considered building a code generation framework to load the schema files and produce an Erlang module (or modules) with a functional API for working with each message type. While this is interesting, it is a bad idea. The second approach is the Jerk application which maintains a database of schemas and provides a simple API for creating, querying, and updating objects.

Code Generation

This is an interesting solution primarily because it would be a good opportunity to learn about code generation. However, it doesn’t meet the actual requirements. It requires extra complexity in the build stage to generate the API module and requires recompiling the whenever the protocol is changed. The code generation approach also makes supporting multiple protocol versions extremely complicate, at best. Finally, the complexity of the message structures leads me to doubt whether a per-message API is even feasible. There are just too many attributes and sub-types to make a useable per-message API.

Jerk

This leads me to Jerk. There are some existing tools for validating agains JSON schemas, but they are purely focused on checking complete JSON objects so they don’t quite satisfy my need to update individual fields or easily get the type (i.e. the schema it must satisfy) of a particular object. Like other schema validators Jerk loads schemas into a database keyed by the URI of the schemas; however, unlike simple validators it provides an API for constructing, querying, and updating object attributes. The application consists of a loader for loading schemas into the database, and a basic API for the three operations listed above. Multiple schema versions are supported transparently by keying the database with a URI.

The actual data structure, a jerkterm, is opaque, but for now looks like a 3-tuple containing a short ID for the type, the URI of the schema, and a map containing the schema attributes. The API for creating a new jerk term simply takes a schema name and a list of key-value pairs specifying the attributes.

%% @doc Create a new `JerkTerm' of the type specified by `Schema'. The
%% values of the attributes are given in `Attributes'. If any required
%% values are not in the attribute list or if any values do not
%% conform to the schema the call fails with reason `badarg'.
-spec new(Schema :: schemaname(),
          AttributeList :: [{attribute_name(), attribute_value()}]) ->
          jerkterm().

Three functions exist for accessing and manipulating jerk terms.

%% @doc Return the names of all defined attributes in `JerkTerm'.
-spec attributes(JerkTerm :: jerkterm()) -> [attribute_name()].

%% @doc Return the value of `AttributeName'. If the attribute is not
%% defined the call fails with reason `badarg'.
-spec get_value(JerkTerm :: jerkterm(),
                AttributeName :: attribute_name()) -> attribute_value().

%% @doc Set the value of `Attribute' to `Value' in `JerkTerm'. If the
%% attribute is not allowed in schema of `JerkTerm' or if the value is
%% not allowed the call fails with reason `badarg'.
-spec set_value(JerkTerm,
                Attribute :: attribute_name(),
                Value :: attribute_value()) ->
          JerkTerm when JerkTerm :: jerkterm().

Another departure from libraries such as jesse is that the database of schemas stores schemas defined within another schema (e.g. in "$defs") in the same table as standalone schemas. This enables the construction of sub-terms by themselves. For example the Open Charge Point Protocol "BootNotificationRequest" contains a sub-schema "ChargingStationType" defined sort of like this.

{
  "$id": "BootNotificationRequest",
  "definitions": {
    "ChargingStationType": {
      "type": "object",
      "properties": {
        "serialNumber": {
          "type": "string",
          "maxLength": 25
        },
        "model": {
          "type": "string",
          "maxLength": 20
        },
        "modem": {
          "$ref": "#/definitions/ModemType"
        },
        "vendorName": {
          "type": "string",
          "maxLength": 50
        },
        "firmwareVersion": {
          "type": "string",
          "maxLength": 50
        }
      },
      "required": [
        "model",
        "vendorName"
      ]
    },
    "ModemType": {...},
    "CustomDataType": {...}
  },
  "properties": {
    "chargingStation": {
      "$ref": "#/definitions/ChargingStationType"
    },
    "reason": {
      "$ref": "#/definitions/BootReasonEnumType"
    }
  },
  "required": [
    "reason",
    "chargingStation"
  ]
}

Jerk can create an instance of a"ChargingStationType" independently of the "BootNotificationRequest" without the need to traverse the whole schema tree rooted at the "BootNotoficationRequest".

jerk:new(<<"BootNotificationRequest/definitions/ChargingStationType">>,
         [{<<"serialNumber">>, <<"xyz">>},
          {<<"model">>, <<"hard-charger">>},
          {<<"vendorName">>, <<"hardcore-chargers">>}]).

The last couple paragraphs weren’t part of my lightning talk, but they provide some nice context here. You can find the (very incomplete) code on GitHub.

Chicken photo by Jairo Alzate