<commit_before>"use strict";

const Endpoints = require("./Endpoints");
const HTTPS = require("https");
const MultipartData = require("../util/MultipartData");
const SequentialBucket = require("../util/SequentialBucket");
const Zlib = require("zlib");

/**
* Handles API requests
*/
class RequestHandler {
    constructor(client, forceQueueing) {
        this._client = client;
        this.baseURL = Endpoints.BASE_URL;
        this.userAgent = `DiscordBot (https://github.com/abalabahaha/eris, ${require("../../package.json").version})`;
        this.ratelimits = {};
        this.latencyRef = {
            latency: 500,
            raw: [500, 500, 500, 500, 500, 500, 500, 500, 500, 500],
            total: 5000,
            timeOffset: 0,
            lastTimeOffsetCheck: 0
        };
        this.globalBlock = false;
        this.readyQueue = [];
        if(forceQueueing) {
            this.globalBlock = true;
            this._client.once("shardPreReady", () => this.globalUnblock());
        }
    }

    globalUnblock() {
        this.globalBlock = false;
        while(this.readyQueue.length > 0) {
            this.readyQueue.shift()();
        }
    }

    routefy(url) {
        return url.replace(/\/([a-z-]+)\/(?:[0-9]{17,})+?/g, function(match, p) {
            return p === "channels" || p === "guilds" ? match : `/${p}/:id`;
        }).replace(/\/reactions\/.+/g, "/reactions/:id");
    }

    /**
    * Make an API request
    * @arg {String} method Uppercase HTTP method
    * @arg {String} url URL of the endpoint
    * @arg {Boolean} auth Whether to add the Authorization header and token or not
    * @arg {Object} [body] Request payload
    * @arg {Object} [file] File object
    * @arg {String} file.file A buffer containing file data
    * @arg {String} file.name What to name the file
    * @returns {Promise<Object>} Resolves with the returned JSON data
    */
    request(method, url, auth, body, file, _route, short) {
        var route = _route || this.routefy(url);

        return new Promise((resolve, reject) => {
            var attempts = 0;

            var actualCall = (cb) => {
                var headers = {
                    "User-Agent": this.userAgent,
                    "Accept-Encoding": "gzip,deflate"
                };
                var data;

                try {
                    if(auth) {
                        headers.Authorization = this._client.token;
                    }
                    if(body && body.reason) { // Audit log reason sniping
                        if(body.reason === decodeURI(body.reason)) {
                            body.reason = encodeURIComponent(body.reason);
                        }
                        headers["X-Audit-Log-Reason"] = body.reason;
                        delete body.reason;
                    }
                    if(body && body.queryReason) {
                        body.reason = body.queryReason;
                        delete body.queryReason;
                    }
                    var finalURL = url;
                    if(file) {
                        if(Array.isArray(file)) {
                            data = new MultipartData();
                            headers["Content-Type"] = "multipart/form-data; boundary=" + data.boundary;
                            file.forEach(function(f) {
                                if(!f.file) {
                                    return;
                                }
                                data.attach(f.name, f.file, f.name);
                            });
                            if(body) {
                                data.attach("payload_json", body);
                            }
                            data = data.finish();
                        } else if(file.file) {
                            data = new MultipartData();
                            headers["Content-Type"] = "multipart/form-data; boundary=" + data.boundary;
                            data.attach("file", file.file, file.name);
                            if(body) {
                                data.attach("payload_json", body);
                            }
                            data = data.finish();
                        } else {
                            throw new Error("Invalid file object");
                        }
                    } else if(body) {
                        if(method === "GET" || (method === "PUT" && url.includes("/bans/"))) { // TODO remove PUT case when devs fix
                            var qs = "";
                            Object.keys(body).forEach(function(key) {
                                if(body[key] != undefined) {
                                     if(Array.isArray(body[key])) {
                                        body[key].forEach(function(val) {
                                            qs += `&${encodeURIComponent(key)}=${encodeURIComponent(val)}`;
                                        });
                                    } else {
                                        qs += `&${encodeURIComponent(key)}=${encodeURIComponent(body[key])}`;
                                    }
                                }
                            });
                            finalURL += "?" + qs.substring(1);
                        } else {
                            data = JSON.stringify(body);
                            headers["Content-Type"] = "application/json";
                        }
                    }
                } catch(err) {
                    cb();
                    reject(err);
                    return;
                }

                var req = HTTPS.request({
                    method: method,
                    host: "discordapp.com",
                    path: this.baseURL + finalURL,
                    headers: headers
                });

                var reqError;

                req.once("abort", () => {
                    cb();
                    reqError = reqError || new Error(`Request aborted by client on ${method} ${url}`);
                    reqError.req = req;
                    reject(reqError);
                }).once("aborted", () => {
                    cb();
                    reqError = reqError || new Error(`Request aborted by server on ${method} ${url}`);
                    reqError.req = req;
                    reject(reqError);
                }).once("error", (err) => {
                    reqError = err;
                    req.abort();
                });

                var latency = Date.now();

                req.once("response", (resp) => {
                    latency = Date.now() - latency;
                    this.latencyRef.total = this.latencyRef.total - this.latencyRef.raw.shift() + latency;
                    this.latencyRef.latency = ~~(this.latencyRef.total / this.latencyRef.raw.push(latency));

                    if(this.latencyRef.lastTimeOffsetCheck < Date.now() - 60000) {
                        var timeOffset = Date.parse(resp.headers["date"]) - (this.latencyRef.lastTimeOffsetCheck = Date.now());
                        if(~~(this.latencyRef.timeOffset) - this.latencyRef.latency >= 1000 && ~~(timeOffset) - this.latencyRef.latency >= 1000) {
                            this._client.emit("error", new Error(`Your clock is ${this.latencyRef.timeOffset}ms behind Discord's server clock. Please check your connection and system time.`));
                        }
                        this.latencyRef.timeOffset = timeOffset;
                    }

                    var response = "";

                    var _respStream = resp;
                    if(resp.headers["content-encoding"]) {
                        if(~resp.headers["content-encoding"].indexOf("gzip")) {
                            _respStream = resp.pipe(Zlib.createGunzip());
                        } else if(~resp.headers["content-encoding"].indexOf("deflate")) {
                            _respStream = resp.pipe(Zlib.createInflate());
                        }
                    }

                    _respStream.on("data", (str) => {
                        response += str;
                    }).once("end", () => {
                        var now = Date.now();

                        if(resp.headers["x-ratelimit-limit"]) {
                            this.ratelimits[route].limit = +resp.headers["x-ratelimit-limit"];
                        }

                        this.ratelimits[route].remaining = resp.headers["x-ratelimit-remaining"] === undefined ? 1 : +resp.headers["x-ratelimit-remaining"] || 0;

                        if(resp.headers["retry-after"]) {
                            if(resp.headers["x-ratelimit-global"]) {
                                this.globalBlock = true;
                                setTimeout(() => this.globalUnblock(), +resp.headers["retry-after"] || 1);
                            } else {
                                this.ratelimits[route].reset = (+resp.headers["retry-after"] || 1) + now;
                            }
                        } else if(resp.headers["x-ratelimit-reset"]) {
                            this.ratelimits[route].reset = Math.max(+resp.headers["x-ratelimit-reset"] * (route.endsWith("/reactions/:id") ? 250 : 1000) + this.latencyRef.timeOffset, now);
                        } else {
                            this.ratelimits[route].reset = now;
                        }

                        if(resp.statusCode !== 429) {
                            this._client.emit("debug", `${body && body.content} ${now} ${route} ${resp.statusCode}: ${latency}ms (${this.latencyRef.latency}ms avg) | ${this.ratelimits[route].remaining}/${this.ratelimits[route].limit} left | Reset ${this.ratelimits[route].reset} (${this.ratelimits[route].reset - now}ms left)`);
                        }

                        if(resp.statusCode >= 300) {
                            if(resp.statusCode === 429) {
                                this._client.emit("warn", `${resp.headers["x-ratelimit-global"] ? "Global" : "Unexpected"} 429 (╯°□°）╯︵ ┻━┻: ${response}\n${body && body.content} ${now} ${route} ${resp.statusCode}: ${latency}ms (${this.latencyRef.latency}ms avg) | ${this.ratelimits[route].remaining}/${this.ratelimits[route].limit} left | Reset ${this.ratelimits[route].reset} (${this.ratelimits[route].reset - now}ms left)`);
                                if(resp.headers["retry-after"]) {
                                    setTimeout(() => {
                                        cb();
                                        this.request(method, url, auth, body, file, route, true).then(resolve).catch(reject);
                                    }, +resp.headers["retry-after"]);
                                    return;
                                } else {
                                    cb();
                                    this.request(method, url, auth, body, file, route, true).then(resolve).catch(reject);
                                    return;
                                }
                            } else if(resp.statusCode === 502 && ++attempts < 4) {
                                this._client.emit("warn", "A wild 502 appeared! Thanks CloudFlare!");
                                setTimeout(() => {
                                    this.request(method, url, auth, body, file, route, true).then(resolve).catch(reject);
                                }, Math.floor(Math.random() * 1900 + 100));
                                return cb();
                            }
                            cb();
                            var err = new Error(`${resp.statusCode} ${resp.statusMessage} on ${method} ${url}\n\n${response.substring(0, 200)}`);
                            err.resp = resp;
                            err.response = response;
                            err.req = req;
                            reject(err);
                            return;
                        }
                        if(response.length > 0) {
                            if(resp.headers["content-type"] === "application/json") {
                                try {
                                    response = JSON.parse(response);
                                } catch(err) {
                                    cb();
                                    reject(err);
                                    return;
                                }
                            }
                        }
                        cb();
                        resolve(response);
                    });
                });

                req.setTimeout(15000, function() {
                    reqError = new Error(`Request timed out (>15000ms) on ${method} ${url}`);
                    req.abort();
                });

                if(Array.isArray(data)) {
                    for(var chunk of data) {
                        req.write(chunk);
                    }
                    req.end();
                } else {
                    req.end(data);
                }
            };

            if(this.globalBlock && auth) {
                this.readyQueue.push(() => {
                    if(!this.ratelimits[route]) {
                        this.ratelimits[route] = new SequentialBucket(1, this.latencyRef);
                    }
                    this.ratelimits[route].queue(actualCall, short);
                });
            } else {
                if(!this.ratelimits[route]) {
                    this.ratelimits[route] = new SequentialBucket(1, this.latencyRef);
                }
                this.ratelimits[route].queue(actualCall, short);
            }
        });
    }
}

module.exports = RequestHandler;
<commit_msg>More conservative ratelimit handling<commit_after>"use strict";

const Endpoints = require("./Endpoints");
const HTTPS = require("https");
const MultipartData = require("../util/MultipartData");
const SequentialBucket = require("../util/SequentialBucket");
const Zlib = require("zlib");

/**
* Handles API requests
*/
class RequestHandler {
    constructor(client, forceQueueing) {
        this._client = client;
        this.baseURL = Endpoints.BASE_URL;
        this.userAgent = `DiscordBot (https://github.com/abalabahaha/eris, ${require("../../package.json").version})`;
        this.ratelimits = {};
        this.latencyRef = {
            latency: 500,
            raw: [500, 500, 500, 500, 500, 500, 500, 500, 500, 500],
            timeOffset: 0,
            timeOffsets: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            lastTimeOffsetCheck: 0
        };
        this.globalBlock = false;
        this.readyQueue = [];
        if(forceQueueing) {
            this.globalBlock = true;
            this._client.once("shardPreReady", () => this.globalUnblock());
        }
    }

    globalUnblock() {
        this.globalBlock = false;
        while(this.readyQueue.length > 0) {
            this.readyQueue.shift()();
        }
    }

    routefy(url) {
        return url.replace(/\/([a-z-]+)\/(?:[0-9]{17,})+?/g, function(match, p) {
            return p === "channels" || p === "guilds" ? match : `/${p}/:id`;
        }).replace(/\/reactions\/.+/g, "/reactions/:id");
    }

    /**
    * Make an API request
    * @arg {String} method Uppercase HTTP method
    * @arg {String} url URL of the endpoint
    * @arg {Boolean} auth Whether to add the Authorization header and token or not
    * @arg {Object} [body] Request payload
    * @arg {Object} [file] File object
    * @arg {String} file.file A buffer containing file data
    * @arg {String} file.name What to name the file
    * @returns {Promise<Object>} Resolves with the returned JSON data
    */
    request(method, url, auth, body, file, _route, short) {
        var route = _route || this.routefy(url);

        return new Promise((resolve, reject) => {
            var attempts = 0;

            var actualCall = (cb) => {
                var headers = {
                    "User-Agent": this.userAgent,
                    "Accept-Encoding": "gzip,deflate"
                };
                var data;

                try {
                    if(auth) {
                        headers.Authorization = this._client.token;
                    }
                    if(body && body.reason) { // Audit log reason sniping
                        if(body.reason === decodeURI(body.reason)) {
                            body.reason = encodeURIComponent(body.reason);
                        }
                        headers["X-Audit-Log-Reason"] = body.reason;
                        delete body.reason;
                    }
                    if(body && body.queryReason) {
                        body.reason = body.queryReason;
                        delete body.queryReason;
                    }
                    var finalURL = url;
                    if(file) {
                        if(Array.isArray(file)) {
                            data = new MultipartData();
                            headers["Content-Type"] = "multipart/form-data; boundary=" + data.boundary;
                            file.forEach(function(f) {
                                if(!f.file) {
                                    return;
                                }
                                data.attach(f.name, f.file, f.name);
                            });
                            if(body) {
                                data.attach("payload_json", body);
                            }
                            data = data.finish();
                        } else if(file.file) {
                            data = new MultipartData();
                            headers["Content-Type"] = "multipart/form-data; boundary=" + data.boundary;
                            data.attach("file", file.file, file.name);
                            if(body) {
                                data.attach("payload_json", body);
                            }
                            data = data.finish();
                        } else {
                            throw new Error("Invalid file object");
                        }
                    } else if(body) {
                        if(method === "GET" || (method === "PUT" && url.includes("/bans/"))) { // TODO remove PUT case when devs fix
                            var qs = "";
                            Object.keys(body).forEach(function(key) {
                                if(body[key] != undefined) {
                                     if(Array.isArray(body[key])) {
                                        body[key].forEach(function(val) {
                                            qs += `&${encodeURIComponent(key)}=${encodeURIComponent(val)}`;
                                        });
                                    } else {
                                        qs += `&${encodeURIComponent(key)}=${encodeURIComponent(body[key])}`;
                                    }
                                }
                            });
                            finalURL += "?" + qs.substring(1);
                        } else {
                            data = JSON.stringify(body);
                            headers["Content-Type"] = "application/json";
                        }
                    }
                } catch(err) {
                    cb();
                    reject(err);
                    return;
                }

                var req = HTTPS.request({
                    method: method,
                    host: "discordapp.com",
                    path: this.baseURL + finalURL,
                    headers: headers
                });

                var reqError;

                req.once("abort", () => {
                    cb();
                    reqError = reqError || new Error(`Request aborted by client on ${method} ${url}`);
                    reqError.req = req;
                    reject(reqError);
                }).once("aborted", () => {
                    cb();
                    reqError = reqError || new Error(`Request aborted by server on ${method} ${url}`);
                    reqError.req = req;
                    reject(reqError);
                }).once("error", (err) => {
                    reqError = err;
                    req.abort();
                });

                var latency = Date.now();

                req.once("response", (resp) => {
                    latency = Date.now() - latency;
                    this.latencyRef.raw.push(latency);
                    this.latencyRef.latency = ~~(this.latencyRef.latency - this.latencyRef.raw.shift() / 10 + latency / 10);

                    if(this.latencyRef.lastTimeOffsetCheck < Date.now() - 5000) {
                        var timeOffset = ~~((this.latencyRef.lastTimeOffsetCheck = Date.now()) - Date.parse(resp.headers["date"]));
                        if(this.latencyRef.timeOffset - this.latencyRef.latency >= 1000 && timeOffset - this.latencyRef.latency >= 1000) {
                            this._client.emit("error", new Error(`Your clock is ${this.latencyRef.timeOffset}ms behind Discord's server clock. Please check your connection and system time.`));
                        }
                        this.latencyRef.timeOffset = ~~(this.latencyRef.timeOffset - this.latencyRef.timeOffsets.shift() / 10 + timeOffset / 10);
                        console.log(timeOffset, this.latencyRef.timeOffset, Date.parse(resp.headers["date"]));
                        this.latencyRef.timeOffsets.push(timeOffset);
                    }

                    var response = "";

                    var _respStream = resp;
                    if(resp.headers["content-encoding"]) {
                        if(~resp.headers["content-encoding"].indexOf("gzip")) {
                            _respStream = resp.pipe(Zlib.createGunzip());
                        } else if(~resp.headers["content-encoding"].indexOf("deflate")) {
                            _respStream = resp.pipe(Zlib.createInflate());
                        }
                    }

                    _respStream.on("data", (str) => {
                        response += str;
                    }).once("end", () => {
                        var now = Date.now();

                        if(resp.headers["x-ratelimit-limit"]) {
                            this.ratelimits[route].limit = +resp.headers["x-ratelimit-limit"];
                        }

                        this.ratelimits[route].remaining = resp.headers["x-ratelimit-remaining"] === undefined ? 1 : +resp.headers["x-ratelimit-remaining"] || 0;

                        if(resp.headers["retry-after"]) {
                            if(resp.headers["x-ratelimit-global"]) {
                                this.globalBlock = true;
                                setTimeout(() => this.globalUnblock(), +resp.headers["retry-after"] || 1);
                            } else {
                                this.ratelimits[route].reset = (+resp.headers["retry-after"] || 1) + now;
                            }
                        } else if(resp.headers["x-ratelimit-reset"]) {
                            if(route.endsWith("/reactions/:id")) {
                                this.ratelimits[route].reset = now + Math.max(this.latencyRef.timeOffset, 250);
                            } else {
                                this.ratelimits[route].reset = Math.max(+resp.headers["x-ratelimit-reset"] * 1000 + this.latencyRef.timeOffset, now);
                            }
                        } else {
                            this.ratelimits[route].reset = now;
                        }

                        if(resp.statusCode !== 429) {
                            this._client.emit("debug", `${body && body.content} ${now} ${route} ${resp.statusCode}: ${latency}ms (${this.latencyRef.latency}ms avg) | ${this.ratelimits[route].remaining}/${this.ratelimits[route].limit} left | Reset ${this.ratelimits[route].reset} (${this.ratelimits[route].reset - now}ms left)`);
                            // this._client.emit("debug", resp.headers["x-ratelimit-limit"] + " " + resp.headers["x-ratelimit-remaining"] + " " + resp.headers["x-ratelimit-reset"]);
                        }

                        if(resp.statusCode >= 300) {
                            if(resp.statusCode === 429) {
                                this._client.emit("warn", `${resp.headers["x-ratelimit-global"] ? "Global" : "Unexpected"} 429 (╯°□°）╯︵ ┻━┻: ${response}\n${body && body.content} ${now} ${route} ${resp.statusCode}: ${latency}ms (${this.latencyRef.latency}ms avg) | ${this.ratelimits[route].remaining}/${this.ratelimits[route].limit} left | Reset ${this.ratelimits[route].reset} (${this.ratelimits[route].reset - now}ms left)`);
                                if(resp.headers["retry-after"]) {
                                    setTimeout(() => {
                                        cb();
                                        this.request(method, url, auth, body, file, route, true).then(resolve).catch(reject);
                                    }, +resp.headers["retry-after"]);
                                    return;
                                } else {
                                    cb();
                                    this.request(method, url, auth, body, file, route, true).then(resolve).catch(reject);
                                    return;
                                }
                            } else if(resp.statusCode === 502 && ++attempts < 4) {
                                this._client.emit("warn", "A wild 502 appeared! Thanks CloudFlare!");
                                setTimeout(() => {
                                    this.request(method, url, auth, body, file, route, true).then(resolve).catch(reject);
                                }, Math.floor(Math.random() * 1900 + 100));
                                return cb();
                            }
                            cb();
                            var err = new Error(`${resp.statusCode} ${resp.statusMessage} on ${method} ${url}\n\n${response.substring(0, 200)}`);
                            err.resp = resp;
                            err.response = response;
                            err.req = req;
                            reject(err);
                            return;
                        }
                        if(response.length > 0) {
                            if(resp.headers["content-type"] === "application/json") {
                                try {
                                    response = JSON.parse(response);
                                } catch(err) {
                                    cb();
                                    reject(err);
                                    return;
                                }
                            }
                        }
                        cb();
                        resolve(response);
                    });
                });

                req.setTimeout(15000, function() {
                    reqError = new Error(`Request timed out (>15000ms) on ${method} ${url}`);
                    req.abort();
                });

                if(Array.isArray(data)) {
                    for(var chunk of data) {
                        req.write(chunk);
                    }
                    req.end();
                } else {
                    req.end(data);
                }
            };

            if(this.globalBlock && auth) {
                this.readyQueue.push(() => {
                    if(!this.ratelimits[route]) {
                        this.ratelimits[route] = new SequentialBucket(1, this.latencyRef);
                    }
                    this.ratelimits[route].queue(actualCall, short);
                });
            } else {
                if(!this.ratelimits[route]) {
                    this.ratelimits[route] = new SequentialBucket(1, this.latencyRef);
                }
                this.ratelimits[route].queue(actualCall, short);
            }
        });
    }
}

module.exports = RequestHandler;
<|endoftext|>


<commit_before>import assert from 'assert';
import express from 'express';
import Promise from 'bluebird';
import { log } from '../utils';
import Error from './error';
import Engine from './engine';
import blackburn from 'blackburn';

/**
 * Application instances are little more than specialized Engines, designed to
 * kick off the loading, mounting, and launching stages of booting up.
 *
 * @title Application
 */
export default Engine.extend({

  /**
   * There are three stages involved when booting up a Denali application:
   *
   * 1. **load stage** - the application loads it's configuration and code from
   * the filesystem. It also discovers any child engines present, and triggers
   * the same for them.
   *
   * 2. **mount stage** - the application assembles a unified picture of itself
   * based on it's own code and config, as well as those of the child engines it
   * discovered during the load stage.
   *
   * 3. **launch** - now that the application has merged in the child engines,
   * it is ready to launch the HTTP server, connect to a port, and begin
   * accepting connections.
   *
   * When you instantiate a new Application instances, the first two stages run
   * immediately. However, the application does not launch until you run the
   * `start()` method. This is useful if you want to load an Application into
   * memory to inspect or use, but without booting up the actual HTTP server.
   *
   * @constructor
   */
  init(options = {}) {
    options.environment = options.environment || process.env.DENALI_ENV || process.env.NODE_ENV || 'development';
    this._super.call(this, options);
    this.router = new express.Router();
  },

  /**
   * Launch this Application. Runs the initializers, then boots up the HTTP
   * server and binds to a port. Returns a Promise which resolves once the
   * server is live and accepting connections.
   *
   * @method start
   *
   * @return {Promise} resolves when the server has successfully bound to it's
   * port and is accepting connections
   */
  start() {
    this.load();

    return Promise.resolve(this.initializers)
      .each((initializer) => {
        return initializer(this);
      }).then(() => {
        return this.launchServer();
      });
  },

  /**
   * Boots up the express HTTP server, mounts the Application's router to it,
   * and binds the server to the Application's port.
   *
   * @method launchServer
   * @private
   *
   * @return {Promise} resolves when the server has successfully bound to it's
   * port and is accepting connections
   */
  launchServer() {
    console.log(this.config.PORT);
    let port = this.config.PORT || 3000;
    return new Promise((resolve) => {
      this.server = express();
      this.injectBlackburnPlusForaker(this.server);
      this.server.use(this.router);
      this.server.use(this.handleErrors.bind(this));
      this.server.listen(port, resolve);
    }).then(() => {
      this.log(`${ this.pkg.name }@${ this.pkg.version } server up on port ${port}`);
    });
  },

  /**
   * A simple proxy for the utils/log module, which silences any logs if we are
   * currently testing
   *
   * @method log
   */
  log(level) {
    if (this.env !== 'test' || level === 'error') {
      log.apply(this, arguments);
    }
  },

  /* eslint-disable no-unused-vars */
  handleErrors(err, req, res, next) {
    let error = err;
    if (!res._rendered) {
      if (!error.status) {
        error = new Error.InternalServerError(err.stack);
        this.log('error', error.stack);
      }
      error.code = error.code || error.name;
      return res.render(error);
    }
  },
  /* eslint-enable no-unused-vars */

  injectBlackburnPlusForaker(server) {
    // Setup vanilla blackburn installation
    server.use(blackburn({
      adapters: this.adapters,
      serializers: this.serializers
    }));
    // Now wrap blackburn's render method with one which picks the corresponding
    // serializer for that controller. This is the glue that joins the blackburn
    // serialization with foraker's controllers.
    server.use((req, res, next) => {
      // This controller property is injected into the context below, in the
      // handle method
      let render = res.render;
      res.render = (...args) => {
        // We need to identify options arg, which can be second or third.
        // It's only second when no status is supplied. It can be omitted as
        // well, so if it is, default it to an empty object.
        let options;
        if (typeof args[0] !== 'number' && !args[2]) {
          options = args[1] = args[1] || {};
        } else {
          options = args[2] = args[2] || {};
        }

        let controller = req.context && req.context.controller;
        options.serializer = options.serializer
                             || controller && controller.serializer
                             || controller && controller.name;
        options.adapter = options.adapter
                          || controller && controller.adapter
                          || controller && controller.name;
        return render.apply(res, args);
      };
      next();
    });
  },

  /**
   * Takes a controllerAction string and returns an express-compatible route
   * handler function that will route the request to that controller and action.
   * This is defined only on the Application since there is only one server
   * shared across all the Engines.
   *
   * @method handle
   * @private
   *
   * @param  {String} controllerAction  a string indicating the controller and
   * action that should be routed to. For example, `'books.create'` would route
   * to the `BooksController` `create` action.
   *
   * @return {Function} an express-compatible route handler function
   */
  handle(controllerAction) {
    let [ controllerName, actionName ] = controllerAction.split('.');
    let controller = this.controllers[controllerName];
    assert(controller, `Attempted to route to ${ controllerAction }, but ${ controllerName } controller not found.`);
    let action = controller.action(actionName);
    return function invokeActionForRoute(req, res, next) {
      // Inject the controller into the incoming request (it's context object).
      let context = req.context = req.context || {};
      context.controller = context.controller || controller;
      action(req, res, next);
    };
  }

});
<commit_msg>don't default the environment in app init<commit_after>import assert from 'assert';
import express from 'express';
import Promise from 'bluebird';
import { log } from '../utils';
import Error from './error';
import Engine from './engine';
import blackburn from 'blackburn';

