src/mixins/convert.js

'use strict';

const Promise = require('bluebird');
const imageProcessor = require('../processor/image');
const unfoldReaderResult = require('../util/unfold-reader-result');
const responseWriter = require('../writer/response');
const readerForUrl = require('../util/reader-for-url');
const {InvalidInputError} = require('../util/errors');
const {HOOKS: {EXTRACT_PROCESS}} = require('../addon');

module.exports = (SuperClass/*:Route*/) => {
  /**
   * Basic mixin that represents the flamingo conversation process.
   * @mixin
   */
  class Convert extends SuperClass {
    /**
     * Resolves if the given operation is valid.
     *
     * @param {FlamingoOperation} operation
     * @returns {Promise.<FlamingoOperation>}
     * @example
     * validOperation(operation) => operation.input.protocol ?
     *    Promise.resolve(operation) :
     *    Promise.reject(new InvalidInputError('target has no protocol'))
     */
    validOperation(operation) {
      return Promise.resolve(operation);
    }

    /**
     * Builds a read function that reads a given operation.
     *
     * @param {FlamingoOperation} operation
     * @returns {function(FlamingoOperation): Promise.<{type: string, stream: function(): Promise<Stream>}>}
     */
    read(operation) {
      return (op) => op.reader(op);
    }

    /**
     * Function that preprocesses the reader result.
     * Can be used i.e. to transform a non image input to an image stream.
     *
     * @param {FlamingoOperation} operation
     * @returns {function(): Promise.<Stream>} function that returns a promise which resolves an image stream
     * @example
     * preprocess(operation) {
     *  return (readerResult) => markdown2Image(operation.request.path.md);
     * }
     */
    preprocess(operation) {
      // same as return (readerResult) => unfoldReaderResult(readerResult);
      return unfoldReaderResult;
    }

    /**
     * Builds a validation function that resolves if the given stream is valid
     * @param {FlamingoOperation} operation
     * @returns {function(): Promise.<Stream>}
     */
    validStream(operation) {
      return (stream) => Promise.resolve(stream);
    }

    /**
     * Builds a function that takes a stream and transforms it.
     * This is the point where the incoming image stream is transformed.
     * @param {FlamingoOperation} operation
     * @returns {function(Stream):Stream}
     */
    process(operation) {
      // same as return (stream) => transform(stream);
      return imageProcessor(operation);
    }

    /**
     * Builds a function that takes a stream and writes it somewhere.
     * @param {FlamingoOperation} operation
     * @returns {function(Stream):Promise}
     */
    write(operation) {
      // same as return (stream) => operation.response.write(stream);
      return operation.writer(operation);
    }

    /**
     * Extract a processing instruction from a given operation.
     * The processing instruction can also expose data useful for the response writer.
     *
     * @param {FlamingoOperation} operation
     * @return {Promise.<{process: Array, response: {}}>}
     */
    extractProcess(operation) {
      return Promise.resolve({process: [], response: {}});
    }

    /**
     * Function that resolves a url pointing to the input that should be converted
     *
     * @param {FlamingoOperation} operation
     * @return {Promise.<undefined>}
     */
    extractInput(operation) {
      return Promise.resolve(undefined);
    }

    /**
     * Function that resolves a reader for the given input.
     * Rejects with InvalidInputError if no compatible reader is found.
     *
     * @param {Url} input
     * @return {Promise.<function>} reader
     * @example
     * (input) =>
     *   Promise.resolve((operation) => ({stream: fs.createReadStream('path/to/image.png'), type: 'file'}))
     */
    extractReader(input) {
      const reader = readerForUrl(input);

      if (!reader) {
        return Promise.reject(new InvalidInputError('No reader available for given input', input));
      }

      return Promise.resolve(reader);
    }

    /**
     * Function that builds an operation for a given request.
     * Note: don't overwrite the buildOperation, if you don't know what you're doing.
     * It's the core convert method that calls other convert methods, used in other mixins.
     *
     * @param {ClientRequest} request incoming http request
     * @param {function} reply hapi reply function
     * @return {Promise.<FlamingoOperation>} Promise that resolves the build operation
     */
    buildOperation(request, reply) {
      const server = this.server;
      return super.buildOperation(request, reply).then(operation =>
        Promise.all([
          this.extractInput(operation),
          this.extractProcess(operation).then(extracted => {
            server.addonsLoader.hook(EXTRACT_PROCESS)(extracted, operation);
            return extracted;
          })
        ]).then(([input, {response, process}]) =>
          this.extractReader(input).then(reader => {
            operation.input = input;
            operation.process = process;
            operation.response = response;
            operation.reader = reader;
            operation.writer = responseWriter;

            return operation;
          })));
    }

    /**
     * Overwrites the Routes handle function to start the conversation process
     * @param {FlamingoOperation} operation
     * @returns {Promise} promise that contains the whole convert process
     * @see flamingo/src/model/Route
     */
    handle(operation) {
      return this.validOperation(operation)
        .then(this.read(operation))
        .then(this.preprocess(operation))
        .then(this.validStream(operation))
        .then(this.process(operation))
        .then(this.write(operation));
    }
  }

  return Convert;
};