Introducing Heracles - Hydra Core Hypermedia Client

Lately I’ve been working on a library to consume Hydra Core hypermedia-rich APIs. This is something I’ve been planning for a long time now and given that the Argolis server-side component pretty much works it was about time I started working on consuming the API Documentation.

In this post I showcase the simplest usage of heracles and describe some design decisions. I guess I should write about Argolis too in the near future.

The source code of heracles is naturally on GitHub. It is written in TypeScript and bundled as an AMD format package.

Getting started

Installation

To start using heracles first download it using JSPM package manager.

1
jspm install wikibus/heracles

Basic usage

Now you are ready to start using the library. It is as simple as importing and executing the static load function. It returns a promise of a resource.

1
2
3
4
5
6
import {Hydra} from 'wikibus/heracles';

Hydra.loadResource('http://my.api/my/resource')
    .then(res => {
        // do something with the resource
    });

The returned model will always be expanded although in the future I could consider adding an optional @context parameter.

Every time a resource is loaded the Linked Hydra API Documentation will be fetched as well to discover possible operations for the resource(s). Here’s an example of a documentation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{
    "@context": [
        "http://www.w3.org/ns/hydra/context.jsonld",
        {
            "vocab": "http://my.api/vocab#",
            "foaf": http://xmlns.com/foaf/0.1/"
         }
    ],
    "supportedClass": [
        {
            "@id": "vocab:Person",
            "supportedOperation": [
                {
                    "method": "GET",
                    "expects": "owl:Nothing",
                    "returns": "vocab:Person"
                }
            ],
            "supportedProperty": [
                {
                    "readable": true,
                    "writable": false,
                    "required": false,
                    "property": {
                        "@id": "vocab:pets"
                    },
                    "supportedOperation": [
                        {
                            "method": "POST",
                            "expects": "vocab:Pet",
                            "returns": "owl:Nothing"
                        }
                    ]
                }
            ]
        },
        {
            "@id": "vocab:Pet",
            "supportedProperty": [
                {
                    "readable": true,
                    "writable": true,
                    "required": true,
                    "property": "foaf:name"
               }
           ]
        }
    ]
}

The above states a number of facts about the API:

The server is known to return resources of the type vocab:Person.

A GET request is known to be supported for resources of types vocab:Person

The vocab:Person class can be expected to include a vocab:pets link to another resource

That other, linked resource can be requested using POST with an instance of class vocab:Pet

A valid instance of vocab:Pet must include the foaf:name property

All this information can be accessed from resources loaded using the Hydra.Resource.load method above. Given a representation of the resource http://my.api/Tomasz

1
2
3
4
5
6
7
8
9
{
    "@context": {
        "vocab": "http://my.api/vocab#",
        "foaf": "http://xmlns.com/foaf/0.1/"
    },
    "@id": "http://my.api/Tomasz",
    "@type": "vocab:Person",
    "vocab:pets": { "@id": http://my.api/Tomasz/pets" }
}

It is possible to discover operations available for any of the instances

1
2
3
4
5
6
7
8
9
10
11
// assume loaded earlier with Hydra.Resource.load('http://my.api/Tomasz')
var resource;

resource.getOperations().then(ops => {
    // will return the GET operation supported by the vocab:Person class
});

resource['http://my.api/vocab#pets'].getOperations().then(ops => {
    // will return the POST operation supported by the link type
    expect(ops[0].method)
});

Important bits and pieces

There are some decisions I made, which may influence how the server and client must act. Most notably

Resources are expanded

First of all, as I’ve stated above, the loaded resource representation is expanded by default. This is because otherwise it would be quite difficult to process them. This is true for example for inspecting the resource @type.

load returns object with matching @id

If a resource representation is a larger graph of objects, the load function will always look for that identifier and return that object even if it was not the root of the JSON-LD document. For example, the current design of collections in Hydra is that each collection can be partitioned into views (for example for the purpose of paging). Requesting a resource http://my.api/Tomasz/pets?page=2 could return something similar to:

1
2
3
4
5
6
7
8
9
10
11
12
{
    "@context": "http://my.api/some/context",
    "@id": "http://my.api/Tomasz/pets",
    "@type": "hydra:Collection",
    "hydra:member": [ ]
    "hydra:view": {
        "@id": "http://my.api/Tomasz/pets?page=2",
        "@type": "hydra:PartialCollectionView",
        "hydra:previous": "http://my.api/Tomasz/pets",
        "hydra:next": "http://my.api/Tomasz/pets?page=3"
    }
}

As you see the requested resource is not the root of the representation tree. Still the load promise will resolve with that object and not http://my.api/Tomasz/pets. This may be counterintuitive in the case of simple JSON-LD documents but considering that the server could be returning expanded or flattened documents it seems the only logical way. Not to mention that other RDF media type could be requested by the client in which case, there would no obvious root object.

Each common case from Hydra Core vocabulary like the PartialCollectionView (possible any object of the hydra:view property) will be enriched with a link to the parent collection. Otherwise it wouldn’t be possible to access it from the returned object.

Hydra documentation objects are compacted

For convenience elements of the Hydra Core vocabulary are compacted with the default hydra @context so that on can write op.method instead of op['https://www.w3.org/ns/hydra/core#method']. If the object contained any non-standard content, such as SHACL constraints for a supported property, it is possible to recompact with a custom context:

1
2
3
operation.compact(myContext).then(compacted => {
    // access the properties as you see fit
});

Going further

A rich interaction with the loaded resource isn’t possible just yet. As you see above currently only the basic metadata about operations is available. I’ve also started work on accessing supported properties. In the future I plan a number of facilities to ease invoking operations, handling common Hydra objects in specific ways, easier extensions, improved error handling, etc.

Comments