<commit_before>/* See license.txt for terms of usage */

"use strict";

module.metadata = {
  "stability": "experimental"
};

// Add-on SDK
const { Cu, Ci } = require("chrome");
const { EventTarget } = require("sdk/event/target");
const { extend } = require("sdk/core/heritage");
const { defer, resolve } = require("sdk/core/promise");

// Firebug SDK
const { Trace, TraceError } = require("./core/trace.js").get(module.id);
const { Context } = require("./context.js");
const { Dispatcher } = require("./dispatcher.js");

// DevTools
const { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});

/**
 * TODO docs
 */
var ToolboxChrome = extend(EventTarget.prototype,
/** @lends ToolboxChrome */
{
  // Initialization

  initialize: function() {
    this.initialized = true;
    this.contexts = new Map();
    this.registeredOverlays = new Map();

    // Bind DevTools event handlers.
    this.onToolboxCreated = this.onToolboxCreated.bind(this);
    this.onToolboxReady = this.onToolboxReady.bind(this);
    this.onToolboxDestroy = this.onToolboxDestroy.bind(this);
    this.onToolboxClosed = this.onToolboxClosed.bind(this);
    this.updateOption = this.updateOption.bind(this);

    // Hook developer tools events.
    gDevTools.on("toolbox-created", this.onToolboxCreated);
    gDevTools.on("toolbox-ready", this.onToolboxReady);
    gDevTools.on("toolbox-destroy", this.onToolboxDestroy);
    gDevTools.on("toolbox-destroyed", this.onToolboxClosed);
    gDevTools.on("pref-changed", this.updateOption);

    return Dispatcher.emit("initialize");
  },

  shutdown: function() {
    gDevTools.off("toolbox-created", this.onToolboxCreated);
    gDevTools.off("toolbox-ready", this.onToolboxReady);
    gDevTools.off("toolbox-destroy", this.onToolboxDestroy);
    gDevTools.off("toolbox-destroyed", this.onToolboxClosed);
    gDevTools.off("pref-changed", this.updateOption);

    return Dispatcher.emit("shutdown");
  },

  // DevTools Event Handlers

  onToolboxCreated: function(eventId, toolbox) {
    Trace.sysout("ToolboxChrome.onToolboxCreated;", toolbox);

    // Make sure to create the toolbox context as soon as possible.
    // 'toolbox-created' has been introduced in Fx 39, so use
    // 'toolbox-ready' for previous versions.
    let context = this.getContext(toolbox);

    Dispatcher.emit("onToolboxCreated", arguments);
  },

  onToolboxReady: function(event, toolbox) {
    Trace.sysout("ToolboxChrome.onToolboxReady; ", toolbox);

    // Make sure to create the toolbox context as soon as possible.
    let context = this.getContext(toolbox);

    Dispatcher.emit("onToolboxReady", arguments);
  },

  onToolboxDestroy: function(eventId, target) {
    Trace.sysout("ToolboxChrome.onToolboxDestroy;", target);

    let context = this.contexts.get(target);
    if (!context) {
      Trace.sysout("ToolboxChrome.onToolboxDestroy; ERROR unknown target!",
        target);
      return;
    }

    Dispatcher.emit("onToolboxDestroy", arguments);

    this.contexts.delete(target);

    context.destroy();
  },

  onToolboxClosed: function(eventId, target) {
    Trace.sysout("ToolboxChrome.onToolboxClosed;", target);

    let context = this.contexts.get(target);
    if (!context) {
      Trace.sysout("ToolboxChrome.onToolboxClosed; ERROR unknown target!",
        target);
      return;
    }
  },

  // Options

  updateOption: function(eventType, data) {
    Trace.sysout("firebug.updateOption; ", data);

    emit(this, "updateOption", data.pref, data.newValue, data.oldValue);
  },

  // Overlays

  registerPanelOverlay: function(overlay) {
    let overlayId = overlay.prototype.overlayId;
    let panelId = overlay.prototype.panelId;

    if (!panelId) {
      panelId = overlayId;
    }

    if (!panelId || !overlayId) {
      TraceError.sysout("ToolboxChrome.registerOverlay; ERROR " +
        "Missing overlay ID!");
      return;
    }

    Trace.sysout("ToolboxChrome.registerOverlay; " + overlayId, overlay);

    // Listen for panel initialization event.
    let onApplyOverlay = (eventId, toolbox, panelFrame) => {
      Trace.sysout("ToolboxChrome.onApplyOverlay; " + overlayId, panelFrame);

      let context = this.getContext(toolbox);

      // Bail out if the overlay already exists for this context/toolbox.
      if (context.getOverlay(overlayId)) {
        return;
      }

      try {
        // Create instance of an overlay
        let instance = new overlay({
          panelFrame: panelFrame,
          toolbox: toolbox,
          chrome: this,
          id: overlay.id,
          context: context
        });

        context.overlays.set(overlayId, instance);

        // Register for 'build' event (panel instance created).
        toolbox.once(panelId + "-build", (eventId, panel) => {
          Trace.sysout("ToolboxChrome.applyOverlay; " + eventId, panel);
          instance.onBuild({toolbox: toolbox, panel: panel});
        });

        // Register for 'ready' event (panel frame loaded).
        toolbox.once(panelId + "-ready", (eventId, panel) => {
          Trace.sysout("ToolboxChrome.applyOverlay; " + eventId, panel);
          instance.onReady({toolbox: toolbox, panel: panel});
        });
      }
      catch (err) {
        TraceError.sysout("ToolboxChrome.initialize; Overlay for: " +
          overlay.id + " EXCEPTION " + err, err);
      }
    };

    // Use 'on' (not 'once') listener since the '*-init' event is sent
    // every time the toolbox is closed and opened again. The listener
    // will be removed in destroyOverlay method when the extension is
    // destroyed.
    gDevTools.on(panelId + "-init", onApplyOverlay);

    this.registeredOverlays.set(overlayId, {
      ctor: overlay,
      creator: onApplyOverlay
    })
  },

  unregisterPanelOverlay: function(overlay) {
    let overlayId = overlay.prototype.overlayId;

    Trace.sysout("ToolboxChrome.unregisterOverlay; " + overlayId, overlay);

    let entry = this.registeredOverlays.get(overlayId);
    gDevTools.off(overlayId + "-init", entry.creator);

    this.registeredOverlays.delete(overlayId);
  },

  registerToolboxOverlay: function(overlay) {
    let overlayId = overlay.prototype.overlayId;

    Trace.sysout("ToolboxChrome.registerToolboxOverlay; " +
      overlayId, overlay);

    let onApplyOverlay = (eventId, toolbox) => {
      Trace.sysout("ToolboxChrome.onApplyToolboxOverlay; " +
        overlayId, toolbox);

      let context = this.getContext(toolbox);

      // Bail out if the overlay already exists for this context/toolbox.
      if (context.getOverlay(overlayId)) {
        return;
      }

      try {
        // Create instance of an overlay
        let instance = new overlay({
          toolbox: toolbox,
          chrome: this,
          context: context,
        });

        context.overlays.set(overlayId, instance);

        // Execute 'onReady' on the overlay instance when the toolbox
        // is ready. There are two options, (a) the toolbox is already
        // ready at this point (extension installed dynamically in the
        // middle of Firefox session), (b) the toolbox is not ready yet
        // and it's necessary to wait for the event.
        if (toolbox.isReady) {
          instance.onReady({toolbox: toolbox});
        } else {
          gDevTools.once("toolbox-ready", () => {
            instance.onReady({toolbox: toolbox});
          });
        }
      }
      catch (err) {
        TraceError.sysout("ToolboxChrome.initialize; Overlay for: " +
          overlay.id + " EXCEPTION " + err, err);
      }
    };

    // Use 'on' (not 'once') listener since the create event is sent
    // every time a toolbox is created (can be also on another browser tab).
    gDevTools.on("toolbox-created", onApplyOverlay);

    this.registeredOverlays.set(overlayId, {
      ctor: overlay,
      creator: onApplyOverlay
    })

    // Apply the overlay on all existing toolboxes. Registered toolbox
    // overlays must be always instantiated before registered panel
    // overlays.
    for (let [target, toolbox] of gDevTools._toolboxes) {
      target.makeRemote().then(() => {
        onApplyOverlay(null, toolbox);
      });
    }
  },

  unregisterToolboxOverlay: function(overlay) {
    let overlayId = overlay.prototype.overlayId;

    Trace.sysout("ToolboxChrome.unregisterToolboxOverlay; " +
      overlayId, overlay);

    let entry = this.registeredOverlays.get(overlayId);
    gDevTools.off("toolbox-ready", entry.creator);

    this.registeredOverlays.delete(overlayId);
  },

  getOverlay: function(toolbox, overlayId) {
    let context = this.getContext(toolbox);
    return context.getOverlay(overlayId);
  },

  // Context

  getContext: function(toolbox) {
    let target = toolbox.target;
    let context = this.contexts.get(target);
    if (!context) {
      context = new Context(toolbox);
      this.contexts.set(target, context);
    }
    return context;
  },
});

function getToolboxWhenReady(toolId) {
  let deferred = defer();
  showToolbox(toolId).then(toolbox => {
    deferred.resolve(toolbox);
  });
  return deferred.promise;
}

// Exports from this module
exports.ToolboxChrome = ToolboxChrome;
<commit_msg>Emite theme-changed event<commit_after>/* See license.txt for terms of usage */

"use strict";

module.metadata = {
  "stability": "experimental"
};

// Add-on SDK
const { Cu, Ci } = require("chrome");
const { EventTarget } = require("sdk/event/target");
const { extend } = require("sdk/core/heritage");
const { defer, resolve } = require("sdk/core/promise");
const { emit } = require("sdk/event/core");

// Firebug SDK
const { Trace, TraceError } = require("./core/trace.js").get(module.id);
const { Context } = require("./context.js");
const { Dispatcher } = require("./dispatcher.js");

// DevTools
const { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});

/**
 * TODO docs
 */
