Type Safe Request URLs

Since I have spent a good deal of time working with dynamic programming languages, using a statically typed language is something of a novelty. Perhaps as the result of my previous, abortive attempts at learning Haskell, I am particularly interested in exploiting the type-system where I can.

One of my current interests is building clients for interacting with remote REST web-APIs. This is something I’ve done more than once and I’ve never been entirely happy with how I’ve accomplished it to this point.

So as part of my quest for type-safety and nice web client’s, I started a small project, Yopuy. It is a library for building REST clients, with a number of specific goals.

  • Type-safety; it should be impossible to construct invalid paths
  • Encoding agnostic; it should have no opinion about what library is used to turn JSON into values
  • HTTP client agnostic; it should not implement HTTP support, but rather defer to which-ever client the user prefers

So here is a taste of how the library might be used — with many details elided.

let path = Blog.show(10) / Post.show(1) / Comment.delete(12)
let result = webService.call(path) { result in
    print(result)
}

We construct a type-safe path, which also captures the HTTP method via Comment.delete. This is then passed to an instance of the Service, type which coordinates the actual request.

In this post I will be focussing on the type-safe construction of paths. In later posts I will discuss other features of the library.

Type-Safety

The first goal for the library is achieved through the use of protocols and associated types. The core part of this library is the Resource protocol. The basic idea is that the resources on the API are represented with types that conform to the Resource protocol and other protocols which we will discuss later.

Conforming to these protocols adds a number of static functions to the types. These functions produce values which represent either full or partial paths. For example, lets consider a Blog resource. After conforming to the protocols we can generate path values.

let path = Blog.show(19)

The result is a strongly typed value of Path<Blog, SingularPath, GET>. This type captures a number of things.

  • The resource; Blog in this case
  • If the path points to a single resource or collection
  • The HTTP method

This path can then be passed off to an instance of the Service struct in order to make a request or composed with other paths.

Representing a Hierarchy

Since REST resources can also be arranged in a hierarchy, Yopuy allows us to capture that relationship in the types. In our example, the Post resource is a child of the Blog. This is done by making Post conform to the ChildResource protocol. It requires an associatedtype called Parent. In case of Post it is declared as typealias Parent = Blog.

struct Blog: RootResource {

}

struct Post: ChildResource {
    typealias Parent = Blog
}

// We can do this
let good = Blog.show(1) / Post.list

// But not this
let bad = Post.show(1) / Blog.show(3)

Defining the hierarchy via associated types means it’s impossible to construct invalid paths. Additionally any ChildResource type can also be a parent to other resources; the hierarchy can be as deep as necessary.

REST Actions

By default a resource doesn’t actually have any of the static properties or functions for constructing paths. They are all optional, since potentially not all actions would be supported for a given API. For example, you might have an API which disallows the DELETE method on certain resources.

Yopuy provides protocols for the different capabilities of a resource.

  • IsListable; can be retrieved as a collection e.g. GET /posts
  • IsShowable; can be retrieved as a single record e.g. GET /posts/1
  • IsCreatable; new resources can be created e.g. POST /posts
  • IsReplaceable; a resource can be updated via PUT e.g. PUT /posts/1
  • IsPatchable; a resource can be updated via PATCH e.g. PATCH /posts/1
  • IsDeletable; a resource can be deleted via DELETE e.g. DELETE /posts/1

Here is an example of a record that can be updated, but not created or deleted.

struct Foo: RootResource, IsPatchable, IsShowable {
  // Details elided...
}

Custom actions

It’s not unusual to have an API that requires an action outside of the standard set provided by REST. Yopuy does support this, but it does unfortunately require some knowledge of how the library works. I’m investigating ways to make this easier.

But Wait!

There’s more! Yopuy provides more than just type-safe paths. It also has other interesting features. In a future post I will discuss how any HTTP client or JSON library can be integrated into Yopuy.

◀︎ Return to homepage