udoprog.github.io

Building services with reproto

APIs are ubiquitous.

The most popular form by far are JSON-based HTTP APIs (all though GraphQL are giving them a run for their money). Sometimes these are referred to as restful - because we collectively have an aversion towards taking REST seriously.

This post isn’t about REST. It’s about a project I’ve been working on for the last year to handle the lifecycle of JSON-based APIs:

Rethinking Protocols - reproto.

reproto is a number of things, but most importantly it’s an interface description language (IDL) in which you can write specifications that describe the structure of JSON objects. This IDL aims to be compact and descriptive.

A simple .reproto specification looks like this:

# File: src/cats.reproto

type Cat {
  name: string;
}

This describes an object which has a single field name, like: {"name": "Charlie"}.

Using reproto, we can now generate bindings for this in various languages.

$ reproto build --lang rust --package cats --path src --out src/generated

For Rust, this would be using Serde:

// File: src/generated/cats.rs

#[derive(Serialize, Deserialize, Debug)]
struct Cat {
  name: String,
}

In Java, Jackson would be used:

// File: src/main/java/Cat.java

import lombok.Data;

@Data
public static class Cat {
  private final String name;

  @JsonCreator
  public Cat(@JsonProperty("name") final String name) {
    this.name = name;
  }
}

reproto tries to integrate with the target language using the best frameworks available1.

Dependencies

A system is something greater than the sum of its parts.

Say you want to write a service that communicate with with many other services, it’s typically painful and error prone to copy things around by yourself.

To solve this reproto is not only a language specification, but also a package manager.

Provide reproto with a build manifest in reproto.toml like this:

language = "rust"
output = "src/generated"

[modules.chrono]

[packages]
"io.reproto.toystore" = "^1"

Run:

$ reproto update
$ reproto build

And reproto will have downloaded and built io.reproto.toystore from the central repository.

Importing a manifest from somewhere else inside of a specification will automatically use the repository:

use io.reproto.toystore "^1" as toystore;

type Shelf {
  toys: [toystore::Toy];
}

Dealing with many different versions of a package is handled through clever namespacing.

This makes it possible to import and use multiple different versions of a specification at once:

use io.reproto.toystore "^1" as toystore1;
use io.reproto.toystore "^2" as toystore2;

type Shelf {
  toys: [toystore1::Toy];
  toys_v2: [toystore2::Toy];
}

Documentation

Good documentation is key to effectively using an API.

reproto comes with a built-in documentation tool in reproto doc, which will generate documentation for you by reading rust-style documentation comments.

You can check out the example documentation for io.reproto.toystore here.

Fearless versioning

With package management comes the problems associated with breaking changes.

reproto insists on using semantic versioning, and will actively check that any version you try to publish doesn’t violate it:

$ reproto publish
src/io/reproto/toystore.reproto:12:3-22:
 12:   category: Category;
       ^^^^^^^^^^^^^^^^^^^ - minor change violation: field changed to be required
io.reproto.toystore-1.0.0:12:3-23:
 12:   category?: Category;
       ^^^^^^^^^^^^^^^^^^^^ - from here

This is all based on a module named semck that operates on the AST-level.

Not everything is covered yet, but it’s rapidly getting there.

Finally

In contrast to something like purely an api specification language, reproto aims to be a complete system to hold your hands during the entire lifecycle of service development.

My litmus test will be when I’ve produced a mostly generated client for Heroic, which is well on its way.

It’s also written in Rust, a language where a lot of these ideas have been shamelessly stolen from.

There is still a lot of work to be done! If you are interested in the problem domain and have spare cycles, please join me on Gitter.

Comments on reddit.

  1. The exact approach is configurable through modules documented under Language Support