/**
 * Application instances are little more than specialized Engines, designed to
 * kick off the loading, mounting, and launching stages of booting up.
 *
 * @title Application
 */
export default Engine.extend({

  /**
   * There are three stages involved when booting up a Denali application:
   *
   * 1. **load stage** - the application loads it's configuration and code from
   * the filesystem. It also discovers any child engines present, and triggers
   * the same for them.
   *
   * 2. **mount stage** - the application assembles a unified picture of itself
   * based on it's own code and config, as well as those of the child engines it
   * discovered during the load stage.
   *
   * 3. **launch** - now that the application has merged in the child engines,
   * it is ready to launch the HTTP server, connect to a port, and begin
   * accepting connections.
   *
   * When you instantiate a new Application instances, the first two stages run
   * immediately. However, the application does not launch until you run the
   * `start()` method. This is useful if you want to load an Application into
   * memory to inspect or use, but without booting up the actual HTTP server.
   *
   * @constructor
   */
  init() {
    this._super(...arguments);
    this.router = new express.Router();
  },

  /**
   * Launch this Application. Runs the initializers, then boots up the HTTP
   * server and binds to a port. Returns a Promise which resolves once the
   * server is live and accepting connections.
   *
   * @method start
   *
   * @return {Promise} resolves when the server has successfully bound to it's
   * port and is accepting connections
   */
  start() {
    this.load();

    return Promise.resolve(this.initializers)
      .each((initializer) => {
        return initializer(this);
      }).then(() => {
        return this.launchServer();
      });
  },

  /**
   * Boots up the express HTTP server, mounts the Application's router to it,
   * and binds the server to the Application's port.
   *
   * @method launchServer
   * @private
   *
   * @return {Promise} resolves when the server has successfully bound to it's
   * port and is accepting connections
   */
  launchServer() {
    console.log(this.config.PORT);
    let port = this.config.PORT || 3000;
    return new Promise((resolve) => {
      this.server = express();
      this.injectBlackburnPlusForaker(this.server);
      this.server.use(this.router);
      this.server.use(this.handleErrors.bind(this));
      this.server.listen(port, resolve);
    }).then(() => {
      this.log(`${ this.pkg.name }@${ this.pkg.version } server up on port ${port}`);
    });
  },

  /**
   * A simple proxy for the utils/log module, which silences any logs if we are
   * currently testing
   *
   * @method log
   */
  log(level) {
    if (this.env !== 'test' || level === 'error') {
      log.apply(this, arguments);
    }
  },

  /* eslint-disable no-unused-vars */
  handleErrors(err, req, res, next) {
    let error = err;
    if (!res._rendered) {
      if (!error.status) {
        error = new Error.InternalServerError(err.stack);
        this.log('error', error.stack);
      }
      error.code = error.code || error.name;
      return res.render(error);
    }
  },
  /* eslint-enable no-unused-vars */

  injectBlackburnPlusForaker(server) {
    // Setup vanilla blackburn installation
    server.use(blackburn({
      adapters: this.adapters,
      serializers: this.serializers
    }));
    // Now wrap blackburn's render method with one which picks the corresponding
    // serializer for that controller. This is the glue that joins the blackburn
    // serialization with foraker's controllers.
    server.use((req, res, next) => {
      // This controller property is injected into the context below, in the
      // handle method
      let render = res.render;
      res.render = (...args) => {
        // We need to identify options arg, which can be second or third.
        // It's only second when no status is supplied. It can be omitted as
        // well, so if it is, default it to an empty object.
        let options;
        if (typeof args[0] !== 'number' && !args[2]) {
          options = args[1] = args[1] || {};
        } else {
          options = args[2] = args[2] || {};
        }

        let controller = req.context && req.context.controller;
        options.serializer = options.serializer
                             || controller && controller.serializer
                             || controller && controller.name;
        options.adapter = options.adapter
                          || controller && controller.adapter
                          || controller && controller.name;
        return render.apply(res, args);
      };
      next();
    });
  },

  /**
   * Takes a controllerAction string and returns an express-compatible route
   * handler function that will route the request to that controller and action.
   * This is defined only on the Application since there is only one server
   * shared across all the Engines.
   *
   * @method handle
   * @private
   *
   * @param  {String} controllerAction  a string indicating the controller and
   * action that should be routed to. For example, `'books.create'` would route
   * to the `BooksController` `create` action.
   *
   * @return {Function} an express-compatible route handler function
   */
  handle(controllerAction) {
    let [ controllerName, actionName ] = controllerAction.split('.');
    let controller = this.controllers[controllerName];
    assert(controller, `Attempted to route to ${ controllerAction }, but ${ controllerName } controller not found.`);
    let action = controller.action(actionName);
    return function invokeActionForRoute(req, res, next) {
      // Inject the controller into the incoming request (it's context object).
      let context = req.context = req.context || {};
      context.controller = context.controller || controller;
      action(req, res, next);
    };
  }

});
<|endoftext|>


<commit_before>/** @babel */

/**
 * @TODO: Custom sorting?
 * @TODO: Complete quotes for strings
 */

import { Point, Range } from 'atom'

import { client } from '../connection'
import modules from './modules'

import { blocks } from '../misc'

const bracketScope = 'meta.bracket.julia'
const completions = client.import('completions')

class AutoCompleteProvider {
  selector = '.source.julia'
  disableForSelector = `.source.julia .comment`
  excludeLowerPriority = false
  inclusionPriority = 3
  suggestionPriority = atom.config.get('julia-client.juliaOptions.autoCompletionSuggestionPriority')
  filterSuggestions = false

  getSuggestions (data) {
    if (!client.isActive()) return []

    // Don't show suggestions if preceding char is a space (unless activate manually)
    if (!data.activatedManually) {
      const { editor, bufferPosition } = data
      const { row, column } = bufferPosition
      if (column === 0) return []
      const prevCharPosition = new Point(row, column - 1)
      const range = new Range(prevCharPosition, bufferPosition)
      const text = editor.getTextInBufferRange(range)
      // If the current position is in function call, show `REPLCompletions.MethodCompletion`s
      // whatever whitespaces precede
      const { scopes } = editor.scopeDescriptorForBufferPosition(bufferPosition)
      if (!scopes.includes(bracketScope) && text.match(/\s/)) return []
    }

    const suggestions = this.rawCompletions(data).then(({ completions, prefix }) => {
      try {
        return this.processCompletions(completions, prefix)
      } catch (err) {
        return []
      }
    })
    return Promise.race([suggestions, this.sleep()])
  }

  rawCompletions (data) {
    const { editor, bufferPosition: { row, column }, activatedManually } = data
    const startPoint = new Point(row, 0)
    const endPoint = new Point(row, column)
    const lineRange = new Range(startPoint, endPoint)

    let block = blocks.get(editor)
    block = block[block.length - 1]
    const text = block ? block.text : ''
    const blockStartLine = block ? block.line : ''

    return completions({
      path: editor.getPath(),
      mod: modules.current(),
      line: editor.getTextInBufferRange(lineRange),
      column: column + 1,
      lineNumber: row + 1,
      editorContent: text,
      startLine: blockStartLine,
      force: activatedManually || false
    })
  }

  processCompletions (completions, prefix) {
    return completions.map((completion) => {
      return this.toCompletion(completion, prefix)
    })
  }

  toCompletion (completion, prefix) {
    if (completion.constructor === String) {
      completion = {
        text: completion
      }
    }
    completion.replacementPrefix = completion._prefix ? completion._prefix : prefix
    const icon = this.makeIcon(completion.icon)
    if (icon) completion.iconHTML = icon
    // workaround https://github.com/atom/autocomplete-plus/issues/868
    if (!completion.description && completion.descriptionMoreURL)
      completion.description = ' '
    return completion
  }

