@dwp/govuk-casa
Version:
Framework for creating basic GOVUK Collect-And-Submit-Applications
223 lines (205 loc) • 6.83 kB
JavaScript
const fs = require('fs');
const path = require('path');
const nUrl = require('url');
const resolveObjPath = require('object-resolve-path');
const logger = require('./Logger')('util');
/**
* Generate an object path string that we can use to traverse an object using
* `objectPathValue()`.
*
* @param {...string} paths Path components.
* @returns {string} Constructed path.
*/
function objectPathString(...paths) {
let pathString = paths.length ? paths.join('.') : '';
// FIX: To support unquoted objects, wrap them in quotes
// TASK: Contribute this back to vendor repo.
pathString = pathString.replace(/\[([a-zA-Z_]+[a-zA-Z0-9_]*)\]/g, '["$1"]');
return pathString;
}
/**
* Find the value at the given path of an object. For example, given the
* object ...
* {
* rooms: {
* lounge: {
* objects: [
* 'TV'
* ]
* }
* }
* }
* ... A call to `objectPathValue(obj, 'rooms.lounge.objects[0]')` would
* return "TV".
*
* If using square-braces to access object keys, you also need to wrap the key
* in quotes as you would in JavaScript for any strings that contain non
* alphanumeric character, or begin with a number.
*
* Ref:
* https://www.npmjs.com/package/object-resolve-path
*
* @param {object} obj Object to traverse
* @param {string} paths Path component(s) to use
* @returns {any} The value of the object, or `undefined` if not found
*/
function objectPathValue(obj, ...paths) {
let pathString = objectPathString(...paths);
// If the first path element contains non-valid path characters (^a-z0-9_)
// then wrap it with square notation
pathString = pathString.replace(/^([^[.]+)?($|\[|\.)+/i, '["$1"]$2');
try {
return resolveObjPath(obj, pathString);
} catch (e) {
logger.debug('Object path is invalid: %s (%s)', pathString, e.message);
return undefined;
}
}
/**
* Given an object path string generated by `objectPathString()`, create a
* normalized version that can be used to refer to and name HTML form fields.
* Specifically, this notation should use plain square braces for objects, with
* no quotations.
*
* @param {string} pathString Path to normalize.
* @returns {string} Normalized path.
*/
function normalizeHtmlObjectPath(pathString) {
return pathString.replace(/\[["']([^\]]+?)['"]\]/g, '[$1]').replace(/[[\]]/g, '.').replace(/\.+/g, '.').replace(/\.+/g, '][')
.replace(/\[+$/g, '')
.replace(/^([^[\]]+)]/g, '$1')
.replace(/\[([^\]]+)$/g, '[$1]')
.replace(/\]+/g, ']')
.replace(/\[+/g, '[');
}
/**
* Determine if value is empty. Recurse over objects.
*
• Options:
* RegExp regexRemove = characters matching this regex are removed before test
*
* @param {any} e Value to check
* @param {object} options Options (see above)
* @returns {boolean} True if the object is empty
*/
function isEmpty(e, options) {
let val = e;
if (typeof e === 'string' && options && options.regexRemove) {
val = e.replace(options.regexRemove, '');
}
if (
val === null
|| typeof val === 'undefined'
|| (typeof val === 'string' && val === '')
) {
return true;
}
if (Array.isArray(val) || typeof val === 'object') {
return Object.keys(val).filter((k) => !isEmpty(val[k], options)).length === 0;
}
return false;
}
/**
* Determine which journey is associated with the "journey" part of the given
* url. In multiple-journey mode, URLs are formatted as so:
* /<journey-guid>/<waypoint-id>
*
* @param {Array} journeys Array of UserJourney.Map instances
* @param {string} url URL to parse
* @returns {UserJourney.Map|null} The associated journey
*/
function getJourneyFromUrl(journeys, url) {
// Single-journeys are not reflected in URLs
if (journeys.length === 1) {
return journeys[0];
}
const urlParts = url.replace(/^\/+/, '').split('/');
return urlParts.length ? (journeys.find((j) => (j.guid === urlParts[0])) || null) : null;
}
function getPageIdFromUrl(url) {
return nUrl.parse(url).pathname.replace(/^\/+(.+)$/, '$1').replace(/\/+$/g, '');
}
function getPageIdFromJourneyUrl(journey, url) {
const strippedUrl = (journey && journey.guid) ? url.replace(new RegExp(`^/*${journey.guid}/*`), '/') : url;
return getPageIdFromUrl(strippedUrl);
}
/**
* Extract the originId, and current waypoint being visited from the given url.
*
* @param {string} url URL.
* @returns {object} The origin ID, and current waypoint.
*/
function parseOriginWaypointInUrl(url) {
try {
const u = new URL(url, 'http://placeholder/');
const paths = u.pathname.match(/([^/]+)/g);
let originId;
let waypoint;
if (paths) {
[originId, waypoint] = paths.length > 1 ? paths : [undefined, paths[0]];
}
return { originId, waypoint };
} catch (ex) {
logger.error(ex.message);
return {
originId: undefined,
waypoint: undefined,
};
}
}
function isObjectType(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
function isObjectWithKeys(target, keys = []) {
const isObject = Object.prototype.toString.call(target) === '[object Object]';
let hasKeys = true;
if (isObject) {
keys.forEach((k) => {
hasKeys = hasKeys && Object.prototype.hasOwnProperty.call(target, k);
});
}
return isObject && hasKeys;
}
/**
* Discover the root folder of the specified npm module.
*
* @param {string} module Name of npm module to go and find.
* @param {Array} paths Paths to search on for module folder.
* @returns {string} The absolute path to the named module, if found.
* @throws Error When the module cannot be found.
* @throws SyntaxError When the module name contains invalid characters.
*/
function resolveModulePath(module = '', paths = []) {
// Strip rogue chars from module name; only valid npm package names are
// expected (https://docs.npmjs.com/files/package.json#name)
const modName = module.replace(/[^a-z0-9\-_.]/ig, '').replace(/\.+/i, '.');
if (modName !== module) {
throw new SyntaxError('Module name contains invalid characters');
}
// Look for the module in the same places NodeJS would
const resolved = paths.filter((p) => fs.existsSync(path.normalize(`${p}/${modName}`)));
if (resolved.length) {
return path.normalize(`${resolved[0]}/${modName}`);
}
throw new Error(`Cannot resolve module '${module}'`);
}
module.exports = {
/**
* Convert a given URL into an ID that we can use to uniquely identify a
* specific page. This function will not verify the page ID is valid.
*
* @param {string} url Url to parse.
* @returns {string} Page ID.
*/
getPageIdFromUrl,
getPageIdFromJourneyUrl,
getJourneyFromUrl,
objectPathValue,
objectPathString,
normalizeHtmlObjectPath,
isEmpty,
isObjectType,
isObjectWithKeys,
resolveModulePath,
parseOriginWaypointInUrl,
};