var ToolboxChrome = extend(EventTarget.prototype,
/** @lends ToolboxChrome */
{
  // Initialization

  initialize: function() {
    this.initialized = true;
    this.contexts = new Map();
    this.registeredOverlays = new Map();

    // Bind DevTools event handlers.
    this.onToolboxCreated = this.onToolboxCreated.bind(this);
    this.onToolboxReady = this.onToolboxReady.bind(this);
    this.onToolboxDestroy = this.onToolboxDestroy.bind(this);
    this.onToolboxClosed = this.onToolboxClosed.bind(this);
    this.updateOption = this.updateOption.bind(this);

    // Hook developer tools events.
    gDevTools.on("toolbox-created", this.onToolboxCreated);
    gDevTools.on("toolbox-ready", this.onToolboxReady);
    gDevTools.on("toolbox-destroy", this.onToolboxDestroy);
    gDevTools.on("toolbox-destroyed", this.onToolboxClosed);
    gDevTools.on("pref-changed", this.updateOption);

    return Dispatcher.emit("initialize");
  },

  shutdown: function() {
    gDevTools.off("toolbox-created", this.onToolboxCreated);
    gDevTools.off("toolbox-ready", this.onToolboxReady);
    gDevTools.off("toolbox-destroy", this.onToolboxDestroy);
    gDevTools.off("toolbox-destroyed", this.onToolboxClosed);
    gDevTools.off("pref-changed", this.updateOption);

    return Dispatcher.emit("shutdown");
  },

  // DevTools Event Handlers

  onToolboxCreated: function(eventId, toolbox) {
    Trace.sysout("ToolboxChrome.onToolboxCreated;", toolbox);

    // Make sure to create the toolbox context as soon as possible.
    // 'toolbox-created' has been introduced in Fx 39, so use
    // 'toolbox-ready' for previous versions.
    let context = this.getContext(toolbox);

    Dispatcher.emit("onToolboxCreated", arguments);
  },

  onToolboxReady: function(event, toolbox) {
    Trace.sysout("ToolboxChrome.onToolboxReady; ", toolbox);

    // Make sure to create the toolbox context as soon as possible.
    let context = this.getContext(toolbox);

    Dispatcher.emit("onToolboxReady", arguments);
  },

  onToolboxDestroy: function(eventId, target) {
    Trace.sysout("ToolboxChrome.onToolboxDestroy;", target);

    let context = this.contexts.get(target);
    if (!context) {
      Trace.sysout("ToolboxChrome.onToolboxDestroy; ERROR unknown target!",
        target);
      return;
    }

    Dispatcher.emit("onToolboxDestroy", arguments);

    this.contexts.delete(target);

    context.destroy();
  },

  onToolboxClosed: function(eventId, target) {
    Trace.sysout("ToolboxChrome.onToolboxClosed;", target);

    let context = this.contexts.get(target);
    if (!context) {
      Trace.sysout("ToolboxChrome.onToolboxClosed; ERROR unknown target!",
        target);
      return;
    }
  },

  // Options

  updateOption: function(eventType, data) {
    Trace.sysout("firebug.updateOption; ", data);

    emit(this, "theme-changed", data.newValue, data.oldValue);

    // Obsolete
    emit(this, "updateOption", data.pref, data.newValue, data.oldValue);
  },

  // Overlays

  registerPanelOverlay: function(overlay) {
    let overlayId = overlay.prototype.overlayId;
    let panelId = overlay.prototype.panelId;

    if (!panelId) {
      panelId = overlayId;
    }

    if (!panelId || !overlayId) {
      TraceError.sysout("ToolboxChrome.registerOverlay; ERROR " +
        "Missing overlay ID!");
      return;
    }

    Trace.sysout("ToolboxChrome.registerOverlay; " + overlayId, overlay);

    // Listen for panel initialization event.
    let onApplyOverlay = (eventId, toolbox, panelFrame) => {
      Trace.sysout("ToolboxChrome.onApplyOverlay; " + overlayId, panelFrame);

      let context = this.getContext(toolbox);

      // Bail out if the overlay already exists for this context/toolbox.
      if (context.getOverlay(overlayId)) {
        return;
      }

      try {
        // Create instance of an overlay
        let instance = new overlay({
          panelFrame: panelFrame,
          toolbox: toolbox,
          chrome: this,
          id: overlay.id,
          context: context
        });

        context.overlays.set(overlayId, instance);

        // Register for 'build' event (panel instance created).
        toolbox.once(panelId + "-build", (eventId, panel) => {
          Trace.sysout("ToolboxChrome.applyOverlay; " + eventId, panel);
          instance.onBuild({toolbox: toolbox, panel: panel});
        });

        // Register for 'ready' event (panel frame loaded).
        toolbox.once(panelId + "-ready", (eventId, panel) => {
          Trace.sysout("ToolboxChrome.applyOverlay; " + eventId, panel);
          instance.onReady({toolbox: toolbox, panel: panel});
        });
      }
      catch (err) {
        TraceError.sysout("ToolboxChrome.initialize; Overlay for: " +
          overlay.id + " EXCEPTION " + err, err);
      }
    };

    // Use 'on' (not 'once') listener since the '*-init' event is sent
    // every time the toolbox is closed and opened again. The listener
    // will be removed in destroyOverlay method when the extension is
    // destroyed.
    gDevTools.on(panelId + "-init", onApplyOverlay);

    this.registeredOverlays.set(overlayId, {
      ctor: overlay,
      creator: onApplyOverlay
    })
  },

  unregisterPanelOverlay: function(overlay) {
    let overlayId = overlay.prototype.overlayId;

    Trace.sysout("ToolboxChrome.unregisterOverlay; " + overlayId, overlay);

    let entry = this.registeredOverlays.get(overlayId);
    gDevTools.off(overlayId + "-init", entry.creator);

    this.registeredOverlays.delete(overlayId);
  },

  registerToolboxOverlay: function(overlay) {
    let overlayId = overlay.prototype.overlayId;

    Trace.sysout("ToolboxChrome.registerToolboxOverlay; " +
      overlayId, overlay);

    let onApplyOverlay = (eventId, toolbox) => {
      Trace.sysout("ToolboxChrome.onApplyToolboxOverlay; " +
        overlayId, toolbox);

      let context = this.getContext(toolbox);

      // Bail out if the overlay already exists for this context/toolbox.
      if (context.getOverlay(overlayId)) {
        return;
      }

      try {
        // Create instance of an overlay
        let instance = new overlay({
          toolbox: toolbox,
          chrome: this,
          context: context,
        });

        context.overlays.set(overlayId, instance);

        // Execute 'onReady' on the overlay instance when the toolbox
        // is ready. There are two options, (a) the toolbox is already
        // ready at this point (extension installed dynamically in the
        // middle of Firefox session), (b) the toolbox is not ready yet
        // and it's necessary to wait for the event.
        if (toolbox.isReady) {
          instance.onReady({toolbox: toolbox});
        } else {
          gDevTools.once("toolbox-ready", () => {
            instance.onReady({toolbox: toolbox});
          });
        }
      }
      catch (err) {
        TraceError.sysout("ToolboxChrome.initialize; Overlay for: " +
          overlay.id + " EXCEPTION " + err, err);
      }
    };

    // Use 'on' (not 'once') listener since the create event is sent
    // every time a toolbox is created (can be also on another browser tab).
    gDevTools.on("toolbox-created", onApplyOverlay);

    this.registeredOverlays.set(overlayId, {
      ctor: overlay,
      creator: onApplyOverlay
    })

    // Apply the overlay on all existing toolboxes. Registered toolbox
    // overlays must be always instantiated before registered panel
    // overlays.
    for (let [target, toolbox] of gDevTools._toolboxes) {
      target.makeRemote().then(() => {
        onApplyOverlay(null, toolbox);
      });
    }
  },

  unregisterToolboxOverlay: function(overlay) {
    let overlayId = overlay.prototype.overlayId;

    Trace.sysout("ToolboxChrome.unregisterToolboxOverlay; " +
      overlayId, overlay);

    let entry = this.registeredOverlays.get(overlayId);
    gDevTools.off("toolbox-ready", entry.creator);

    this.registeredOverlays.delete(overlayId);
  },

  getOverlay: function(toolbox, overlayId) {
    let context = this.getContext(toolbox);
    return context.getOverlay(overlayId);
  },

  // Context

  getContext: function(toolbox) {
    let target = toolbox.target;
    let context = this.contexts.get(target);
    if (!context) {
      context = new Context(toolbox);
      this.contexts.set(target, context);
    }
    return context;
  },
});

function getToolboxWhenReady(toolId) {
  let deferred = defer();
  showToolbox(toolId).then(toolbox => {
    deferred.resolve(toolbox);
  });
  return deferred.promise;
}

// Exports from this module
exports.ToolboxChrome = ToolboxChrome;
<|endoftext|>


<commit_before>'use strict';

const Promise = require('bluebird');
const loadSpecTests = require('../../spec').loadSpecTests;
const ConnectionPool = require('../../../lib/cmap/connection_pool').ConnectionPool;
const EventEmitter = require('events').EventEmitter;
const mock = require('mongodb-mock-server');
const BSON = require('bson');
const cmapEvents = require('../../../lib/cmap/events');

const chai = require('chai');
chai.use(require('../../functional/spec-runner/matcher').default);
const expect = chai.expect;

const ALL_POOL_EVENTS = new Set([
  'connectionPoolCreated',
  'connectionPoolClosed',
  'connectionCreated',
  'connectionReady',
  'connectionClosed',
  'connectionCheckOutStarted',
  'connectionCheckOutFailed',
  'connectionCheckedOut',
  'connectionCheckedIn',
  'connectionPoolCleared'
]);

const PROMISIFIED_POOL_FUNCTIONS = {
  checkOut: Promise.promisify(ConnectionPool.prototype.checkOut),
  close: Promise.promisify(ConnectionPool.prototype.close)
};

function closePool(pool) {
  return new Promise(resolve => {
    ALL_POOL_EVENTS.forEach(ev => pool.removeAllListeners(ev));
    pool.close(resolve);
  });
}

