Skip to content

Instantly share code, notes, and snippets.

@xogeny
Last active April 27, 2018 20:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xogeny/5cb2abc8c45ccd129adc5aa8b93d9f06 to your computer and use it in GitHub Desktop.
Save xogeny/5cb2abc8c45ccd129adc5aa8b93d9f06 to your computer and use it in GitHub Desktop.
A quick example of defining a collection of Siren resources

Introduction

I've been thinking a lot lately about the value of having a high-level profile for a hypermedia APIs. These are some thoughts. I should point out that I have a "Siren-centric" focus here, but I suspect such a proile, if done right, could really open things up for hypermedia APIs in general, independent of format, by providing a single source of truth about the potential resources, their properties, their actions and their relations.

Goals

The goal here is to describe a Siren API so that it can be both documented and validated. To accomplish this, the description of the API is broken into three different things being documented: resources, relations and schemas.

Documentation

Ideally, the information in this description could be used to create high-level diagrams around an API that would demonstrate to users how to navigate between resources. For example, the description included in this gist could be easily translated into the following diagram:

Class Diagram

(you can see how this diagram was created here)

Validation

The description documents the structure not only of a resources properties, but also the arguments required for its actions. This information could be used in a number of ways. For example, it could be used to synthesize types (e.g., for languages like TypeScript or Rust) used by either the client or the server. It could also be used during testing to assert that all data being exchanged conforms to the schemas in the documentation.

Caching

The expectation is that this profile information is independent of the actual resource responses. For this reason, any profile information should be pretty much invariant and can easily be cached.

Format

Resources

The value associated with resources is an object where each key represents a class. Each class includes information about both the properties of that class as well as its potential actions (whether an action is actually available would be determined by a real response). Part of the goal of this effort is to eliminate repetitive information from responses. Much of the information about Siren actions is repeated. By documenting it in a profile, it is no longer necessary for the server to include it in responses. of course, this means that the client will be required to parse the profile information in order to have a complete picture of each resource.

Relations

The relations key documents all the relations known in the API. Furthermore, it documents each instance where a relation can be used as an "edge" between resources. With this information one can easily visualize how to navigate an API.

Schemas

The schemas key is used as a way to consolidate reuable schemas. The schemas can always be used inline in the descriptions of each resource or action. But in cases where a given schema appears more than once, it can be placed in the schemas section so that it can be referred to via a reference (i.e., { "$ref": "#/schemas/..." })

{
"resources": {
"order": {
"properties": {
"type": "object",
"properties": {
"orderNumber": { "type": "number" },
"itemCount": { "type": "number" },
"status": { "type": "string", "enum": ["submitted", "pending", "completed"] }
},
"required": ["orderNumber", "itemCount", "status"]
},
"actions": {
"add-item": {
"title": "Add Item",
"method": "POST",
"type": "application/x-www-form-urlencoded",
"fields": { "$ref": "#/schemas/add-item" }
}
}
},
"order-items": {},
"customer": {
"properties": { "$ref": "#/schemas/customer-properties" }
}
},
"relations": {
"collection": [{ "from": "order", "to": ["order-items"] }],
"items": [{ "from": "order", "to": ["order-items"] }],
"customer": [{ "from": "order", "to": ["customer"] }],
"info": [{ "from": "order", "to": ["customer"] }],
"next": [{ "from": "order", "to": ["order"] }],
"prev": [{ "from": "order", "to": ["order"] }]
},
"schemas": {
"add-item": {
"type": "object",
"properties": {
"orderNumber": { "type": "number" },
"productCode": { "type": "text" },
"quantity": { "type": "number" }
}
},
"customer-properties": {
"type": "object",
"properties": {
"customerId": { "type": "string" },
"name": { "type": "string" }
},
"required": ["customerId", "name"]
}
}
}
# TypeScript definitions
import { ISchema } from "@xogeny/ts-schema";
export type SchemaReference = { $ref: string };
export type Schema = ISchema | SchemaReference;
export interface Profile {
resources?: { [className: string]: ClassProfile };
relations?: { [rel: string]: Array<RelationProfile> };
schemas?: { [schema: string]: Schema };
}
export interface ClassProfile {
properties?: Schema;
actions?: { [actionName: string]: ActionProfile };
}
export interface ActionProfile {
title?: string;
method?: "GET" | "POST" | "PUT" | "PATCH" | "OPTIONS" | "DELETE";
type?: string;
fields?: Schema;
}
export interface RelationProfile {
from: string;
to: string[];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment