UNPKG

@openscope/validator

Version:

A CLI used to validate an airport definition file used in the [openscope](http://openscope.co) ATC Simulator

476 lines (427 loc) 13.9 kB
const _snakeCase = require('lodash').snakeCase; const hasAllKeys = require('../hasAllKeys'); const findMissingKeys = require('../findMissingKeys'); const MessageType = require('./types/message-type').MessageType; const MessageListType = require('./types/message-type').MessageListType; const REQUIRED_KEYS = require('./requiredKeys'); const ERROR = require('./errorMessage'); /** * Iterator used to set the `#_id` property of an * instance of `ValidatorAbstract` * * Value will increment only on instantiation * * @private * @property _id * @type {number} * @default 0 */ let _id = 0; /** * A validator is class concerned with a specific section of an `airport` definition. * This class provides properties and methods from which a validiator can inherit. * * This class is meant to be inherited (extended) by some other validator and provides * central methods that _all_ validator classes are able to call. * * An extending class should: * - provide a definition for `.validate()`, `ValidatorAbstract.validate()` * will throw if called directly * - call the appropriate `.validate()` method based on the type of `#_data` * - call the appropriate `.validateInterface()` method based on the interface defined for `#_data` * * @class ValidatorAbstract */ class ValidatorAbstract { /** * An array of found errors * * Is used after validation to render error messages. Though this property * is defined here in `ValidatorAbstract`, an extending class will be concerned * with calling the appropriate methods to generate any errors. * * @property errors * @type {MessageListType} */ get errors() { return this._errors; } /** * Boolean encapsulation used to determine if a particular validator is * valid or not * * Valid is defined as having no errors, so this state can change based * on what parts of validation have run and what parts have not yet run * * @property isValid * @type {boolean} */ get isValid() { return this._errors.length === 0; } /** * Given a `#_name`, this provides a way to get the correct object * key defined within `REQUIRED_KEYS` * * @property keyName * @type {string} */ get keyName() { return _snakeCase(this._name).toUpperCase(); } /** * Name of the validator * * This will be set by the extending class * * @property name * @type {string} */ get name() { return this._name; } /** * @constructor * @param {string} name Name of an extending class. the CONSTANT_CASE version of `#_name` * is used in `REQUIRED_KEYS` * @param {object} json data from an airport definition file. an extending class should be * concerned with only a single section of an airport definition */ constructor(name, json) { /** * unique identifier of an extending class * * provides an easy way to differentiate each extending class * and should be used only for debugging purposes * * will auto-increment for every instantiation * * @private * @property _id * @type {number} */ this._id = (_id)++; // TODO: type this property /** * list of error messages generated during `.validate()` operations * * when adding new error messages, only the `.registerErrors()` * method should be used * * `#_errors` should never be mutated outside of the * `.registerErrors()` method * * @private * @property _errors * @type {MessageListType} * @default */ this._errors = []; /** * raw data from the airport definition under validation * * will likely be only a subset of an airport definition * * should not be mutated and should be considered readonly * * @private * @property _data * @type {object} * @default json */ this._data = json; /** * name of an extending class * * should map directly to the subset of an airport defintion * * example: * - if `#_name` is passed as `radio` * - the class should be concerned with only * * ```json "radio": { "twr": "Seatle Tower", "app": "Seattle Approach", "dep": "Seattle Departure" } * ``` * * @private * @property _name * @type {string} * @default name */ this._name = name; // FIXME: fix this duplication if (typeof this._data === 'undefined') { const errorMessageStr = ERROR.UNDEFINED.BASE_MESSAGE.replace('{KEY}', this._name); const errorMessage = MessageType({ message: errorMessageStr, level: ERROR.UNDEFINED.LEVEL }); this.registerError(errorMessage); return; } else if (this._isNameOrKeyUndefined()) { const errorMessageStr = ERROR.UNDEFINED.BASE_MESSAGE.replace('{KEY}', 'base'); const errorMessage = MessageType({ message: errorMessageStr, level: ERROR.UNDEFINED.LEVEL }); this.registerError(errorMessage); return; } return; } /** * Single method used to add a new `errorMessage` to the `#_errors` list * * @public * @for ValidatorAbstract * @method registerError * @param {ErrorMessageType} errorMessage */ registerError(errorMessage) { if (!MessageType.is(errorMessage)) { throw new TypeError(`Invalid type passed to ValidatorAbstract#registerError, expected a MessageType.`); } this._errors.push(errorMessage); } /** * Entry point to all other validation methods * * This single method is called on each validator within a `for` loop. * Any validation for a specific validator should be called within * this method in an extending class * * At minimum, this method should call one of: * `.validateList()`, `.validateObj()` or `.validateSingle()` * * and also one of: * `.validateInterfaceObj()`, `.validateInterfaceList()` or `.validateInterface()` * * @public * @for ValidatorAbstract * @method validate */ validate() { throw new Error('ValidatorAbstract#validate should be overwritten by the extending class'); } /** * should be called by an extending class when `#_data` is an array * * this should be called only within an extending class * definition for `.validate()` * * @public * @for ValidatorAbstract * @method validateList */ validateList() { if (!this.isValid) { return; } for (let i = 0; i < this._data.length; i++) { const item = this._data[i]; this._validateItem(item); } } /** * should be called by an extending class when `#_data` is * an object of objects * * this should be called only within an extending class * definition for `.validate()` * * @public * @for ValidatorAbstract * @method validateObj */ validateObj() { if (!this.isValid) { return; } for (const key in this._data) { const item = this._data[key]; this._validateItem(item, key); } } /** * should be called by an extending class when `#_data` is a simple object * * this should be called only within an extending class * definition for `.validate()` * * @public * @for ValidatorAbstract * @method validateSingle * @param {object} item * @param {srting} parentKey */ validateSingle(item) { if (!this.isValid) { return; } this._validateItem(item); } /** * should be called by an extending class when `#_data` is * an object of objects * * this should be called only within an extending class * definition for `.validate()` * * @public * @for ValidatorAbstract * @method validateInterfaceObj * @param {tcomb.interface} IValidatorItem */ validateInterfaceObj(IValidatorItem) { if (!this.isValid) { return; } for (const key in this._data) { const item = this._data[key]; this._validateInterfaceItem(IValidatorItem, item, key); } } /** * should be called by an extending class when `#_data` is * an array * * this should be called only within an extending class * definition for `.validate()` * * @public * @for ValidatorAbstract * @method validateInterfaceList * @param {tcomb.interface} IValidatorList * @param {tcomb.interface} IValidatorItem */ validateInterfaceList(IValidatorList, IValidatorItem) { if (!this.isValid) { return; } for (let i = 0; i < this._data.length; i++) { const item = this._data[i]; this._validateInterfaceItem(IValidatorItem, item); } this._validateInterfaceItem(IValidatorList, this._data); } /** * should be called by an extending class when `#_data` is * a simple object * * this should be called only within an extending class * definition for `.validate()` * * @public * @for ValidatorAbstract * @method validateInterface * @param {tcomb.interface} IValidator */ validateInterface(IValidatorItem) { if (!this.isValid) { return; } this._validateInterfaceItem(IValidatorItem, this._data); } /** * helper method used to build an error message for missing keys as defined within `REQUIRED_KEYS` * * @private * @for ValidatorAbstract * @method _buildMissingKeysErrorMessage * @param {string[]} missingKeys * @returns {MessageType} Error message to be stored into `#_errors` */ _buildMissingKeysErrorMessage(missingKeys, parentKey = '') { if (parentKey !== '') { return this._buildMissingKeysErrorMessageForParent(missingKeys, parentKey); } const errorMessageStr = `${ERROR.MISSING_KEYS.BASE_MESSAGE.replace('{KEY}', this.name)}: ${missingKeys.join(', ')}`; const errorMessage = new MessageType({ message: errorMessageStr, level: ERROR.MISSING_KEYS.LEVEL }); return errorMessage; } /** * helper method used to build an error message for missing keys as defined within `REQUIRED_KEYS` * * should be called only when looping through an object of object * * @private * @for ValidatorAbstract * @method _buildMissingKeysErrorMessageForParent * @param {string[]} missingKeys * @param {string} parentKey * @returns {string} Error message to be stored into `#_errors` */ _buildMissingKeysErrorMessageForParent(missingKeys, parentKey) { const errorMessageStr = `${ERROR.MISSING_KEYS.BASE_MESSAGE.replace('{KEY}', `${this.name}\` - \`${parentKey}`)}: ${missingKeys.join(', ')}`; const errorMessage = new MessageType({ message: errorMessageStr, level: ERROR.MISSING_KEYS.LEVEL }); return errorMessage; } /** * Encapsulation method used to determine if either `#_name` or `#keyName` is `undefined` * * @private * @for ValidatorAbstract * @method _isNameOrKeyUndefined * @returns {boolean} */ _isNameOrKeyUndefined() { return typeof this._name === 'undefined' || typeof REQUIRED_KEYS[this.keyName] === 'undefined'; } /** * called by any of the `.validate()` methods * * will verify all the keys required for a particular object exist. * in cases where required keys are missing, an errorMessage * (or errorMessages) will be generated then stored in `#_errors` * * @private * @for ValidatorAbstract * @method _validateItem * @param {object} item * @param {string[]} parentKey */ _validateItem(item, parentKey = '') { if (hasAllKeys(REQUIRED_KEYS[this.keyName], item)) { return; } const missingKeys = findMissingKeys(REQUIRED_KEYS[this.keyName], item); const errorMessage = this._buildMissingKeysErrorMessage(missingKeys, parentKey); this.registerError(errorMessage); } /** * called by any of the `.validateInterface()` methods * * will verify all the keys required for a particular object exist. * in cases where required keys are missing, an errorMessage * (or errorMessages) will be generated then stored in `#_errors` * * @private * @for ValidatorAbstract * @method _validateInterfaceItem * @param {tcomb.interface} IValidator * @param {any} item */ _validateInterfaceItem(IValidator, item, key = '') { try { const validDataType = IValidator(item); } catch (error) { const errorMessageStr = String(error).split('[tcomb] ')[1]; const errorMessage = new MessageType({ message: errorMessageStr, level: ERROR.INVALID_TYPE.LEVEL }); this.registerError(errorMessage); } } } module.exports = ValidatorAbstract;