parks/tags.js

// this file handles storing different tag types

import {tagType} from './parkTypes.js';

/**
 * Validate a rider height tag. Used for min/max heights
 * @param {string} key
 * @param {*} data
 * @return {boolean}
 */
const riderHeightValidate = (key, data) => {
  if (!validateObjectContainsOnlyGivenKeys(data, ['height', 'unit'])) {
    return false;
  }

  const validUnits = ['cm', 'in'];
  if (validUnits.indexOf(data.unit) < 0) {
    return false;
  }

  return !isNaN(data.height) && data.height >= 0;
};

/**
 * Simple tags that don't have a value entry.
 * Instead, these tags are either present or not.
 */
const simpleTags = {
  [tagType.fastPass]: true,
  [tagType.mayGetWet]: true,
  [tagType.unsuitableForPregnantPeople]: true,
  [tagType.onRidePhoto]: true,
  [tagType.singleRider]: true,
  [tagType.childSwap]: true,
};

/**
 * Each tag type must have a validator function to confirm the incoming tag value is correct
 */
const validators = {
  // Location tags
  [tagType.location]: (key, data) => {
    // make sure we have our longitude and latitude keys
    if (!validateObjectContainsOnlyGivenKeys(data, ['longitude', 'latitude'])) {
      return false;
    }

    // make sure our keys are valid numbers
    if (isNaN(data.longitude) || isNaN(data.latitude)) {
      return false;
    }

    return true;
  },
  // minimum height allowed to ride
  //  must contain "height" as a number, and a "unit" which can be 'cm' (centimeters) or 'in' (inches)
  [tagType.minimumHeight]: riderHeightValidate,
  // maximum height allowed to ride
  //  must contain "height" as a number, and a "unit" which can be 'cm' (centimeters) or 'in' (inches)
  [tagType.maximumHeight]: riderHeightValidate,
};


/**
 * Given an object, validate that it only contains the given keys
 * Will return false if the object is missing any keys, or has additional keys not listed
 * @param {object} data
 * @param {array<string>} keys
 * @return {boolean}
 */
function validateObjectContainsOnlyGivenKeys(data, keys) {
  // make sure our input is an object
  if (typeof data !== 'object' || data === null) return false;

  // make sure our incoming keys is an array
  keys = [].concat(keys);

  // get the keys of our incoming object
  const dataKeys = Object.keys(data);
  // early bail if we have a different number of keys
  if (dataKeys.length !== keys.length) return false;

  // filter all our keys against our data key
  // TODO - this may get slow for large objects, look to optimise
  const matchingKeys = keys.filter((key) => {
    return dataKeys.indexOf(key) >= 0;
  });
  // if our filtered keys is still the same length, we have all the keys we want
  return (matchingKeys.length === dataKeys.length);
}

/**
 * Is the supplied tag type supported?
 * @param {tagType} type
 * @return {boolean}
 */
export function isValidTagType(type) {
  return validators[type] !== undefined || simpleTags[type] !== undefined;
}

/**
 * Is the given type a "simple tag" (one with no actual value)
 * @param {tagType} type
 * @return {boolean}
 */
export function isSimpleTagType(type) {
  return simpleTags[type] !== undefined;
}

/**
 * Validate a tag value based on its type
 * @param {string} key Tag name - some tags must have a valid name
 * @param {tagType} type The type for this tag entry
 * @param {*} value The tag data to validate matches this tag format's data structure
 * @return {boolean}
 */
export function isValidTag(key, type, value) {
  if (!isValidTagType(type)) {
    return false;
  }

  // simple tags don't need to run a validator
  if (isSimpleTagType(type)) {
    return true;
  }

  if (key === undefined) return false;

  // run tag validator to confirm we are a valid tag
  const validator = validators[type];
  return (validator(key, value));
}

/**
 * Given a tag key, type, and value - parse, validate, and return the full expected tag object
 * @param {string} key Tag name - some tags must have a valid name
 * @param {tagType} type The type for this tag entry
 * @param {*} value The tag data to validate matches this tag format's data structure
 * @return {object} The tag object to use, or undefined if it isn't valid
 */
export function getValidTagObject(key, type, value) {
  if (!isValidTag(key, type, value)) {
    return undefined;
  }

  // return data structure based on tag type
  if (isSimpleTagType(type)) {
    return {
      type,
    };
  } else {
    return {
      key,
      value,
      type,
    };
  }
}