Heracles Resources vs JSON-LD Compaction - Enumerable Js Properties

In my previous post I presented the first incarnation of Heracles, the Hydra Core client library. While trying to replace my makeshift client I’d implemented for an in-house training project at PGS I quickly decided that I’m going to need a way to compact my resources. It wasn’t that hard but there was one simple hurdle to overcome.

TL;DR; with Heracles you can do this:

1
2
3
4
5
6
7
Hydra.Resource.load('http://my.api/my/resource')
    .then(res => {
        return jsonld.promises.compact(res, context);
    })
    .then(compacted => {
        // do something with the compacted resource
    });

URI properties are a nuisance

Just as in heracles, in my proof of concept code I too mostly worked with expanded JSON-LD objects. This has the downside that any time I needed to access the properties full property identifiers must be used. Also it is not possible with Polymer to use the indexer notation for declarative data binding:

1
2
3
4
5
<!-- Such markup is not valid data binding syntax in Polymer -->
<span>{{myObject['http://xmlns.com/foaf/0.1/name']}}</span>

<!-- Databound object's properties must be accessed with the dot notation -->
<span>{{myObject.name}}</span>

This is precisely what JSON-LD compaction algorithm is for. It translates URI keys in a compacted JSON object. This translation is defined in a @context object.

1
2
3
4
5
6
7
8
9
10
11
12
// before
var original = {
    'http://xmlns.com/foaf/0.1/name': 'Tomasz Pluskiewicz'
}

// after
var compacted = {
    '@context': {
        'name': 'http://xmlns.com/foaf/0.1/'
    },
    'name': 'Tomasz Pluskiewicz'
}

There are many tricks up compaction’s sleeve, which can help turning ugly JSON-LD into a digestive form. Have a look at this presentation by Markus Manthaler for some more examples.

My code before

In my code I used compaction to get rid of long URI keys so that I can take advantage of Polymer’s data binding without verbose methods like computed properties or wrapping the object in a view model class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
getAuthors(model)
{
    var context = {
        "@vocab": "http://wykop.pgs-soft.com/vocab#WeeksLink/",
        "member": "http://www.w3.org/ns/hydra/core#member",
        "links": {
            "@id": "http://wykop.pgs-soft.com/vocab#WeeksLinksBySubmitter/links",
            "@container":"@set"
        },
        "submitter": "http://wykop.pgs-soft.com/vocab#WeeksLinksBySubmitter/submitter"
    };

    jsonld.promises.compact(model, context).then(model => {
        this.compactedModel = model;
    });
}

This is simple, the jsonld.js library takes care of the heavy lifting and produces a compacted object which is data binding friendly.

Enter heracles

How is this relevant to the heracles library? In my previous post I showed the Operation type (and other parts of the ApiDocumentation classes) can be compacted so that working with them is easier.

Resources however are a little different. They are always returned expanded and thus should be ready for being compacted. I was surprised to see that jsonld.promises.compact throws a stack overflow error. The reason is that JSON-LD algorithms are not designed to work with cyclical object graphs. It simply loops until the call stack runs out.

The Resource class

In my code I have this PartialCollectionView class (excerpt):

1
2
3
4
5
6
7
8
9
10
11
12
13
export class PartialCollectionView extends Resource {
    private _collection;

    constructor(actualResource, apiDoc:IApiDocumentation, incomingLinks) {
        super(actualResource, apiDoc, incomingLinks);

        this._collection = findCollection(incomingLinks);
    }

    get collection() {
        return this._collection;
    }
}

See the collection getter? This is where I had a cycle (collection -> view -> collection …). There was also another cycle inside the apiDocumentation getter in the base Resource class. There are actually two thing going on here. The first and obvious culprit is the private field. Of course this is just TypeScript sugar, because it will become just a typical field in the compiled JavaScript. JavaScript has no such notion of private members.

Solution

The first step was to get rid of the field. There is no perfect way to do that but a friend of mine sent me this post, which presents the use of WeakMap as a possible solution. With that I changed my code so that it no longer contains unwanted fields. (actual code is actually a little different but you get the drift)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var _collection = new WeakMap();

export class PartialCollectionView extends Resource {

    constructor(actualResource, apiDoc:IApiDocumentation, incomingLinks) {
        super(actualResource, apiDoc, incomingLinks);

        _collection = _collection.set(this, findCollection(incomingLinks));
    }

    get collection() {
        return _collection.get(this);
    }
}

Unfortunately the compaction algorithm still entered the vicious cycle and failed. Why is that? Because enumerable properties. jsonld.js iterates over the object using simple for (var i in obj) loop, which also returns all getters by default. One way is to use the native Object.defineProperty method instead of ES6 get x() syntax but it breaks TypeScript code analysis and generally smells. There is a better way though.

Solution part two

Luckily TypeScript has the decorators and there is a decorator, which does precisely what I wanted. Instead of writing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var _collection = new WeakMap();

export class PartialCollectionView extends Resource {

    constructor(actualResource, apiDoc:IApiDocumentation, incomingLinks) {
        super(actualResource, apiDoc, incomingLinks);

        _collection = _collection.set(this, findCollection(incomingLinks));

        Object.defineProperty(this, 'collection', {
            get: () => _collection.get(this)
        });
    }
}

I can simply install the core-decorators package from jspm (npm) and decorate the property with @nonenumerable

1
jpsm install npm:core-decorators
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { nonenumerable } from 'core-decorators';

var _collection = new WeakMap();

export class PartialCollectionView extends Resource {

    constructor(actualResource, apiDoc:IApiDocumentation, incomingLinks) {
        super(actualResource, apiDoc, incomingLinks);

        _collection = _collection.set(this, findCollection(incomingLinks));
    }

    @nonenumerable
    get collection() {
        return _collection.get(this);
    }
}

One Caveat

Of course this will still fail if there are actual cycles in the object graph. I’m hoping though that it won’t be the case all too often. And for the rare occasion a library like circular-json can be used as suggested in this github issue. It will make sure that there are no reference cycles. Unfortunately it is a only replacement for JSON.stringify and so to use it with jsonld.js it’s necessary to deserialize and serialize every time:

1
2
3
4
5
import * as CircularJSON from 'circular-json';

var serialized = CircularJSON.stringify(object);
var jsonLd = JSON.parse(serialized);
jsonld.promises.compact(jsonLd, context).then(/* ... */)

This is because jsonld.js wants to treat a string parameter as URI.

Please let me know if there is a better way for handling cyclical objects…

Comments