describe('Connection Pool', function() {
  let server;
  after(() => mock.cleanup());
  before(() => {
    mock.createServer().then(s => (server = s));
  });

  it('should destroy connections which have been closed', function(done) {
    server.setMessageHandler(request => {
      const doc = request.document;
      if (doc.ismaster) {
        request.reply(mock.DEFAULT_ISMASTER_36);
      } else {
        // destroy on any other command
        request.connection.destroy();
      }
    });

    const pool = new ConnectionPool(
      Object.assign({ bson: new BSON(), maxPoolSize: 1 }, server.address())
    );

    const events = [];
    pool.on('connectionClosed', event => events.push(event));

    pool.checkOut((err, conn) => {
      expect(err).to.not.exist;

      conn.command('admin.$cmd', { ping: 1 }, (err, result) => {
        expect(err).to.exist;
        expect(result).to.not.exist;

        pool.checkIn(conn);

        expect(events).to.have.length(1);
        const closeEvent = events[0];
        expect(closeEvent)
          .have.property('reason')
          .equal('error');
      });
    });

    pool.withConnection(
      (err, conn, cb) => {
        expect(err).to.not.exist;
        cb();
      },
      () => {
        pool.close(done);
      }
    );
  });

  it('should propagate socket timeouts to connections', function(done) {
    server.setMessageHandler(request => {
      const doc = request.document;
      if (doc.ismaster) {
        request.reply(mock.DEFAULT_ISMASTER_36);
      } else {
        // blackhole other requests
      }
    });

    const pool = new ConnectionPool(
      Object.assign({ bson: new BSON(), maxPoolSize: 1, socketTimeout: 50 }, server.address())
    );

    pool.withConnection(
      (err, conn, cb) => {
        expect(err).to.not.exist;
        conn.command('admin.$cmd', { ping: 1 }, (err, result) => {
          expect(err).to.exist;
          expect(result).to.not.exist;
          expect(err).to.match(/timed out/);
          cb();
        });
      },
      () => pool.close(done)
    );
  });

  describe('withConnection', function() {
    it('should manage a connection for a successful operation', function(done) {
      server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(mock.DEFAULT_ISMASTER_36);
        }
      });

      const pool = new ConnectionPool(Object.assign({ bson: new BSON() }, server.address()));
      const callback = (err, result) => {
        expect(err).to.not.exist;
        expect(result).to.exist;
        pool.close(done);
      };

      pool.withConnection((err, conn, cb) => {
        expect(err).to.not.exist;

        conn.command('$admin.cmd', { ismaster: 1 }, (cmdErr, ismaster) => {
          expect(cmdErr).to.not.exist;
          cb(undefined, ismaster);
        });
      }, callback);
    });

    it('should allow user interaction with an error', function(done) {
      server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.connection.destroy();
        }
      });

      const pool = new ConnectionPool(
        Object.assign({ bson: new BSON(), waitQueueTimeoutMS: 200 }, server.address())
      );

      const callback = err => {
        expect(err).to.exist;
        expect(err).to.match(/Timed out/);
        pool.close(done);
      };

      pool.withConnection((err, conn, cb) => {
        expect(err).to.exist;
        expect(err).to.match(/Timed out/);
        cb(err);
      }, callback);
    });

    it('should return an error to the original callback', function(done) {
      server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(mock.DEFAULT_ISMASTER_36);
        }
      });

      const pool = new ConnectionPool(Object.assign({ bson: new BSON() }, server.address()));
      const callback = (err, result) => {
        expect(err).to.exist;
        expect(result).to.not.exist;
        expect(err).to.match(/my great error/);
        pool.close(done);
      };

      pool.withConnection((err, conn, cb) => {
        expect(err).to.not.exist;
        cb(new Error('my great error'));
      }, callback);
    });

    it('should still manage a connection if no callback is provided', function(done) {
      server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(mock.DEFAULT_ISMASTER_36);
        }
      });

      const pool = new ConnectionPool(
        Object.assign({ bson: new BSON(), maxPoolSize: 1 }, server.address())
      );

      const events = [];
      pool.on('connectionCheckedOut', event => events.push(event));
      pool.on('connectionCheckedIn', event => {
        events.push(event);

        expect(events).to.have.length(2);
        expect(events[0]).to.be.instanceOf(cmapEvents.ConnectionCheckedOutEvent);
        expect(events[1]).to.be.instanceOf(cmapEvents.ConnectionCheckedInEvent);
        pool.close(done);
      });

      pool.withConnection((err, conn, cb) => {
        expect(err).to.not.exist;
        cb();
      });
    });
  });

  describe('spec tests', function() {
    const threads = new Map();
    const connections = new Map();
    const orphans = new Set();
    const poolEvents = [];
    const poolEventsEventEmitter = new EventEmitter();
    let pool = undefined;

    function createPool(options) {
      options = Object.assign({}, options, { bson: new BSON() }, server.address());
      pool = new ConnectionPool(options);
      ALL_POOL_EVENTS.forEach(ev => {
        pool.on(ev, x => {
          poolEvents.push(x);
          poolEventsEventEmitter.emit('poolEvent');
        });
      });
    }

    function getThread(name) {
      let thread = threads.get(name);
      if (!thread) {
        thread = new Thread();
        threads.set(name, thread);
      }

      return thread;
    }

    function eventType(event) {
      const eventName = event.constructor.name;
      return eventName.substring(0, eventName.lastIndexOf('Event'));
    }

    const OPERATION_FUNCTIONS = {
      checkOut: function(op) {
        return PROMISIFIED_POOL_FUNCTIONS.checkOut.call(pool).then(connection => {
          if (op.label != null) {
            connections.set(op.label, connection);
          } else {
            orphans.add(connection);
          }
        });
      },
      checkIn: function(op) {
        const connection = connections.get(op.connection);
        connections.delete(op.connection);

        if (!connection) {
          throw new Error(`Attempted to release non-existient connection ${op.connection}`);
        }

        return pool.checkIn(connection);
      },
      clear: function() {
        return pool.clear();
      },
      close: function() {
        return PROMISIFIED_POOL_FUNCTIONS.close.call(pool);
      },
      wait: function(options) {
        const ms = options.ms;
        return new Promise(r => setTimeout(r, ms));
      },
      start: function(options) {
        const target = options.target;
        const thread = getThread(target);
        thread.start();
      },
      waitForThread: function(options) {
        const name = options.name;
        const target = options.target;
        const suppressError = options.suppressError;

        const threadObj = threads.get(target);

        if (!threadObj) {
          throw new Error(`Attempted to run op ${name} on non-existent thread ${target}`);
        }

        return threadObj.finish().catch(e => {
          if (!suppressError) {
            throw e;
          }
        });
      },
      waitForEvent: function(options) {
        const event = options.event;
        const count = options.count;
        return new Promise(resolve => {
          function run() {
            if (poolEvents.filter(ev => eventType(ev) === event).length >= count) {
              return resolve();
            }

            poolEventsEventEmitter.once('poolEvent', run);
          }
          run();
        });
      }
    };

    class Thread {
      constructor() {
        this._killed = false;
        this._error = undefined;
        this._promise = new Promise(resolve => {
          this.start = () => setTimeout(resolve);
        });
      }

      run(op) {
        if (this._killed || this._error) {
          return;
        }

        this._promise = this._promise
          .then(() => this._runOperation(op))
          .catch(e => (this._error = e));
      }

      _runOperation(op) {
        const operationFn = OPERATION_FUNCTIONS[op.name];
        if (!operationFn) {
          throw new Error(`Invalid command ${op.name}`);
        }

        return Promise.resolve()
          .then(() => operationFn(op, this))
          .then(() => new Promise(r => setTimeout(r)));
      }

      finish() {
        this._killed = true;
        return this._promise.then(() => {
          if (this._error) {
            throw this._error;
          }
        });
      }
    }

    before(() => {
      // we aren't testing errors yet, so it's fine for the mock server to just accept
      // and establish valid connections
      server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(mock.DEFAULT_ISMASTER_36);
        }
      });
    });

    afterEach(() => {
      const p = pool ? closePool(pool) : Promise.resolve();
      return p
        .then(() => {
          const connectionsToDestroy = Array.from(orphans).concat(Array.from(connections.values()));
          return Promise.each(connectionsToDestroy, conn => {
            return new Promise((resolve, reject) =>
              conn.destroy({ force: true }, err => {
                if (err) return reject(err);
                resolve();
              })
            );
          });
        })
        .then(() => {
          pool = undefined;
          threads.clear();
          connections.clear();
          orphans.clear();
          poolEvents.length = 0;
          poolEventsEventEmitter.removeAllListeners();
        });
    });

    loadSpecTests('connection-monitoring-and-pooling').forEach(test => {
      it(test.description, function() {
        const operations = test.operations;
        const expectedEvents = test.events || [];
        const ignoreEvents = test.ignore || [];
        const expectedError = test.error;
        const poolOptions = test.poolOptions || {};

        let actualError;

        const MAIN_THREAD_KEY = Symbol('Main Thread');
        const mainThread = new Thread();
        threads.set(MAIN_THREAD_KEY, mainThread);
        mainThread.start();

        createPool(poolOptions);

        let basePromise = Promise.resolve();

        for (let idx in operations) {
          const op = operations[idx];

          const threadKey = op.thread || MAIN_THREAD_KEY;
          const thread = getThread(threadKey);

          basePromise = basePromise.then(() => {
            if (!thread) {
              throw new Error(`Invalid thread ${threadKey}`);
            }

            return Promise.resolve()
              .then(() => thread.run(op))
              .then(() => new Promise(r => setTimeout(r)));
          });
        }

        return basePromise
          .then(() => mainThread.finish())
          .catch(e => (actualError = e))
          .then(() => {
            const actualEvents = poolEvents.filter(ev => ignoreEvents.indexOf(ev.type) < 0);

            if (expectedError) {
              if (!actualError) {
                expect(actualError).to.matchMongoSpec(expectedError);
              } else {
                const ae = Object.assign({}, actualError, { message: actualError.message });
                expect(ae).to.matchMongoSpec(expectedError);
              }
            } else if (actualError) {
              throw actualError;
            }

            expectedEvents.forEach((expected, index) => {
              const actual = actualEvents[index];
              expect(actual).to.matchMongoSpec(expected);
            });
          });
      });
    });
  });
});
<commit_msg>test: no longer look for `type` in cmap spec test runner<commit_after>'use strict';

const Promise = require('bluebird');
const loadSpecTests = require('../../spec').loadSpecTests;
const ConnectionPool = require('../../../lib/cmap/connection_pool').ConnectionPool;
const EventEmitter = require('events').EventEmitter;
const mock = require('mongodb-mock-server');
const BSON = require('bson');
const cmapEvents = require('../../../lib/cmap/events');

const chai = require('chai');
chai.use(require('../../functional/spec-runner/matcher').default);
const expect = chai.expect;

const ALL_POOL_EVENTS = new Set([
  'connectionPoolCreated',
  'connectionPoolClosed',
  'connectionCreated',
  'connectionReady',
  'connectionClosed',
  'connectionCheckOutStarted',
  'connectionCheckOutFailed',
  'connectionCheckedOut',
  'connectionCheckedIn',
  'connectionPoolCleared'
]);

const PROMISIFIED_POOL_FUNCTIONS = {
  checkOut: Promise.promisify(ConnectionPool.prototype.checkOut),
  close: Promise.promisify(ConnectionPool.prototype.close)
};

function closePool(pool) {
  return new Promise(resolve => {
    ALL_POOL_EVENTS.forEach(ev => pool.removeAllListeners(ev));
    pool.close(resolve);
  });
}

