src/Input.js
const assert = require('assert');
const TypeCheck = require('js-typecheck');
const ValidationFail = require('./MeboErrors/ValidationFail');
const Utils = require('./Utils');
// symbols used for private members to avoid any potential clashing
// caused by re-implementations
const _name = Symbol('name');
const _readOnly = Symbol('readOnly');
const _properties = Symbol('properties');
const _lockedProperties = Symbol('lockedProperties');
const _cache = Symbol('cache');
const _propertiesCache = Symbol('propertiesCache');
const _registeredInputs = Symbol('registeredInputs');
const _registeredProperties = Symbol('registeredProperties');
/**
* An input holds a value that is used for the execution of the {@link Action}.
*
* The value carried by the input gets checked through a wide range of validations
* that make sure the value meets the necessary requirements for the execution
* of the {@link Action}.
*
* The validations are performed asynchronously which enables an implementation
* that can go far beyond checking the data type or matching the value through
* a regex. In most cases these validations are driven by `properties`. Properties are
* usually defined at construction time. Also, non-generic validations can be
* implemented through `extendedValidation`, making possible to define validations
* that are tied with an action itself.
*
* Inputs are created through {@link Input.create} using a
* [syntactic sugar](https://en.wikipedia.org/wiki/Syntactic_sugar) that describes
* their name and type (aka [TypeScript](https://www.typescriptlang.org/)), for instance:
*
* ```javascript
* Input.create('myInput: bool')
* ```
*
* Any input can be defined as a vector by using the short array syntax `[]`:
*
* ```javascript
* Input.create('myInput: bool[]')
* ```
*
* In case you want to hide specific inputs from readers ({@link Reader}), you can
* use the prefix `_` in the input name. By doing that the input is automatically
* assigned as hidden (same effect as setting the property `hidden=true` at the
* construction of the input):
*
* ```javascript
* Input.create('_myInput: bool[]')
* ```
*
* Additionally, you can specify if an input is optional (not required) by adding
* `?` beside of the input name:
*
* ```javascript
* Input.create('myInput?: bool[]')
* ```
*
* You can also create inputs through a verbose (boilerplate) interface where each
* of the options described above can be defined at construction time via properties:
* ```javascript
* Input.create('myInput', {type: 'bool', vector: true, required: false, hidden: false})
* ```
*
* Since inputs are used by actions they can be created directly inside of an {@link Action} via
* {@link Action.createInput} that internally triggers {@link Input.create} factory method:
*
* ```
* class HelloWorld extends Mebo.Action{
* constructor(){
* super();
* // ♥ compact version
* this.createInput('myInputA?: bool[]');
*
* // same as effect as above, but not so friendly to read
* this.addInput(Input.create('myInputB?: bool[]'));
* }
* }
* ```
*
* To register a new input type, take a look at {@link Input.register}
*
* <h2>Property Summary</h2>
*
* Property Name | Description | Defined by Default | Default Value
* --- | --- | :---: | :---:
* required | boolean telling if the value is required (defined at the construction time) \
* |
|
* immutable | boolean telling if the data of the value cannot be altered \
* overtime, however the value of the input can still be replaced by \
* the input value setter, in order to prevent it you can set an input as \
* {@link readOnly} |
|
* defaultValue | default value of the input |
|
* elementType | tells how the input should be presented via {@link Cli}: \
* `'option'` or `'argument'` ({@link http://docopt.org}) |
| `'option'`
* vector | boolean telling if the input holds a vector value (defined \
* at the construction time) |
|
* hidden | boolean telling if the input is hidden from the {@link Reader}, \
* therefore it should only be used internally |
|
* autofill | key name about a value that may be under the {@link Session.autofill}. This \
* value is used to initialize the input. It occurs when a session is assigned \
* to an action ({@link Action.setSession}) |
|
* description | description about the input. This information is displayed \
* when showing the help about the action |
|
* shortOption | short version of the input name used to speficy when running \
* through the {@link Cli} |
|
*
* <br/>The assignment of a property value can be done at construction time or through
* the setter {@link Input.assignProperty}. A property value can be queried by the
* getter {@link Input.property}. To add new properties to an input type, please take
* a look at {@link Input.registerProperty}.
*/
class Input{
/**
* Creates an input
*
* @param {string} name - name of the input
* @param {Object} [properties={}] - plain object containing the properties which
* will be assigned to the {@link Input}
* @param {function} [extendedValidation] - callback that can be defined to supply
* custom validations to the {@link Input}
*/
constructor(name, properties={}, extendedValidation=null){
assert(TypeCheck.isString(name) && name.length, 'name cannot be empty!');
assert(TypeCheck.isPlainObject(properties), "properties need to be defined as dict {'key': value}");
assert(extendedValidation === null || TypeCheck.isCallback(extendedValidation),
'extendedValidation needs to be defined as function(contextValue) or null');
// validating reserved names
assert(!(['help'].includes(name)), `Input name '${name}' is reserved, it cannot be used!`);
// validating input name
assert(name.length, 'input name cannot be empty!');
assert((/^([\w_.-])+$/gi).test(name), `Illegal input name: ${name}`);
this[_name] = name;
this[_readOnly] = false;
this[_properties] = new Map();
this[_lockedProperties] = new Set();
this[_cache] = new Utils.ImmutableMap();
// defining custom properties that may override the default ones
for (const propertyKey in properties){
this.assignProperty(propertyKey, properties[propertyKey]);
}
// locking properties
this.lockProperty('type');
this.lockProperty('vector');
this.lockProperty('defaultValue');
this.setValue(this.property('defaultValue'));
this._extendedValidation = extendedValidation;
}
/**
* Creates an input instance.
*
* @param {string} inputInterface - string followed by either the pattern `name: type`
* or `name?: type` in case of optional {@link Input}. The type is case-insensitive
* @param {Object} [properties={}] - plain object containing the properties which
* will be assigned to the {@link Input}
* @param {function} [extendedValidation] - callback that can be defined to supply
* custom validations to the {@link Input}
* @return {Input}
*
*
* @example
* // minimal
* Input.create('inputName: numeric');
*
* @example
* // full
* Input.create('inputName: numeric', {min: 1, max: 5}, function(at){
* return new Promise((resolve, reject) => {
* if (this.valueAt(at) === 3)
* reject(new ValidationFail('Failed for some reason'));
* else
* resolve(this.value());
* });
* })
*/
static create(inputInterface, properties={}, extendedValidation=null){
const inputInterfaceParts = inputInterface.split(':');
const propertiesFinal = {...properties};
if (inputInterfaceParts.length === 2){
for (let i=0, len=inputInterfaceParts.length; i < len; ++i){
inputInterfaceParts[i] = inputInterfaceParts[i].trim();
}
// not required syntax
if (inputInterfaceParts[0].endsWith('?')){
inputInterfaceParts[0] = inputInterfaceParts[0].slice(0, -1);
propertiesFinal.required = false;
}
// vector syntax
if (inputInterfaceParts[1].endsWith('[]')){
inputInterfaceParts[1] = inputInterfaceParts[1].slice(0, -2);
propertiesFinal.vector = true;
}
propertiesFinal.type = inputInterfaceParts[1];
}
else if (inputInterfaceParts.length > 2){
throw new Error("Invalid input interface, it should follow the pattern: 'name: type'");
}
const InputTypeClass = this.registeredInput(propertiesFinal.type);
// storing the input type in lower-case to keep the same convention
// found in the registration
propertiesFinal.type = propertiesFinal.type.toLowerCase();
// adding hidden property when input name starts with '_'
if (inputInterfaceParts[0].startsWith('_')){
propertiesFinal.hidden = true;
}
return new InputTypeClass(inputInterfaceParts[0], propertiesFinal, extendedValidation);
}
/**
* Returns if the value of the input is empty. This is used mainly by
* the property `required=false` to know if the input does not have a value
* assigned to it.
*
* @return {boolean} If the input is empty
*/
isEmpty(){
return TypeCheck.isNone(this.value()) || (this.property('vector')
&& TypeCheck.isList(this.value()) && this.value().length === 0);
}
/**
* Returns if the value of the input is a vector. This information is defined
* by the property `vector=true`
*
* @return {boolean} if the input is a vector
*/
isVector(){
return this.property('vector') === true;
}
/**
* Returns if the value of the input is required. This information is defined
* by the property `required=true`
*
* @return {boolean} if the input is required
*/
isRequired(){
return this.property('required') === true;
}
/**
* Sets the input value by avoiding the overhead that may occur when the
* same value is used across actions that have the input type, therefore
* this method avoids the re-computation by copying the caches and value
* associated with the source input to the current input. The cache will be
* only copied if both source and target inputs have the `immutable` property
* enabled (default true).
*
* @param {Input} sourceInput - input used as source to setup the current input
* @param {null|number} [at] - index used when the target input is defined as vector to
* tells which value should be used
* @param {boolean} [cache=true] - tells if the cache will be copied as well
*/
setupFrom(sourceInput, at=null, cache=true){
assert(TypeCheck.isSameType(sourceInput, this), 'Inputs are not the same type!');
if (at !== null && !sourceInput.isVector()){
throw new Error(`Can't use at, since the source input is not a vector`);
}
else if (at !== null && this.isVector()){
throw new Error(`Can't use at, from a source vector input to a target vector input`);
}
else if (at === null && sourceInput.isVector() && !this.isVector()){
throw new Error(`Target input is not a vector, can't setup from a vector target input without supplying 'at'`);
}
// transferring value:
// when the source is not a vector, but the target is
if (this.isVector() && !sourceInput.isVector()){
this.setValue([sourceInput.value()]);
}
// when source and target are the same either scalar or vector
else if (at === null){
this.setValue(sourceInput.value());
}
// when an index is specific from a source vector to a scalar input
else{
this.setValue(sourceInput.value()[at]);
}
// transferring cache to the current input:
if (cache && sourceInput.property('immutable') && this.property('immutable')){
assert(sourceInput.cache() instanceof Utils.ImmutableMap);
const sourceCache = sourceInput.cache();
// when the source is not a vector, but the target is
if (this.isVector() && !sourceInput.isVector()){
for (const key of sourceCache.keys()){
this._setToCache(key.slice(0, -2), sourceCache.get(key), 0);
}
}
// when source and target are the same either scalar or vector
else if (at === null){
for (const key of sourceCache.keys()){
this.cache().set(key, sourceCache.get(key));
}
}
// when an index is specific from a source vector to a scalar input
else{
const indexToken = `(${at})`;
for (const key of sourceCache.keys()){
if (key.endsWith(indexToken)){
this._setToCache(key.slice(0, -indexToken.length), sourceCache.get(key));
}
}
}
}
}
/**
* Returns the cache used by the input
*
* This method is called by ({@link setupFrom}) to setup the input based on an
* already existing input
*
* @return {ImmutableMap}
*/
cache(){
return this[_cache];
}
/**
* Forces to flush the internal input cache
*/
clearCache(){
this.cache().clear();
}
/**
* Executes the input validations ({@link _validation}), in case of a failed
* validation then an exception of type {@link ValidationFail} is raised
*
* @return {Promise<*>} Returns the value of the input
*/
async validate(){
try{
// required check
if (this.isEmpty()){
if (this.isRequired() !== false){
throw new ValidationFail(
'Input is required, it cannot be empty!',
'28a03a60-a405-4737-b94d-2b695b6ce156',
);
}
}
// vector check
else if (this.isVector() && !TypeCheck.isList(this.value())){
throw new ValidationFail(
'Input needs to be a vector!',
'e03709a0-6c31-4a33-9f63-fa751948a6cb',
);
}
// otherwise perform the asynchronous validations
else{
const validationPromises = [];
const length = this.isVector() ? this.value().length : 1;
for (let i=0; i < length; ++i){
// setting the context index
const at = this.isVector() ? i : null;
// cannot have null/undefined as part of the array
if (this.isVector() && TypeCheck.isNone(this.valueAt(at))){
throw new ValidationFail(
'Vector cannot contain null/undefined data!',
'3ced667c-3a3c-4034-91ca-3bde10695f87',
);
}
validationPromises.push(this._validation(at));
// running extended validations
if (TypeCheck.isCallback(this._extendedValidation)){
validationPromises.push(this._extendedValidation.call(this, at));
}
}
// running generic validations
await Promise.all(validationPromises);
}
}
catch(err){
// including the input name to the validation fail
if (err instanceof ValidationFail){
err.inputName = this.name();
// disabling the output when input is marked as hidden
if (this.property('hidden')){
err.output = false;
}
}
throw err;
}
return true;
}
/**
* Returns the property value for the input property name
*
* @param {string} name - name of the property
* @return {Promise<*>} The value of the property otherwise raises an exception
* in case the property is not assigned
*/
property(name){
assert(TypeCheck.isString(name), 'property name needs to be defined as string');
if (this[_properties].has(name)){
return this[_properties].get(name);
}
// checking if the property is valid
const allRegisteredProperties = Input._allRegisteredProperties(this.constructor);
if (allRegisteredProperties.has(name)){
return allRegisteredProperties.get(name);
}
throw new Error(`Property ${name} is not registered!`);
}
/**
* Sets a property to the input. In case the property already exists then the value
* is going to be overridden. By default the only properties that can be modified are
* the ones found under {@link Input.registeredInputNames}. However you can
* assign a non-registered property by enabling `loose` parameter.
*
* Property values can be locked, therefore preventing modifications
* ({@link Input.lockProperty}).
*
* @param {string} name - name of the property
* @param {*} value - value for the property
* @param {boolean} [loose=false] - when true lets to assign a property that is not
* registered to the input ({@link Input.registeredInputNames})
*/
assignProperty(name, value, loose=false){
assert(TypeCheck.isString(name), 'property name needs to be defined as string');
// checking if input is read-only
if (this.readOnly()){
throw new Error(`Input ${this.name()} is read only, cannot be modified!`);
}
// checking if property is valid
if (!loose && !Input._allRegisteredProperties(this.constructor).has(name)){
throw new Error(`Property ${name} is not registered!`);
}
// checking if property has been locked
if (this[_lockedProperties].has(name)){
throw new Error(`Property ${name} is locked!`);
}
this[_properties].set(name, value);
this.clearCache();
}
/**
* Prevents a property value to be modified by {@link Input.assignProperty}
*
* @param {string} name - name of the property
* @param {boolean} [lock=true] - if true it locks the property otherwise
* it removes the lock from the property
*/
lockProperty(name, lock=true){
// checking if input is read-only
if (this.readOnly()){
throw new Error(`Input ${this.name()} is read only, cannot be modified!`);
}
if (lock){
this[_lockedProperties].add(name);
}
else{
this[_lockedProperties].delete(name);
}
}
/**
* Returns a boolean telling if the property is locked ({@link Input.lockProperty})
*
* @param {string} name - name of the property
* @return {boolean}
*/
isPropertyLocked(name){
return this[_lockedProperties].has(name);
}
/**
* Returns a boolean telling if the input property name is assigned to the input
*
* @param {string} name - name of the property
* @return {boolean}
*/
hasProperty(name){
assert(TypeCheck.isString(name), 'property name needs to be defined as string');
return Input._allRegisteredProperties(this.constructor).has(name) || this[_properties].has(name);
}
/**
* Returns a list containing the property names assigned to the input
*
* @return {Array<string>}
*/
propertyNames(){
return [...new Set([...this[_properties].keys(),
...Input._allRegisteredProperties(this.constructor).keys(),
])];
}
/**
* Sets the value of the input
*
* @param {*} value - value that should be set to the input
*/
setValue(value){
// checking if input is read-only
if (this.readOnly()){
throw new Error(`Input ${this.name()} is read only, cannot be modified!`);
}
// Due the overhead that may occur on going through recursively and freezing
// the whole hierarchy of the value, it freezes only value itself. In case
// this is not enough consider in changing it to perform a deep-freeze instead
this._value = (!this.property('immutable') || TypeCheck.isNone(value))
|| value instanceof Buffer ? value : Object.freeze(value);
// flushing the cache when a new value is set
this.clearCache();
}
/**
* Returns the value of the input
*
* @return {*}
*/
value(){
return this._value;
}
/**
* Returns the name of the input which is defined at construction time (inputs cannot
* be renamed)
*
* @return {string}
*/
name(){
return this[_name];
}
/**
* Returns if the input is serializable
*
* This method should be re-implemented by derived classes to tell if the input can be
* serialized (default true).
*
* In case of a serializable input, the methods {@link Input._encodeScalar} and {@link Input._decode}
* are expected to be implemented.
*
* @return {boolean}
*/
isSerializable(){
return true;
}
/**
* Changes the read-only state of the input. A read-only input cannot have its
* value and properties modified. It's mainly used during the execution of the {@link Action}
* where all inputs are assigned as read-only, therefore when the execution is completed
* the inputs are restored with the original read-only value assigned before
* the execution of the action
*
* @param {boolean} enable - tells if a input is read-only
*/
setReadOnly(enable){
this[_readOnly] = enable;
}
/**
* Returns a boolean telling if the input is in read-only mode. A read-only input
* cannot have its value and properties modified
*
* @return {boolean}
*/
readOnly(){
return this[_readOnly];
}
/**
* Decodes a value represented as string to the type that is compatible with the input.
* This method is called by the {@link Handler} or when an action is loaded/created using
* {@link Action.fromJSON}/{@link Action.createFromJSON}. In case the input
* is defined as vector then the value can be defined using an array encoded in JSON.
* The parsed value gets returned and assigned to the input as well (you can control this
* by the `assignValue` argument).
*
* The implementation of the decoding is done by the methods {@link Input._decode}
* & {@link Input._decodeVector}. To know if an input supports decoding
* checkout the {@link Input.isSerializable}.
*
* To know how the serialization is done for the inputs bundled with
* Mebo take a look at {@link Reader} documentation.
*
* @param {string} value - string containing the serialized data
* @param {boolean} [assignValue=true] - tells if the parsed value should be assigned
* to the input
* @return {*}
*/
parseValue(value, assignValue=true){
assert(this.isSerializable(), 'parsing not supported!');
assert(TypeCheck.isString(value), 'value needs to be defined as string or string array');
let result;
// empty string is treated as null
if (value.length === 0){
result = null;
}
else if (this.isVector()){
result = this.constructor._decodeVector(value);
assert(TypeCheck.isList(result));
}
else{
result = this.constructor._decodeScalar(value);
}
if (assignValue){
this.setValue(result);
}
return result;
}
/**
* This method should return a string representation about the current value in a way that
* can be recovered later through {@link parseValue}.
*
* The encode implementation is done by the methods {@link Input._encodeScalar} &
* {@link Input._encodeVector}. To know if an input supports serialization
* checkout the {@link Input.isSerializable}.
*
* Also, in case you want to know the serialization form for the inputs bundled with
* Mebo checkout {@link Reader}.
*
* @return {Promise<string>}
*/
async serializeValue(){
assert(this.isSerializable(), 'serialization not supported!');
// making sure the value can be serialized without any issues
await this.validate();
// serializing the value
let result;
if (this.isEmpty()){
result = '';
}
else if (this.isVector()){
result = this.constructor._encodeVector(this.value());
}
else{
result = this.constructor._encodeScalar(this.value());
}
assert(TypeCheck.isString(result));
return result;
}
/**
* Decodes the input value from the string representation ({@link Input._encodeScalar}) to the
* data type of the input. This method is called internally during {@link Input.parseValue}
*
* @param {string} value - encoded value
* @return {*}
* @protected
*/
static _decodeScalar(value){
assert(TypeCheck.isString(value), 'value needs to be defined as string');
return value;
}
/**
* Decodes a vector value from the string representation ({{@link Input._encodeScalar} &
* {@link Input._encodeVector}) to the
* data type of the input. This method is called internally during {@link Input.parseValue}
*
* @param {string} value - encoded value
* @return {*}
* @protected
*/
static _decodeVector(value){
assert(TypeCheck.isString(value), 'value needs to be defined as string');
const decodedValue = [];
const parsedValue = JSON.parse(value);
assert(TypeCheck.isList(parsedValue), 'Could not parse, unexpected data type');
for (const parsedItem of parsedValue){
let finalParsedItem = parsedItem;
// empty string is treated as null
if (finalParsedItem === ''){
finalParsedItem = null;
}
// decoding the value
decodedValue.push(finalParsedItem ? this._decodeScalar(finalParsedItem) : null);
}
return decodedValue;
}
/**
* Encodes the input value to a string representation that can be later decoded
* through {@link Input._decode}. This method is called internally during the
* {@link serializeValue}
*
* @param {*} value - value that should be encoded to a string
* @return {string}
* @protected
*/
static _encodeScalar(value){
return String(value);
}
/**
* Encodes a vector value to a string representation that can be later decoded
* through {@link Input._decodeVector}. This method is called internally during the
* {@link serializeValue}
*
* @param {Array<string>} values - value that should be encoded to a string
* @return {string}
* @protected
*/
static _encodeVector(values){
assert(TypeCheck.isList(values), 'values needs to be defined as array');
const encodedValues = [];
for (const item of values){
encodedValues.push(this._encodeScalar(item));
}
return JSON.stringify(encodedValues);
}
/**
* Registers a new input type to the available inputs
*
* @param {Input} inputClass - input implementation that will be registered
* @param {string} [name] - string containing the registration name for the
* input. In case of an empty string, the registration is done by using the name
* of the type (this information is stored in lowercase)
*/
static register(inputClass, name=''){
assert(TypeCheck.isSubClassOf(inputClass, Input), 'Invalid input type!');
assert(TypeCheck.isString(name), 'Invalid optional registration name!');
const nameFinal = ((name === '') ? inputClass.name : name).toLowerCase();
// validating name
assert((/^([\w_\.\-])+$/gi).test(nameFinal), `Illegal input name: ${nameFinal}`); // eslint-disable-line no-useless-escape
this[_registeredInputs].set(nameFinal, inputClass);
}
/**
* Returns the input type based on the registration name
*
* @param {string} name - name of the registered input type
* @return {Input}
*/
static registeredInput(name){
assert(TypeCheck.isString(name), 'Invalid name!');
const normalizedName = name.toLowerCase();
if (this[_registeredInputs].has(normalizedName)){
return this[_registeredInputs].get(normalizedName);
}
throw new Error(`Input ${name} is not registered!`);
}
/**
* Returns a list containing the names of the registered input types
*
* @return {Array<string>}
*/
static registeredInputNames(){
return [...this[_registeredInputs].keys()];
}
/**
* This method enforces the context of the value being queried.
* Since the input can behave as vector it makes sure that when that's
* the case the index must be supplied otherwise it raises an exception.
* Use this method when you need to query the value through validations where
* the index (at) is always passed to them, whether if the input is vector
* or scalar
*
* @param {number} [index] - used when the input is set to a vector to tell the
* index of the value
* @return {*}
*/
valueAt(index=null){
if (this.isVector()){
assert(index !== null, 'Could not determine the index of the vector');
return this.value()[index];
}
return this.value();
}
/**
* Registers a property for the input type (also available as `Mebo.Input.registerProperty`)
*
* ```
* // example of registering a new property
* Mebo.Input.registerProperty('text', 'myCustomProperty', 'A initial value if necessary')
* ```
*
* @param {string|Input} inputClassOrRegisteredName - registered input name or input class
* in which the property should be registered
* @param {string} name - name of the property (in case the property name already
* exists than it going to be overridden for the input type)
* @param {*} [initialValue] - optional initial value for the property (undefined by default)
*/
static registerProperty(inputClassOrRegisteredName, name, initialValue){
assert(TypeCheck.isString(name), 'property name needs to be defined as string');
assert(name.length, 'property name cannot be empty');
const inputType = this._resolveInputType(inputClassOrRegisteredName);
// appending to the existing registered properties
if (!this[_registeredProperties].has(inputType)){
this[_registeredProperties].set(inputType, new Map());
}
this[_registeredProperties].get(inputType).set(name, initialValue);
// resetting the properties caches
this[_propertiesCache].clear();
}
/**
* Returns a list about all registered property names including the inherited ones for
* the input type
*
* ```
* // returning all properties for an input type (using 'numeric' as example)
* Mebo.Input.registeredPropertyNames('numeric');
* ```
*
* @param {string|Input} inputClassOrRegisteredName - registered input name or input class
* @return {Array<string>}
*/
static registeredPropertyNames(inputClassOrRegisteredName){
const inputType = this._resolveInputType(inputClassOrRegisteredName);
return [...this._allRegisteredProperties(inputType).keys()];
}
/**
* Use this method to implement generic validations
* for your input implementation. In case any validation fails this method
* should return a {@link ValidationFail} (This method is called when the
* validations are perform through {@link Input.validate})
*
* @param {null|number} at - index used when input has been created as a vector that
* tells which value should be used
* @return {Promise<*>} Returns the value of the input
* @protected
*/
async _validation(at){
return this.valueAt(at);
}
/**
* Auxiliary method used internally by input implementations to check if the key
* is under the cache
*
* @param {string} name - name of the key
* @param {null|number} [at] - used when the input is set to a vector to tell the
* index of the value
* @return {boolean}
* @protected
*/
_isCached(name, at=null){
return this.cache().has(this._cacheEntry(name, at));
}
/**
* Auxiliary method used internally by input implementations to set a value to
* the cache
*
* @param {string} name - name of the key
* @param {*} value - value that should be set in the cache
* @param {null|number} [at] - used when the input is set to a vector to tell the
* index of the value
* @protected
*/
_setToCache(name, value, at=null){
this.cache().set(this._cacheEntry(name, at), value);
}
/**
* Auxiliary method used internally by the input implementations to get a value
* from the cache
*
* @param {string} name - name of the key
* @param {null|number} [at] - used when the input is set to a vector to tell the
* index of the value
* @return {*}
* @protected
*/
_getFromCache(name, at=null){
return this.cache().get(this._cacheEntry(name, at));
}
/**
* Returns the cache entry based on the name and index (at)
*
* @param {string} name - name of the key
* @param {number|null} at - used when the input is set to a vector to tell the
* @return {string}
*
* @private
*/
_cacheEntry(name, at){
if (this.isVector()){
assert(at !== null, 'Could not determine the index of the vector');
}
return (this.isVector()) ? `${name}(${at})` : `${name}()`;
}
/**
* Auxiliary method that resolves the registered input type based
*
* @param {string|Input} inputClassOrRegisteredName - registered input name or input class
* @return {Input}
* @private
*/
static _resolveInputType(inputClassOrRegisteredName){
if (TypeCheck.isSubClassOf(inputClassOrRegisteredName, Input) || inputClassOrRegisteredName === Input){
return inputClassOrRegisteredName;
}
return this.registeredInput(inputClassOrRegisteredName);
}
/**
* Auxiliary method that collects all available registered property names for the input type
* and returns them
*
* @param {Input} inputType - input class that should be used to collect the properties
* @return {Array<string>}
* @private
*/
static _allRegisteredProperties(inputType){
if (!this[_propertiesCache].has(inputType)){
let currentClass = inputType;
const inputProperties = new Map();
while (currentClass.name !== ''){
if (this[_registeredProperties].has(currentClass)){
for (const [propertyName, propertyValue] of this[_registeredProperties].get(currentClass)){
// if a property has been overriding by a sub class then skip it
if (!inputProperties.has(propertyName)){
inputProperties.set(propertyName, propertyValue);
}
}
}
currentClass = Object.getPrototypeOf(currentClass);
}
this[_propertiesCache].set(inputType, inputProperties);
}
return this[_propertiesCache].get(inputType);
}
}
Input[_propertiesCache] = new Map();
Input[_registeredInputs] = new Map();
Input[_registeredProperties] = new Map();
// registering properties
Input.registerProperty(Input, 'required', true);
Input.registerProperty(Input, 'type', true);
Input.registerProperty(Input, 'immutable', true);
Input.registerProperty(Input, 'defaultValue', null);
Input.registerProperty(Input, 'vector', false);
Input.registerProperty(Input, 'hidden');
Input.registerProperty(Input, 'autofill');
Input.registerProperty(Input, 'description');
module.exports = Input;