siren-parser
v9.1.0
Published
[![Build Status](https://travis-ci.org/Brightspace/node-siren-parser.svg?branch=master)](https://travis-ci.org/Brightspace/node-siren-parser) [![Coverage Status](https://coveralls.io/repos/Brightspace/node-siren-parser/badge.svg?branch=master&service=gith
Downloads
35,999
Readme
node-siren-parser
Parses a Siren object (or Siren JSON string) into an Entity object that is intended to be easier to work with and test, and prevent having to parse replies from Siren APIs manually. Fully implements the Siren spec, including all restrictions and requirements on entities, links, fields, etc. Kinda complements node-siren-writer, in that they're intended to sort of be opposites. Also includes a plugin for use with chai.
Installation
Install from NPM:
npm install siren-parser
Usage
There are three ways to use siren-parser
's functionality.
In Node.js,
require
it as you would any other NPM package:const SirenParse = require('siren-parser').default; var parsedEntity = SirenParse('{"class":["foo","bar"]}');
An ES6 module is available as well for import:
import SirenParse from 'siren-parser'; var parsedEntity = SirenParse('{"class":["foo","bar"]}');
You can also import
Action
,Entity
, andLink
by name if you need to add custom functionality to parsed entities.import SirenParse, { Action, Entity, Link } from 'siren-parser'; Entity.prototype.printEntity = function() { console.log(this) }; var parsedEntity = SirenParse('{"class":["foo","bar"]}'); // parsedEntity will have printEntity()
An ES6 module installed on the window as a global:
<script type="module" src="siren-parser/global.js"></script> <script> var parsedEntity = D2L.Hypermedia.Siren.Parse('{"class":["foo","bar"]}'); </script>
Note that this is a
deumdify
'd browser bundle, which should prevent collisions with other modules on the page that are exposed by browserify's standalone UMD bundle.
API
const sirenParser = require('siren-parser');
const sirenParserChai = require('siren-parser/chai');
const sirenSuperagent = require('siren-parser/superagent');
const sirenJson = {
title: 'My title',
class: ['outer'],
links: [{
rel: ['self', 'crazy'],
href: 'http://example.com'
}, {
rel: ['crazy'],
href: 'http://example2.com'
}],
actions: [{
name: 'fancy-action',
href: 'http://example.com',
title: 'A fancy action!',
method: 'GET',
fields: [{
name: 'max',
title: 'Maximum value'
}]
}],
entities: [{
class: ['inner', 'smaller'],
rel: ['child'],
links: [{
rel: ['self'],
href: 'http://example.com/child',
title: 'Child entity'
}]
}],
properties: {
one: 1,
two: 2,
pi: 'is exactly three'
}
};
const resource = sirenParser(sirenJson);
// ... assuming you've got all your chai stuff set up
expect(resource).to.have.sirenAction('fancy-action');
const request = require('superagent');
sirenSuperagent.perform(request, resource.getAction('fancy-action'))
.submit({key: 'value'}) // overrides default field(s) specified in action
.parse(sirenSuperagent.parse)
.end(function(err, res) {
const resource = res.body; // parsed siren resource
expect(resource).to.have.sirenProperty('some-field');
});
// Alternatively, add the parser to the global superagent parser
request.parse['application/vnd.siren+json'] = sirenSuperagent.parse;
API
sirenParser(String|Object siren)
Returns an Entity object with all Siren attributes specified. Input can be a Siren object or a Siren JSON string.
Helper functions
A set of helper functions are defined in the parser to help make working with the Siren resources easier. In general, each resource type has a set of hasXByY
and getXByY
functions, where X and Y are Siren resource types and Siren resource properties, respectively. There are some "shortcut" functions that are kept around for compatibility (e.g. Entity.hasAction(name)
), but these are less explicit, so it is recommended to use the newer, more explicit functions (i.e. Entity.hasActionByName(name)
).
These are contextually correct with respect to the Siren spec - as an example, there is no Entity.getActionsByName
(only Entity.getActionByName
, singular), as Action names must be unique on an Entity, meaning only one would ever be returned anyway.
It is also important to note that any "singular" functions (e.g. Entity.getActionByName
, as opposed to Entity.getActionsByName
) just returns the first matching resource, with no guarantees of order. This is useful in situations where you know there is only one matching resource, but do be careful with its use.
See each resource section for a full list of helper functions for that resource type.
Entity
Attributes:
rel
(array of strings) Required if Entity is a sub-entitytitle
(string)type
(string)properties
(object)class
(array of strings)actions
(array of Actions)links
(array of Links)entities
(array of Entities)
Each of these can be accessed as Entity.attribute
, e.g. if one of the input's properties
isfoo
, it would be accessed as Entity.properties.foo
.
Note that only those attributes present in the input will be copied into the
Entity
, i.e. if your input has no links,Entity.links
will not be set, rather than being an empty array.
Entity.hasXByY(String|RegExp key)
Returns true if the Entity has an X with a Y of key
(or which matches RegExp key
), otherwise false.
hasActionByName(name)
hasActionByClass(class)
hasClass(class)
hasSubEntityByRel(rel)
hasSubEntityByClass(class)
hasSubEntityByType(type)
hasLinkByRel(rel)
hasLinkByClass(class)
hasLinkByType(type)
hasProperty(prop)
resource.hasActionByName('fancy-action'); // true
resource.hasClass('inner'); // false
resource.hasSubEntityByRel('child'); // true
resource.hasSubEntityByType('child'); // false
resource.hasLinkByRel('crazy'); // true
resource.hasProperty('three'); // false
Entity.getXByY(String|RegExp key)
Returns the resource(s) of type X with a Y value of key
(or which matches RegExp key
). If the requested X is singular, then the result is either the matching instance of X, or undefined. If the requested X is plural, then the result is either an Array of the matching instances of X, or an empty Array.
getActionByName(name)
- returns Action or undefinedgetActionByClass(class)
- returns Action or undefinedgetLinkByRel(rel)
- returns Link or undefinedgetLinkByClass(class)
- returns Link or undefinedgetLinkByType(type)
- returns Link or undefinedgetSubEntityByRel(rel)
(getSubEntity(rel)
) - returns Entity or undefinedgetSubEntityByClass(class)
- returns Entity or undefinedgetSubEntityByType(type)
- returns Entity or undefinedgetActionsByClass(class)
- returns Array of Actions (empty Array if none match)getLinksByRel(rel)
- returns Array of Links (empty Array if none match)getLinksByClass(class)
- returns Array of Links (empty Array if none match)getLinksByType(type)
- returns Array of Links (empty Array if none match)getSubEntitiesByRel(rel)
- returns Array of Entities (empty Array if none match)getSubEntitiesByClass(class)
- returns Array of Entities (empty Array if none match)getSubEntitiesByType(type)
- returns Array of Entities (empty Array if none match)
resource.getActionByName('fancy-action'); // The 'fancy-action' Action instance
resource.getActionByName('fancy-action').title; // 'A fancy action!'
resource.getLinkByRel('self'); // The 'self' Link instance
resource.getLinkByRel('crazy'); // The same Link instance as above
resource.getLinkByRel('self').rel; // ['self', 'crazy']
resource.getLinksByRel('crazy'); // Array containing two Links
resource.getSubEntitiesByRel('child'); // Array of two entities
resource.getSubEntityByRel('child'); // Single entity
resource.getSubEntityByRel('child').getLink('self').title; // 'Child entity'
resource.getSubEntityByClass('inner'); // Entity with 'inner' class
resource.getSubEntitiesByClass('inner'); // [ All Entities with 'inner' class ]
Entity.getXByY'es(Array[String|RegExp key])
Returns the resource(s) of type X with matching Y values of multiple key
(or which matches RegExp key
). The requested X must contain each of the Y values. If the requested X is singular, then the result is either the matching instance of X, or undefined. If the requested X is plural, then the result is either an Array of the matching instances of X, or an empty Array.
getActionByClasses(classes)
- returns Action or undefinedgetActionsByClasses(classes)
- returns Array of Action (empty Array if none match)getFieldByClasses(classes)
- returns Field or undefinedgetFieldsByClasses(classes)
- returns Array of Field (empty Array if none match)getLinkByClasses(class)
- returns Link or undefinedgetLinksByClasses(class)
- return Array of Link (empty Array if none match)getLinkByRels(class)
- returns Link or undefinedgetLinksByRels(class)
- return Array of Link (empty Array if none match)getSubEntityByClasses(classes)
- returns Entity or undefinedgetSubEntitiesByClasses(classes)
- returns Array of Entity (empty Array if none match)getSubEntityByRels(classes)
- returns Entity or undefinedgetSubEntitiesByRels(classes)
- returns Array of Entity (empty Array if none match)
resource.getActionByClasses(['crazy', 'self']) // An action instance that contains both 'self' and 'crazy' classes.
resource.getActionsByClasses(['crazy', 'cool']) // Array containing two actions, both of which contain both 'crazy' and 'cool' classes. Note that an action containing only one of those will not be included in the array.
resource.getFieldByClasses(['crazy', 'self']) // A field instance that contains both 'self' and 'crazy' classes.
resource.getFieldsByClasses(['crazy', 'cool']) // Array containing two fields, both of which contain both 'crazy' and 'cool' classes. Note that a field containing only one of those will not be included in the array.
resource.getLinkByClasses(['crazy', 'self']) // A link instance that contains both 'self' and 'crazy' classes.
resource.getLinksByClasses(['crazy', 'cool']) // Array containing two links, both of which contain both 'crazy' and 'cool' classes. Note that a link containing only one of those will not be included in the array.
resource.getLinkByRels(['thing1', 'thing2']) // A link instance that contains both 'thing1' and 'thing2' rels.
resource.getLinksByRels(['thing1', 'thing2']) // Array containing two links, both of which contain both 'thing1' and 'thing2' rels. Note that a link containing only one of those will not be included in the array.
resource.getSubEntityByClasses(['crazy', 'self']) // An entity instance that contains both 'self' and 'crazy' classes.
resource.getSubEntitiesByClasses(['crazy', 'cool']) // Array containing two entities, both of which contain both 'crazy' and 'cool' classes. Note that an entity containing only one of those will not be included in the array.
resource.getSubEntityByRels(['thing1', 'thing2']) // An entity instance that contains both 'thing1' and 'thing2' rels.
resource.getSubEntitiesByRels(['thing1', 'thing2']) // Array containing two entities, both of which contain both 'thing1' and 'thing2' rels. Note that an entity containing only one of those will not be included in the array.
Link
Attributes:
rel
(array of strings) Requiredhref
(string) Requiredclass
(array of strings)title
(string)type
(string) Must be a Siren Link type
Link.hasClass(String class)
Links only have this one helper function. Returns true if the Link has the specified class
, otherwise false.
resource.getLink('crazy').hasClass('foo'); // false
Action
Attributes:
name
(string) Requiredhref
(string) Requiredclass
(array of strings)method
(string)title
(string)type
(string) Must be a Siren Action typefields
(array of Fields)
Action.hasXByY(String|RegExp key)
Returns true if the Action has an X with a Y of key
(or which matches RegExp key
), otherwise false.
hasFieldByName(name)
(hasField(name)
) - returns true if any action on the entity has an action namedname
hasFieldByClass(class)
hasFieldByType(type)
hasClass(class)
resource.hasClass('foo'); // false
resource.getActionByName('fancy-action').hasFieldByName('max'); // true
Action.getXByY(String|RegExp key)
Returns the resource(s) of type X with a Y value of key
(or which matches RegExp key
). If the requested X is singular, then the result is either the matching instance of X, or undefined. If the requested X is plural, then the result is either an Array of the matching instances of X, or an empty Array.
getFieldByName(name)
(getField(name)
) - returns Field or undefinedgetFieldByClass(class)
- returns Field or undefinedgetFieldByType(type)
- returns Field or undefinedgetFieldsByClass(class)
- returns Array of Fields (empty Array if none match)getFieldsByType(type)
- returns Array of Fields (empty Array if none match)
resource.getActionByName('fancy-action').getFieldByName('max'); // The 'max' Field instance
resource.getActionByName('fancy-action').getFieldByName('max').title; // 'Maximum value'
Field
Attributes:
name
(string) Requiredvalue
(string)class
(array of strings)type
(string) Must be a Siren Field typetitle
(string)min
(number)max
(number)
Field.hasClass(String class)
Fields only have this one helper function. Returns true if the Field has the specified class
, otherwise false.
resource.getAction('fancy-action').getField('max').hasClass('foo'); // false
chai
interface
There are a few helper chai
methods included with this module, under ./chai
. These are intended to make testing with chai
cleaner. The chai interface adds the following methods and properties:
- Properties
- sirenAction/sirenActions - changes the subject of the assertion to the entity's Actions (or throws if it has none)
- sirenEntity/sirenEntities - changes the subject of the assertion to the entity's sub-Entities (or throws if it has none)
- sirenLink/sirenLinks - changes the subject of the assertion to the entity's Links (or throws if it has none)
- sirenProperty/sirenProperties - changes the subject of the assertion to the entity's Properties (or throws if it has none)
- sirenField/sirenFields - changes the subject of the assertion to the action's Fields (or throws if it has none)
- all - flags further operations to occur on all of the subject, rather than just any
- Methods:
- classes(cls1, cls2, ...) - asserts whether the subject has all the given classes
- href(href) - asserts whether the subject has the given href
- method(method) - asserts whether the subject has the given method
- name(name) - asserts whether the subject (Action(s)) has the given name
- rels(rel1, rel2, ...) - asserts whether the subject has all the given rels
- title(title) - asserts whether the subject has the given title
- type(type) - asserts whether the subject has the given title
- value(value) - asserts whether the subject (Field(s)) has the given value
expect(resource).to.have.a.sirenAction.with.method('GET');
expect(resource).to.have.sirenLinks.with.classes('foo', 'bar'); // Will pass if at least 1 of resource's actions have each given class
expect(resource).to.have.sirenLinks.all.with.classes('foo', 'bar'); // Passes only if all of resource's actions have all given class
expect(resource).to.have.a.sirenEntity.with.a.sirenEntity.with.title('foo'); // Check a sub-sub-entity's title
superagent
interface
There are two helper superagent
methods included with this module, under ./superagent
.
.parse(sirenSuperagent.parse)
- To be used withsuperagent
's.parse()
methodsirenSuperagent.perform(request, action)
- Returns unendedsuperagent
request object
Testing
npm test
Versioning and Releasing
This repo is configured to use semantic-release
. Commits prefixed with fix:
and feat:
will trigger patch and minor releases when merged to main
.
To learn how to create major releases and release from maintenance branches, refer to the semantic-release GitHub Action documentation.
Code Style
This repository is configured with EditorConfig and ESLint and rules.