describe('Connection Pool', function() {
  let server;
  after(() => mock.cleanup());
  before(() => {
    mock.createServer().then(s => (server = s));
  });

  it('should destroy connections which have been closed', function(done) {
    server.setMessageHandler(request => {
      const doc = request.document;
      if (doc.ismaster) {
        request.reply(mock.DEFAULT_ISMASTER_36);
      } else {
        // destroy on any other command
        request.connection.destroy();
      }
    });

    const pool = new ConnectionPool(
      Object.assign({ bson: new BSON(), maxPoolSize: 1 }, server.address())
    );

    const events = [];
    pool.on('connectionClosed', event => events.push(event));

    pool.checkOut((err, conn) => {
      expect(err).to.not.exist;

      conn.command('admin.$cmd', { ping: 1 }, (err, result) => {
        expect(err).to.exist;
        expect(result).to.not.exist;

        pool.checkIn(conn);

        expect(events).to.have.length(1);
        const closeEvent = events[0];
        expect(closeEvent)
          .have.property('reason')
          .equal('error');
      });
    });

    pool.withConnection(
      (err, conn, cb) => {
        expect(err).to.not.exist;
        cb();
      },
      () => {
        pool.close(done);
      }
    );
  });

  it('should propagate socket timeouts to connections', function(done) {
    server.setMessageHandler(request => {
      const doc = request.document;
      if (doc.ismaster) {
        request.reply(mock.DEFAULT_ISMASTER_36);
      } else {
        // blackhole other requests
      }
    });

    const pool = new ConnectionPool(
      Object.assign({ bson: new BSON(), maxPoolSize: 1, socketTimeout: 50 }, server.address())
    );

    pool.withConnection(
      (err, conn, cb) => {
        expect(err).to.not.exist;
        conn.command('admin.$cmd', { ping: 1 }, (err, result) => {
          expect(err).to.exist;
          expect(result).to.not.exist;
          expect(err).to.match(/timed out/);
          cb();
        });
      },
      () => pool.close(done)
    );
  });

  describe('withConnection', function() {
    it('should manage a connection for a successful operation', function(done) {
      server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(mock.DEFAULT_ISMASTER_36);
        }
      });

      const pool = new ConnectionPool(Object.assign({ bson: new BSON() }, server.address()));
      const callback = (err, result) => {
        expect(err).to.not.exist;
        expect(result).to.exist;
        pool.close(done);
      };

      pool.withConnection((err, conn, cb) => {
        expect(err).to.not.exist;

        conn.command('$admin.cmd', { ismaster: 1 }, (cmdErr, ismaster) => {
          expect(cmdErr).to.not.exist;
          cb(undefined, ismaster);
        });
      }, callback);
    });

    it('should allow user interaction with an error', function(done) {
      server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.connection.destroy();
        }
      });

      const pool = new ConnectionPool(
        Object.assign({ bson: new BSON(), waitQueueTimeoutMS: 200 }, server.address())
      );

      const callback = err => {
        expect(err).to.exist;
        expect(err).to.match(/Timed out/);
        pool.close(done);
      };

      pool.withConnection((err, conn, cb) => {
        expect(err).to.exist;
        expect(err).to.match(/Timed out/);
        cb(err);
      }, callback);
    });

    it('should return an error to the original callback', function(done) {
      server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(mock.DEFAULT_ISMASTER_36);
        }
      });

      const pool = new ConnectionPool(Object.assign({ bson: new BSON() }, server.address()));
      const callback = (err, result) => {
        expect(err).to.exist;
        expect(result).to.not.exist;
        expect(err).to.match(/my great error/);
        pool.close(done);
      };

      pool.withConnection((err, conn, cb) => {
        expect(err).to.not.exist;
        cb(new Error('my great error'));
      }, callback);
    });

    it('should still manage a connection if no callback is provided', function(done) {
      server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(mock.DEFAULT_ISMASTER_36);
        }
      });

      const pool = new ConnectionPool(
        Object.assign({ bson: new BSON(), maxPoolSize: 1 }, server.address())
      );

      const events = [];
      pool.on('connectionCheckedOut', event => events.push(event));
      pool.on('connectionCheckedIn', event => {
        events.push(event);

        expect(events).to.have.length(2);
        expect(events[0]).to.be.instanceOf(cmapEvents.ConnectionCheckedOutEvent);
        expect(events[1]).to.be.instanceOf(cmapEvents.ConnectionCheckedInEvent);
        pool.close(done);
      });

      pool.withConnection((err, conn, cb) => {
        expect(err).to.not.exist;
        cb();
      });
    });
  });

  describe('spec tests', function() {
    const threads = new Map();
    const connections = new Map();
    const orphans = new Set();
    const poolEvents = [];
    const poolEventsEventEmitter = new EventEmitter();
    let pool = undefined;

    function createPool(options) {
      options = Object.assign({}, options, { bson: new BSON() }, server.address());
      pool = new ConnectionPool(options);
      ALL_POOL_EVENTS.forEach(ev => {
        pool.on(ev, x => {
          poolEvents.push(x);
          poolEventsEventEmitter.emit('poolEvent');
        });
      });
    }

    function getThread(name) {
      let thread = threads.get(name);
      if (!thread) {
        thread = new Thread();
        threads.set(name, thread);
      }

      return thread;
    }

    function eventType(event) {
      const eventName = event.constructor.name;
      return eventName.substring(0, eventName.lastIndexOf('Event'));
    }

    const OPERATION_FUNCTIONS = {
      checkOut: function(op) {
        return PROMISIFIED_POOL_FUNCTIONS.checkOut.call(pool).then(connection => {
          if (op.label != null) {
            connections.set(op.label, connection);
          } else {
            orphans.add(connection);
          }
        });
      },
      checkIn: function(op) {
        const connection = connections.get(op.connection);
        connections.delete(op.connection);

        if (!connection) {
          throw new Error(`Attempted to release non-existient connection ${op.connection}`);
        }

        return pool.checkIn(connection);
      },
      clear: function() {
        return pool.clear();
      },
      close: function() {
        return PROMISIFIED_POOL_FUNCTIONS.close.call(pool);
      },
      wait: function(options) {
        const ms = options.ms;
        return new Promise(r => setTimeout(r, ms));
      },
      start: function(options) {
        const target = options.target;
        const thread = getThread(target);
        thread.start();
      },
      waitForThread: function(options) {
        const name = options.name;
        const target = options.target;
        const suppressError = options.suppressError;

        const threadObj = threads.get(target);

        if (!threadObj) {
          throw new Error(`Attempted to run op ${name} on non-existent thread ${target}`);
        }

        return threadObj.finish().catch(e => {
          if (!suppressError) {
            throw e;
          }
        });
      },
      waitForEvent: function(options) {
        const event = options.event;
        const count = options.count;
        return new Promise(resolve => {
          function run() {
            if (poolEvents.filter(ev => eventType(ev) === event).length >= count) {
              return resolve();
            }

            poolEventsEventEmitter.once('poolEvent', run);
          }
          run();
        });
      }
    };

    class Thread {
      constructor() {
        this._killed = false;
        this._error = undefined;
        this._promise = new Promise(resolve => {
          this.start = () => setTimeout(resolve);
        });
      }

      run(op) {
        if (this._killed || this._error) {
          return;
        }

        this._promise = this._promise
          .then(() => this._runOperation(op))
          .catch(e => (this._error = e));
      }

      _runOperation(op) {
        const operationFn = OPERATION_FUNCTIONS[op.name];
        if (!operationFn) {
          throw new Error(`Invalid command ${op.name}`);
        }

        return Promise.resolve()
          .then(() => operationFn(op, this))
          .then(() => new Promise(r => setTimeout(r)));
      }

      finish() {
        this._killed = true;
        return this._promise.then(() => {
          if (this._error) {
            throw this._error;
          }
        });
      }
    }

    before(() => {
      // we aren't testing errors yet, so it's fine for the mock server to just accept
      // and establish valid connections
      server.setMessageHandler(request => {
        const doc = request.document;
        if (doc.ismaster) {
          request.reply(mock.DEFAULT_ISMASTER_36);
        }
      });
    });

    afterEach(() => {
      const p = pool ? closePool(pool) : Promise.resolve();
      return p
        .then(() => {
          const connectionsToDestroy = Array.from(orphans).concat(Array.from(connections.values()));
          return Promise.each(connectionsToDestroy, conn => {
            return new Promise((resolve, reject) =>
              conn.destroy({ force: true }, err => {
                if (err) return reject(err);
                resolve();
              })
            );
          });
        })
        .then(() => {
          pool = undefined;
          threads.clear();
          connections.clear();
          orphans.clear();
          poolEvents.length = 0;
          poolEventsEventEmitter.removeAllListeners();
        });
    });

    loadSpecTests('connection-monitoring-and-pooling').forEach(test => {
      it(test.description, function() {
        const operations = test.operations;
        const expectedEvents = test.events || [];
        const ignoreEvents = test.ignore || [];
        const expectedError = test.error;
        const poolOptions = test.poolOptions || {};

        let actualError;

        const MAIN_THREAD_KEY = Symbol('Main Thread');
        const mainThread = new Thread();
        threads.set(MAIN_THREAD_KEY, mainThread);
        mainThread.start();

        createPool(poolOptions);

        let basePromise = Promise.resolve();

        for (let idx in operations) {
          const op = operations[idx];

          const threadKey = op.thread || MAIN_THREAD_KEY;
          const thread = getThread(threadKey);

          basePromise = basePromise.then(() => {
            if (!thread) {
              throw new Error(`Invalid thread ${threadKey}`);
            }

            return Promise.resolve()
              .then(() => thread.run(op))
              .then(() => new Promise(r => setTimeout(r)));
          });
        }

        return basePromise
          .then(() => mainThread.finish())
          .catch(e => (actualError = e))
          .then(() => {
            const actualEvents = poolEvents.filter(ev => ignoreEvents.indexOf(ev.type) < 0);

            if (expectedError) {
              expect(actualError).to.exist;
              expect(actualError)
                .property('message')
                .to.equal(expectedError.message);
            } else if (actualError) {
              throw actualError;
            }

            expectedEvents.forEach((expected, index) => {
              const actual = actualEvents[index];
              expect(actual).to.matchMongoSpec(expected);
            });
          });
      });
    });
  });
});
<|endoftext|>


<commit_before>// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

const path = require('path');

// true by default
// const COVERAGE_ENABLED = !process.env['NOCOVERAGE'];

// false by default
const DEBUG_ENABLED = !!process.env['DEBUG'];

const GEN_DIRECTORY = path.join(__dirname, '..', '..', '..');
const ROOT_DIRECTORY = path.join(GEN_DIRECTORY, '..', '..', '..');

const browsers = DEBUG_ENABLED ? ['Chrome'] : ['ChromeHeadless'];
// const reporters = COVERAGE_ENABLED ? ['dots', 'coverage-istanbul'] : ['dots'];

module.exports = function(config) {
  const options = {
    basePath: ROOT_DIRECTORY,

    files: [
      {pattern: path.join(__dirname, '**/*_test.js'), type: 'module'},
      {pattern: path.join(__dirname, '**/*_test.js.map'), served: true, included: false},
      {pattern: path.join(ROOT_DIRECTORY, 'test/unittests/**/*_test.ts'), served: true, included: false},
      {pattern: path.join(GEN_DIRECTORY, 'front_end/**/*.js'), served: true, included: false},
      {pattern: path.join(GEN_DIRECTORY, 'front_end/**/*.js.map'), served: true, included: false},
      {pattern: path.join(ROOT_DIRECTORY, 'front_end/**/*.ts'), served: true, included: false},
    ],

    reporters: ['dots'],

    browsers,

    frameworks: ['mocha', 'chai'],

    plugins: [
      require('karma-chrome-launcher'),
      require('karma-mocha'),
      require('karma-chai'),
      require('karma-sourcemap-loader'),
    ],

    preprocessors: {
      '**/*.js': ['sourcemap'],
    },

    singleRun: !DEBUG_ENABLED,
  };

  config.set(options);
};
<commit_msg>Add coverage reporting to ninja typescript<commit_after>// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

const path = require('path');

// true by default
const COVERAGE_ENABLED = !process.env['NOCOVERAGE'];
const TEXT_COVERAGE_ENABLED = COVERAGE_ENABLED && !process.env['NO_TEXT_COVERAGE'];

// false by default
const DEBUG_ENABLED = !!process.env['DEBUG'];

const GEN_DIRECTORY = path.join(__dirname, '..', '..', '..');
const ROOT_DIRECTORY = path.join(GEN_DIRECTORY, '..', '..', '..');

const browsers = DEBUG_ENABLED ? ['Chrome'] : ['ChromeHeadless'];

const coverageReporters = COVERAGE_ENABLED ? ['coverage-istanbul'] : [];
const coveragePreprocessors = COVERAGE_ENABLED ? ['karma-coverage-istanbul-instrumenter'] : [];
const commonIstanbulReporters = ['html', 'json-summary'];
const istanbulReportOutputs = TEXT_COVERAGE_ENABLED ? ['text', ...commonIstanbulReporters] : commonIstanbulReporters;

module.exports = function(config) {
  const options = {
    basePath: ROOT_DIRECTORY,

    files: [
      {pattern: path.join(__dirname, '**/*_test.js'), type: 'module'},
      {pattern: path.join(__dirname, '**/*_test.js.map'), served: true, included: false},
      {pattern: path.join(ROOT_DIRECTORY, 'test/unittests/**/*_test.ts'), served: true, included: false},
      {pattern: path.join(GEN_DIRECTORY, 'front_end/**/*.js'), served: true, included: false},
      {pattern: path.join(GEN_DIRECTORY, 'front_end/**/*.js.map'), served: true, included: false},
      {pattern: path.join(ROOT_DIRECTORY, 'front_end/**/*.ts'), served: true, included: false},
    ],

    reporters: [
      'dots',
      ...coverageReporters,
    ],

    browsers,

    frameworks: ['mocha', 'chai'],

    plugins: [
      require('karma-chrome-launcher'),
      require('karma-mocha'),
      require('karma-chai'),
      require('karma-sourcemap-loader'),
      require('karma-coverage-istanbul-instrumenter'),
      require('karma-coverage-istanbul-reporter'),
    ],

    preprocessors: {
      '**/*.js': ['sourcemap'],
      [path.join(GEN_DIRECTORY, 'front_end/**/*.js')]: [...coveragePreprocessors],
    },

    coverageIstanbulInstrumenter: {esModules: true},

    coverageIstanbulReporter: {
      reports: istanbulReportOutputs,
      dir: 'karma-coverage',
      skipFilesWithNoCoverage: true,
    },

    singleRun: !DEBUG_ENABLED,
  };

  config.set(options);
};
<|endoftext|>


