This is the second post of a series about cubicweb-jsonschema. The first post mainly dealt with JSON Schema representations of CubicWeb entities along with a brief description of the JSON API. In this second post, I'll describe another aspect of the project that aims at building an hypermedia API by leveraging the JSON Hyper Schema specification.
Hypermedia APIs and JSON Hyper Schema
Hypermedia API is somehow a synonymous of RESTful API but it makes it clearer that the API serves hypermedia responses, i.e. content that helps discoverability of other resources.
At the heart of an hypermedia API is the concept of link relation which both aims at describing relationships between resources as well as provinding ways to manipulate them.
In JSON Hyper Schema terminology, link relations take the form of a collection of Link Description Objects gathered into a links property of a JSON Schema document. These Link Description Objects thus describes relationships between the instance described by the JSON Schema document at stake and other resources; they hold a number of properties that makes relationships manipulation possible:
- rel is the name of the relation, it is usually one of relation names registered at IANA;
- href indicates the URI of the target of the relation, it may be templated by a JSON Schema;
- targetSchema is a JSON Schema document (or reference) describing the target of the link relation;
- schema (recently renamed as submissionSchema) is a JSON Schema document (or reference) describing what the target of the link expects when submitting data.
Hypermedia walkthrough
In the remaining of the article, I'll walk through a navigation path that is made possible by hypermedia controls provided by cubicweb-jsonschema. I'll continue on the example application described in the first post of the series which schema consists of Book, Author and Topic entity types. In essence, this walkthrough is typical of what an intelligent client could do when exposed to the API, i.e. from any resource, discover other resources and navigate or manipulate them.
This walkthrough assumes that, given any resource (i.e. something that has a URL like /book/1), the server would expose data at the main URL when the client asks for JSON through the Accept header and it would expose the JSON Schema of the resource at a schema view of the same URL (i.e. /book/1/schema). This assumption can be regarded as a kind of client/server coupling, which might go away in later implementation.
Site root
While client navigation could start from any resource, we start from the root resource and retrieve its schema:
GET /schema Accept: application/schema+json HTTP/1.1 200 OK Content-Type: application/json { "links": [ { "href": "/author/", "rel": "collection", "schema": { "$ref": "/author/schema?role=creation" }, "targetSchema": { "$ref": "/author/schema" }, "title": "Authors" }, { "href": "/book/", "rel": "collection", "schema": { "$ref": "/book/schema?role=creation" }, "targetSchema": { "$ref": "/book/schema" }, "title": "Books" }, { "href": "/topic/", "rel": "collection", "schema": { "$ref": "/topic/schema?role=creation" }, "targetSchema": { "$ref": "/topic/schema" }, "title": "Topics" } ] }
So at root URL, our application serves a JSON Hyper Schema that only consists of links. It has no JSON Schema document, which is natural since there's usually no data bound to the root resource (think of it as empty rset in CubicWeb terminology).
These links correspond to top-level entity types, i.e. those that would appear in the default startup page of a CubicWeb application. They all have "rel": "collection" relation name (this comes from RFC6573) as their target is a collection of entities. We also have schema and targetSchema properties.
Links as actions
So let's pick one of these links, the Books link. We can either just follow the link URI href="/book/" using a GET request, but what if we wanted to create a new book? We'd need to perform a POST request at href URI with a body that conforms to the JSON Schema of schema property. So let's first retrieve this schema:
GET /book/schema?role=creation Accept: application/schema+json HTTP/1.1 200 OK Content-Length: 836 Content-Type: application/json Date: Tue, 04 Apr 2017 09:46:53 GMT Server: waitress { "$ref": "#/definitions/Book", "definitions": { "Book": { "additionalProperties": false, "properties": { "author": { "items": { "oneOf": [ { "enum": [ "856" ], "title": "Ernest Hemingway" }, { "enum": [ "855" ], "title": "Victor Hugo" } ], "type": "string" }, "maxItems": 1, "minItems": 1, "title": "author", "type": "array" }, "publication_date": { "format": "date-time", "title": "publication date", "type": "string" }, "title": { "title": "title", "type": "string" } }, "required": [ "title", "publication_date" ], "title": "Book", "type": "object" } } }
(This is essentially the same schema that we had in the last example of the first post.)
With this we may form a POST request with a JSON document as body that conforms to this schema, but how do we know that we can actually send the request? Hypermedia link relations do not provide any information for this. Instead, we rely on the underlying protocol (HTTP) and on the server to advertize its capabilities. This means we need to fetch the target ressource and inspect the Allow header in the response. Most of the times, we would already have fetched the resource (verb GET) so these headers would be readily available. But in this case, we are on the root resource and we have not fetched targets of the links, so we use a HEAD request:
HEAD /book/ HTTP/1.1 200 OK Allow: GET, POST
Verbs listed in this Allow header just describe what actions we are allowed to perform on the resource. Under the hood, this is determined by permissions lookup (i.e. "read" permission matches with GET verb, "add" permission matches with POST verb and so on).
So we can create a Book!
POST /book/ Accept: application/json {"title": "L'homme qui rit", "author": ["855"], "publication_date": "1869-04-01"} HTTP/1.1 201 Created Content-Type: application/json; charset=UTF-8 Location: http://localhost:6543/Book/859 { "author": [ "Victor Hugo" ], "publication_date": "1869-04-01", "title": "L'homme qui rit" }
What's important in this response is the Location header which indicates where our new resource lives. Our intelligent client can then proceed with its navigation using this information.
From collection to items
Now that we have added a new book, let's step back and use our books link to retrieve data (verb GET):
GET /book/ Accept: application/json HTTP/1.1 200 OK Allow: GET, POST Content-Type: application/json [ { "id": "859", "title": "L'homme qui rit" }, { "id": "858", "title": "The Old Man and the Sea" }, ]
which, as always, needs to be completed by a JSON Schema:
GET /book/schema Accept: application/schema+json HTTP/1.1 200 OK Content-Type: application/json { "$ref": "#/definitions/Book_plural", "definitions": { "Book_plural": { "items": { "properties": { "id": { "type": "string" }, "title": { "type": "string" } }, "type": "object" }, "title": "Books", "type": "array" } }, "links": [ { "href": "/book/", "rel": "collection", "schema": { "$ref": "/book/schema?role=creation" }, "targetSchema": { "$ref": "/book/schema" }, "title": "Books" }, { "href": "/book/{id}", "rel": "item", "targetSchema": { "$ref": "/book/schema?role=view" }, "title": "Book" } ] }
Consider the last item of links in the above schema. It has a "rel": "item" property which indicates how to access items of the collection; its href property is a templated URI which can be expanded using instance data and schema (here we only have a single id template variable).
So our client may navigate to the first item of the collection (id="859") at /book/859 URI, and retrieve resource data:
GET /book/859 Accept: application/json HTTP/1.1 200 OK Allow: GET, PUT, DELETE Content-Type: application/json { "author": [ "Victor Hugo" ], "publication_date": "1869-04-01T00:00:00", "title": "L'homme qui rit" }
and schema:
GET /book/859/schema Accept: application/schema+json HTTP/1.1 200 OK Content-Type: application/json { "$ref": "#/definitions/Book", "definitions": { "Book": { "additionalProperties": false, "properties": { "author": { "items": { "type": "string" }, "title": "author", "type": "array" }, "publication_date": { "format": "date-time", "title": "publication date", "type": "string" }, "title": { "title": "title", "type": "string" }, "topics": { "items": { "type": "string" }, "title": "topics", "type": "array" } }, "title": "Book", "type": "object" } }, "links": [ { "href": "/book/", "rel": "up", "targetSchema": { "$ref": "/book/schema" }, "title": "Book_plural" }, { "href": "/book/859/", "rel": "self", "schema": { "$ref": "/book/859/schema?role=edition" }, "targetSchema": { "$ref": "/book/859/schema?role=view" }, "title": "Book #859" } ] }
Entity resource
The resource obtained above as an item of a collection is actually an entity. Notice the rel="self" link. It indicates how to manipulate the current resource (i.e. at which URI, using a given schema depending on what actions we want to perform). Still this link does not indicate what actions may be performed. This indication is found in the Allow header of the data response above:
Allow: GET, PUT, DELETE
With these information bits, our intelligent client is able to, for instance, form a request to delete the resource. On the other hand, the action to update the resource (which is allowed because of the presence of PUT in Allow header, per HTTP semantics) would take the form of a request which body conforms to the JSON Schema pointed at by the schema property of the link.
Also note the rel="up" link which makes it possible to navigate to the collection of books.
Conclusions
This post introduced the main hypermedia capabilities of cubicweb-jsonschema, built on top of the JSON Hyper Schema specification. The resulting Hypermedia API makes it possible for an intelligent client to navigate through hypermedia resources and manipulate them by using both link relation semantics and HTTP verbs.
In the next post, I'll deal with relationships description and manipulation both in terms of API (endpoints) and hypermedia representation.