  // should sync with atom-ink/lib/workspace/workspace.js
  makeIcon(icon) {
    // if not specified, just fallback to `completion.type`
    if (!icon) return ''
    if (icon.startsWith('icon-')) return `<span class="${icon}"}></span>`
    return icon.length === 1 ? icon : ''
  }

  sleepSecond = 1
  sleep () {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(null)
      }, this.sleepSecond * 1000)
    })
  }

  onDidInsertSuggestion ({ editor, suggestion: { type } }) {
    if (type !== 'function' || atom.config.get('julia-client.juliaOptions.noAutoParenthesis')) return
    editor.mutateSelectedText(selection => {
      if (!selection.isEmpty()) return
      const { row, column } = selection.getBufferRange().start
      const currentPoint = new Point(row, column)
      const nextPoint = new Point(row, column + 1)
      const range = new Range(currentPoint, nextPoint)
      const finishRange = new Range(nextPoint, nextPoint)
      if (editor.getTextInBufferRange(range) !== '(') {
        selection.insertText('()')
      }
      selection.setBufferRange(finishRange)
    })
  }
}

export default new AutoCompleteProvider()
<commit_msg>fix fallback<commit_after>/** @babel */

/**
 * @TODO: Custom sorting?
 * @TODO: Complete quotes for strings
 */

import { Point, Range } from 'atom'

import { client } from '../connection'
import modules from './modules'

import { blocks } from '../misc'

const bracketScope = 'meta.bracket.julia'
const completions = client.import('completions')

class AutoCompleteProvider {
  selector = '.source.julia'
  disableForSelector = `.source.julia .comment`
  excludeLowerPriority = false
  inclusionPriority = 3
  suggestionPriority = atom.config.get('julia-client.juliaOptions.autoCompletionSuggestionPriority')
  filterSuggestions = false

  getSuggestions (data) {
    if (!client.isActive()) return []

    // Don't show suggestions if preceding char is a space (unless activate manually)
    if (!data.activatedManually) {
      const { editor, bufferPosition } = data
      const { row, column } = bufferPosition
      if (column === 0) return []
      const prevCharPosition = new Point(row, column - 1)
      const range = new Range(prevCharPosition, bufferPosition)
      const text = editor.getTextInBufferRange(range)
      // If the current position is in function call, show `REPLCompletions.MethodCompletion`s
      // whatever whitespaces precede
      const { scopes } = editor.scopeDescriptorForBufferPosition(bufferPosition)
      if (!scopes.includes(bracketScope) && text.match(/\s/)) return []
    }

    const suggestions = this.rawCompletions(data).then(({ completions, prefix }) => {
      try {
        return this.processCompletions(completions, prefix)
      } catch (err) {
        return []
      }
    })
    return Promise.race([suggestions, this.sleep()])
  }

  rawCompletions (data) {
    const { editor, bufferPosition: { row, column }, activatedManually } = data
    const startPoint = new Point(row, 0)
    const endPoint = new Point(row, column)
    const lineRange = new Range(startPoint, endPoint)

    let block = blocks.get(editor)
    block = block[block.length - 1]
    const text = block ? block.text : ''
    const blockStartLine = block ? block.line : 1

    return completions({
      path: editor.getPath(),
      mod: modules.current(),
      line: editor.getTextInBufferRange(lineRange),
      column: column + 1,
      lineNumber: row + 1,
      editorContent: text,
      startLine: blockStartLine,
      force: activatedManually || false
    })
  }

  processCompletions (completions, prefix) {
    return completions.map((completion) => {
      return this.toCompletion(completion, prefix)
    })
  }

  toCompletion (completion, prefix) {
    if (completion.constructor === String) {
      completion = {
        text: completion
      }
    }
    completion.replacementPrefix = completion._prefix ? completion._prefix : prefix
    const icon = this.makeIcon(completion.icon)
    if (icon) completion.iconHTML = icon
    // workaround https://github.com/atom/autocomplete-plus/issues/868
    if (!completion.description && completion.descriptionMoreURL)
      completion.description = ' '
    return completion
  }

  // should sync with atom-ink/lib/workspace/workspace.js
  makeIcon(icon) {
    // if not specified, just fallback to `completion.type`
    if (!icon) return ''
    if (icon.startsWith('icon-')) return `<span class="${icon}"}></span>`
    return icon.length === 1 ? icon : ''
  }

  sleepSecond = 1
  sleep () {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(null)
      }, this.sleepSecond * 1000)
    })
  }

  onDidInsertSuggestion ({ editor, suggestion: { type } }) {
    if (type !== 'function' || atom.config.get('julia-client.juliaOptions.noAutoParenthesis')) return
    editor.mutateSelectedText(selection => {
      if (!selection.isEmpty()) return
      const { row, column } = selection.getBufferRange().start
      const currentPoint = new Point(row, column)
      const nextPoint = new Point(row, column + 1)
      const range = new Range(currentPoint, nextPoint)
      const finishRange = new Range(nextPoint, nextPoint)
      if (editor.getTextInBufferRange(range) !== '(') {
        selection.insertText('()')
      }
      selection.setBufferRange(finishRange)
    })
  }
}

export default new AutoCompleteProvider()
<|endoftext|>


<commit_before>import path from 'path';
import assert from 'assert';
import Serializer from '../serializer';
import isArray from 'lodash/lang/isArray';
import isEmpty from 'lodash/lang/isEmpty';
import set from 'lodash/object/set';
import mapValues from 'lodash/object/mapValues';
import uniq from 'lodash/array/uniq';

function setIfNotEmpty(obj, key, value) {
  if (!isEmpty(value)) {
    set(obj, key, value);
  }
}

/**
 * Renders the payload according to the JSONAPI 1.0 spec, including related
 * resources, included records, and support for meta and links.
 *
 * @title JSONAPISerializer
 */

