UNPKG

@dwp/govuk-casa

Version:

Framework for creating basic GOVUK Collect-And-Submit-Applications

228 lines (208 loc) 7.14 kB
const dot = require('dot-object'); const fs = require('fs'); const path = require('path'); const nUrl = require('url'); const logger = require('./Logger')('util'); const OBJECT_TYPE = '[object Object]'; /** * 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". * * @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 * @throws {Error} When given an invalid path */ function objectPathValue(obj, ...paths) { // Using dot-object, it is only used to read from an object. It does not // support quoted properties, so need to remove those, e.g. ["property"]. // dot-object has some quirks, such as `a[b.c]` resolving to `a.b.c`, // so we remove any of these to avoid ambiguity. const pathString = paths.join('.').replace(/["']/g, ''); if (pathString.match(/\[[^\]]+\./)) { return undefined; } try { return dot.pick(pathString, obj); } catch (e) { logger.debug('Object path is invalid: %s (%s)', pathString, e.message); throw e; } } /** * 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 guid = (journey && journey.guid) ? String(journey.guid).replace(/[^a-z0-9-]/ig, '') : undefined; const strippedUrl = guid ? url.replace(new RegExp(`^/*${guid}/*`), '/') : url; return getPageIdFromUrl(strippedUrl); } function isObjectType(obj) { return Object.prototype.toString.call(obj) === OBJECT_TYPE; } function isObjectWithKeys(target, keys = []) { const isObject = Object.prototype.toString.call(target) === OBJECT_TYPE; let hasKeys = true; if (isObject) { keys.forEach((k) => { hasKeys = hasKeys && Object.prototype.hasOwnProperty.call(target, k); }); } return isObject && hasKeys; } function hasProp(obj, prop) { return Object.prototype.toString.call(obj) === OBJECT_TYPE && Object.prototype.hasOwnProperty.call(obj, prop); } /** * 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}'`); } /** * Test is a value can be stringifed (numbers or strings) * * @param {any} value Item to test * @returns {boolean} Whether the value is stringable or not */ function isStringable(value) { return typeof value === 'string' || typeof value === 'number'; } /** * Coerce an input to a string. * * @param {any} input Input to be stringified * @param {string} fallback Fallback to use if input can't be stringified * @returns {string} The stringified input */ function stringifyInput(input, fallback) { // Not using param defaults here as the fallback may be explicitly "undefined" const fb = arguments.length === 2 && (isStringable(fallback) || fallback === undefined) ? fallback : ''; return isStringable(input) ? String(input) : fb; } 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, hasProp, stringifyInput, isStringable, };