halfred
Version:
parses JSON HAL resources (Hypertext Application Language)
205 lines (177 loc) • 5.63 kB
JavaScript
;
var Resource = require('./resource')
, Stack = require('./immutable_stack');
var linkSpec = {
href: { required: true, defaultValue: null },
templated: { required: false, defaultValue: false },
type: { required: false, defaultValue: null },
deprecation: { required: false, defaultValue: null },
name: { required: false, defaultValue: null },
profile: { required: false, defaultValue: null },
title: { required: false, defaultValue: null },
hreflang: { required: false, defaultValue: null }
};
var logger;
function Parser(_logger) {
logger = _logger;
}
Parser.prototype.parse = function parse(unparsed, validationFlag) {
var validation = validationFlag ? [] : null;
return _parse(unparsed, validation, new Stack());
};
function _parse(unparsed, validation, path) {
if (unparsed == null) {
return unparsed;
}
var allLinkArrays = parseLinks(unparsed._links, validation,
path.push('_links'));
var curies = parseCuries(allLinkArrays);
var allEmbeddedArrays = parseEmbeddedResourcess(unparsed._embedded,
validation, path.push('_embedded'));
var resource = new Resource(allLinkArrays, curies, allEmbeddedArrays,
validation);
copyNonHalProperties(unparsed, resource);
resource._original = unparsed;
return resource;
}
function parseLinks(links, validation, path) {
links = parseHalProperty(links, parseLink, validation, path);
if (links == null || links.self == null) {
// No links at all? Then it implictly misses the self link which it SHOULD
// have according to spec
reportValidationIssue('Resource does not have a self link', validation,
path);
}
return links;
}
function parseCuries(linkArrays) {
if (linkArrays) {
return linkArrays.curies;
} else {
return [];
}
}
function parseEmbeddedResourcess(original, parentValidation, path) {
var embedded = parseHalProperty(original, identity, parentValidation, path);
if (embedded == null) {
return embedded;
}
Object.keys(embedded).forEach(function(key) {
embedded[key] = embedded[key].map(function(embeddedElement) {
var childValidation = parentValidation != null ? [] : null;
var embeddedResource = _parse(embeddedElement, childValidation,
path.push(key));
embeddedResource._original = embeddedElement;
return embeddedResource;
});
});
return embedded;
}
/*
* Copy over non-hal properties (everything that is not _links or _embedded)
* to the parsed resource.
*/
function copyNonHalProperties(unparsed, resource) {
Object.keys(unparsed).forEach(function(key) {
if (key !== '_links' && key !== '_embedded') {
resource[key] = unparsed[key];
}
});
}
/*
* Processes one of the two main hal properties, that is _links or _embedded.
* Each sub-property is turned into a single element array if it isn't already
* an array. processingFunction is applied to each array element.
*/
function parseHalProperty(property, processingFunction, validation, path) {
if (property == null) {
return property;
}
// create a shallow copy of the _links/_embedded object
var copy = {};
// normalize each link/each embedded object and put it into our copy
Object.keys(property).forEach(function(key) {
copy[key] = arrayfy(key, property[key], processingFunction,
validation, path);
});
return copy;
}
function arrayfy(key, object, fn, validation, path) {
if (isArray(object)) {
return object.map(function(element) {
return fn(key, element, validation, path);
});
} else {
return [fn(key, object, validation, path)];
}
}
function parseLink(linkKey, link, validation, path) {
if (!isObject(link)) {
throw new Error('Link object is not an actual object: ' + link +
' [' + typeof link + ']');
}
// create a shallow copy of the link object
var copy = shallowCopy(link);
// add missing properties mandated by spec and do generic validation
Object.keys(linkSpec).forEach(function(key) {
if (copy[key] == null) {
if (linkSpec[key].required) {
reportValidationIssue('Link misses required property ' + key + '.',
validation, path.push(linkKey));
}
if (linkSpec[key].defaultValue != null) {
copy[key] = linkSpec[key].defaultValue;
}
}
});
// check more inter-property relations mandated by spec
if (copy.deprecation) {
logger.warn('Link ' + pathToString(path.push(linkKey)) +
' is deprecated, see ' + copy.deprecation);
}
if (copy.templated !== true && copy.templated !== false) {
copy.templated = false;
}
if (!validation) {
return copy;
}
if (copy.href && copy.href.indexOf('{') >= 0 && !copy.templated) {
reportValidationIssue('Link seems to be an URI template ' +
'but its "templated" property is not set to true.', validation,
path.push(linkKey));
}
return copy;
}
function isArray(o) {
return Object.prototype.toString.call(o) === '[object Array]';
}
function isObject(o) {
return typeof o === 'object';
}
function identity(key, object) {
return object;
}
function reportValidationIssue(message, validation, path) {
if (validation) {
validation.push({
path: pathToString(path),
message: message
});
}
}
function shallowCopy(source) {
var copy = {};
Object.keys(source).forEach(function(key) {
copy[key] = source[key];
});
return copy;
}
function pathToString(path) {
var s = '$.';
for (var i = 0; i < path.array().length; i++) {
s += path.array()[i] + '.';
}
s = s.substring(0, s.length - 1);
return s;
}
module.exports = Parser;