Home Manual Reference Source Test

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');
  }
}