export default Serializer.extend({

  /**
   * Take a payload (a model, an array of models, or an Error) and render it as
   * a JSONAPI compliant document
   *
   * @method render
   *
   * @param  {Object|Array|Error}   payload
   * @param  {Object}               options
   *
   * @return {Object}         the rendered JSONAPI document
   */
  render(payload, options) {
    let document = {};
    this.renderPrimary(payload, document, options);
    this.renderIncluded(payload, document, options);
    this.renderMeta(payload, document, options);
    this.renderLinks(payload, document, options);
    this.renderVersion(payload, document, options);
    this.dedupeIncluded(document);
    return document;
  },

  /**
   * Render the primary payload for a JSONAPI document (either a model or array
   * of models).
   *
   * @method renderPrimary
   * @see {@link http://jsonapi.org/format/#document-top-level|JSONAPI spec}
   *
   * @param  {Object|Array}   payload    errors or models to render
   * @param  {Object}         document   top level document to render into
   * @param  {Object}         options
   */
  renderPrimary(payload, document, options) {
    if (isArray(payload)) {
      if (payload[0] instanceof Error) {
        document.errors = payload.map((error) => {
          return this.renderError(error, options);
        });
      } else {
        document.data = payload.map((record) => {
          return this.renderRecord(record, document, options);
        });
      }
    } else {
      if (payload instanceof Error) {
        document.errors = [ this.renderError(payload, options) ];
      } else {
        document.data = this.renderRecord(payload, document, options);
      }
    }
  },

  /**
   * Render any included records into the top level of the document
   *
   * @method renderIncluded
   *
   * @param  {Object|Array} payload
   * @param  {Object}       document  top level JSONAPI document
   * @param  {Object}       options
   * @param  {Object}       options.included  array of records to sideload
   */
  renderIncluded(payload, document, options) {
    if (options.included) {
      assert(isArray(options.included), 'included records must be passed in as an array');
      document.included = options.included.map((includedRecord) => {
        return this.renderRecord(includedRecord, options);
      });
    }
  },

  /**
   * Render top level meta object for a document. Default uses meta supplied in
   * options call to res.render().
   *
   * @method renderMeta
   *
   * @param  {Object|Array}   payload
   * @param  {Object}         document  top level JSONAPI document
   * @param  {Object}         options
   * @param  {Object}         options.meta
   */
  renderMeta(payload, document, options) {
    if (options.meta) {
      document.meta = options.meta;
    }
  },

  /**
   * Render top level links object for a document. Defaults to the links
   * supplied in options to res.render().
   *
   * @method renderLinks
   *
   * @param  {Object|Array}   payload
   * @param  {Object}         document  top level JSONAPI document
   * @param  {Object}         options
   * @param  {Object}         options.links
   */
  renderLinks(payload, document, options) {
    if (options.links) {
      document.links = options.links;
    }
  },

  /**
   * Render the version of JSONAPI supported.
   *
   * @method renderVersion
   *
   * @param  {Object|Array}   payload
   * @param  {Object}         document  top level JSONAPI document
   * @param  {Object}         options
   */
  renderVersion(payload, document) {
    document.jsonapi = {
      version: '1.0'
    };
  },

  /**
   * Render the supplied record as a resource object.
   *
   * @method renderRecord
   * @see {@link http://jsonapi.org/format/#document-resource-objects|JSONAPI spec}
   *
   * @param  {Object}     options
   * @param  {Object}     record
   *
   * @return {Object}             a resource object representing the record
   */
  renderRecord(record, document, options) {
    let serializedRecord = {
      type: options.adapter.typeForRecord(record, options),
      id: options.adapter.idForRecord(record, options)
    };
    setIfNotEmpty(serializedRecord, 'attributes', this.attributesForRecord(record, options));
    setIfNotEmpty(serializedRecord, 'relationships', this.relationshipsForRecord(record, document, options));
    setIfNotEmpty(serializedRecord, 'links', this.linksForRecord(record, options));
    setIfNotEmpty(serializedRecord, 'meta', this.metaForRecord(record, options));
    return serializedRecord;
  },

  /**
   * Returns the JSONAPI attributes object representing this record's
   * relationships
   *
   * @method attributesForRecord
   * @see {@link http://jsonapi.org/format/#document-resource-object-attributes|JSONAPI spec}
   *
   * @param  {Object}            record
   * @param  {Object}            options
   *
   * @return {Object}                    the JSONAPI attributes object
   */
  attributesForRecord(record, options) {
    return this.serializeAttributes(record, options);
  },

  /**
   * Returns the JSONAPI relationships object representing this record's
   * relationships
   *
   * @method relationshipsForRecord
   * @see {@link http://jsonapi.org/format/#document-resource-object-relationships|JSONAPI spec
   *
   * @param  {Object}            record
   * @param  {Object}            options
   *
   * @return {Object}                    the JSONAPI relationships object
   */
  relationshipsForRecord(record, document, options) {
    let relationships = this.serializeRelationships(record, options);
    return mapValues(relationships, (descriptor, relationshipName) => {
      let relationship = {};
      setIfNotEmpty(relationship, 'links', this.linksForRelationship(relationshipName, descriptor, record, options));
      setIfNotEmpty(relationship, 'data', this.dataForRelationship(relationshipName, descriptor, record, document, options));
      setIfNotEmpty(relationship, 'meta', this.metaForRelationship(relationshipName, descriptor, record, options));
      return relationship;
    });
  },

  /**
   * Takes a relationship descriptor and the record it's for, and returns any
   * links for that relationship for that record. I.e. '/books/1/author'
   *
   * @method linksForRelationship
   *
   * @param  {String}             name       name of the relationship
   * @param  {Object}             descriptor descriptor for the relationship
   * @param  {Object}             record     parent record containing the
   *                                         relationships
   *
   * @return {Object}                        the links object for the supplied
   *                                         relationship
   */
  linksForRelationship(name, descriptor, record, options) {
    let recordURL = this.linksForRecord(record, options).self;
    return {
      self: path.join(recordURL, 'relationships/' + name),
      related: path.join(recordURL, name)
    };
  },

  /**
   * Takes a relationship descriptor and a record it's for, and returns the
   * data for that relationship. Also sideloads any included records in the
   * top level included namespace.
   *
   * @method dataForRelationship
   *
   * @param  {String}               name       name of the relationship
   * @param  {Object}               descriptor
   * @param  {Object[]|Object|String|Number|String[]|Number[]}    descriptor.data  the related id(s) or record(s)
   * @param  {String|Number}        descriptor.config  the user supplied relationship config
   * @param  {Object}               record     the parent record that holds this
   *                                           relationship
   * @param  {Object}               document   top level JSONAPI document
   * @param  {Object}               options
   *
   * @return {Object[]}                       array of resource identifiers
   */
  dataForRelationship(name, descriptor, record, document, options) {
    let { strategy, type } = descriptor.config;
    let data = descriptor.data;

    if (strategy === 'ids') {
      return data.map((id) => {
        return { type, id };
      });
    } else if (strategy === 'id') {
      return { type, id: descriptor.data };
    } else if (strategy === 'records') {
      return data.map((relatedRecord) => {
        this.includeRecord(name, relatedRecord, descriptor, document, options);
        return { type, id: options.adapter.idForRecord(relatedRecord) };
      });
    } else if (strategy === 'record') {
      this.includeRecord(name, data, descriptor, document, options);
      return { type, id: options.adapter.idForRecord(data) };
    }
  },

  /**
   * Returns any meta for a given relationship and record. No meta included by
   * default.
   *
   * @method metaForRelationship
   *
   * @param  {String}            name       name of the relationship
   * @param  {Object}            descriptor descriptor for the relationship
   * @param  {Object}            record     parent record containing the
   *                                        relationship
   * @param  {Object}            options
   *
   * @return {Object}
   */
  metaForRelationship() {},

  /**
   * Returns links for a particular record, i.e. self: "/books/1"
   *
   * @method linksForRecord
   *
   * @param  {Object}       record  [description]
   * @param  {Object}       options [description]
   *
   * @return {Object}               [description]
   */
  linksForRecord(record, options) {
    return {
      self: '/' + options.adapter.typeForRecord(record, options) + '/' + options.adapter.idForRecord(record, options)
    };
  },

  /**
   * Returns meta for a particular record.
   *
   * @method metaForRecord
   *
   * @param  {Object}      record
   * @param  {Object}      options
   *
   * @return {Object}
   */
  metaForRecord() {},

  /**
   * Sideloads a record into the top level "included" array
   *
   * @method includeRecord
   * @private
   *
   * @param  {Object}      record   the record to sideload
   * @param  {Object}      descriptor
   * @param  {Object}      descriptor.config
   * @param  {Object}      descriptor.config.type
   * @param  {Object}      descriptor.config.strategy
   * @param  {Object}      descriptor.data
   * @param  {Object}      document the top level JSONAPI document
   * @param  {Object}      options
   */
  includeRecord(name, record, descriptor, document, options) {
    if (!isArray(document.included)) {
      document.included = [];
    }
    options = options.relationships[name];
    let relatedSerializer = descriptor.config.serializer || this.serializerFor(record, options);
    document.included.push(relatedSerializer.renderRecord(record, document, options));
  },

  /**
   * Render the supplied error
   *
   * @method renderError
   *
   * @param  {Error}    error
   *
   * @return {Object}          the JSONAPI error object
   */
  renderError(error) {
    let renderedError = {
      id: error.id,
      status: error.status || 500,
      code: error.code || 'InternalServerError',
      title: error.title,
      detail: error.message
    };
    setIfNotEmpty(renderedError, 'source', this.sourceForError(error));
    setIfNotEmpty(renderedError, 'meta', this.metaForError(error));
    setIfNotEmpty(renderedError, 'links', this.linksForError(error));
    return renderedError;
  },

  /**
   * Given an error, return a JSON Pointer, a URL query param name, or other
   * info indicating the source of the error.
   *
   * @method sourceForError
   * @see {@link http://jsonapi.org/format/#error-objects|JSONAPI spec}
   *
   * @param  {Error}      error
   *
   * @return {Object}            an error source object, optionally including a
   *                             "pointer" JSON Pointer or "parameter" for the
   *                             query param that caused the error.
   */
  sourceForError(error) {
    return error.source;
  },

  /**
   * Return the meta for a given error object. You could use this for example,
   * to return debug information in development environments.
   *
   * @method metaForError
   *
   * @param  {Error}     error
   *
   * @return {Object}
   */
  metaForError() {},

  /**
   * Return a links object for an error. You could use this to link to a bug
   * tracker report of the error, for example.
   *
   * @method linksForError
   *
   * @param  {Error}      error
   *
   * @return {Object}
   */
  linksForError() {},

  /**
   * Remove duplicate entries from the sideloaded data.
   *
   * @method dedupeIncluded
   * @private
   *
   * @param  {Object}       document  the top level JSONAPI document
   */
  dedupeIncluded(document) {
    if (isArray(document.included)) {
      document.included = uniq(document.included, function(resource) {
        return resource.type + '/' + resource.id;
      });
    }
  }

});
<commit_msg>Use related options for related serializers<commit_after>import path from 'path';
import assert from 'assert';
import Serializer from '../serializer';
import isArray from 'lodash/lang/isArray';
import isEmpty from 'lodash/lang/isEmpty';
import set from 'lodash/object/set';
import mapValues from 'lodash/object/mapValues';
import uniq from 'lodash/array/uniq';

function setIfNotEmpty(obj, key, value) {
  if (!isEmpty(value)) {
    set(obj, key, value);
  }
}