<commit_before>import { observable, observe, action, computed, isObservableArray, toJS, asMap, untracked } from 'mobx';
import _ from 'lodash';
import utils from './utils';

import Base from './Base';

import {
  $try,
  defaultClearValue,
  parseFieldValue,
  parseGetLabel } from './parser';

export default class Field extends Base {

  fields = observable.map ? observable.map({}) : asMap({});
  incremental = false;
  isField = true;
  disposeValidation = null;

  id;
  key;
  name;
  path;
  type;
  state;

  $rules;
  $validate;
  $related;
  $options;
  $observers;

  $parser = $ => $;
  $formatter = $ => $;

  @observable $value = undefined;
  @observable $label = undefined;
  @observable $placeholder = undefined;
  @observable $default = undefined;
  @observable $initial = undefined;
  @observable $bindings = undefined;
  @observable $type = undefined;

  @observable $disabled = false;
  @observable $focus = false;
  @observable $touched = false;
  @observable $changed = false;

  @observable showError = true;

  @observable errorSync = null;
  @observable errorAsync = null;

  @observable validationErrorStack = [];
  @observable validationFunctionsData = [];
  @observable validationAsyncData = {};

  constructor({ key, path, data = {}, props = {}, update = false, state }) {
    super();

    this.state = state;

    this.setupField(key, path, data, props, update);
    this.checkDVRValidationPlugin();
    this.initNestedFields(data, update);

    this.incremental = (this.hasIncrementalNestedFields !== 0);

    this.observeValidation();
    this.initObservers();
  }

  /* ------------------------------------------------------------------ */
  /* COMPUTED */

  @computed get checkValidationErrors() {
    return ((this.validationAsyncData.valid === false)
      && !_.isEmpty(this.validationAsyncData))
      || !_.isEmpty(this.validationErrorStack)
      || _.isString(this.errorAsync)
      || _.isString(this.errorSync);
  }

  @computed get value() {
    return this.getComputedProp('value');
  }

  set value(newVal) {
    if (this.$value === newVal) return;
    // handle numbers
    if (this.state.options.get('autoParseNumbers') === true) {
      if (_.isNumber(this.$initial)) {
        if (!_.endsWith(newVal, '.') && !_.endsWith(_.split(newVal, '.', 2)[1], '0')) {
          const numericVal = _.toNumber(newVal);
          if (!_.isString(numericVal) && !_.isNaN(numericVal)) {
            this.$value = numericVal;
            return;
          }
        }
      }
    }
    // handle parse value
    this.$value = newVal;
  }

  @computed get initial() {
    return this.getComputedProp('initial');
  }

  set initial(val) {
    this.$initial = parseFieldValue(this.$parser, { separated: val });
  }

  @computed get default() {
    return this.getComputedProp('default');
  }

  set default(val) {
    this.$default = parseFieldValue(this.$parser, { separated: val });
  }

  @computed get label() {
    return parseGetLabel(this.$label);
  }

  @computed get placeholder() {
    return this.$placeholder;
  }

  @computed get options() {
    return this.$options;
  }

  @computed get bindings() {
    return this.$bindings;
  }

  @computed get type() {
    return this.$type;
  }

  @computed get related() {
    return this.$related;
  }

  @computed get disabled() {
    return this.$disabled;
  }

  @computed get rules() {
    return this.$rules;
  }

  @computed get validators() {
    return this.$validate;
  }

  @computed get error() {
    if (this.showError === false) return null;
    return (this.errorAsync || this.errorSync);
  }

  @computed get hasError() {
    return this.checkValidationErrors
      || this.check('hasError', true);
  }

  @computed get isValid() {
    return !this.checkValidationErrors
      && this.check('isValid', true);
  }

  @computed get isDirty() {
    return this.hasNestedFields
      ? this.check('isDirty', true)
      : !_.isEqual(this.$default, this.value);
  }

  @computed get isPristine() {
    return this.hasNestedFields
      ? this.check('isPristine', true)
      : _.isEqual(this.$default, this.value);
  }

  @computed get isDefault() {
    return this.hasNestedFields
      ? this.check('isDefault', true)
      : _.isEqual(this.$default, this.value);
  }

  @computed get isEmpty() {
    if (this.hasNestedFields) return this.check('isEmpty', true);
    if (_.isBoolean(this.value)) return !!this.$value;
    if (_.isNumber(this.value)) return false;
    if (_.isDate(this.value)) return false;
    return _.isEmpty(this.value);
  }

  @computed get focus() {
    return this.hasNestedFields
      ? this.check('focus', true)
      : this.$focus;
  }

  @computed get touched() {
    return this.hasNestedFields
      ? this.check('touched', true)
      : this.$touched;
  }

  @computed get changed() {
    return this.hasNestedFields
      ? this.check('changed', true)
      : this.$changed;
  }

  /* ------------------------------------------------------------------ */
  /* EVENTS */

  sync = action((e, v = null) => {
    this.$changed = true;

    const $isBool = $ =>
      _.isBoolean(this.$value) &&
      _.isBoolean($.target.checked);

    const $get = $ => $isBool($)
        ? $.target.checked
        : $.target.value;

    // assume "v" or "e" are the values
    if (_.isNil(e) || _.isNil(e.target)) {
      if (!_.isNil(v) && !_.isNil(v.target)) {
        v = $get(v); // eslint-disable-line
      }

      this.set('value', $try(e, v));
      return;
    }

    if (!_.isNil(e.target)) {
      this.set('value', $get(e));
      return;
    }

    this.set('value', e);
  });

  onChange = this.sync;
  onToggle = this.sync;

  onFocus = action(() => {
    this.$focus = true;
    this.$touched = true;
  });

  onBlur = action(() => {
    this.$focus = false;
  });
}

/**
  Prototypes
*/
export const prototypes = {

  @action
  setupField($key, $path, $data, $props, update) {
    this.key = $key;
    this.path = $path;
    this.id = utils.makeId(this.path);

    const isEmptyArray = (_.has($data, 'fields') && _.isArray($data.fields));
    const checkArray = val => isEmptyArray ? [] : val;

    const {
      $value,
      $label,
      $placeholder,
      $default,
      $initial,
      $disabled,
      $bindings,
      $type,
      $options,
      $related,
      $validate,
      $rules,
      $parse,
      $format,
      $observers,
    } = $props;

    // eslint-disable-next-line
    if (_.isNil($data)) $data = '';

    if (_.isPlainObject($data)) {
      const {
        value,
        name,
        label,
        placeholder,
        disabled,
        bindings,
        type,
        options,
        related,
        validate,
        rules,
        parse,
        format,
        observers,
      } = $data;

      this.$type = $type || type || 'text';
      this.$parser = $try($parse, parse, this.$parser);
      this.$formatter = $try($format, format, this.$formatter);

      this.$initial = parseFieldValue(this.$parser, {
        isEmptyArray,
        type: this.type,
        unified: checkArray(value),
        separated: $initial,
      });

      this.$default = parseFieldValue(this.$parser, {
        isEmptyArray,
        type: this.type,
        unified: update ? '' : checkArray($data.default),
        separated: checkArray($default),
        initial: this.$initial,
      });

      this.name = _.toString(name || $key);
      this.$value = this.$initial;
      this.$label = $label || label || this.name;
      this.$placeholder = $placeholder || placeholder || '';
      this.$disabled = $disabled || disabled || false;
      this.$bindings = $bindings || bindings || 'default';
      this.$options = $options || options || null;
      this.$related = $related || related || [];
      this.$validate = toJS($validate || validate || null);
      this.$rules = $rules || rules || null;
      this.$observers = $observers || observers || null;
      return;
    }

    /* The field IS the value here */
    this.name = _.toString($key);
    this.$type = $type || 'text';

    this.$initial = parseFieldValue(this.$parser, {
      isEmptyArray,
      type: this.type,
      unified: checkArray($data),
      separated: checkArray($value),
    });

    this.$default = parseFieldValue(this.$parser, {
      isEmptyArray,
      type: this.type,
      unified: update ? '' : checkArray($data.default),
      separated: checkArray($default),
      initial: this.$initial,
    });

    this.$value = this.$initial;
    this.$label = $label || this.name;
    this.$placeholder = $placeholder || '';
    this.$disabled = $disabled || false;
    this.$bindings = $bindings || 'default';
    this.$options = $options || null;
    this.$related = $related || [];
    this.$validate = toJS($validate || null);
    this.$rules = $rules || null;
    this.$observers = $observers || null;
  },

  getComputedProp(key) {
    if (this.incremental || this.hasNestedFields) {
      const $val = (key === 'value')
        ? this.get(key, false)
        : untracked(() => this.get(key, false));

      return !_.isEmpty($val) ? $val : [];
    }

    const val = this[`$${key}`];

    if (_.isArray(val) || isObservableArray(val)) {
      return [].slice.call(val);
    }

    return toJS(val);
  },

  checkDVRValidationPlugin() {
    const validators = this.state.form.validator.validators;
    if (_.isNil(validators.dvr) && !_.isNil(this.rules)) {
      // eslint-disable-next-line
      console.warn(
        'The DVR validation rules are defined',
        'but no plugin provided (DVR). Field:',
        this.path,
      );
    }
  },

  @action
  initNestedFields(field, update) {
    const fields = _.isNil(field) ? '' : field.fields;
    this.initFields({ fields }, update);
  },

  validate() {
    return this.state.form.validate({
      path: this.path,
      field: this,
      showErrors: true,
      related: true,
    });
  },

  @action
  invalidate(message, async = false) {
    if (async === true) {
      this.errorAsync = message;
      return;
    }

    if (_.isArray(message)) {
      this.validationErrorStack = message;
      this.showErrors(true);
      return;
    }

    this.validationErrorStack.unshift(message);
    this.showErrors(true);
  },

  @action
  setValidationAsyncData(obj = {}) {
    this.validationAsyncData = obj;
  },

  @action
  resetValidation(deep = false) {
    this.showError = true;
    this.errorSync = null;
    this.errorAsync = null;
    this.validationAsyncData = {};
    this.validationFunctionsData = [];
    this.validationErrorStack = [];
    // recursive resetValidation
    if (deep) this.deepAction('resetValidation');
  },

  @action
  clear(deep = true) {
    this.showErrors(false);
    this.$value = defaultClearValue({ value: this.$value });
    // recursive clear fields
    if (deep && this.fields.size) this.deepAction('clear');
  },

  @action
  reset(deep = true) {
    const useDefaultValue = (this.$default !== this.$initial);
    if (useDefaultValue) this.value = this.$default;
    if (!useDefaultValue) this.value = this.$initial;
    // recursive reset fields
    if (deep && this.fields.size) this.deepAction('reset');
  },

  @action
  showErrors(showErrors = true) {
    this.showError = showErrors;
    this.errorSync = _.head(this.validationErrorStack);
  },

  @action
  showAsyncErrors() {
    if (this.validationAsyncData.valid === false) {
      this.errorAsync = this.validationAsyncData.message;
      return;
    }
    this.errorAsync = null;
  },

  observeValidation() {
    if (this.state.options.get('validateOnChange') === false) return;
    this.disposeValidation = observe(this, '$value', () => this.validate());
  },

  initObservers() {
    if (!_.isArray(this.$observers)) return;
    this.$observers.map(obj => this.observe(_.omit(obj, 'path')));
  },

  bind(props = {}) {
    return this.state.bindings.load(this, this.bindings, props);
  },

};
<commit_msg>fix: #218<commit_after>import { observable, observe, action, computed, isObservableArray, toJS, asMap, untracked } from 'mobx';
import _ from 'lodash';
import utils from './utils';

