src/Reader.js
const assert = require('assert');
const TypeCheck = require('js-typecheck');
const Action = require('./Action');
const Utils = require('./Utils');
// symbols used for private members to avoid any potential clashing
// caused by re-implementations
const _action = Symbol('action');
const _options = Symbol('options');
const _result = Symbol('result');
/**
* A reader is used by the handler during the execution ({@link Handler.runAction})
* to query the {@link Input} and {@link Session} information that is going be used
* during the execution of the action.
*
* In case of new implements it's expected to implement the {@link Reader._perform}.
*
* When a value is found for an input it's decoded using {@link Input.parseValue}
* where each input implementation has its own way of parsing the serialized data,
* to find out about how a value is serialized for an specific input type you could simply
* set an arbitrary value to an input then query it back through
* {@link Input.serializeValue}. The reference bellow shows the basic serialization
* for the inputs blundled with Mebo:
*
* Input Type | Scalar Serialization | Vector Serialization (compatible with JSON)
* --- | --- | ---
* {@link Text} | `'value'` | `'["valueA","valueB"]'`
* {@link FilePath} | `'/tmp/a.txt'` | `'["/tmp/a.txt","/tmp/b.txt"]'`
* {@link Bool} | `'true`' or `'1'` | `'[true,false]'` or `'[1,0]'`
* {@link Numeric} | `'20'` | `'[20,30]'`
* {@link Email} | `'test@email.com'` | `'["test@email.com","test2@email.com"]'`
* {@link Ip} | `'192.168.0.1'` | `'["192.168.0.1","192.168.0.2"]'`
* {@link Timestamp} | `'2017-02-02T22:26:30.431Z'` | \
* `'["2017-02-02T22:26:30.431Z","2017-02-02T22:27:19.066Z"]'`
* {@link Url} | `'http://www.google.com'` | \
* `'["http://www.google.com","http://www.wikipedia.com"]'`
* {@link Version} | `'10.1.1'` | `'["10.1.1","10.2"]'`
* {@link Buf} | `'aGVsbG8='` | `'["aGVsbG8=","d29ybGQ="]'`
* {@link Hex} | `'ffff00'` | `'["ffff00","ff"]'`
* {@link Hash} | `'d65709ab'` | `'["d65709ab","b94d6fe4"]'`
* {@link UUID} | `'075054e0-810a-11e6-8c1d-e5fb28c699ca'` | \
* `'["075054e0-810a-11e6-8c1d-e5fb28c699ca","98e631d3-6255-402a-88bd-66056e1ca9df"]'`
*
* <br/>**Options:**
* Custom options can be assigned to readers ({@link Reader.setOption}). They are
* passed from the handler to the reader during the handler's execution
* ({@link Handler.runAction}).
*
* ```
* const myHandler = Mebo.Handler.create('someHandler');
*
* // setting reading options
* myHandler.runAction('myAction', {
* someOption: 10,
* });
* ```
*
* When an action is executed through a handler it can define options via
* the {@link Metadata} support. Detailed information about that can be found
* at {@link Metadata}:
*
* ```
* @Mebo.register('myAction')
* class MyAction extends Mebo.Action{
* constructor(){
* super();
*
* // defining a custom reading option
* this.setMeta('$myOption', {
* someOption: 10,
* });
*
* // ...
* }
* }
* ```
*
* **Hiding inputs from readers:**
* A reader only sees inputs that are capable of serialization
* ({@link Input.isSerializable}) or visible inputs. Therefore, any input assigned
* with the property `hidden` is not visible by readers, for instance:
*
* ```
* class Example extends Mebo.Action{
* constructor(){
* super();
* this.createInput('readerCantSeeMe: numeric', {hidden: true});
* this.createInput('readerSeeMe: numeric');
* }
* }
* ```
*/
class Reader{
/**
* Creates a reader.
*
* @param {Action} action - action used for the querying the values
*/
constructor(action){
// note: currently reader & writer are completely separated entities that don't
// have a common parent class (aka HandlerOperation). The reason for
// that is currently they are so distinctive from each other that the only member in
// common is the option. In case they start to share more characteristics in common
// then a base class should be created.
this._setAction(action);
this[_result] = null;
this[_options] = new Utils.HierarchicalCollection();
}
/**
* Returns the action associated with the reader.
*
* @return {Action}
*/
action(){
return this[_action];
}
/**
* Returns an option
*
* @param {string} path - path about where the option is localized (the levels
* must be separated by '.'). In case of an empty string it returns the
* entire options
* @param {*} [defaultValue] - default value returned in case a value was
* not found for the path
* @return {*}
*/
option(path, defaultValue=undefined){
assert(TypeCheck.isString(path), 'path needs to be defined as string');
return this[_options].query(path, defaultValue);
}
/**
* Sets a value under the options
*
* @param {string} path - path about where the option should be stored under
* the options (the levels must be separated by '.')
* @param {*} value - value that is going to be stored under the collection
* @param {boolean} [merge=true] - this option is used to decide in case of the
* last level is already existing under the collection, if the value should be
* either merged (default) or overridden.
*/
setOption(path, value, merge=true){
assert(TypeCheck.isString(path), 'path needs to be defined as string');
this[_options].insert(path, value, merge);
}
/**
* Returns a list of valid input names that should be used for the parsing.
* This avoids hidden inputs to get exposed in the parsing.
*
* @return {Array<string>}
*/
validInputNames(){
const inputs = [];
for (const inputName of this[_action].inputNames()){
const input = this[_action].input(inputName);
if (input.isSerializable() && !input.property('hidden')){
inputs.push(input);
}
}
return inputs;
}
/**
* Reads the input values and returns it through a plain object.
*
* @return {Promise<object>}
*/
async inputValues(){
if (!this[_result]){
await this._parse();
}
return this[_result];
}
/**
* Reads the autofill information and returns it through a plain object.
*
* If the autofill information is already assigned under autofill ({@link Action.session})
* then that information is skipped otherwise it adds the parsed information the result.
*
* @return {Promise<Object>}
*/
async autofillValues(){
if (!this[_result]){
await this._parse();
}
const result = {};
const action = this.action();
const session = action.session();
for (const inputName in this[_result]){
const autofillName = action.input(inputName).property('autofill');
if (autofillName){
// if the input name is already under autofill (assigned previously
// then not overriding them)
if (session && session.hasAutofill(autofillName)){
continue;
}
result[autofillName] = this[_result][inputName];
}
}
return result;
}
/**
* This method should be re-implemented by derived classes to perform the handler parsing.
*
* It should return a plain object containing the input name and the value for that.
* Where any input value from either String or Array types are considered valid values that
* are later ({@link Reader.inputValues}, {@link Reader.autofillValues})
* used to parse the value of the input ({@link Input.parseValue}), otherwise the value
* is ignored.
*
* Only return the ones that were found by the parsing. Also, in case of any error
* during the parsing then an exception should be raised.
*
* @param {Array<Input>} inputList - Valid list of inputs that should be used for
* the parsing
* @return {Promise<Object>}
*
* @protected
*/
_perform(inputList){
return Promise.reject(new Error('Not implemented'));
}
/**
* Sets the action associated with the reader.
*
* @param {Action} value - action instance
*/
_setAction(value){
assert(value instanceof Action, 'Invalid action');
this[_action] = value;
}
/**
* Auxiliary method that triggers the parsing if needed (in case it has not been
* triggered yet).
*
* @return {Promise}
* @private
*/
async _parse(){
if (!this._parsed){
this[_result] = await this._perform(this.validInputNames());
const action = this.action();
// decoding the values if needed
for (const inputName in this[_result]){
const input = action.input(inputName);
const value = this[_result][inputName];
if (TypeCheck.isString(value)){
this[_result][inputName] = input.parseValue(value, false);
}
else if (TypeCheck.isList(value)){
// currently it's converting any array to a JSON string which is supported
// by the input parsing. Lets keep an eye on this for now, since it may cause
// an overhead
this[_result][inputName] = input.parseValue(JSON.stringify(value), false);
}
}
}
return this[_result];
}
}
module.exports = Reader;