UNPKG

@dwp/govuk-casa

Version:

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

234 lines (206 loc) 7.39 kB
const JourneyRoad = require('./JourneyRoad.js'); const Plan = require('./Plan.js'); const RoadConverter = require('./RoadConverter.js'); const ERR_START_UNDEFINED = 'Start of journey has not been defined!'; const privates = new WeakMap(); class JourneyMap { /** * The `guid` is only required if you are using multiple journeys, as each one * must have a unique identifier. This identifier will be used to prefix * waypoints in order to help CASA identify which journey the user is * requesting. Thefore, if used, then it must be a valid URL slug. * * @class * @param {string} guid An ID that uniquely represents this journey. * @throws {TypeError} When guid is invalid * @throws {SyntaxError} When guid is misformatted */ constructor(guid = null) { if (guid !== null) { if (typeof guid !== 'string') { throw new TypeError('guid must be a string'); } else if (!guid.match(/^[0-9a-z-]+$/)) { throw new SyntaxError('guid must contain only 0-9, a-z, -'); } } privates.set(this, { guid, startRoad: undefined, }); } /** * Get guid * * @returns {string} Journey GUID */ get guid() { return privates.get(this).guid; } /** * Define the starting point for this map - the first road that will be * traversed. * * @param {JourneyRoad} road First road on the journey. * @returns {ujMap} (chain). * @throws {Error} When a non-Road is given as the starting point */ startAt(road) { const priv = privates.get(this); if (!(road instanceof JourneyRoad)) { throw new Error('Only Roads may be defined as starting points'); } priv.startRoad = road; privates.set(this, priv); return this; } /** * Return all possible waypoints on this map. This will follow all roads, and * all forks to build up an exhaustive list of registered waypoints. * * The order of the returned array is insignificant. The purpose of this method * is to provide a means of determining if a waypoint exists on the map. * * @returns {Array} List of all waypoints on the map. * @throws {Error} When a starting waypoint is undefined. */ allWaypoints() { const priv = privates.get(this); if (typeof priv.startRoad === 'undefined') { throw new Error(ERR_START_UNDEFINED); } const waypoints = []; // Used to keep track of which roads have already been followed so we don't // end up with recursions when roads loop back on themselves. const followedRoads = []; /** * Follow a road. * * @param {JourneyRoad} road Road to follow. * @returns {void} * @throws {Error} For invalid POI type. */ function followRoad(road) { if (followedRoads.indexOf(road) > -1) { return; } followedRoads.push(road); const pois = road.getPOIs(); for (let i = 0, l = pois.length; i < l; i += 1) { const poi = pois[i]; switch (poi.type) { case JourneyRoad.POI_WAYPOINT: waypoints.push(poi.id); break; case JourneyRoad.POI_FORK: for (let ri = 0, rl = poi.roads.length; ri < rl; ri += 1) { followRoad(poi.roads[ri]); } break; case JourneyRoad.POI_MERGE: followRoad(poi.road); break; case JourneyRoad.POI_END: break; default: throw new Error('Invalid POI type'); } } } followRoad(priv.startRoad); return waypoints; } /** * Determine if the map contains the specified waypoint at all. This will look * at _all_ waypoints in the map. * * @param {string} waypointId Waypoint ID to find. * @returns {boolean} Whether waypoint is present in the map or not. */ containsWaypoint(waypointId) { return this.allWaypoints().indexOf(waypointId) > -1; } /** * Traverse the map, using the provided context to make decisions on * visiting/forking/merging along the way. * * The resulting list of waypoints include those that either a) have related * data in context (i.e. Context[waypoint.id] exists and is not empty) and there * are no validation errors on that waypoint, or b) exhaust the list of possible * waypoints (we reach the end of the journey). * * The context associated with a waypoint is considered not-empty if it is an * object, and it has at least one attribute specified within that object. * * If the traversed journey loops back on itself at any point, then the * traversal will stop at the last good waypoint. * * Data and validation contexts are provided separately (rather than passing in * a JourneyContext instance, for example) so that caller can arbitrarily decide * whether or not to include a validation context whilst traversing. * * @param {object} dataContext Data for each waypoint in the journey. * @param {object} validationContext Validation errors for each waypoint in the journey. * @returns {Array} List of waypoint IDs that have been traversed (in order). * @throws {Error} When start point is undefined. */ traverse(dataContext = {}, validationContext = {}) { const priv = privates.get(this); if (typeof priv.startRoad === 'undefined') { throw new Error(ERR_START_UNDEFINED); } let poi = priv.startRoad.getPOIs()[0]; const waypoints = []; do { // Preventative measure against a looping journey if (waypoints.indexOf(poi.id) > -1) { break; } if (typeof poi.show !== 'function' || poi.show(dataContext)) { waypoints.push(poi.id); if (typeof poi.passable !== 'function' || !poi.passable(dataContext, validationContext)) { break; } } poi = poi.nextWaypoint(dataContext); } while (poi); return waypoints; } /** * Traverse the whole journey, including future waypoints, based on the given * data context. This is a useful function to look ahead and see which waypoints * will feature in the user's journey given the current context state. * * Any forking and conditional logic that features in your UserJourney, must be * careful to check that data exists prior to testing it, because this method * will not necessarily provide the expected data when executing that logic * (because it may not have been gathered yet). * * @param {object} dataContext Data for each waypoint in the journey. * @returns {Array} List of waypoint IDs that have been traversed (in order). * @throws {Error} When a starting point is undefined */ traverseAhead(dataContext = {}) { const priv = privates.get(this); if (typeof priv.startRoad === 'undefined') { throw new Error(ERR_START_UNDEFINED); } let poi = priv.startRoad.getPOIs()[0]; const waypoints = []; do { // Preventative measure against a looping journey if (waypoints.indexOf(poi.id) > -1) { break; } waypoints.push(poi.id); poi = poi.nextWaypoint(dataContext); } while (poi); return waypoints; } convertToPlan() { const plan = new Plan(); const { startRoad } = privates.get(this); plan.addOrigin('main', startRoad.getPOIs()[0].id); return new RoadConverter(plan, startRoad).convert().plan; } } module.exports = JourneyMap;