/**
* Checks if a given value is an Object {}
*
* @function
* @name _isObject
* @private
* @sig * -> Boolean
* @param {*} val A value which may possibly be an object
* @returns {boolean} Whether or not the given values is an object
*/
function _isObject(val) {
return typeof val === 'object' && val && typeof val.length === 'undefined'
}
/**
* Checks a given object to make sure that all its values are also objects
*
* @function
* @name _allValuesAreObjects
* @private
* @sig {k: v} -> Boolean
* @param {Object<string, Object<string, string>>} obj An Object of (only) Objects. Inside each of those Objects all values are Strings which correspond to keys on the outer Object.
* @returns {boolean} Whether or not the object contains values which are all also objects
*/
function _allValuesAreObjects(obj) {
return Object.keys(obj || {}).every(k => _isObject(obj[k]))
}
/**
* Checks that each value the transitions lead to on a state machine are also states on the state machine
*
* @function
* @name _allValuesFromTransitionsAreStates
* @private
* @sig {k: v} -> Boolean
* @param {Object<string, Object<string, string>>} obj An Object of (only) Objects. Inside each of those Objects all values are Strings which correspond to keys on the outer Object.
* @returns {boolean} Whether or not all values that the transitions lead to are also states on the state machine
*/
function _allValuesFromTransitionsAreStates(obj = {}) {
const isInMachine = transitionState => Object.prototype.hasOwnProperty.call(obj, transitionState)
return Object
.keys(obj)
.map(state => obj[state])
.every(transitions => Object.keys(transitions).every(tstate => isInMachine(tstate)))
}
/**
* Checks a given state machine to make sure it is valid, and throws an error if not
*
* @function
* @name validateStateMachine
* @sig {k: v} -> undefined
* @throws {TypeError} If the state machine is missing or not an object
* @throws {TypeError} If the state machine has any values which are not objects
* @throws {TypeError} If the state machine's "sub-objects" has values that are not keys on the root of the object
*
* @param {Object<string, Object<string, string>>} stateMachine An Object of (only) Objects. Inside each of those Objects all values are Strings which correspond to keys on the outer Object.
*/
function validateStateMachine(stateMachine) {
if (!_isObject(stateMachine)) {
throw new TypeError('A state machines must be an Object')
}
if (!_allValuesAreObjects(stateMachine)) {
throw new TypeError('A state machine must be an Object of (only) Objects')
}
if (_allValuesFromTransitionsAreStates(stateMachine)) {
throw new TypeError(
'All the registered transitions (for each possible state) in a machine must lead to another state in the machine'
)
}
}
/**
* Gets the first state in a given state machine
*
* @function
* @name getFirstState
* @sig {k: v} -> String -> String
* @param {Object<string, Object<string, string>>} stateMachine An Object of (only) Objects. Inside each of those Objects all values are Strings which correspond to keys on the outer Object.
* @param {string} initialState The state at which the machine should be initialized (will default to the first key name in the state machine)
* @returns {string} The state value at which the machine should start
*/
function getFirstState(stateMachine, initialState) {
const currentState = initialState == null
? Object.keys(stateMachine)[0]
: initialState
if (typeof currentState !== 'string' || !Object.prototype.hasOwnProperty.call(stateMachine, currentState)) {
throw new Error(`The "initialState" must be one of the keys on the "stateMachine" itself ${currentState}`)
}
return currentState
}
module.exports = {
getFirstState,
validateStateMachine,
_isObject,
_allValuesAreObjects,
_allValuesFromTransitionsAreStates
}