sideshowcoder/canned

View on GitHub
lib/canned.js

Summary

Maintainability
D
2 days
Test Coverage
"use strict";

var url = require('url')
var fs = require('fs')
var util = require('util')
var Response = require('./response')
var querystring = require('querystring')
var cannedUtils = require('./utils')
var lookup = require('./lookup')
var _ = require('lodash')

function Canned(dir, options) {
  this.logger = options.logger
  this.wildcard = options.wildcard || 'any'
  this.relaxed_accept = options.relaxed_accept
  this.sanitize = options.sanitize !== undefined ? options.sanitize : true
  var cors_headers = options.cors_headers
  if (cors_headers && cors_headers.join) {
    cors_headers = cors_headers.join(', ')
  }
  this.response_opts = {
    response_delay: options.response_delay,
    cors_enabled: options.cors,
    cors_headers: cors_headers
  }
  this.dir = process.cwd() + '/' + dir
}

function matchFile(matchString, fname, method, ctype) {
  if(!ctype) {
    ctype = '(.+)';
  }
  return matchString.match(
    new RegExp(fname + '\\.' + method + '\\.' + ctype)
  )
}

function matchFileWithQuery(matchString, ctype) {
  if(!ctype) {
    ctype = '(.+)';
  }
  return matchString.match(
    new RegExp('(.*)\\?(.*)\\.(.*)\\.' + ctype)
    )
}

function matchFileWithExactQuery(matchString, fname, queryString, method, ctype) {
  var escapedQueryString = cannedUtils.escapeRegexSpecialChars(queryString)
  return matchString.match(
    new RegExp(fname +
               "(?=.*" +
               escapedQueryString.split("&").join(")(?=.*") +
               ").+" +
               method + "\\." + ctype)
  )
}

Canned.prototype._getFileFromRequest = function(httpObj, files) {

  if (!files) return false

  var m, i, e, matchString, fileMatch, ctype

  // if query params, match regexp based on fname to request
  if(httpObj.query)
  {
    for (i = 0, e = files[i]; e != null; e = files[++i]) {
      fileMatch = matchFileWithQuery(e, httpObj.ctype)
      if (fileMatch)
      {
        ctype = httpObj.ctype || fileMatch[4];
        matchString = httpObj.fname + "?" + httpObj.query + "." + httpObj.method + "." + ctype
        m = matchFileWithExactQuery(matchString, fileMatch[1], fileMatch[2], fileMatch[3], ctype)
        if (m) return { fname: e, mimetype: ctype }
      }
    }
  }

  // if match regexp based on request to fname
  for (i = 0, e = files[i]; e != null; e = files[++i]) {
    var contentTypes = [httpObj.ctype];
    if (this.relaxed_accept) {
      contentTypes = httpObj.matchingContentTypes;
    }
    for (var j = 0; j < contentTypes.length; j++) {
      m = matchFile(e, httpObj.fname, httpObj.method, contentTypes[j])
      if (m) {
        ctype = contentTypes[j] || m[1];
        return { fname : m[0], mimetype : ctype }
      }
    }
  }
  return false
}

function getContentType(mimetype){
  return Response.content_types[mimetype]
}

function stringifyValues(object) {
  _.each(object, function(value, key) {
    if (typeof value === "object") {
      stringifyValues(value);
    } else {
      object[key] = String(value)
    }
  })
}

function isContentTypeJson(request) {
  return request.headers &&
         request.headers['content-type'] &&
         request.headers['content-type'].indexOf('application/json') !== -1;
}

Canned.prototype.parseMetaData = function(response) {
  var metaData = {}
  // convert CR+LF => LF+LF, CR => LF, fixes line breaks causing issues in windows
  response = response.replace("\r", "\n");
  var lines = response.split("\n")
  var that = this
  var requestMatch = new RegExp(/\/\/! [body|params|header]+: (.*)/g)
  lines.forEach(function(line) {
    var optionsMatch = new RegExp(/\/\/!.*[statusCode|contentType|customHeader]/g)

    if(line.indexOf("//!") === 0) { // special comment line
      var matchedRequest = requestMatch.exec(line)
      if(matchedRequest) {
        try {
          metaData.request = JSON.parse(matchedRequest[1])
          stringifyValues(metaData.request);
        } catch (e) {
          metaData.request = matchedRequest[1];
        }
        return
      }
      var matchedOptions = optionsMatch.exec(line)
      if(matchedOptions) {
        try {
          line = line.replace("//!", '')
          var content = line.split(',').map(function (s) {
            var parts = s.split(':');
            parts[0] = '"' + parts[0].trim() + '"'
            return parts.join(':')
          }).join(',')
          var opts = JSON.parse('{' + content  + '}')
          if(opts.hasOwnProperty('customHeader')) {
              if(metaData.hasOwnProperty('customHeaders')) metaData.customHeaders.push(opts.customHeader)
              else metaData.customHeaders = [opts.customHeader]
          } else {
            cannedUtils.extend(metaData, opts)
          }
        } catch(e) {
          that._log('Invalid file header format try //! statusCode: 201')
        }
        return
      }
    }
  })

  return metaData
}

