src/Writer.js
const stream = require('stream');
const assert = require('assert');
const TypeCheck = require('js-typecheck');
const debug = require('debug')('Mebo');
const Utils = require('./Utils');
const MeboErrors = require('./MeboErrors');
// symbols used for private members to avoid any potential clashing
// caused by re-implementations
const _options = Symbol('options');
const _value = Symbol('value');
/**
* A writer is used to output a value through the {@link Handler}.
*
* The output is determined by the kind of value that's passed to the writer where
* exceptions are interpreted as error output otherwise, the value is interpreted
* as success value. Therefore, new implements are expected to implement both a success
* ({@link Writer._successOutput}) and error ({@link Writer._errorOutput}) outputs.
*
* **Options:** Custom options can be assigned to writers ({@link Writer.setOption}).
* They are passed from the handler to the writer during the output process
* ({@link Handler.output}).
*
* ```
* const myHandler = Mebo.Handler.create('someHandler');
*
* // setting output options during the output
* myHandler.output(value, {
* someOption: 10,
* });
* ```
*
* When an action is executed through a handler it can define options for
* readers and writers via {@link Metadata} support.
*
* <h2>Options Summary</h2>
*
* Option Name | Description | Default Value
* --- | --- | :---:
* convertBufferToReadableStream | Tells if a buffer value should be converted to a \
* readable stream |
* result | Overrides the value returned by {@link Writer.value} to an arbitrary \
* value (only affects the success output) |
*/
class Writer{
/**
* Creates a writer
*
* @param {*} value - arbitrary value passed to the writer
*/
constructor(value){
// 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[_value] = value;
this[_options] = new Utils.HierarchicalCollection();
// default options
this.setOption('convertBufferToReadableStream', true);
}
/**
* 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 the value that should be serialized ({@link Writer.serialize}) by the writer.
*
* @return {*}
*/
value(){
return this[_value];
}
/**
* Serializes a writer value ({@link Writer.value}) in case the value is an
* exception it's serialize as {@link Writer._errorOutput} otherwise it's serialized
* as {@link Writer._successOutput}.
*/
serialize(){
if (this.value() instanceof Error){
this._errorOutput();
}
else{
this._successOutput();
}
}
/**
* Translates an {@link Error} to a data structure that is later serialized by a writer
* implementation as output. This method gets triggered when an exception is passed
* as value by the {@link Handler.output}.
*
* By default the contents of the error output are driven by the `err.message`,
* however if an error contains `err.toJSON` property ({@link ValidationFail.toJSON})
* then that's used instead of the message.
*
* Also, you can avoid specific errors to be handled via output process by defining the member
* `output` assigned with `false` to the error (for instance ```err.output = false;```). If
* that is in place the error gets thrown which triggers the event {@link Handler.onErrorDuringOutput}.
*
* **Tip:** You can set the env variable `NODE_ENV=development` to get the traceback information
* included in the error output
*
* @return {Object}
* @protected
*/
_errorOutput(){
const err = this.value();
// checking if the error can be handled by the writer
if (err.output === false){
throw err;
}
// objects with the property 'toJSON' can define a custom representation
// when they are serialized using json using (otherwise use the error message), reference:
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
const result = (TypeCheck.isCallable(err.toJSON)) ? err.toJSON() : err.message;
// printing the stack-trace information when running in development mode
/* istanbul ignore next */
if (process.env.NODE_ENV === 'development' && err.stack && !(err instanceof MeboErrors.Help)){
process.stderr.write(`${err.stack}\n`);
debug(err.stack);
}
return result;
}
/**
* Translates the success value to a data structure that is later serialized
* by a handler implementation as output.
*
* /todo:
* This value is either driven by the option 'result' (when defined) or by the
* value defined at constructor time.
*
* All writers shipped with Mebo have support for streams where in case of
* any readable stream or buffer value are piped to the output,
* otherwise the result is encoded using JSON (defined per writer bases).
*
* Note: any Buffer value passed to this method gets automatically converted to
* a readable stream (this behavior is driven by the option
* 'convertBufferToReadableStream').
*
* This method is called by {@link Handler.output}.
*
* @return {Object|Stream}
* @protected
*/
_successOutput(){
const optionOutput = this.option('result');
const result = (optionOutput === undefined) ? this.value() : optionOutput;
// stream output
if (this.option('convertBufferToReadableStream') && result instanceof Buffer){
const bufferStream = new stream.PassThrough();
bufferStream.end(result);
return bufferStream;
}
return result;
}
}
module.exports = Writer;