src/lib/transformers/Transformer.js
import { join as joinPath } from 'path';
import { Buffer } from 'buffer';
import { replaceExtension } from 'gulp-util';
import through from 'through2';
/**
* The directions a Transformer can work in
* @type {{FromDB: string, FromFilesystem: string}}
*/
export const TransformDirection = {
FromDB: 'FromDB',
FromFilesystem: 'FromFilesystem',
};
/**
* Controls how files are transformed from and to the filesystem. **Must be subclassed.**
*/
export default class Transformer {
/**
* Creates a new Transformer based on some options
* @param {Object} options The options to use.
*/
constructor(options = {}) {
this._options = options;
}
/**
* Transforms a {@link NodeStream}.
* @abstract
* @param {NodeStream} stream The stream used.
* @param {Node} node The node to transform.
* @param {String} enc The encoding used.
* @param {function(err: Error, node: Node)} cb Callback to call after transformation.
*/
transformFromDB(stream, node, enc, cb) {
cb(new Error('Transformer.transformFromDB must be implemented by all subclasses'));
}
/**
* A function that is applied after all transformations passed.
* @see https://nodejs.org/api/stream.html#stream_transform_flush_callback
* @abstract
* @param {NodeStream} stream The stream used.
* @param {function(err: Error)} cb Callback to call after flushing.
*/
flushFromDB(stream, cb) { cb(); }
/**
* Transforms a stream of {@link AtviseFile}s.
* @abstract
* @param {Stream} stream The stream used.
* @param {AtviseFile} file The file to transform.
* @param {String} enc The encoding used.
* @param {function(err: Error, file: AtviseFile)} cb Callback to call after transformation.
*/
transformFromFilesystem(stream, file, enc, cb) {
cb(new Error('Transformer.transformFromFilesystem must be implemented by all subclasses'));
}
/**
* A function that is applied after all transformations passed.
* @see https://nodejs.org/api/stream.html#stream_transform_flush_callback
* @abstract
* @param {NodeStream} stream The stream used.
* @param {function(err: Error)} cb Callback to call after flushing.
*/
flushFromFilesystem(stream, cb) { cb(); }
/**
* Applies transformation for a given {@link TransformDirection}.
* @param {TransformDirection} direction The direction to use for transformation.
* @returns {Stream} A stream of {@link AtviseFile}s.
*/
transform(direction) {
const self = this;
function _transform(file, encoding, cb) {
if (self.constructor.applyToTypes[file.rc.typeDefinition]) {
self[`transform${direction}`](this, file, encoding, cb);
} else {
cb(null, file);
}
}
const flushName = `flush${direction}`;
const flush = self.constructor.prototype[flushName] !== Transformer.prototype[flushName] ?
function(cb) {
self[flushName](this, cb);
} : undefined;
return through.obj(_transform, flush);
}
// Helpers
/**
* Encapsules a file. Useful helper when splitting a file into multiple others.
* @param {AtviseFile} file The file to encapsule.
* @param {String} [extname] The file extension to use. Defaults to `file`'s extension.
*
* @example <caption>Basic usage</caption>
* const file = new AtviseFile({
* path: 'path/to/file.ext'
* });
*
* // Results in a file with path 'path/to/file.ext/file.json'
* Transformer.encapsule(file, '.json');
*
* // Results in a file with path 'path/to/file.ext/file.ext'
* Transformer.encapsule(file);
*/
static encapsule(file, extname) {
const base = replaceExtension(file.basename, '');
file.path = joinPath(file.path, `${base}${extname || file.extname}`);
}
/**
* Takes a file and returns an encapsuled file apon it. Useful helper when splitting a file into
* multiple others. {@link Transformer.encapsule} is used to create the new file's path.
* @param {AtviseFile} file The file to clone
* @param {String} [extname] The file extension to use. Defaults to `file`'s extension.
* @param {String} [contentString] The new file's contents given as a string.
* @returns {AtviseFile} The encapsuled file.
*/
static encapsuledFile(file, extname, contentString) {
const enc = file.clone({ contents: false });
Transformer.encapsule(enc, extname);
enc.stat = file.stat;
if (contentString !== undefined) {
enc.contents = Buffer.from(contentString);
}
return enc;
}
/**
* Returns a string representation of the current Transformer.
* @return {String}
*/
toString() {
return `${this.constructor.name} ${JSON.stringify(this._options)}`;
}
// Constants
/**
* The NodeTypes the transform should be applied to.
* @type {Map<NodeType, Boolean>}
*/
static get applyToTypes() {
throw new Error('Transformer.applyToTypes must be implemented by all subclasses');
}
}