Canned.prototype.getSelectedResponse = function(responses, content, headers) {
  var that = this
  var inputObj
  var response = responses[0]
  var metaData = that.parseMetaData(response)
  var selectedResponse = {
    data: cannedUtils.removeSpecialComments(response),
    statusCode: metaData.statusCode || 200,
    contentType: metaData.contentType,
    customHeaders: metaData.customHeaders
  }

  stringifyValues(content);
  // put the contents of the body and the headers into a big, indexed object.
  inputObj = cannedUtils.extend({}, content, headers)

  responses.forEach(function(response) {
    var metaData = that.parseMetaData(response)

    for(var contentString in content) {
      if (Object.hasOwnProperty.call(content, contentString)) break
    }

    if (contentString === metaData.request) {
      //exact match of request body with body comment in file
      selectedResponse.data = cannedUtils.removeSpecialComments(response)
      if(metaData.statusCode) selectedResponse.statusCode = metaData.statusCode
      return
    }

    if (typeof metaData.request !== 'object') return;

    if(_.isMatch(inputObj, metaData.request)) {
        selectedResponse.data = cannedUtils.removeSpecialComments(response)
        if(metaData.statusCode) selectedResponse.statusCode = metaData.statusCode
      }
    })

  return selectedResponse
}

// return multiple response bodies as array
Canned.prototype.getEachResponse = function(data) {
  if (this.sanitize) {
    data = cannedUtils.removeJSLikeComments(data)
  }
  var responses = data.split(/\n\n(?=[\/\/!])/).filter(function (e) { return e !== '' })
  return responses
}

Canned.prototype.getVariableResponse = function(data, content, headers) {
  if(!data.length) {
    return { statusCode: 204, data: '' }
  }

  var responses = this.getEachResponse(data)
  var response = this.getSelectedResponse(responses, content, headers)
  return response
}

Canned.prototype.sanatizeContent = function (data, fileObject) {
  var sanatized

  if (data.length === 0 || !this.sanitize) {
    return data
  }

  switch (fileObject.mimetype) {
  case 'json':
    // make sure we return valid JSON even so we support comments
    try {
      sanatized = JSON.stringify(JSON.parse(cannedUtils.removeJSLikeComments(data)))
    } catch (err) {
      this._log("problem sanatizing content for " + fileObject.fname + " " + err)
      return false
    }
    break
  default:
    sanatized = data
  }
  return sanatized
}

Canned.prototype._responseForFile = function (httpObj, files, cb) {
  var that = this
  var fileObject = this._getFileFromRequest(httpObj, files);
  httpObj.filename = fileObject.fname
  if (fileObject) {
    var filePath = httpObj.path + '/' + fileObject.fname
    fs.readFile(filePath, { encoding: 'utf8' }, function (err, data) {
      var response
      if (err) {
        response = new Response(getContentType('html'), '', 404, httpObj.res, that.response_opts)
        cb('Not found', response)
      } else {
        data = data.replace(/\r/g, "");
        var _data = that.getVariableResponse(data, httpObj.content, httpObj.headers)
        data = _data.data
        var statusCode = _data.statusCode
        var content = that.sanatizeContent(data, fileObject)

        if (content !== false) {
          response = new Response(_data.contentType || getContentType(fileObject.mimetype), content, statusCode, httpObj.res, that.response_opts, _data.customHeaders)
          cb(null, response)
        } else {
          content = 'Internal Server error invalid input file'
          response = new Response(getContentType('html'), content, 500, httpObj.res, that.response_opts)
          cb(null, response)
        }
      }
    })
  } else {
    var response = new Response(getContentType('html'), '', 404, httpObj.res, that.response_opts)
    cb('Not found', response)
  }
}