/**
 * Renders the payload according to the JSONAPI 1.0 spec, including related
 * resources, included records, and support for meta and links.
 *
 * @title JSONAPISerializer
 */

export default Serializer.extend({

  /**
   * Take a payload (a model, an array of models, or an Error) and render it as
   * a JSONAPI compliant document
   *
   * @method render
   *
   * @param  {Object|Array|Error}   payload
   * @param  {Object}               options
   *
   * @return {Object}         the rendered JSONAPI document
   */
  render(payload, options) {
    let document = {};
    this.renderPrimary(payload, document, options);
    this.renderIncluded(payload, document, options);
    this.renderMeta(payload, document, options);
    this.renderLinks(payload, document, options);
    this.renderVersion(payload, document, options);
    this.dedupeIncluded(document);
    return document;
  },

  /**
   * Render the primary payload for a JSONAPI document (either a model or array
   * of models).
   *
   * @method renderPrimary
   * @see {@link http://jsonapi.org/format/#document-top-level|JSONAPI spec}
   *
   * @param  {Object|Array}   payload    errors or models to render
   * @param  {Object}         document   top level document to render into
   * @param  {Object}         options
   */
  renderPrimary(payload, document, options) {
    if (isArray(payload)) {
      if (payload[0] instanceof Error) {
        document.errors = payload.map((error) => {
          return this.renderError(error, options);
        });
      } else {
        document.data = payload.map((record) => {
          return this.renderRecord(record, document, options);
        });
      }
    } else {
      if (payload instanceof Error) {
        document.errors = [ this.renderError(payload, options) ];
      } else {
        document.data = this.renderRecord(payload, document, options);
      }
    }
  },

  /**
   * Render any included records into the top level of the document
   *
   * @method renderIncluded
   *
   * @param  {Object|Array} payload
   * @param  {Object}       document  top level JSONAPI document
   * @param  {Object}       options
   * @param  {Object}       options.included  array of records to sideload
   */
  renderIncluded(payload, document, options) {
    if (options.included) {
      assert(isArray(options.included), 'included records must be passed in as an array');
      document.included = options.included.map((includedRecord) => {
        return this.renderRecord(includedRecord, options);
      });
    }
  },

  /**
   * Render top level meta object for a document. Default uses meta supplied in
   * options call to res.render().
   *
   * @method renderMeta
   *
   * @param  {Object|Array}   payload
   * @param  {Object}         document  top level JSONAPI document
   * @param  {Object}         options
   * @param  {Object}         options.meta
   */
  renderMeta(payload, document, options) {
    if (options.meta) {
      document.meta = options.meta;
    }
  },

  /**
   * Render top level links object for a document. Defaults to the links
   * supplied in options to res.render().
   *
   * @method renderLinks
   *
   * @param  {Object|Array}   payload
   * @param  {Object}         document  top level JSONAPI document
   * @param  {Object}         options
   * @param  {Object}         options.links
   */
  renderLinks(payload, document, options) {
    if (options.links) {
      document.links = options.links;
    }
  },

  /**
   * Render the version of JSONAPI supported.
   *
   * @method renderVersion
   *
   * @param  {Object|Array}   payload
   * @param  {Object}         document  top level JSONAPI document
   * @param  {Object}         options
   */
  renderVersion(payload, document) {
    document.jsonapi = {
      version: '1.0'
    };
  },

  /**
   * Render the supplied record as a resource object.
   *
   * @method renderRecord
   * @see {@link http://jsonapi.org/format/#document-resource-objects|JSONAPI spec}
   *
   * @param  {Object}     options
   * @param  {Object}     record
   *
   * @return {Object}             a resource object representing the record
   */
  renderRecord(record, document, options) {
    let serializedRecord = {
      type: options.adapter.typeForRecord(record, options),
      id: options.adapter.idForRecord(record, options)
    };
    setIfNotEmpty(serializedRecord, 'attributes', this.attributesForRecord(record, options));
    setIfNotEmpty(serializedRecord, 'relationships', this.relationshipsForRecord(record, document, options));
    setIfNotEmpty(serializedRecord, 'links', this.linksForRecord(record, options));
    setIfNotEmpty(serializedRecord, 'meta', this.metaForRecord(record, options));
    return serializedRecord;
  },

  /**
   * Returns the JSONAPI attributes object representing this record's
   * relationships
   *
   * @method attributesForRecord
   * @see {@link http://jsonapi.org/format/#document-resource-object-attributes|JSONAPI spec}
   *
   * @param  {Object}            record
   * @param  {Object}            options
   *
   * @return {Object}                    the JSONAPI attributes object
   */
  attributesForRecord(record, options) {
    return this.serializeAttributes(record, options);
  },

  /**
   * Returns the JSONAPI relationships object representing this record's
   * relationships
   *
   * @method relationshipsForRecord
   * @see {@link http://jsonapi.org/format/#document-resource-object-relationships|JSONAPI spec
   *
   * @param  {Object}            record
   * @param  {Object}            options
   *
   * @return {Object}                    the JSONAPI relationships object
   */
  relationshipsForRecord(record, document, options) {
    let relationships = this.serializeRelationships(record, options);
    return mapValues(relationships, (descriptor, relationshipName) => {
      let relationship = {};
      setIfNotEmpty(relationship, 'links', this.linksForRelationship(relationshipName, descriptor, record, options));
      setIfNotEmpty(relationship, 'data', this.dataForRelationship(relationshipName, descriptor, record, document, options));
      setIfNotEmpty(relationship, 'meta', this.metaForRelationship(relationshipName, descriptor, record, options));
      return relationship;
    });
  },

  /**
   * Takes a relationship descriptor and the record it's for, and returns any
   * links for that relationship for that record. I.e. '/books/1/author'
   *
   * @method linksForRelationship
   *
   * @param  {String}             name       name of the relationship
   * @param  {Object}             descriptor descriptor for the relationship
   * @param  {Object}             record     parent record containing the
   *                                         relationships
   *
   * @return {Object}                        the links object for the supplied
   *                                         relationship
   */
  linksForRelationship(name, descriptor, record, options) {
    let recordURL = this.linksForRecord(record, options).self;
    return {
      self: path.join(recordURL, 'relationships/' + name),
      related: path.join(recordURL, name)
    };
  },

  /**
   * Takes a relationship descriptor and a record it's for, and returns the
   * data for that relationship. Also sideloads any included records in the
   * top level included namespace.
   *
   * @method dataForRelationship
   *
   * @param  {String}               name       name of the relationship
   * @param  {Object}               descriptor
   * @param  {Object[]|Object|String|Number|String[]|Number[]}    descriptor.data  the related id(s) or record(s)
   * @param  {String|Number}        descriptor.config  the user supplied relationship config
   * @param  {Object}               record     the parent record that holds this
   *                                           relationship
   * @param  {Object}               document   top level JSONAPI document
   * @param  {Object}               options
   *
   * @return {Object[]}                       array of resource identifiers
   */
  dataForRelationship(name, descriptor, record, document, options) {
    let { strategy, type } = descriptor.config;
    let data = descriptor.data;

    if (strategy === 'ids') {
      return data.map((id) => {
        return { type, id };
      });
    } else if (strategy === 'id') {
      return { type, id: descriptor.data };
    } else if (strategy === 'records') {
      return data.map((relatedRecord) => {
        this.includeRecord(name, relatedRecord, descriptor, document, options);
        return { type, id: options.adapter.idForRecord(relatedRecord) };
      });
    } else if (strategy === 'record') {
      this.includeRecord(name, data, descriptor, document, options);
      return { type, id: options.adapter.idForRecord(data) };
    }
  },

  /**
   * Returns any meta for a given relationship and record. No meta included by
   * default.
   *
   * @method metaForRelationship
   *
   * @param  {String}            name       name of the relationship
   * @param  {Object}            descriptor descriptor for the relationship
   * @param  {Object}            record     parent record containing the
   *                                        relationship
   * @param  {Object}            options
   *
   * @return {Object}
   */
  metaForRelationship() {},

  /**
   * Returns links for a particular record, i.e. self: "/books/1"
   *
   * @method linksForRecord
   *
   * @param  {Object}       record  [description]
   * @param  {Object}       options [description]
   *
   * @return {Object}               [description]
   */
  linksForRecord(record, options) {
    return {
      self: '/' + options.adapter.typeForRecord(record, options) + '/' + options.adapter.idForRecord(record, options)
    };
  },

  /**
   * Returns meta for a particular record.
   *
   * @method metaForRecord
   *
   * @param  {Object}      record
   * @param  {Object}      options
   *
   * @return {Object}
   */
  metaForRecord() {},

  /**
   * Sideloads a record into the top level "included" array
   *
   * @method includeRecord
   * @private
   *
   * @param  {Object}      record   the record to sideload
   * @param  {Object}      descriptor
   * @param  {Object}      descriptor.config
   * @param  {Object}      descriptor.config.type
   * @param  {Object}      descriptor.config.strategy
   * @param  {Object}      descriptor.data
   * @param  {Object}      document the top level JSONAPI document
   * @param  {Object}      options
   */
  includeRecord(name, record, descriptor, document, options) {
    if (!isArray(document.included)) {
      document.included = [];
    }
    let relatedOptions = options.relationships && options.relationships[name] || options;
    let relatedSerializer = descriptor.config.serializer || this.serializerFor(record, relatedOptions);
    assert(relatedSerializer, `No serializer found for the ${ name } relationship. You must either supply a serializer as part of the relationship config object, or supply one for the ${ descriptor.config.type } type in the serializers hash.`);
    document.included.push(relatedSerializer.renderRecord(record, document, relatedOptions));
  },

  /**
   * Render the supplied error
   *
   * @method renderError
   *
   * @param  {Error}    error
   *
   * @return {Object}          the JSONAPI error object
   */
  renderError(error) {
    let renderedError = {
      id: error.id,
      status: error.status || 500,
      code: error.code || 'InternalServerError',
      title: error.title,
      detail: error.message
    };
    setIfNotEmpty(renderedError, 'source', this.sourceForError(error));
    setIfNotEmpty(renderedError, 'meta', this.metaForError(error));
    setIfNotEmpty(renderedError, 'links', this.linksForError(error));
    return renderedError;
  },

  /**
   * Given an error, return a JSON Pointer, a URL query param name, or other
   * info indicating the source of the error.
   *
   * @method sourceForError
   * @see {@link http://jsonapi.org/format/#error-objects|JSONAPI spec}
   *
   * @param  {Error}      error
   *
   * @return {Object}            an error source object, optionally including a
   *                             "pointer" JSON Pointer or "parameter" for the
   *                             query param that caused the error.
   */
  sourceForError(error) {
    return error.source;
  },

  /**
   * Return the meta for a given error object. You could use this for example,
   * to return debug information in development environments.
   *
   * @method metaForError
   *
   * @param  {Error}     error
   *
   * @return {Object}
   */
  metaForError() {},

  /**
   * Return a links object for an error. You could use this to link to a bug
   * tracker report of the error, for example.
   *
   * @method linksForError
   *
   * @param  {Error}      error
   *
   * @return {Object}
   */
  linksForError() {},

  /**
   * Remove duplicate entries from the sideloaded data.
   *
   * @method dedupeIncluded
   * @private
   *
   * @param  {Object}       document  the top level JSONAPI document
   */
  dedupeIncluded(document) {
    if (isArray(document.included)) {
      document.included = uniq(document.included, function(resource) {
        return resource.type + '/' + resource.id;
      });
    }
  }

});
<|endoftext|>