import Base from './Base';

import {
  $try,
  defaultClearValue,
  parseFieldValue,
  parseGetLabel } from './parser';

export default class Field extends Base {

  fields = observable.map ? observable.map({}) : asMap({});
  incremental = false;
  isField = true;
  disposeValidation = null;

  id;
  key;
  name;
  path;
  type;
  state;

  $rules;
  $validate;
  $related;
  $options;
  $observers;

  $parser = $ => $;
  $formatter = $ => $;

  @observable $value = undefined;
  @observable $label = undefined;
  @observable $placeholder = undefined;
  @observable $default = undefined;
  @observable $initial = undefined;
  @observable $bindings = undefined;
  @observable $type = undefined;

  @observable $disabled = false;
  @observable $focus = false;
  @observable $touched = false;
  @observable $changed = false;

  @observable showError = true;

  @observable errorSync = null;
  @observable errorAsync = null;

  @observable validationErrorStack = [];
  @observable validationFunctionsData = [];
  @observable validationAsyncData = {};

  constructor({ key, path, data = {}, props = {}, update = false, state }) {
    super();

    this.state = state;

    this.setupField(key, path, data, props, update);
    this.checkDVRValidationPlugin();
    this.initNestedFields(data, update);

    this.incremental = (this.hasIncrementalNestedFields !== 0);

    this.observeValidation();
    this.initObservers();
  }

  /* ------------------------------------------------------------------ */
  /* COMPUTED */

  @computed get checkValidationErrors() {
    return ((this.validationAsyncData.valid === false)
      && !_.isEmpty(this.validationAsyncData))
      || !_.isEmpty(this.validationErrorStack)
      || _.isString(this.errorAsync)
      || _.isString(this.errorSync);
  }

  @computed get value() {
    return this.getComputedProp('value');
  }

  set value(newVal) {
    if (this.$value === newVal) return;
    // handle numbers
    if (this.state.options.get('autoParseNumbers') === true) {
      if (_.isNumber(this.$initial)) {
        if (!_.endsWith(newVal, '.') && !_.endsWith(_.split(newVal, '.', 2)[1], '0')) {
          const numericVal = _.toNumber(newVal);
          if (!_.isString(numericVal) && !_.isNaN(numericVal)) {
            this.$value = numericVal;
            return;
          }
        }
      }
    }
    // handle parse value
    this.$value = newVal;
  }

  @computed get initial() {
    return this.getComputedProp('initial');
  }

  set initial(val) {
    this.$initial = parseFieldValue(this.$parser, { separated: val });
  }

  @computed get default() {
    return this.getComputedProp('default');
  }

  set default(val) {
    this.$default = parseFieldValue(this.$parser, { separated: val });
  }

  @computed get label() {
    return parseGetLabel(this.$label);
  }

  @computed get placeholder() {
    return this.$placeholder;
  }

  @computed get options() {
    return this.$options;
  }

  @computed get bindings() {
    return this.$bindings;
  }

  @computed get type() {
    return this.$type;
  }

  @computed get related() {
    return this.$related;
  }

  @computed get disabled() {
    return this.$disabled;
  }

  @computed get rules() {
    return this.$rules;
  }

  @computed get validators() {
    return this.$validate;
  }

  @computed get error() {
    if (this.showError === false) return null;
    return (this.errorAsync || this.errorSync);
  }

  @computed get hasError() {
    return this.checkValidationErrors
      || this.check('hasError', true);
  }

  @computed get isValid() {
    return !this.checkValidationErrors
      && this.check('isValid', true);
  }

  @computed get isDirty() {
    return this.hasNestedFields
      ? this.check('isDirty', true)
      : !_.isEqual(this.$default, this.value);
  }

  @computed get isPristine() {
    return this.hasNestedFields
      ? this.check('isPristine', true)
      : _.isEqual(this.$default, this.value);
  }

  @computed get isDefault() {
    return this.hasNestedFields
      ? this.check('isDefault', true)
      : _.isEqual(this.$default, this.value);
  }

  @computed get isEmpty() {
    if (this.hasNestedFields) return this.check('isEmpty', true);
    if (_.isBoolean(this.value)) return !!this.$value;
    if (_.isNumber(this.value)) return false;
    if (_.isDate(this.value)) return false;
    return _.isEmpty(this.value);
  }

  @computed get focus() {
    return this.hasNestedFields
      ? this.check('focus', true)
      : this.$focus;
  }

  @computed get touched() {
    return this.hasNestedFields
      ? this.check('touched', true)
      : this.$touched;
  }

  @computed get changed() {
    return this.hasNestedFields
      ? this.check('changed', true)
      : this.$changed;
  }

  /* ------------------------------------------------------------------ */
  /* EVENTS */

  sync = action((e, v = null) => {
    this.$changed = true;

    const $isBool = $ =>
      _.isBoolean(this.$value) &&
      _.isBoolean($.target.checked);

    const $get = $ => $isBool($)
        ? $.target.checked
        : $.target.value;

    // assume "v" or "e" are the values
    if (_.isNil(e) || _.isNil(e.target)) {
      if (!_.isNil(v) && !_.isNil(v.target)) {
        v = $get(v); // eslint-disable-line
      }

      this.value = $try(e, v);
      return;
    }

    if (!_.isNil(e.target)) {
      this.value = $get(e);
      return;
    }

    this.value = e;
  });

  onChange = this.sync;
  onToggle = this.sync;

  onFocus = action(() => {
    this.$focus = true;
    this.$touched = true;
  });

  onBlur = action(() => {
    this.$focus = false;
  });
}

/**
  Prototypes
*/
export const prototypes = {

  @action
  setupField($key, $path, $data, $props, update) {
    this.key = $key;
    this.path = $path;
    this.id = utils.makeId(this.path);

    const isEmptyArray = (_.has($data, 'fields') && _.isArray($data.fields));
    const checkArray = val => isEmptyArray ? [] : val;

    const {
      $value,
      $label,
      $placeholder,
      $default,
      $initial,
      $disabled,
      $bindings,
      $type,
      $options,
      $related,
      $validate,
      $rules,
      $parse,
      $format,
      $observers,
    } = $props;

    // eslint-disable-next-line
    if (_.isNil($data)) $data = '';

    if (_.isPlainObject($data)) {
      const {
        value,
        name,
        label,
        placeholder,
        disabled,
        bindings,
        type,
        options,
        related,
        validate,
        rules,
        parse,
        format,
        observers,
      } = $data;

      this.$type = $type || type || 'text';
      this.$parser = $try($parse, parse, this.$parser);
      this.$formatter = $try($format, format, this.$formatter);

      this.$initial = parseFieldValue(this.$parser, {
        isEmptyArray,
        type: this.type,
        unified: checkArray(value),
        separated: $initial,
      });

      this.$default = parseFieldValue(this.$parser, {
        isEmptyArray,
        type: this.type,
        unified: update ? '' : checkArray($data.default),
        separated: checkArray($default),
        initial: this.$initial,
      });

      this.name = _.toString(name || $key);
      this.$value = this.$initial;
      this.$label = $label || label || this.name;
      this.$placeholder = $placeholder || placeholder || '';
      this.$disabled = $disabled || disabled || false;
      this.$bindings = $bindings || bindings || 'default';
      this.$options = $options || options || null;
      this.$related = $related || related || [];
      this.$validate = toJS($validate || validate || null);
      this.$rules = $rules || rules || null;
      this.$observers = $observers || observers || null;
      return;
    }

    /* The field IS the value here */
    this.name = _.toString($key);
    this.$type = $type || 'text';

    this.$initial = parseFieldValue(this.$parser, {
      isEmptyArray,
      type: this.type,
      unified: checkArray($data),
      separated: checkArray($value),
    });

    this.$default = parseFieldValue(this.$parser, {
      isEmptyArray,
      type: this.type,
      unified: update ? '' : checkArray($data.default),
      separated: checkArray($default),
      initial: this.$initial,
    });

    this.$value = this.$initial;
    this.$label = $label || this.name;
    this.$placeholder = $placeholder || '';
    this.$disabled = $disabled || false;
    this.$bindings = $bindings || 'default';
    this.$options = $options || null;
    this.$related = $related || [];
    this.$validate = toJS($validate || null);
    this.$rules = $rules || null;
    this.$observers = $observers || null;
  },

  getComputedProp(key) {
    if (this.incremental || this.hasNestedFields) {
      const $val = (key === 'value')
        ? this.get(key, false)
        : untracked(() => this.get(key, false));

      return !_.isEmpty($val) ? $val : [];
    }

    const val = this[`$${key}`];

    if (_.isArray(val) || isObservableArray(val)) {
      return [].slice.call(val);
    }

    return toJS(val);
  },

  checkDVRValidationPlugin() {
    const validators = this.state.form.validator.validators;
    if (_.isNil(validators.dvr) && !_.isNil(this.rules)) {
      // eslint-disable-next-line
      console.warn(
        'The DVR validation rules are defined',
        'but no plugin provided (DVR). Field:',
        this.path,
      );
    }
  },

  @action
  initNestedFields(field, update) {
    const fields = _.isNil(field) ? '' : field.fields;
    this.initFields({ fields }, update);
  },

  validate() {
    return this.state.form.validate({
      path: this.path,
      field: this,
      showErrors: true,
      related: true,
    });
  },

  @action
  invalidate(message, async = false) {
    if (async === true) {
      this.errorAsync = message;
      return;
    }

    if (_.isArray(message)) {
      this.validationErrorStack = message;
      this.showErrors(true);
      return;
    }

    this.validationErrorStack.unshift(message);
    this.showErrors(true);
  },

  @action
  setValidationAsyncData(obj = {}) {
    this.validationAsyncData = obj;
  },

  @action
  resetValidation(deep = false) {
    this.showError = true;
    this.errorSync = null;
    this.errorAsync = null;
    this.validationAsyncData = {};
    this.validationFunctionsData = [];
    this.validationErrorStack = [];
    // recursive resetValidation
    if (deep) this.deepAction('resetValidation');
  },

  @action
  clear(deep = true) {
    this.showErrors(false);
    this.$value = defaultClearValue({ value: this.$value });
    // recursive clear fields
    if (deep && this.fields.size) this.deepAction('clear');
  },

  @action
  reset(deep = true) {
    const useDefaultValue = (this.$default !== this.$initial);
    if (useDefaultValue) this.value = this.$default;
    if (!useDefaultValue) this.value = this.$initial;
    // recursive reset fields
    if (deep && this.fields.size) this.deepAction('reset');
  },

  @action
  showErrors(showErrors = true) {
    this.showError = showErrors;
    this.errorSync = _.head(this.validationErrorStack);
  },

  @action
  showAsyncErrors() {
    if (this.validationAsyncData.valid === false) {
      this.errorAsync = this.validationAsyncData.message;
      return;
    }
    this.errorAsync = null;
  },

  observeValidation() {
    if (this.state.options.get('validateOnChange') === false) return;
    this.disposeValidation = observe(this, '$value', () => this.validate());
  },

  initObservers() {
    if (!_.isArray(this.$observers)) return;
    this.$observers.map(obj => this.observe(_.omit(obj, 'path')));
  },

  bind(props = {}) {
    return this.state.bindings.load(this, this.bindings, props);
  },

};
<|endoftext|>


