9.1.0 • Published 4 months ago

siren-parser v9.1.0

Weekly downloads
1,401
License
Apache-2.0
Repository
github
Last release
4 months ago

node-siren-parser

Build Status Coverage Status

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.

  1. In Node.js, require it as you would any other NPM package:

    const SirenParse = require('siren-parser').default;
    var parsedEntity = SirenParse('{"class":["foo","bar"]}');
  2. 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, and Link 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()
  3. 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-entity
  • title (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 undefined
  • getActionByClass(class) - returns Action or undefined
  • getLinkByRel(rel) - returns Link or undefined
  • getLinkByClass(class) - returns Link or undefined
  • getLinkByType(type) - returns Link or undefined
  • getSubEntityByRel(rel) (getSubEntity(rel)) - returns Entity or undefined
  • getSubEntityByClass(class) - returns Entity or undefined
  • getSubEntityByType(type) - returns Entity or undefined
  • getActionsByClass(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 undefined
  • getActionsByClasses(classes) - returns Array of Action (empty Array if none match)
  • getFieldByClasses(classes) - returns Field or undefined
  • getFieldsByClasses(classes) - returns Array of Field (empty Array if none match)
  • getLinkByClasses(class) - returns Link or undefined
  • getLinksByClasses(class) - return Array of Link (empty Array if none match)
  • getLinkByRels(class) - returns Link or undefined
  • getLinksByRels(class) - return Array of Link (empty Array if none match)
  • getSubEntityByClasses(classes) - returns Entity or undefined
  • getSubEntitiesByClasses(classes) - returns Array of Entity (empty Array if none match)
  • getSubEntityByRels(classes) - returns Entity or undefined
  • getSubEntitiesByRels(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) Required
  • href (string) Required
  • class (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) Required
  • href (string) Required
  • class (array of strings)
  • method (string)
  • title (string)
  • type (string) Must be a Siren Action type
  • fields (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 named name
  • 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 undefined
  • getFieldByClass(class) - returns Field or undefined
  • getFieldByType(type) - returns Field or undefined
  • getFieldsByClass(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) Required
  • value (string)
  • class (array of strings)
  • type (string) Must be a Siren Field type
  • title (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 with superagent's .parse() method
  • sirenSuperagent.perform(request, action) - Returns unended superagent 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.

9.1.0

4 months ago

8.6.1

2 years ago

8.6.0

2 years ago

9.0.0

2 years ago

8.5.3

2 years ago

8.5.2

2 years ago

8.5.1

2 years ago

8.5.0

3 years ago

8.4.0

3 years ago

8.2.0

5 years ago

8.1.0

5 years ago

8.0.0

5 years ago

7.1.0

6 years ago

7.0.0

6 years ago

6.6.0

6 years ago

6.5.0

6 years ago

6.4.0

6 years ago

6.3.1

6 years ago

6.3.0

6 years ago

6.2.1

6 years ago

6.1.0

7 years ago

6.0.0

7 years ago

5.2.2

7 years ago

5.2.1

7 years ago

5.2.0

7 years ago

5.1.1

7 years ago

5.1.0

8 years ago

5.0.3

8 years ago

5.0.1

8 years ago