<commit_before>'use strict';

// Runs `bower install` in cwd
var path = require('path');
var existsSync = require('exists-sync');
var Promise = require('../ext/promise');
var Task = require('../models/task');
var formatPackageList = require('../utilities/format-package-list');

module.exports = Task.extend({
  // Options: Boolean verbose
  run: function(options) {
    var chalk = require('chalk');
    var bower = this.bower || require('bower');
    var bowerJson = path.join(this.project.root, '/bower.json');
    var bowerConfig = this.bowerConfig || require('bower-config');
    var ui = this.ui;
    var packages = options.packages || [];
    var installOptions = options.installOptions || { save: true };

    if (!existsSync(bowerJson)) {
      ui.writeWarnLine('Skipping bower install: bower.json not found');
      return;
    }

    var startMessage = this.formatStartMessage(packages);
    var completeMessage = this.formatCompleteMessage(packages);

    ui.startProgress(chalk.green(startMessage));

    var config = bowerConfig.read();
    config.interactive = true;

    return new Promise(function(resolve, reject) {
      bower.commands.install(packages, installOptions, config) // Packages, options, config
        .on('log', logBowerMessage)
        .on('prompt', ui.prompt.bind(ui))
        .on('error', reject)
        .on('end', resolve);
    })
      .finally(function() { ui.stopProgress(); })
      .then(function() {
        ui.writeLine(chalk.green(completeMessage));
      });

    function logBowerMessage(message) {
      if (message.level === 'conflict') {
        // e.g.
        //   conflict Unable to find suitable version for ember-data
        //     1) ember-data 1.0.0-beta.6
        //     2) ember-data ~1.0.0-beta.7
        ui.writeLine('  ' + chalk.red('conflict') + ' ' + message.message);
        message.data.picks.forEach(function(pick, index) {
          ui.writeLine('    ' + chalk.green((index + 1) + ')') + ' ' +
                       message.data.name + ' ' + pick.endpoint.target);
        });
      } else if (message.level === 'info' && options.verbose) {
        // e.g.
        //   cached git://example.com/some-package.git#1.0.0
        ui.writeLine('  ' + chalk.green(message.id) + ' ' + message.message);
      }
    }
  },

  formatStartMessage: function(packages) {
    return 'Bower: Installing ' + formatPackageList(packages) + ' ...';
  },

  formatCompleteMessage: function(packages) {
    return 'Bower: Installed ' + formatPackageList(packages);
  },
});
<commit_msg>tasks/bower-install: Skip require() calls if "bower.json" is missing<commit_after>'use strict';

// Runs `bower install` in cwd
var path = require('path');
var existsSync = require('exists-sync');
var Promise = require('../ext/promise');
var Task = require('../models/task');
var formatPackageList = require('../utilities/format-package-list');

module.exports = Task.extend({
  // Options: Boolean verbose
  run: function(options) {
    var bowerJson = path.join(this.project.root, '/bower.json');
    var ui = this.ui;

    if (!existsSync(bowerJson)) {
      ui.writeWarnLine('Skipping bower install: bower.json not found');
      return;
    }

    var chalk = require('chalk');
    var bower = this.bower || require('bower');
    var bowerConfig = this.bowerConfig || require('bower-config');
    var packages = options.packages || [];
    var installOptions = options.installOptions || { save: true };

    var startMessage = this.formatStartMessage(packages);
    var completeMessage = this.formatCompleteMessage(packages);

    ui.startProgress(chalk.green(startMessage));

    var config = bowerConfig.read();
    config.interactive = true;

    return new Promise(function(resolve, reject) {
      bower.commands.install(packages, installOptions, config) // Packages, options, config
        .on('log', logBowerMessage)
        .on('prompt', ui.prompt.bind(ui))
        .on('error', reject)
        .on('end', resolve);
    })
      .finally(function() { ui.stopProgress(); })
      .then(function() {
        ui.writeLine(chalk.green(completeMessage));
      });

    function logBowerMessage(message) {
      if (message.level === 'conflict') {
        // e.g.
        //   conflict Unable to find suitable version for ember-data
        //     1) ember-data 1.0.0-beta.6
        //     2) ember-data ~1.0.0-beta.7
        ui.writeLine('  ' + chalk.red('conflict') + ' ' + message.message);
        message.data.picks.forEach(function(pick, index) {
          ui.writeLine('    ' + chalk.green((index + 1) + ')') + ' ' +
                       message.data.name + ' ' + pick.endpoint.target);
        });
      } else if (message.level === 'info' && options.verbose) {
        // e.g.
        //   cached git://example.com/some-package.git#1.0.0
        ui.writeLine('  ' + chalk.green(message.id) + ' ' + message.message);
      }
    }
  },

  formatStartMessage: function(packages) {
    return 'Bower: Installing ' + formatPackageList(packages) + ' ...';
  },

  formatCompleteMessage: function(packages) {
    return 'Bower: Installed ' + formatPackageList(packages);
  },
});
<|endoftext|>


<commit_before>"use strict";
var fs = require('fs'),
    async = require('async');

module.exports = function(ctx, done){
    if(!ctx.includes || ctx.includes.length === 0){
        return done();
    }
    async.parallel(Object.keys(ctx.includes).map(function(src){
        return function(cb){
            var rs = fs.createReadStream(ctx.path(src)),
                ws = fs.createWriteStream(ctx.dest('includes', src));

            rs.pipe(ws);
            rs.once('end', function(){
                cb();
            });
        };
    }), done);
};<commit_msg>Can simplfy because of the awesome fs-extra<commit_after>"use strict";
var fs = require('fs-extra'),
    async = require('async');

module.exports = function(ctx, done){
    if(!ctx.includes || ctx.includes.length === 0){
        return done();
    }

    async.parallel(Object.keys(ctx.includes).map(function(src){
        return function(cb){
            fs.copy(src, ctx.dest('includes', src), cb);
        };
    }), done);
};<|endoftext|>