<commit_before>(function () {
    // Add CSRF token header for Ajax request
    $.ajaxSetup({
        beforeSend: function (xhr, settings) {
            if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) {
                xhr.setRequestHeader("X-CSRFToken", g.csrfToken);
            }
        }
    });

    // Find out params in routing rules
    var pattern = new RegExp("<[^:]*:?([^>]+)>", "g");
    var result = null;

    $.each(g.rules, function (endpoint, rules) {
        $.each(rules, function (index, rule) {
            rule.params = [];
            while ((result = pattern.exec(rule.rule)) !== null) {
                rule.params.push(result[1]);
            }
        });
    });

    /**
     * Generate url for the endpoint.
     * urlFor(endpoint [, parameters] [, external])
     * @param endpoint
     * @param parameters
     * @param external
     * @returns url for the endpoint.
     */
    function urlFor(endpoint, parameters, external) {
        var url = null,
            params = [],
            maxMatchDegree = 0.0,
            keys;

        if ($.type(parameters) === "boolean") {
            external = parameters
        }

        parameters = ($.type(parameters) !== 'undefined') ? parameters : {};
        external = ($.type(external) !== 'undefined') ? external : false;

        if (g.rules[endpoint] === undefined) {
            throw new Error("Uncorrect endpoint in " + "urlFor(\"" + endpoint + "\", " +
                JSON.stringify(parameters) + ")");
        }

        keys = $.map(parameters, function (value, key) {
            return key;
        });

        // Find the first matched rule among rules in this endpoint.
        $.each(g.rules[endpoint], function (index, rule) {
            var match = true,
                currentMatchDegree = 0.0;

            $.each(rule.params, function (index, param) {
                if ($.inArray(param, keys) === -1) {
                    match = false;
                    return false;
                }
            });

            if (match) {
                currentMatchDegree = parseFloat(rule.params.length) / keys.length;
                if (currentMatchDegree > maxMatchDegree || url === null) {
                    maxMatchDegree = currentMatchDegree;
                    url = rule.rule;
                    params = rule.params;
                }
            }
        });

        if (url) {
            $.each(keys, function (index, key) {
                // Build in params
                if ($.inArray(key, params) > -1) {
                    url = url.replace(new RegExp("<[^:]*:?" + key + ">"), parameters[key]);
                } else {
                    // Query string params
                    if (url.indexOf("?") === -1) {
                        url += "?";
                    }
                    if (!endsWith(url, '?')) {
                        url += "&";
                    }
                    url += key + "=" + parameters[key];
                }
            });
        } else {
            throw new Error("Uncorrect parameters in " + "urlFor(\"" + endpoint + "\", " +
                JSON.stringify(parameters) + ")");
        }

        if (external) {
            url = g.domain + url
        }

        return url;
    }

    /**
     * Check whether str ends with suffix.
     * @param str
     * @param suffix
     * @returns {boolean}
     */
    function endsWith(str, suffix) {
        return str.indexOf(suffix, str.length - suffix.length) !== -1;
    }

    /**
     * Register context into global variable g.
     * @param context
     */
    function registerContext(context) {
        if (typeof g === 'undefined') {
            throw new Error("Global variable g is not defined.");
        }

        $.each(context, function (key, value) {
            if (g.hasOwnProperty(key)) {
                throw new Error("The key '" + key + "' already exists in the global variable g.");
            }
            g[key] = value;
        });
    }

    window.urlFor = urlFor;
    window.registerContext = registerContext;
})();
<commit_msg>Clean codes in urlFor.<commit_after>(function () {
    // Add CSRF token header for Ajax request
    $.ajaxSetup({
        beforeSend: function (xhr, settings) {
            if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) {
                xhr.setRequestHeader("X-CSRFToken", g.csrfToken);
            }
        }
    });

    // Find out params in routing rules
    var pattern = new RegExp("<[^:]*:?([^>]+)>", "g");
    var result = null;

    $.each(g.rules, function (endpoint, rules) {
        $.each(rules, function (index, rule) {
            rule.params = [];
            while ((result = pattern.exec(rule.rule)) !== null) {
                rule.params.push(result[1]);
            }
        });
    });

    /**
     * Generate url for the endpoint.
     * urlFor(endpoint [, parameters] [, external])
     * @param endpoint
     * @param parameters
     * @param external
     * @returns url for the endpoint.
     */
    function urlFor(endpoint, parameters, external) {
        var url = null,
            params = [],
            maxMatchDegree = 0.0,
            keys;

        if ($.type(parameters) === "boolean") {
            external = parameters
        }

        parameters = ($.type(parameters) !== 'undefined') ? parameters : {};
        external = ($.type(external) !== 'undefined') ? external : false;

        if (g.rules[endpoint] === undefined) {
            throw new Error("Uncorrect endpoint in urlFor(\"" + endpoint + "\", " +
                JSON.stringify(parameters) + ")");
        }

        keys = $.map(parameters, function (value, key) {
            return key;
        });

        // Find the first matched rule among rules in this endpoint.
        $.each(g.rules[endpoint], function (index, rule) {
            var match = true,
                currentMatchDegree = 0.0;

            $.each(rule.params, function (index, param) {
                if ($.inArray(param, keys) === -1) {
                    match = false;
                    return false;
                }
            });

            if (match) {
                currentMatchDegree = parseFloat(rule.params.length) / keys.length;
                if (currentMatchDegree > maxMatchDegree || url === null) {
                    maxMatchDegree = currentMatchDegree;
                    url = rule.rule;
                    params = rule.params;
                }
            }
        });

        if (url) {
            $.each(keys, function (index, key) {
                // Build in params
                if ($.inArray(key, params) > -1) {
                    url = url.replace(new RegExp("<[^:]*:?" + key + ">"), parameters[key]);
                } else {
                    // Query string params
                    if (url.indexOf("?") === -1) {
                        url += "?";
                    }
                    if (!endsWith(url, '?')) {
                        url += "&";
                    }
                    url += key + "=" + parameters[key];
                }
            });
        } else {
            throw new Error("Uncorrect parameters in " + "urlFor(\"" + endpoint + "\", " +
                JSON.stringify(parameters) + ")");
        }

        if (external) {
            url = g.domain + url
        }

        return url;
    }

    /**
     * Check whether str ends with suffix.
     * @param str
     * @param suffix
     * @returns {boolean}
     */
    function endsWith(str, suffix) {
        return str.indexOf(suffix, str.length - suffix.length) !== -1;
    }

    /**
     * Register context into global variable g.
     * @param context
     */
    function registerContext(context) {
        if (typeof g === 'undefined') {
            throw new Error("Global variable g is not defined.");
        }

        $.each(context, function (key, value) {
            if (g.hasOwnProperty(key)) {
                throw new Error("The key '" + key + "' already exists in the global variable g.");
            }
            g[key] = value;
        });
    }

    window.urlFor = urlFor;
    window.registerContext = registerContext;
})();
<|endoftext|>


<commit_before>//= require jquery.fileupload
//= require jquery.iframe-transport
//= require dui/uploader/uploader

var UploaderView = Backbone.View.extend({
  events: {
    'click [data-uploader-button]': 'selectFile',
    'click .uploader-filename': 'download',
    'click .uploader-remove-file': 'removeOrAbort'
  },

  availableStatus: ['existing', 'started', 'failed', 'done', 'finished'],
  bytesUnits: [' bytes', 'kb', 'mb', 'gb', 'tb'],

  initialize: function () {
    _(this).bindAll()

    this.configureModel()
    this.configureModelListeners()
    this.configureUI()
    this.configureInitialState()
    this.configureFileUpload()
  },

  configureModel: function () {
    var model = new Uploader

    model.url           = this.$el.data('uploader-status-url')
    model.dataType      = this.$el.data('uploader-status-data-type')
    model.pusherApiKey  = this.$el.data('uploader-pusher-api-key')
    model.pusherChannel = this.$el.data('uploader-pusher-channel') || this._generatePusherChannel()

    this.model = model
  },

  configureModelListeners: function () {
    this.listenTo(this.model, 'change:filename',   this.filenameChanged, this)
    this.listenTo(this.model, 'change:size',       this.sizeChanged, this)
    this.listenTo(this.model, 'change:url',        this.urlChanged, this)
    this.listenTo(this.model, 'change:started_at', this.started, this)
    this.listenTo(this.model, 'change:loaded',     this.updateProgress, this)
    this.listenTo(this.model, 'change:error',      this.error, this)
    this.listenTo(this.model, 'change:done',       this.done, this)
    this.listenTo(this.model, 'change:finished',   this.finished, this)
    this.listenTo(this.model, 'reset',             this.reset, this)
  },

  configureUI: function () {
    this.$input           = this.$('input[type=hidden]')
    this.$progress        = this.$('.uploader-progress')
    this.$details         = this.$('.uploader-details')
    this.$detailsTemplate = this.$('[data-template-name=uploader-details]')
    this.$removeButton    = this.$('.uploader-remove-file')

    this.messages         = this.$el.data('uploader-messages') || {}

    this.$removeButton.tooltip({
      placement: 'left',
      title: this._removeButtonTooltipTitle,
      delay: 200
    })

    this.$details.toggleClass('no-textual-progress', !$.support.xhrFileUpload)
  },

  configureInitialState: function () {
    var fileAttributes = this.$el.data('uploader-file')

    if (fileAttributes && !_(fileAttributes).isEmpty()) {
      fileAttributes = _(fileAttributes).pick('filename', 'url', 'size')

      this.model.set(fileAttributes)
      this._updateStatusClass('existing')
    } else {
      this.reset()
    }
  },

  configureFileUpload: function () {
    var url       = this.$el.data('uploader-url'),
        redirect  = this.$el.data('uploader-iframe-redirect-url')
        params    = _(this.$el.data('uploader-params') || {}).extend({
          channel: this.model.pusherChannel
        })

    this.$('input:file').fileupload({
      url:         url,
      redirect:    redirect,
      formData:    params,
      dataType:    'json',
      dropZone:    null,
      start:       this.uploadStart,
      add:         this.uploadAdd,
      progressall: this.uploadProgressAll,
      fail:        this.uploadFail,
      done:        this.uploadDone
    })
  },

  filenameChanged: function () {
    var filename = this.model.get('filename') || ''

    this._updateDetail('filename', filename)
    this.$input.val(filename)
  },

  sizeChanged: function () {
    var size      = this.model.get('size'),
        humanSize = this._toHumanFileSize(size)

    this._updateDetail('total-size', humanSize)
    this.$details.toggleClass('no-size', _(size).isUndefined())
                 .find('.uploader-text-progress').attr('title', humanSize)
  },

  urlChanged: function () {
    this.$details.find('.uploader-filename').attr('href', this.model.get('url'))
  },

  started: function () {
    if (this.model.get('started_at')) {
      this._updateStatusClass('started')
      this.trigger('uploader:started', this.model)
    }
  },

  reset: function () {
    this._updateDetail('message', this.messages.initial)
    this._updateStatusClass()
    this.trigger('uploader:reset', this.model)
  },

  uploadAdd: function (e, data) {
    var file = data.files[0]

    if (file) {
      this.model.set({
        filename: file.name,
        size: file.size
      })
    }

    this.currentUpload = data.submit()
  },

  updateProgress: function () {
    var percentualProgress = this._toHumanPercentage(this.model.percentualProgress()),
        loadedBytes        = this._toHumanFileSize(this.model.loadedBytes()),
        size               = this.model.get('size'),
        totalSize          = this._toHumanFileSize(size),
        title              = percentualProgress + " - " + loadedBytes + " - " + totalSize

    this.$progress.find('.bar').css('width', percentualProgress)

    if (size) {
      this._updateDetail('loaded-size', loadedBytes)
      this._updateDetail('percentual',  percentualProgress)

      this.$details.removeClass('no-size').find('.uploader-text-progress').attr('title', title)
    } else {
      this.$details.addClass('no-size')
    }
  },

  error: function () {
    var currentError = this.model.get('error')

    if (currentError) {
      this._updateDetail('message', this.messages.error)
      this._updateStatusClass('failed')
      this.trigger('error', this.model.get('error'))
    }
  },

  done: function () {
    if (this.model.isDone()) {
      this._updateStatusClass('done')
    }
  },

  finished: function () {
    if (this.model.get('finished')) {
      this._updateStatusClass('finished')
      this.trigger('uploader:finished', this.model)
    }
  },

  uploadStart: function () {
    this.model.set({
      started_at: new Date,
      url: '',
      loaded: 0,
      total: 0,
      error: undefined,
      done: undefined,
      finished: undefined
    })
  },

  uploadProgressAll: function (e, data) {
    this.model.set({ total: parseInt(data.total, 10), loaded: parseInt(data.loaded, 10) })
  },

  uploadFail: function (e, options) {
    if (options.errorThrown == 'abort') {
      this.model.unset('error')
    } else {
      this.model.set({ error: options, started_at: undefined })
    }
  },

  uploadDone: function (e, data) {
    var url  = data.result[0].url,
        size = data.result[0].size

    this.model.set({ url: url, size: size, done: true })
  },

  selectFile: function () {
    if (!this.model.isUploading()) {
      this.$('input:file').click()
    }

    return false
  },

  download: function () {
    return !this.model.isUploading()
  },

  removeOrAbort: function (event) {
    var $element = $(event.currentTarget)

    $element.tooltip('hide')

    if (this.model.isUploading()) {
      this.currentUpload && this.currentUpload.abort()
      this.model.abort()
    } else {
      this._reset()
    }

    return false
  },

  _reset: function () {
    this.model.reset()
  },

  _updateStatusClass: function (status) {
    var removeStatus = _(this.availableStatus).map(function (s) { return 'uploader-' + s }),
        addClass     = status ? 'uploader-' + status : undefined

    this.$el.removeClass(removeStatus.join(' ')).addClass(addClass)
  },

  _updateDetail: function (name, value) {
    this.$details.find('.uploader-' + name).text(value)
  },

  _toHumanPercentage: function(percentual) {
    return ((percentual * 100).toFixed(1) * 1) + '%'
  },

  _toHumanFileSize: function (size) {
    var power       = size > 0 ? Math.floor(Math.log(size) / Math.log(1024)) : 0,
        sizeInBytes = (size / Math.pow(1024, power)).toFixed(2) * 1

    return sizeInBytes + '' + this.bytesUnits[power]
  },

  _generatePusherChannel: function () {
    return 'uploader-' + Math.floor(Math.random() * 1000000)
  },

  _removeButtonTooltipTitle: function () {
    return this.model.isUploading() ? this.messages.abort : this.messages.remove
  }
})
<commit_msg>Properly resizing the bar when an error occurs<commit_after>//= require jquery.fileupload
//= require jquery.iframe-transport
//= require dui/uploader/uploader