Canned.prototype._log = function (message) {
  if (this.logger) this.logger.write(message)
}

Canned.prototype._logHTTPObject = function (httpObj) {
  this._log(' served via: .' + httpObj.pathname.join('/') + '/' + httpObj.filename + '\n')
}

Canned.prototype.respondWithDir = function (httpObj, cb) {
  var that = this;

  var fpath = httpObj.path + '/' + httpObj.dname
  fs.readdir(fpath, function (err, files) {
    httpObj.fname = 'index'
    httpObj.path  = fpath
    that._responseForFile(httpObj, files, function (err, resp) {
      return cb(err, resp)
    })
  })
}

Canned.prototype.respondWithAny = function (httpObj, files, cb) {
  var that = this;

  httpObj.fname = 'any';
  that._responseForFile(httpObj, files, function (err, resp) {
    return cb(err, resp);
  })
}

Canned.prototype.responder = function(body, req, res) {
  var responseHandler
  var httpObj = {}
  var that = this
  var parsedurl = url.parse(req.url)
  httpObj.headers   = req.headers
  httpObj.accept    = (req.headers && req.headers.accept) ? req.headers.accept.trim().split(',') : []
  httpObj.content   = body
  httpObj.pathname  = parsedurl.pathname.split('/')
  httpObj.dname     = httpObj.pathname.pop()
  httpObj.fname     = '_' + httpObj.dname
  httpObj.path      = this.dir + httpObj.pathname.join('/')
  httpObj.query     = parsedurl.query
  httpObj.method    = req.method.toLowerCase()
  httpObj.res       = res
  httpObj.ctype     = ''
  httpObj.matchingContentTypes  = []

  this._log('request: ' + httpObj.method + ' ' + req.url)

  if (httpObj.method === 'options') {
    that._log('Options request, serving CORS Headers\n')
    var response = new Response(null, '', 200, res, this.response_opts)
    return response.send()
  }

  if (httpObj.accept.length) {
    for(var type in Response.content_types){
      if (Response.content_types.hasOwnProperty(type)) {
        if (Response.content_types[type] === httpObj.accept[0].trim()){
          httpObj.ctype = type;
        }
        for(var i = 0; i < httpObj.accept.length; i++){
          if(Response.content_types[type] === httpObj.accept[i].trim()){
            httpObj.matchingContentTypes.push(type);
          }
        }
      }
    }
  }

  var paths = lookup(httpObj.pathname.join('/'), that.wildcard);
  paths.splice(0,1); // The first path is the default
  responseHandler = function (err, resp) {
    if (err) {
      // Try more paths, if there are any still
      if (paths.length > 0) {
        httpObj.path = that.dir + paths.splice(0, 1)[0];
        httpObj.fname = '_' + httpObj.dname;
        return that.findResponse(httpObj, responseHandler);
      } else {
        that._log(' not found\n');
      }
    } else {
      that._logHTTPObject(httpObj)
    }
    return resp.send();
  }

  // Find a response for the first path
  that.findResponse(httpObj, responseHandler);

}

Canned.prototype.findResponse = function(httpObj, cb) {
  var that = this;
  fs.readdir(httpObj.path, function (err, files) {
    fs.stat(httpObj.path + '/' + httpObj.dname, function (err, stats) {
      if (err) {
        that._responseForFile(httpObj, files, function (err, resp) {
          if (err) {
            that.respondWithAny(httpObj, files, cb)
          } else {
            cb(null, resp)
          }
        })
      } else {
        if (stats.isDirectory()) {
          that.respondWithDir(httpObj, cb)
        } else {
          cb(null, new Response('html', '', 500, httpObj.res))
        }
      }
    })
  })
}

Canned.prototype.responseFilter = function (req, res) {
  var that = this
  var body = ''

  // assemble response body if GET/POST/PUT
  switch(req.method) {
  case 'PUT':
  case 'POST':
    req.on('data', function (data) {
      body += data
    })
    req.on('end', function () {
      var responderBody = querystring.parse(body);
      if (isContentTypeJson(req)) {
        try {
          responderBody = JSON.parse(body)
        } catch (e) {
          that._log('Invalid json content')
        }
      }
      that.responder(responderBody, req, res)
    })
    break
  case 'GET':
    var query = url.parse(req.url).query
    if (query && query.length > 0) {
      body = querystring.parse(query)
    }
    that.responder(body, req, res)
    break
  default:
    that.responder(body, req, res)
    break
  }
}

module.exports = Canned;