var UploaderView = Backbone.View.extend({
  events: {
    'click [data-uploader-button]': 'selectFile',
    'click .uploader-filename': 'download',
    'click .uploader-remove-file': 'removeOrAbort'
  },

  availableStatus: ['existing', 'started', 'failed', 'done', 'finished'],
  bytesUnits: [' bytes', 'kb', 'mb', 'gb', 'tb'],

  initialize: function () {
    _(this).bindAll()

    this.configureModel()
    this.configureModelListeners()
    this.configureUI()
    this.configureInitialState()
    this.configureFileUpload()
  },

  configureModel: function () {
    var model = new Uploader

    model.url           = this.$el.data('uploader-status-url')
    model.dataType      = this.$el.data('uploader-status-data-type')
    model.pusherApiKey  = this.$el.data('uploader-pusher-api-key')
    model.pusherChannel = this.$el.data('uploader-pusher-channel') || this._generatePusherChannel()

    this.model = model
  },

  configureModelListeners: function () {
    this.listenTo(this.model, 'change:filename',   this.filenameChanged, this)
    this.listenTo(this.model, 'change:size',       this.sizeChanged, this)
    this.listenTo(this.model, 'change:url',        this.urlChanged, this)
    this.listenTo(this.model, 'change:started_at', this.started, this)
    this.listenTo(this.model, 'change:loaded',     this.updateProgress, this)
    this.listenTo(this.model, 'change:error',      this.error, this)
    this.listenTo(this.model, 'change:done',       this.done, this)
    this.listenTo(this.model, 'change:finished',   this.finished, this)
    this.listenTo(this.model, 'reset',             this.reset, this)
  },

  configureUI: function () {
    this.$input           = this.$('input[type=hidden]')
    this.$progress        = this.$('.uploader-progress')
    this.$details         = this.$('.uploader-details')
    this.$detailsTemplate = this.$('[data-template-name=uploader-details]')
    this.$removeButton    = this.$('.uploader-remove-file')

    this.messages         = this.$el.data('uploader-messages') || {}

    this.$removeButton.tooltip({
      placement: 'left',
      title: this._removeButtonTooltipTitle,
      delay: 200
    })

    this.$details.toggleClass('no-textual-progress', !$.support.xhrFileUpload)
  },

  configureInitialState: function () {
    var fileAttributes = this.$el.data('uploader-file')

    if (fileAttributes && !_(fileAttributes).isEmpty()) {
      fileAttributes = _(fileAttributes).pick('filename', 'url', 'size')

      this.model.set(fileAttributes)
      this._updateStatusClass('existing')
    } else {
      this.reset()
    }
  },

  configureFileUpload: function () {
    var url       = this.$el.data('uploader-url'),
        redirect  = this.$el.data('uploader-iframe-redirect-url')
        params    = _(this.$el.data('uploader-params') || {}).extend({
          channel: this.model.pusherChannel
        })

    this.$('input:file').fileupload({
      url:         url,
      redirect:    redirect,
      formData:    params,
      dataType:    'json',
      dropZone:    null,
      start:       this.uploadStart,
      add:         this.uploadAdd,
      progressall: this.uploadProgressAll,
      fail:        this.uploadFail,
      done:        this.uploadDone
    })
  },

  filenameChanged: function () {
    var filename = this.model.get('filename') || ''

    this._updateDetail('filename', filename)
    this.$input.val(filename)
  },

  sizeChanged: function () {
    var size      = this.model.get('size'),
        humanSize = this._toHumanFileSize(size)

    this._updateDetail('total-size', humanSize)
    this.$details.toggleClass('no-size', _(size).isUndefined())
                 .find('.uploader-text-progress').attr('title', humanSize)
  },

  urlChanged: function () {
    this.$details.find('.uploader-filename').attr('href', this.model.get('url'))
  },

  started: function () {
    if (this.model.get('started_at')) {
      this._updateStatusClass('started')
      this.trigger('uploader:started', this.model)
    }
  },

  reset: function () {
    this._updateDetail('message', this.messages.initial)
    this._updateStatusClass()
    this.trigger('uploader:reset', this.model)
  },

  uploadAdd: function (e, data) {
    var file = data.files[0]

    if (file) {
      this.model.set({
        filename: file.name,
        size: file.size
      })
    }

    this.currentUpload = data.submit()
  },

  updateProgress: function () {
    var percentualProgress = this._toHumanPercentage(this.model.percentualProgress()),
        loadedBytes        = this._toHumanFileSize(this.model.loadedBytes()),
        size               = this.model.get('size'),
        totalSize          = this._toHumanFileSize(size),
        title              = percentualProgress + " - " + loadedBytes + " - " + totalSize

    this.$progress.find('.bar').css('width', percentualProgress)

    if (size) {
      this._updateDetail('loaded-size', loadedBytes)
      this._updateDetail('percentual',  percentualProgress)

      this.$details.removeClass('no-size').find('.uploader-text-progress').attr('title', title)
    } else {
      this.$details.addClass('no-size')
    }
  },

  error: function () {
    var currentError = this.model.get('error')

    if (currentError) {
      this._updateDetail('message', this.messages.error)
      this.$progress.find('.bar').css('width', '')
      this._updateStatusClass('failed')
      this.trigger('error', this.model.get('error'))
    }
  },

  done: function () {
    if (this.model.isDone()) {
      this._updateStatusClass('done')
    }
  },

  finished: function () {
    if (this.model.get('finished')) {
      this._updateStatusClass('finished')
      this.trigger('uploader:finished', this.model)
    }
  },

  uploadStart: function () {
    this.model.set({
      started_at: new Date,
      url: '',
      loaded: 0,
      total: 0,
      error: undefined,
      done: undefined,
      finished: undefined
    })
  },

  uploadProgressAll: function (e, data) {
    this.model.set({ total: parseInt(data.total, 10), loaded: parseInt(data.loaded, 10) })
  },

  uploadFail: function (e, options) {
    if (options.errorThrown == 'abort') {
      this.model.unset('error')
    } else {
      this.model.set({ error: options, started_at: undefined })
    }
  },

  uploadDone: function (e, data) {
    var url  = data.result[0].url,
        size = data.result[0].size

    this.model.set({ url: url, size: size, done: true })
  },

  selectFile: function () {
    if (!this.model.isUploading()) {
      this.$('input:file').click()
    }

    return false
  },

  download: function () {
    return !this.model.isUploading()
  },

  removeOrAbort: function (event) {
    var $element = $(event.currentTarget)

    $element.tooltip('hide')

    if (this.model.isUploading()) {
      this.currentUpload && this.currentUpload.abort()
      this.model.abort()
    } else {
      this._reset()
    }

    return false
  },

  _reset: function () {
    this.model.reset()
  },

  _updateStatusClass: function (status) {
    var removeStatus = _(this.availableStatus).map(function (s) { return 'uploader-' + s }),
        addClass     = status ? 'uploader-' + status : undefined

    this.$el.removeClass(removeStatus.join(' ')).addClass(addClass)
  },

  _updateDetail: function (name, value) {
    this.$details.find('.uploader-' + name).text(value)
  },

  _toHumanPercentage: function(percentual) {
    return ((percentual * 100).toFixed(1) * 1) + '%'
  },

  _toHumanFileSize: function (size) {
    var power       = size > 0 ? Math.floor(Math.log(size) / Math.log(1024)) : 0,
        sizeInBytes = (size / Math.pow(1024, power)).toFixed(2) * 1

    return sizeInBytes + '' + this.bytesUnits[power]
  },

  _generatePusherChannel: function () {
    return 'uploader-' + Math.floor(Math.random() * 1000000)
  },

  _removeButtonTooltipTitle: function () {
    return this.model.isUploading() ? this.messages.abort : this.messages.remove
  }
})
<|endoftext|>