<commit_before>/**
 * @file
 * A JSON-esque schema parser for parsing compound structures.
 * See unit tests for example usages.
 */
Module("Cactus.Util", function (m) {
  var collection = Cactus.Data.Collection;
  var JSON = Cactus.Util.JSON;
  var object = Cactus.Addon.Object;
  var StrictMap = Cactus.Data.StrictMap;

  var builtInValidators = new StrictMap({
    natural : {
      func : function (v) {
        return v >= 0;
      },
      message : "Expected natural number."
    },
    positive : {
      func : function (v) {
        return v >= 1;
      },
      message : "Expected positive number."
    },
    negative : {
      func : function (v) {
        return v < 0;
      },
      message : "Expected negative number."
    },
    "non empty string" : {
      func : function (v) {
        return v !== "";
      },
      message : "Expected non-empty string."
    }
  });

  var ErrorMessage = Class("ErrorMessage", {
    has : {
      errors : {
        init : function () { return {}; }
      }
    },
    methods : {
      has : function () {
        return !object.isEmpty(this.errors);
      },
      add : function (property, message) {
        this.errors[property] = this.errors[property] || [];
        this.errors[property].push(message);
      },
      addSub : function (property, subErrorProperty, subErrorMessage) {
        this.errors[property + (subErrorProperty ? "." : "") + subErrorProperty] = subErrorMessage;
      },
      addSubObj : function (property, subErrorMessage) {
        var subErrors = subErrorMessage.get();
        for (var p in subErrors) {
          this.addSub(property, p, subErrors[p]);
        }
      },
      get : function () {
        return this.errors;
      },
      _getErrorString : function (p, msg) {
        if (p) {
          return 'Options: Error in property "%s": %s'.format(p, msg);
        } else {
          return 'Options: Error: %s'.format(msg);
        }
      },
      getErrorString : function () {
        var errorStrings = [];
        for (var p in this.errors) {
          errorStrings.push(this._getErrorString(p, this.errors[p].join(" ")));
        }
        return errorStrings.join("\n");
      },
      getMessages : function () {
        var messages = [];
        for (var p in this.errors) {
          messages.push(this._getErrorString(p, this.errors[p]));
        }
        return messages;
      }
    }
  });

  var types = {};
  var Type = Class({
    methods : {
      check : function () {
        throw new Error("Type: abstract check.");
      },
      getName : function () {
        throw new Error("Type: abstract getName.");
      }
    }
  });
  var T_Union = Class({
    isa : Type,
    has : {
      types : null
    },
    methods : {
      BUILD : function (types) {
        return {
          types : types || []
        };
      },
      check : function (actualType, value, em) {
        if (!collection.hasValue(this.types, actualType)) {
          em.add("", "Expected a Union");
        }
      },
      getName : function () {
        return { union : this.types };
      }
    }
  });
  types.T_Union = T_Union;
  var T_Instance = Class({
    isa : Type,
    has : {
      _constructor : null
    },
    methods : {
      BUILD : function (constructor) {
        return {
          _constructor : constructor
        };
      },
      check : function (actualType, value, em) {
        if (!(value instanceof this._constructor)) {
          em.add("", 'Expected an instance of "%s", but got value <%s> (type "%s")'
                 .format(this._getName(), value, actualType));
        }
      },
      _getName : function () {
        var c = this._constructor;
        if ("meta" in c) {
          return c.meta.name;
        } else if (c.name) {
          return c.name;
        } else {
          return "anonymous type";
        }
        throw new Error("T_Instance: unhandled constructor: " + c);
      },
      getName : function () {
        return { type : this._constructor };
      }
    }
  });
  types.T_Instance = T_Instance;

  var T_Enumerable = Class({
    isa : Type,
    has : {
      values : { init : function () { return []; } }
    },
    methods : {
      BUILD : function (values) {
        return {
          values : values
        };
      },
      check : function (actualType, value, em) {
        if (!collection.hasValue(this.values, value)) {
          em.add("", "Expected a value in [" + this.values + "], but got " + value);
        }
      },
      getName : function () {
        return { enumerable : this.values };
      }
    }
  });
  types.T_Enumerable = T_Enumerable;

  var typeFail = function (type, value) {
    return "Expected " + JSON.stringify(Options.gettype(type)) +
      ", but got " + JSON.stringify(Options.typeof(value));
  };

  var T_Mixed = Class({
    isa : Type,
    methods : {
      check : function (actualType, value, em) {
        // Accept all.
      },
      getName : function () {
        return "mixed";
      }
    }
  });
  types.T_Mixed = T_Mixed;

  var T_Array = Class({
    isa : Type,
    has : {
      elementType : null
    },
    methods : {
      BUILD : function (elementType) {
        return {
          elementType : elementType
        };
      },
      check : function (actualType, value, em) {
        var subOptions = [];
        if (value instanceof Array) {
          var vs = [];
          for (var i = 0; i < value.length; i++) {
            var option = new Options(this.elementType);
            subOptions.push(option);
            vs.push(option.parse(value[i], false));
            em.addSubObj(i, option._getErrorMessage());
          }
        } else {
          em.add("", typeFail([this.elementType], value));
        }
      },
      getName : function () {
        return [this.elementType];
      }
    }
  });
  types.T_Array = T_Array;

  var Options = Class("Options", {
    has : {
      definition : null,
      errorMessage : null
    },
    my : {
      methods : {
        /**
         * @return string
         *   The name of a type
         */
        gettype : function (value) {
          if (value instanceof Type) {
            return value.getName();
          } else if (typeof value === "string") {
            return value;
          } else if (value instanceof Array) {
            // > Remove
            return [{ type : value[0].type }];
          } else if (value instanceof Function) {
            // > Remove
            if ("meta" in value) {
              return value.meta.name;
            } else if (value.name) {
              return value.name;
            } else {
              return "anonymous type";
            }
          } else if (value instanceof Object) {
            var h = {};
            for (var p in value) {
              var v = value[p];
              h[p] = { type : v.type };
            }
            return h;
          } else {
            throw new Error("Options.gettype: Unhandled type: " + value + ". Did you specify a type?");
          }
        },
        /**
         * @return string
         *   The type of a value.
         */
        typeof : function (v) {
          if (typeof v === "function") {
            return "Function";
          }
          if (typeof v !== "object") {
            return typeof v;
          }
          if (v === null) {
            return "null";
          }
          if (typeof v === "object") {
            if ("meta" in v) {
              return v.meta.name;
            }
            if (v.constructor.name === "") {
              return "anonymous type";
            }
            return v.constructor.name;
          }
          throw new Error("Options:typeof: unhandled value " + v);
        }
      }
    },
    methods : {
      BUILD : function (definition, throwErrors) {
        throwErrors = throwErrors === undefined ? true : !!throwErrors;
        this.definition = definition;
        if (!(definition instanceof Object)) {
          throw new Error("Options:BUILD: definition must be a hash.");
        }
        if ("defaultValue" in definition || "defaultValueFunc" in definition) {
          definition.required = false;
        } else if ("required" in definition) {
          definition.required = !!definition.required;
        } else {
          definition.required = true;
        }

        definition.validators = definition.validators || [];

        if (definition.union) {
          definition.type = new T_Union(definition.union);
          delete definition.union;
        } else if (definition.enumerable) {
          definition.type = new T_Enumerable(definition.enumerable);
          delete definition.enumerable;
        } else if (definition.type instanceof Array) {
          definition.type = new T_Array(definition.type[0]);
        } else if (definition.type === "mixed") {
          definition.type = new T_Mixed();
        } else if (definition.type instanceof Function) {
          var type = definition.type;
          definition.type = new T_Instance(type);
        }

        if (!(definition instanceof Array) && !("type" in definition) && !(definition.type instanceof Type)) {
          throw new Error('Options:BUILD: Missing "type" or "enumerable" or "union" in definition');
        }
      },
      parse : function (options, throwErrors) {
        this.errorMessage = new ErrorMessage();
        var subOptions = [];
        var validationMessage = this.definition.validationMessage || "";
        throwErrors = throwErrors === undefined ? true : !!throwErrors;
        var throwError = function () {
          if (throwErrors && this.errorMessage.has()) {
            var e = new Error(this.errorMessage.getErrorString());
            e.hash = this.getErrors();
            throw e;
          }
        }.bind(this);
        var fail = function (p, message) {
          this.errorMessage.add(p, message);
          throwError();
        }.bind(this);
        var primitiveFail = function () {
          return fail("", "Expected " + JSON.stringify(Options.gettype(this.definition.type)) +
               ", but got " + JSON.stringify(Options.typeof(options)));
        }.bind(this);
        if (this.definition.type instanceof Type) {
          var actualType = Options.typeof(options);
          this.definition.type.check(actualType, options, this.errorMessage);
          if (this.errorMessage.has() && throwErrors) {
            throw new Error(this.errorMessage.getErrorString());
          }
        } else if (this.definition.map) {
          // Maps.
          var def = { type : this.definition.type };
          var m = {};
          for (var p in options) {
            var option = new Options(def, false);
            m[p] = option.parse(options[p], false);
            this.errorMessage.addSubObj(p, option._getErrorMessage());
          }
          throwError();
          options = m;
        } else if (this.definition.type instanceof Object) {
          // Hashes.
          if (!(options instanceof Object)) {
            primitiveFail();
          }
          var h = {};
          // All properties in the definition.
          for (var p in this.definition.type) if (this.definition.type.hasOwnProperty(p)) {
            var def = this.definition.type[p];
            if (!("required" in def)) {
              def.required = true;
            }
            if ("defaultValue" in def || "defaultValueFunc" in def) {
              def.required = false;
            }

            if (p in options) {
              var val = options[p];
              if (!(def.required === false && val === undefined)) {
                var option = new Options(def, false);
                h[p] = option.parse(val, false);
                this.errorMessage.addSubObj(p, option._getErrorMessage());
              } else {
                // Value is undefined and not required,
                // and the property won't be set in the hash.
                h[p] = undefined;
              }
            } else {
              h[p] = undefined;

              if (def.required === false && "defaultValue" in def) {
                h[p] = def.defaultValue;
              } else if (def.required === false && "defaultValueFunc" in def) {
                h[p] = def.defaultValueFunc();
                var option = new Options(def, false);
                h[p] = option.parse(h[p], false);
                this.errorMessage.addSubObj(p, option._getErrorMessage());
              } else if (def.required === false) {
                delete h[p];
              } else if (def.required !== false) {
                this.errorMessage.add(p, "Missing property");
              }
            }
          }
          // Check for properties in the parsed hash that are not in the definition.
          for (p in options) if (options.hasOwnProperty(p)) {
            if (!(p in h)) {
              this.errorMessage.add(p, "Property lacks definition");
            }
          }
          for (p in h) {
            if (h[p] === undefined) {
              delete h[p];
            }
          }
          throwError();
          options = h;
        } else if (this.definition.type === typeof options) {
          // Standard types: string, number, boolean.
        } else if (this.definition.required === false && options === null) {
          if ("defaultValue" in this.definition) {
            options = this.definition.defaultValue;
          } else if ("defaultValueFunc" in this.definition) {
            options = this.definition.defaultValueFunc();
          } else {
            // .
          }
        } else if (this.definition.required === false && options === undefined) {
          fail("", "undefined is not an allowed atomic value.");
        } else {
          return primitiveFail();
        }

        var vs = this.definition.validators;
        var valid = true;
        var messages = [];
        for (var i = 0; i < vs.length; i++) {
          var f;
          var msg;
          if (typeof vs[i] === "string") {
            if (typeof vs[i] === "string" && !builtInValidators.has(vs[i])) {
              throw new Error('Options:BUILD: undefined built in validator "%s"'.format(vs[i]));
            }

            var validator = builtInValidators.get(vs[i]);
            f = validator.func;
            msg = validator.message;
          } else {
            f = vs[i].func;
            msg = vs[i].message;
          }
          if (!f(options)) {
            valid = false;
            var message = msg || "got " + options + ".";
            this.errorMessage.add("", "Validation failed: " + message);
          }
        }
        if (valid) {
          return options;
        } else {
          if (throwErrors) {
            throw new Error(this.errorMessage.getErrorString());
          }
        }

        return options;
      },
      /**
       * @param mixed value
       */
      _getErrorMessage : function () {
        return this.errorMessage;
      },
      hasErrors : function () {
        return this.errorMessage.has();
      },
      getErrors : function () {
        return this.errorMessage.get();
      },
      getErrorMessages : function () {
        return this.errorMessage.getMessages();
      }
    }
  });
  /**
   * Simpler interface for creating Options parsers.
   * It only supports the atomic types, hashes and arrays.
   * Does not allow for maps, defaultValues, validators.
   * It's possible to have inner types that use the regular interface, just pass
   * a hash with the _type property and then specify types like in the regular
   * interface.
   *
   * @param Hash args
   * @return Options
   */
  Options.simple = function (definition) {
    function createType(simpleType) {
      if (simpleType instanceof Array) {
        return {
          type : [createType(simpleType[0])]
        };
      } else if (simpleType instanceof Object && simpleType._type) {
        return { type : simpleType._type };
      } else if (simpleType instanceof Object) {
        var h = {
          type : {}
        };
        for (var p in simpleType) {
          h.type[p] = createType(simpleType[p]);
        }
        return h;
      } else {
        return { type : simpleType };
      }
    }

    return new Options(createType(definition));
  };

  Options.types = types;
});
<commit_msg>Removed unused code.<commit_after>/**
 * @file
 * A JSON-esque schema parser for parsing compound structures.
 * See unit tests for example usages.
 */
Module("Cactus.Util", function (m) {
  var collection = Cactus.Data.Collection;
  var JSON = Cactus.Util.JSON;
  var object = Cactus.Addon.Object;
  var StrictMap = Cactus.Data.StrictMap;

  var builtInValidators = new StrictMap({
    natural : {
      func : function (v) {
        return v >= 0;
      },
      message : "Expected natural number."
    },
    positive : {
      func : function (v) {
        return v >= 1;
      },
      message : "Expected positive number."
    },
    negative : {
      func : function (v) {
        return v < 0;
      },
      message : "Expected negative number."
    },
    "non empty string" : {
      func : function (v) {
        return v !== "";
      },
      message : "Expected non-empty string."
    }
  });

  var ErrorMessage = Class("ErrorMessage", {
    has : {
      errors : {
        init : function () { return {}; }
      }
    },
    methods : {
      has : function () {
        return !object.isEmpty(this.errors);
      },
      add : function (property, message) {
        this.errors[property] = this.errors[property] || [];
        this.errors[property].push(message);
      },
      addSub : function (property, subErrorProperty, subErrorMessage) {
        this.errors[property + (subErrorProperty ? "." : "") + subErrorProperty] = subErrorMessage;
      },
      addSubObj : function (property, subErrorMessage) {
        var subErrors = subErrorMessage.get();
        for (var p in subErrors) {
          this.addSub(property, p, subErrors[p]);
        }
      },
      get : function () {
        return this.errors;
      },
      _getErrorString : function (p, msg) {
        if (p) {
          return 'Options: Error in property "%s": %s'.format(p, msg);
        } else {
          return 'Options: Error: %s'.format(msg);
        }
      },
      getErrorString : function () {
        var errorStrings = [];
        for (var p in this.errors) {
          errorStrings.push(this._getErrorString(p, this.errors[p].join(" ")));
        }
        return errorStrings.join("\n");
      },
      getMessages : function () {
        var messages = [];
        for (var p in this.errors) {
          messages.push(this._getErrorString(p, this.errors[p]));
        }
        return messages;
      }
    }
  });

  var types = {};
  var Type = Class({
    methods : {
      check : function () {
        throw new Error("Type: abstract check.");
      },
      getName : function () {
        throw new Error("Type: abstract getName.");
      }
    }
  });
  var T_Union = Class({
    isa : Type,
    has : {
      types : null
    },
    methods : {
      BUILD : function (types) {
        return {
          types : types || []
        };
      },
      check : function (actualType, value, em) {
        if (!collection.hasValue(this.types, actualType)) {
          em.add("", "Expected a Union");
        }
      },
      getName : function () {
        return { union : this.types };
      }
    }
  });
  types.T_Union = T_Union;
  var T_Instance = Class({
    isa : Type,
    has : {
      _constructor : null
    },
    methods : {
      BUILD : function (constructor) {
        return {
          _constructor : constructor
        };
      },
      check : function (actualType, value, em) {
        if (!(value instanceof this._constructor)) {
          em.add("", 'Expected an instance of "%s", but got value <%s> (type "%s")'
                 .format(this._getName(), value, actualType));
        }
      },
      _getName : function () {
        var c = this._constructor;
        if ("meta" in c) {
          return c.meta.name;
        } else if (c.name) {
          return c.name;
        } else {
          return "anonymous type";
        }
        throw new Error("T_Instance: unhandled constructor: " + c);
      },
      getName : function () {
        return { type : this._constructor };
      }
    }
  });
  types.T_Instance = T_Instance;

  var T_Enumerable = Class({
    isa : Type,
    has : {
      values : { init : function () { return []; } }
    },
    methods : {
      BUILD : function (values) {
        return {
          values : values
        };
      },
      check : function (actualType, value, em) {
        if (!collection.hasValue(this.values, value)) {
          em.add("", "Expected a value in [" + this.values + "], but got " + value);
        }
      },
      getName : function () {
        return { enumerable : this.values };
      }
    }
  });
  types.T_Enumerable = T_Enumerable;

  var typeFail = function (type, value) {
    return "Expected " + JSON.stringify(Options.gettype(type)) +
      ", but got " + JSON.stringify(Options.typeof(value));
  };

  var T_Mixed = Class({
    isa : Type,
    methods : {
      check : function (actualType, value, em) {
        // Accept all.
      },
      getName : function () {
        return "mixed";
      }
    }
  });
  types.T_Mixed = T_Mixed;

  var T_Array = Class({
    isa : Type,
    has : {
      elementType : null
    },
    methods : {
      BUILD : function (elementType) {
        return {
          elementType : elementType
        };
      },
      check : function (actualType, value, em) {
        var subOptions = [];
        if (value instanceof Array) {
          var vs = [];
          for (var i = 0; i < value.length; i++) {
            var option = new Options(this.elementType);
            subOptions.push(option);
            vs.push(option.parse(value[i], false));
            em.addSubObj(i, option._getErrorMessage());
          }
        } else {
          em.add("", typeFail([this.elementType], value));
        }
      },
      getName : function () {
        return [this.elementType];
      }
    }
  });
  types.T_Array = T_Array;

  var Options = Class("Options", {
    has : {
      definition : null,
      errorMessage : null
    },
    my : {
      methods : {
        /**
         * @return string
         *   The name of a type
         */
        gettype : function (value) {
          if (value instanceof Type) {
            return value.getName();
          } else if (typeof value === "string") {
            return value;
          } else if (value instanceof Array) {
            // > Remove
            return [{ type : value[0].type }];
          } else if (value instanceof Object) {
            var h = {};
            for (var p in value) {
              var v = value[p];
              h[p] = { type : v.type };
            }
            return h;
          } else {
            throw new Error("Options.gettype: Unhandled type: " + value + ". Did you specify a type?");
          }
        },
        /**
         * @return string
         *   The type of a value.
         */
        typeof : function (v) {
          if (typeof v === "function") {
            return "Function";
          }
          if (typeof v !== "object") {
            return typeof v;
          }
          if (v === null) {
            return "null";
          }
          if (typeof v === "object") {
            if ("meta" in v) {
              return v.meta.name;
            }
            if (v.constructor.name === "") {
              return "anonymous type";
            }
            return v.constructor.name;
          }
          throw new Error("Options:typeof: unhandled value " + v);
        }
      }
    },
    methods : {
      BUILD : function (definition, throwErrors) {
        throwErrors = throwErrors === undefined ? true : !!throwErrors;
        this.definition = definition;
        if (!(definition instanceof Object)) {
          throw new Error("Options:BUILD: definition must be a hash.");
        }
        if ("defaultValue" in definition || "defaultValueFunc" in definition) {
          definition.required = false;
        } else if ("required" in definition) {
          definition.required = !!definition.required;
        } else {
          definition.required = true;
        }

        definition.validators = definition.validators || [];

        if (definition.union) {
          definition.type = new T_Union(definition.union);
          delete definition.union;
        } else if (definition.enumerable) {
          definition.type = new T_Enumerable(definition.enumerable);
          delete definition.enumerable;
        } else if (definition.type instanceof Array) {
          definition.type = new T_Array(definition.type[0]);
        } else if (definition.type === "mixed") {
          definition.type = new T_Mixed();
        } else if (definition.type instanceof Function) {
          var type = definition.type;
          definition.type = new T_Instance(type);
        }

        if (!(definition instanceof Array) && !("type" in definition) && !(definition.type instanceof Type)) {
          throw new Error('Options:BUILD: Missing "type" or "enumerable" or "union" in definition');
        }
      },
      parse : function (options, throwErrors) {
        this.errorMessage = new ErrorMessage();
        var subOptions = [];
        var validationMessage = this.definition.validationMessage || "";
        throwErrors = throwErrors === undefined ? true : !!throwErrors;
        var throwError = function () {
          if (throwErrors && this.errorMessage.has()) {
            var e = new Error(this.errorMessage.getErrorString());
            e.hash = this.getErrors();
            throw e;
          }
        }.bind(this);
        var fail = function (p, message) {
          this.errorMessage.add(p, message);
          throwError();
        }.bind(this);
        var primitiveFail = function () {
          return fail("", "Expected " + JSON.stringify(Options.gettype(this.definition.type)) +
               ", but got " + JSON.stringify(Options.typeof(options)));
        }.bind(this);
        if (this.definition.type instanceof Type) {
          var actualType = Options.typeof(options);
          this.definition.type.check(actualType, options, this.errorMessage);
          if (this.errorMessage.has() && throwErrors) {
            throw new Error(this.errorMessage.getErrorString());
          }
        } else if (this.definition.map) {
          // Maps.
          var def = { type : this.definition.type };
          var m = {};
          for (var p in options) {
            var option = new Options(def, false);
            m[p] = option.parse(options[p], false);
            this.errorMessage.addSubObj(p, option._getErrorMessage());
          }
          throwError();
          options = m;
        } else if (this.definition.type instanceof Object) {
          // Hashes.
          if (!(options instanceof Object)) {
            primitiveFail();
          }
          var h = {};
          // All properties in the definition.
          for (var p in this.definition.type) if (this.definition.type.hasOwnProperty(p)) {
            var def = this.definition.type[p];
            if (!("required" in def)) {
              def.required = true;
            }
            if ("defaultValue" in def || "defaultValueFunc" in def) {
              def.required = false;
            }

            if (p in options) {
              var val = options[p];
              if (!(def.required === false && val === undefined)) {
                var option = new Options(def, false);
                h[p] = option.parse(val, false);
                this.errorMessage.addSubObj(p, option._getErrorMessage());
              } else {
                // Value is undefined and not required,
                // and the property won't be set in the hash.
                h[p] = undefined;
              }
            } else {
              h[p] = undefined;

              if (def.required === false && "defaultValue" in def) {
                h[p] = def.defaultValue;
              } else if (def.required === false && "defaultValueFunc" in def) {
                h[p] = def.defaultValueFunc();
                var option = new Options(def, false);
                h[p] = option.parse(h[p], false);
                this.errorMessage.addSubObj(p, option._getErrorMessage());
              } else if (def.required === false) {
                delete h[p];
              } else if (def.required !== false) {
                this.errorMessage.add(p, "Missing property");
              }
            }
          }
          // Check for properties in the parsed hash that are not in the definition.
          for (p in options) if (options.hasOwnProperty(p)) {
            if (!(p in h)) {
              this.errorMessage.add(p, "Property lacks definition");
            }
          }
          for (p in h) {
            if (h[p] === undefined) {
              delete h[p];
            }
          }
          throwError();
          options = h;
        } else if (this.definition.type === typeof options) {
          // Standard types: string, number, boolean.
        } else if (this.definition.required === false && options === null) {
          if ("defaultValue" in this.definition) {
            options = this.definition.defaultValue;
          } else if ("defaultValueFunc" in this.definition) {
            options = this.definition.defaultValueFunc();
          } else {
            // .
          }
        } else if (this.definition.required === false && options === undefined) {
          fail("", "undefined is not an allowed atomic value.");
        } else {
          return primitiveFail();
        }

        var vs = this.definition.validators;
        var valid = true;
        var messages = [];
        for (var i = 0; i < vs.length; i++) {
          var f;
          var msg;
          if (typeof vs[i] === "string") {
            if (typeof vs[i] === "string" && !builtInValidators.has(vs[i])) {
              throw new Error('Options:BUILD: undefined built in validator "%s"'.format(vs[i]));
            }

            var validator = builtInValidators.get(vs[i]);
            f = validator.func;
            msg = validator.message;
          } else {
            f = vs[i].func;
            msg = vs[i].message;
          }
          if (!f(options)) {
            valid = false;
            var message = msg || "got " + options + ".";
            this.errorMessage.add("", "Validation failed: " + message);
          }
        }
        if (valid) {
          return options;
        } else {
          if (throwErrors) {
            throw new Error(this.errorMessage.getErrorString());
          }
        }

        return options;
      },
      /**
       * @param mixed value
       */
      _getErrorMessage : function () {
        return this.errorMessage;
      },
      hasErrors : function () {
        return this.errorMessage.has();
      },
      getErrors : function () {
        return this.errorMessage.get();
      },
      getErrorMessages : function () {
        return this.errorMessage.getMessages();
      }
    }
  });
  /**
   * Simpler interface for creating Options parsers.
   * It only supports the atomic types, hashes and arrays.
   * Does not allow for maps, defaultValues, validators.
   * It's possible to have inner types that use the regular interface, just pass
   * a hash with the _type property and then specify types like in the regular
   * interface.
   *
   * @param Hash args
   * @return Options
   */
  Options.simple = function (definition) {
    function createType(simpleType) {
      if (simpleType instanceof Array) {
        return {
          type : [createType(simpleType[0])]
        };
      } else if (simpleType instanceof Object && simpleType._type) {
        return { type : simpleType._type };
      } else if (simpleType instanceof Object) {
        var h = {
          type : {}
        };
        for (var p in simpleType) {
          h.type[p] = createType(simpleType[p]);
        }
        return h;
      } else {
        return { type : simpleType };
      }
    }

    return new Options(createType(definition));
  };

  Options.types = types;
});
<|endoftext|>


<commit_before>/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
*/

/**
 * Execute a cordova command.  It is up to the native side whether this action
 * is synchronous or asynchronous.  The native side can return:
 *      Synchronous: PluginResult object as a JSON string
 *      Asynchronous: Empty string ""
 * If async, the native side will cordova.callbackSuccess or cordova.callbackError,
 * depending upon the result of the action.
 *
 * @param {Function} success    The success callback
 * @param {Function} fail       The fail callback
 * @param {String} service      The name of the service to use
 * @param {String} action       Action to be run in cordova
 * @param {String[]} [args]     Zero or more arguments to pass to the method
 */
var cordova = require('cordova'),
    nativeApiProvider = require('cordova/plugin/android/nativeapiprovider'),
    jsToNativeModes = {
        PROMPT: 0,
        JS_OBJECT: 1,
        // This mode is currently for benchmarking purposes only. It must be enabled
        // on the native side through the ENABLE_LOCATION_CHANGE_EXEC_MODE
        // constant within CordovaWebViewClient.java before it will work.
        LOCATION_CHANGE: 2
    },
    nativeToJsModes = {
        // Polls for messages using the JS->Native bridge.
        POLLING: 0,
        // For LOAD_URL to be viable, it would need to have a work-around for
        // the bug where the soft-keyboard gets dismissed when a message is sent.
        LOAD_URL: 2,
        // For the ONLINE_EVENT to be viable, it would need to intercept all event
        // listeners (both through addEventListener and window.ononline) as well
        // as set the navigator property itself.
        ONLINE_EVENT: 3,
        // Uses reflection to access private APIs of the WebView that can send JS
        // to be executed.
        // Requires Android 3.2.4 or above.
        PRIVATE_API: 4
    },
    jsToNativeBridgeMode,  // Set lazily.
    nativeToJsBridgeMode = nativeToJsModes.ONLINE_EVENT,
    pollEnabled = false,
    messagesFromNative = [];

function androidExec(success, fail, service, action, args) {
    // Set default bridge modes if they have not already been set.
    if (jsToNativeBridgeMode === undefined) {
        androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
    }

    var callbackId = service + cordova.callbackId++,
        argsJson = JSON.stringify(args);
    if (success || fail) {
        cordova.callbacks[callbackId] = {success:success, fail:fail};
    }

    if (jsToNativeBridgeMode == jsToNativeModes.LOCATION_CHANGE) {
        window.location = 'http://cdv_exec/' + service + '#' + action + '#' + callbackId + '#' + argsJson;
    } else {
        var messages = nativeApiProvider.get().exec(service, action, callbackId, argsJson);
        // Explicit cast to string is required on Android 2.1 to convert from
        // a Java string to a JS string.
        if (messages) {
            messages = String(messages);
        }
        androidExec.processMessages(messages);
    }
}

function pollOnce() {
    var msg = nativeApiProvider.get().retrieveJsMessages();
    androidExec.processMessages(msg);
}

function pollingTimerFunc() {
    if (pollEnabled) {
      pollOnce();
      setTimeout(pollingTimerFunc, 50);
    }
}

function hookOnlineApis() {
    function proxyEvent(e) {
        cordova.fireWindowEvent(e.type);
    }
    // The network module takes care of firing online and offline events.
    // It currently fires them only on document though, so we bridge them
    // to window here (while first listening for exec()-releated online/offline
    // events).
    window.addEventListener('online', pollOnce, false);
    window.addEventListener('offline', pollOnce, false);
    cordova.addWindowEventHandler('online');
    cordova.addWindowEventHandler('offline');
    document.addEventListener('online', proxyEvent, false);
    document.addEventListener('offline', proxyEvent, false);
}

hookOnlineApis();

androidExec.jsToNativeModes = jsToNativeModes;
androidExec.nativeToJsModes = nativeToJsModes;

androidExec.setJsToNativeBridgeMode = function(mode) {
    if (mode == jsToNativeModes.JS_OBJECT && !window._cordovaNative) {
        console.log('Falling back on PROMPT mode since _cordovaNative is missing.');
        mode = jsToNativeModes.PROMPT;
    }
    nativeApiProvider.setPreferPrompt(mode == jsToNativeModes.PROMPT);
    jsToNativeBridgeMode = mode;
};

androidExec.setNativeToJsBridgeMode = function(mode) {
    if (mode == nativeToJsBridgeMode) {
        return;
    }
    if (nativeToJsBridgeMode == nativeToJsModes.POLLING) {
        pollEnabled = false;
    }

    nativeToJsBridgeMode = mode;
    // Tell the native side to switch modes.
    nativeApiProvider.get().setNativeToJsBridgeMode(mode);

    if (mode == nativeToJsModes.POLLING) {
        pollEnabled = true;
        setTimeout(pollingTimerFunc, 1);
    }
};

// Processes a single message, as encoded by NativeToJsMessageQueue.java.
function processMessage(message) {
    try {
        var firstChar = message.charAt(0);
        if (firstChar == 'J') {
            eval(message.slice(1));
        } else if (firstChar == 'S' || firstChar == 'F') {
            var success = firstChar == 'S';
            var keepCallback = message.charAt(1) == '1';
            var spaceIdx = message.indexOf(' ', 2);
            var status = +message.slice(2, spaceIdx);
            var nextSpaceIdx = message.indexOf(' ', spaceIdx + 1);
            var callbackId = message.slice(spaceIdx + 1, nextSpaceIdx);
            var payloadKind = message.charAt(nextSpaceIdx + 1);
            var payload;
            if (payloadKind == 's') {
                payload = message.slice(nextSpaceIdx + 2);
            } else if (payloadKind == 't') {
                payload = true;
            } else if (payloadKind == 'f') {
                payload = false;
            } else if (payloadKind == 'n') {
                payload = +message.slice(nextSpaceIdx + 2);
            } else {
                payload = JSON.parse(message.slice(nextSpaceIdx + 1));
            }
            cordova.callbackFromNative(callbackId, success, status, payload, keepCallback);
        } else {
            console.log("processMessage failed: invalid message:" + message);
        }
    } catch (e) {
        console.log("processMessage failed: Message: " + message);
        console.log("processMessage failed: Error: " + e);
        console.log("processMessage failed: Stack: " + e.stack);
    }
}

// This is called from the NativeToJsMessageQueue.java.
androidExec.processMessages = function(messages) {
    messagesFromNative.push(messages);
    // Check for the reentrant case, and enqueue the message if that's the case.
    if (messagesFromNative.length > 1) {
        return;
    }
    while (messagesFromNative.length) {
        messages = messagesFromNative[0];
        while (messages) {
            if (messages == '*') {
                window.setTimeout(pollOnce, 0);
                break;
            }
            var spaceIdx = messages.indexOf(' ');
            var msgLen = +messages.slice(0, spaceIdx);
            var message = messages.substr(spaceIdx + 1, msgLen);
            messages = messages.slice(spaceIdx + msgLen + 1);
            processMessage(message);
        }
        messagesFromNative.shift();
    }
};

module.exports = androidExec;
<commit_msg>[android] Fix NativeToJsBridgeMode enum not matching up with native<commit_after>/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
*/

/**
 * Execute a cordova command.  It is up to the native side whether this action
 * is synchronous or asynchronous.  The native side can return:
 *      Synchronous: PluginResult object as a JSON string
 *      Asynchronous: Empty string ""
 * If async, the native side will cordova.callbackSuccess or cordova.callbackError,
 * depending upon the result of the action.
 *
 * @param {Function} success    The success callback
 * @param {Function} fail       The fail callback
 * @param {String} service      The name of the service to use
 * @param {String} action       Action to be run in cordova
 * @param {String[]} [args]     Zero or more arguments to pass to the method
 */
var cordova = require('cordova'),
    nativeApiProvider = require('cordova/plugin/android/nativeapiprovider'),
    jsToNativeModes = {
        PROMPT: 0,
        JS_OBJECT: 1,
        // This mode is currently for benchmarking purposes only. It must be enabled
        // on the native side through the ENABLE_LOCATION_CHANGE_EXEC_MODE
        // constant within CordovaWebViewClient.java before it will work.
        LOCATION_CHANGE: 2
    },
    nativeToJsModes = {
        // Polls for messages using the JS->Native bridge.
        POLLING: 0,
        // For LOAD_URL to be viable, it would need to have a work-around for
        // the bug where the soft-keyboard gets dismissed when a message is sent.
        LOAD_URL: 1,
        // For the ONLINE_EVENT to be viable, it would need to intercept all event
        // listeners (both through addEventListener and window.ononline) as well
        // as set the navigator property itself.
        ONLINE_EVENT: 2,
        // Uses reflection to access private APIs of the WebView that can send JS
        // to be executed.
        // Requires Android 3.2.4 or above.
        PRIVATE_API: 3
    },
    jsToNativeBridgeMode,  // Set lazily.
    nativeToJsBridgeMode = nativeToJsModes.ONLINE_EVENT,
    pollEnabled = false,
    messagesFromNative = [];

function androidExec(success, fail, service, action, args) {
    // Set default bridge modes if they have not already been set.
    if (jsToNativeBridgeMode === undefined) {
        androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
    }

    var callbackId = service + cordova.callbackId++,
        argsJson = JSON.stringify(args);
    if (success || fail) {
        cordova.callbacks[callbackId] = {success:success, fail:fail};
    }

    if (jsToNativeBridgeMode == jsToNativeModes.LOCATION_CHANGE) {
        window.location = 'http://cdv_exec/' + service + '#' + action + '#' + callbackId + '#' + argsJson;
    } else {
        var messages = nativeApiProvider.get().exec(service, action, callbackId, argsJson);
        // Explicit cast to string is required on Android 2.1 to convert from
        // a Java string to a JS string.
        if (messages) {
            messages = String(messages);
        }
        androidExec.processMessages(messages);
    }
}

function pollOnce() {
    var msg = nativeApiProvider.get().retrieveJsMessages();
    androidExec.processMessages(msg);
}

function pollingTimerFunc() {
    if (pollEnabled) {
      pollOnce();
      setTimeout(pollingTimerFunc, 50);
    }
}

function hookOnlineApis() {
    function proxyEvent(e) {
        cordova.fireWindowEvent(e.type);
    }
    // The network module takes care of firing online and offline events.
    // It currently fires them only on document though, so we bridge them
    // to window here (while first listening for exec()-releated online/offline
    // events).
    window.addEventListener('online', pollOnce, false);
    window.addEventListener('offline', pollOnce, false);
    cordova.addWindowEventHandler('online');
    cordova.addWindowEventHandler('offline');
    document.addEventListener('online', proxyEvent, false);
    document.addEventListener('offline', proxyEvent, false);
}

hookOnlineApis();

androidExec.jsToNativeModes = jsToNativeModes;
androidExec.nativeToJsModes = nativeToJsModes;

androidExec.setJsToNativeBridgeMode = function(mode) {
    if (mode == jsToNativeModes.JS_OBJECT && !window._cordovaNative) {
        console.log('Falling back on PROMPT mode since _cordovaNative is missing.');
        mode = jsToNativeModes.PROMPT;
    }
    nativeApiProvider.setPreferPrompt(mode == jsToNativeModes.PROMPT);
    jsToNativeBridgeMode = mode;
};

androidExec.setNativeToJsBridgeMode = function(mode) {
    if (mode == nativeToJsBridgeMode) {
        return;
    }
    if (nativeToJsBridgeMode == nativeToJsModes.POLLING) {
        pollEnabled = false;
    }

    nativeToJsBridgeMode = mode;
    // Tell the native side to switch modes.
    nativeApiProvider.get().setNativeToJsBridgeMode(mode);

    if (mode == nativeToJsModes.POLLING) {
        pollEnabled = true;
        setTimeout(pollingTimerFunc, 1);
    }
};

// Processes a single message, as encoded by NativeToJsMessageQueue.java.
function processMessage(message) {
    try {
        var firstChar = message.charAt(0);
        if (firstChar == 'J') {
            eval(message.slice(1));
        } else if (firstChar == 'S' || firstChar == 'F') {
            var success = firstChar == 'S';
            var keepCallback = message.charAt(1) == '1';
            var spaceIdx = message.indexOf(' ', 2);
            var status = +message.slice(2, spaceIdx);
            var nextSpaceIdx = message.indexOf(' ', spaceIdx + 1);
            var callbackId = message.slice(spaceIdx + 1, nextSpaceIdx);
            var payloadKind = message.charAt(nextSpaceIdx + 1);
            var payload;
            if (payloadKind == 's') {
                payload = message.slice(nextSpaceIdx + 2);
            } else if (payloadKind == 't') {
                payload = true;
            } else if (payloadKind == 'f') {
                payload = false;
            } else if (payloadKind == 'n') {
                payload = +message.slice(nextSpaceIdx + 2);
            } else {
                payload = JSON.parse(message.slice(nextSpaceIdx + 1));
            }
            cordova.callbackFromNative(callbackId, success, status, payload, keepCallback);
        } else {
            console.log("processMessage failed: invalid message:" + message);
        }
    } catch (e) {
        console.log("processMessage failed: Message: " + message);
        console.log("processMessage failed: Error: " + e);
        console.log("processMessage failed: Stack: " + e.stack);
    }
}

// This is called from the NativeToJsMessageQueue.java.
androidExec.processMessages = function(messages) {
    messagesFromNative.push(messages);
    // Check for the reentrant case, and enqueue the message if that's the case.
    if (messagesFromNative.length > 1) {
        return;
    }
    while (messagesFromNative.length) {
        messages = messagesFromNative[0];
        while (messages) {
            if (messages == '*') {
                window.setTimeout(pollOnce, 0);
                break;
            }
            var spaceIdx = messages.indexOf(' ');
            var msgLen = +messages.slice(0, spaceIdx);
            var message = messages.substr(spaceIdx + 1, msgLen);
            messages = messages.slice(spaceIdx + msgLen + 1);
            processMessage(message);
        }
        messagesFromNative.shift();
    }
};

module.exports = androidExec;
<|endoftext|>


<commit_before>mini.Module(
	"assets/loader"
)
.requires(
	"assets/loadassembler"
)
.defines(function(LoadAssembler) {
	var Loader = {
		loadMap: function(filePath, onSuccess) {
			LoadAssembler.loadMap(filePath, onSuccess);
		}
	};

	return Loader;
});
<commit_msg>added load function stub in Loader<commit_after>mini.Module(
	"assets/loader"
)
.requires(
	"assets/loadassembler"
)
.defines(function(LoadAssembler) {
	var Loader = {
		loadMap: function(filePath, onSuccess) {
			LoadAssembler.loadMap(filePath, onSuccess);
		},
		addResource: function(path) {

		},
		load: function(onFinished) {
			// TODO: load all required resources, then call callback
			onFinished();
		}
	};

	return Loader;
});
<|endoftext|>


<commit_before>'use_strict';

var m2xReporter = require('./m2xReporter');
var config = require('config');

var m2xMap = config.get('m2x.endpoints');

var winston = require('winston');

// Helper functions
var logger = new (winston.Logger)({
    transports: [
        new (winston.transports.Console)(),
        new (winston.transports.File)({ filename: './logs/coopReporter.log' })
    ]
});

var logMessage = function(streamName, dataValue, logLevel) {
    var logLine = streamName + ' = ' + dataValue;
    logLevel = logLevel || 'info';
    logger.log(logLevel, logLine);
};

function report (streamName, dataValue, callback) {
    var url = m2xMap[streamName];
    if (!url) {
        throw new Error('No endpoint defined for streamName = ' + streamName);
    }
    return m2xReporter(url, dataValue, callback);
}

module.exports = function (coop) {
    coop.on('change', function ( updatedFields ) {
        updatedFields.forEach( function(field) {
            report( field, coop[field], function(err, data){
                if(err) logMessage(err.message + ' ' + field, coop[field], 'error');
                else logMessage(field, coop[field]);
            });
        });
    });

    // for a heartbeat, just update the temperature
    coop.on('heartbeat', function ( ) {
        console.log('heartbeat');
        coop.checkLegality( function (result) {
            console.log('sending heartbeat; valid: "' + result + '"');
            report('legalState', result, function() { return; });
        }
    });
};
<commit_msg>Modified heartbeat state value to aok from legalState<commit_after>'use_strict';

var m2xReporter = require('./m2xReporter');
var config = require('config');

var m2xMap = config.get('m2x.endpoints');

var winston = require('winston');

// Helper functions
var logger = new (winston.Logger)({
    transports: [
        new (winston.transports.Console)(),
        new (winston.transports.File)({ filename: './logs/coopReporter.log' })
    ]
});

var logMessage = function(streamName, dataValue, logLevel) {
    var logLine = streamName + ' = ' + dataValue;
    logLevel = logLevel || 'info';
    logger.log(logLevel, logLine);
};

function report (streamName, dataValue, callback) {
    var url = m2xMap[streamName];
    if (!url) {
        throw new Error('No endpoint defined for streamName = ' + streamName);
    }
    return m2xReporter(url, dataValue, callback);
}

module.exports = function (coop) {
    coop.on('change', function ( updatedFields ) {
        updatedFields.forEach( function(field) {
            report( field, coop[field], function(err, data){
                if(err) logMessage(err.message + ' ' + field, coop[field], 'error');
                else logMessage(field, coop[field]);
            });
        });
    });

    // for a heartbeat, just update the temperature
    coop.on('heartbeat', function ( ) {
        console.log('heartbeat');
        coop.checkLegality( function (result) {
            console.log('sending heartbeat; valid: "' + result + '"');
            report('aok', result, function() { return; });
        }
    });
};
<|endoftext|>


<commit_before><commit_msg>Get the Event's places.<commit_after><|endoftext|>


<commit_before>'use strict';

var inherit = require('inherit'),
    wd = require('wd'),
    q = require('q'),
    chalk = require('chalk'),
    elementRect = require('../element-rect'),
    Image = require('../image'),
    Actions = require('./actions'),
    Element = require('./element'),

    GeminiError = require('../errors/gemini-error'),
    StateError = require('../errors/state-error');

module.exports = inherit({
    __constructor: function(config, name, version) {
        this.name = name;
        this.version = version;
        this.config = config;
        this._browser = wd.promiseRemote(config.gridUrl);

        // optional extra logging
        if (config.debug) {
            this._browser.on('connection', function(code, message, error) {
                console.log(chalk.red(' ! ' + code + ': ' + message));
            });
            this._browser.on('status', function(info) {
                console.log(chalk.cyan(info));
            });
            this._browser.on('command', function(eventType, command, response) {
                if (eventType === 'RESPONSE' && command == 'takeScreenshot()') {
                    response = '<binary-data>';
                }
                console.log(' > ' + chalk.cyan(eventType), command, chalk.grey(response || ''));
            });
            this._browser.on('http', function(meth, path, data) {
                console.log(' > ' + chalk.magenta(meth), path, chalk.grey(data || ''));
            });
        }
    },

    get fullName() {
        if (!this.version) {
            return this.name;
        }
        return this.name + '-v' + this.version;
    },

    open: function(url) {
        var _this = this;
        return this._browser
            .configureHttp(_this.config.http)
            .then(function() {
                return _this._browser.init(_this._capabilities)
            })
            .then(function(capabilities) {
                return _this._browser.get(url);
            })
            .then(function() {
                //maximize is required, because default
                //windows size in phantomjs can prevent
                //some shadows from fitting in
                if (_this._shouldMaximize()) {
                    return _this._maximize();
                }
            })
            .fail(function(e) {
                if (e.code === 'ECONNREFUSED') {
                    return q.reject(new GeminiError(
                        'Unable to connect to ' + _this.config.gridUrl,
                        'Make sure that URL in config file is correct and selenium\nserver is running.'
                    ));
                }
                // sadly, selenium does not provide a way to distinguish different
                // reasons of failure
                return q.reject(new GeminiError(
                    'Cannot launch browser ' +  _this.fullName,
                    'Check that browser is present on grid and its name is spelled correctly'
                ));
            });
    },

    get _capabilities() {
        var result = {
            browserName: this.name,
            takesScreenshot: true,
            version: this.version
        };

        Object.keys(this.config.capabilities).forEach(function(key) {
            result[key] = this.config.capabilities[key];
        }, this);

        return result;
    },

    _shouldMaximize: function() {
        return this.name === 'phantomjs';
    },

    _maximize: function() {
        var _this = this;
        return _this._browser.windowHandle()
            .then(function(handle) {
                return _this._browser.maximize(handle);
            });
    },

    _findElements: function(selectorsList) {
        var _this = this;
        return q.all(selectorsList.map(function(selector) {
            return _this.findElement(selector, true);
        }));
    },

    findElement: function(selector) {
        return this._browser.elementByCssSelector(selector)
            .then(function(wdElement) {
                return new Element(selector, wdElement);
            })
            .fail(function(error) {
                if (error.status === 7) {
                    error.selector = selector;
                }
                return q.reject(error);
            });
    },

    captureState: function(state, context) {
        var _this = this;
        return state.activate(this, context)
            .then(function() {
                return q.all([_this._takeScreenshot(), _this._findRect(state.captureSelectors)])
                    .spread(function(image, rect) {
                        return image.crop(rect);
                    });
            })
            .fail(function(e) {
                if (e instanceof StateError) {
                    //extend error with metadata
                    e.suiteName = state.suite.name;
                    e.stateName = state.name;
                    e.browserName = _this.name;
                }
                return q.reject(e);
            });
    },

    _findRect: function(selectors) {
        return this._findElements(selectors)
            .then(function(elements) {
                return elementRect.getMultiple(elements);
            });
    },

    _takeScreenshot: function() {
        return this._browser.takeScreenshot()
            .then(function(base64) {
                return new Image(new Buffer(base64, 'base64'));
            });
    },

    quit: function() {
        return this._browser.quit();
    },

    createActionSequence: function() {
        return new Actions(this);
    }

});
<commit_msg>Fix few lint errors<commit_after>'use strict';

var inherit = require('inherit'),
    wd = require('wd'),
    q = require('q'),
    chalk = require('chalk'),
    elementRect = require('../element-rect'),
    Image = require('../image'),
    Actions = require('./actions'),
    Element = require('./element'),

    GeminiError = require('../errors/gemini-error'),
    StateError = require('../errors/state-error');

module.exports = inherit({
    __constructor: function(config, name, version) {
        this.name = name;
        this.version = version;
        this.config = config;
        this._browser = wd.promiseRemote(config.gridUrl);

        // optional extra logging
        if (config.debug) {
            this._browser.on('connection', function(code, message, error) {
                console.log(chalk.red(' ! ' + code + ': ' + message));
            });
            this._browser.on('status', function(info) {
                console.log(chalk.cyan(info));
            });
            this._browser.on('command', function(eventType, command, response) {
                if (eventType === 'RESPONSE' && command === 'takeScreenshot()') {
                    response = '<binary-data>';
                }
                console.log(' > ' + chalk.cyan(eventType), command, chalk.grey(response || ''));
            });
            this._browser.on('http', function(meth, path, data) {
                console.log(' > ' + chalk.magenta(meth), path, chalk.grey(data || ''));
            });
        }
    },

    get fullName() {
        if (!this.version) {
            return this.name;
        }
        return this.name + '-v' + this.version;
    },

    open: function(url) {
        var _this = this;
        return this._browser
            .configureHttp(_this.config.http)
            .then(function() {
                return _this._browser.init(_this._capabilities);
            })
            .then(function(capabilities) {
                return _this._browser.get(url);
            })
            .then(function() {
                //maximize is required, because default
                //windows size in phantomjs can prevent
                //some shadows from fitting in
                if (_this._shouldMaximize()) {
                    return _this._maximize();
                }
            })
            .fail(function(e) {
                if (e.code === 'ECONNREFUSED') {
                    return q.reject(new GeminiError(
                        'Unable to connect to ' + _this.config.gridUrl,
                        'Make sure that URL in config file is correct and selenium\nserver is running.'
                    ));
                }
                // sadly, selenium does not provide a way to distinguish different
                // reasons of failure
                return q.reject(new GeminiError(
                    'Cannot launch browser ' +  _this.fullName,
                    'Check that browser is present on grid and its name is spelled correctly'
                ));
            });
    },

    get _capabilities() {
        var result = {
            browserName: this.name,
            takesScreenshot: true,
            version: this.version
        };

        Object.keys(this.config.capabilities).forEach(function(key) {
            result[key] = this.config.capabilities[key];
        }, this);

        return result;
    },

    _shouldMaximize: function() {
        return this.name === 'phantomjs';
    },

    _maximize: function() {
        var _this = this;
        return _this._browser.windowHandle()
            .then(function(handle) {
                return _this._browser.maximize(handle);
            });
    },

    _findElements: function(selectorsList) {
        var _this = this;
        return q.all(selectorsList.map(function(selector) {
            return _this.findElement(selector, true);
        }));
    },

    findElement: function(selector) {
        return this._browser.elementByCssSelector(selector)
            .then(function(wdElement) {
                return new Element(selector, wdElement);
            })
            .fail(function(error) {
                if (error.status === 7) {
                    error.selector = selector;
                }
                return q.reject(error);
            });
    },

    captureState: function(state, context) {
        var _this = this;
        return state.activate(this, context)
            .then(function() {
                return q.all([_this._takeScreenshot(), _this._findRect(state.captureSelectors)])
                    .spread(function(image, rect) {
                        return image.crop(rect);
                    });
            })
            .fail(function(e) {
                if (e instanceof StateError) {
                    //extend error with metadata
                    e.suiteName = state.suite.name;
                    e.stateName = state.name;
                    e.browserName = _this.name;
                }
                return q.reject(e);
            });
    },

    _findRect: function(selectors) {
        return this._findElements(selectors)
            .then(function(elements) {
                return elementRect.getMultiple(elements);
            });
    },

    _takeScreenshot: function() {
        return this._browser.takeScreenshot()
            .then(function(base64) {
                return new Image(new Buffer(base64, 'base64'));
            });
    },

    quit: function() {
        return this._browser.quit();
    },

    createActionSequence: function() {
        return new Actions(this);
    }

});
<|endoftext|>


<commit_before>var _ = require('underscore');
var async = require('async');
var path = require('path');
var fs = require('fs');
var opener = require('./open');
var pluginify = require('./pluginify');
var steal = require('../steal');
var beautify = require('js-beautify');

var utilities = {
	/**
	 * Creates a banner from a template.
	 *
	 * @param {String} banner The banner template
	 * @param {Object} options The options to pass to the banner template
	 */
	banner: function (banner, options) {
		return banner ? _.template(banner, options) : '';
	},

	/**
	 * Prettify a source code string using the optional
	 * options. Removes all multiline comments and
	 * multi-newlines and semicolons.
	 * Beautifies the code with the `beautify` settings.
	 *
	 * @param {String} str The source code to beautify
	 * @param {Object} options The options containing the
	 * optional `banner` template and `beautify` settings.
	 * @returns {String} The prettified source code.
	 */
	prettify: function (str, options) {
		options = options || {};

		// Strip out multiline comments and clean up some other things
		// And run jsBeautifier
		var content = beautify(str.replace(/\/\*([\s\S]*?)\*\//gim, '')
			.replace(/\/\/(\s*)\n/gim, '')
			.replace(/;[\s]*;/gim, '')
			.replace(/(\/\/.*)\n[\s]*;/gi, '')
			.replace(/(\n){3,}/gim, '\n\n'), options);

		return content;
	},

	/**
	 * From a list of Steal objects return every object matching the
	 * id from the given list.
	 *
	 * @param {Array} steals The list of Steals
	 * @param {Array<String>} ids The list of ids
	 * @returns {Array} The list of Steal objects matching the id
	 */
	getSteals: function (steals, ids) {
		var result = [];
		ids.forEach(function (name) {
			var id = steals.opener.id(name).toString();
			var stl = _.find(steals, function (current) {
				return current.options.id.toString() === id;
			});
			if (!stl) {
				throw new Error('Module ' + name + ' is not available.');
			}
			result.push(stl);
		});
		return result;
	},

	/**
	 * Converts a single module name or a list of modules.
	 * Returns a list of the `builder.json` default module names
	 * if no modules have been given.
	 *
	 * @param {Object|String|null} modules The single module
	 * name, a list of modules names or nothing.
	 * @param {Object} builderModules The module list from the
	 * `builder.json` definition
	 * @returns {Array} The list of module ids
	 */
	getIds: function (modules, builderModules) {
		if (!modules || !modules.length) {
			var ids = [];
			_.each(builderModules, function (value, name) {
				if (value.type === "core") {
					ids.push(name);
				}
			});
			return ids;
		}
		return _.isArray(modules) ? modules : [modules];
	},

	/**
	 * Converts a single module name or a list of modules.
	 * Returns a list of the `builder.json` default module names
	 * if no modules have been given.
	 *
	 * @param {Object|String|null} modules The single module
	 * name, a list of modules names or nothing.
	 * @param {Object} builderModules The module list from the
	 * `builder.json` definition
	 * @returns {Array} The list of module ids
	 */
	getPluginIds: function (modules, builderModules) {
		if (!modules || !modules.length) {
			var ids = [];
			_.each(builderModules, function (value, name) {
				if (value.type === "plugin") {
					ids.push(name);
				}
			});
			return ids;
		}
		return _.isArray(modules) ? modules : [modules];
	},

	/**
	 * Get all dependencies for a given Steal id.
	 * Only include visible modules.
	 *
	 * @param {String} id The Steal id string to check for.
	 * @param {Array} steals A list of Steal dependencies to check in
	 * @param {Array} [visibleModules] A list of visible module ids.
	 * If given only these modules will be included in the dependencies.
	 * @returns {Array} A list of ids.
	 */
	getDependencies: function (id, steals, visibleModules) {
		id = steal.id(id).toString();

		var result = [];
		var stl = _.find(steals, function (curStl) {
			return id === curStl.options.id.toString();
		});

		if (stl) {
			var visibles = visibleModules ? _.map(visibleModules, function (mod) {
				return steal.id(mod).toString();
			}) : null;
			opener.visit([stl], function (stl) {
				var currentId = stl.options.id.toString();
				if (steal.id(id).toString() !== currentId &&
					(!visibles || visibles.indexOf(currentId) !== -1)) {
					result.push(currentId);
				}
			});
		}

		return result;
	},

	/**
	 * Load all Steal dependencies for each configuration and add them to the
	 * download builder information object.
	 *
	 * @param {Object} info The download builder information object
	 * @param {String} root The root path to use
	 * @param {Function} callback Callback to call when done loading
	 */
	loadSteals: function (info, config, callback) {
		var configurations = info.configurations;
		var modules = _.keys(info.modules);
		var stealers = _.map(configurations, function (configuration, configName) {
			return function (callback) {
				var stealConfig = utilities.deepExtend(configuration.steal || {}, config);
				opener(modules, stealConfig, function (error, configSteals, opener) {
					callback(error, configName, _.uniq(configSteals, function (stl) {
						return stl.options && stl.options.id.toString();
					}), opener);
				});
			}
		});

		async.series(stealers, function (error, args) {
			if (error) {
				return callback(error);
			}

			var steals = {};
			// Callback from above: [ error, configName, steals, opener ]
			_.each(args, function (rets) {
				var name = rets[0];
				var configSteals = rets[1];
				// We need to store the cloned opener Steal so that we can
				// use it for getting ids with the correct mappings
				configSteals.opener = rets[2];
				steals[name] = configSteals;
			});
			callback(null, steals);
		});
	},

	/**
	 * A small deep extend helper.
	 *
	 * @param {Object} destination The object to recursively extend into
	 * @param {Object} source The object to extend from
	 * @returns {Object} The destination object
	 */
	deepExtend: function (destination, source) {
		for (var property in source) {
			if (source[property] && source[property].constructor &&
				source[property].constructor === Object) {
				destination[property] = destination[property] || {};
				utilities.deepExtend(destination[property], source[property]);
			} else {
				destination[property] = source[property];
			}
		}
		return destination;
	},

	/**
	 * Gets params for plugin function wrappers based on configuration dependencies in builder.json
	 * @param configuration
	 * @returns {String} params to add to function wrapper
	 */
	getConfigParams: function (configuration) {
		var configMap = {
			jquery: "jQuery"
		};

		return configMap[configuration];
	},

	/**
	 * Build with a given configuration.
	 *
	 * @param {{}} options The options to use
	 * @option {Array} ids An array of ids to build (`isDefault` modules will be used if empty)
	 * @option {String} url The source URL to put in the banner
	 * @option {String} configuration The configuration name to use (uses default configuration if not available)
	 * @param {Object} info The builder configuration information as returned by `loadInfo`
	 * @param {Function} callback The callback to call with the source output
	 * @returns {*}
	 */
	build: function (options, info, callback) {
		// Grab our ids from the `modules`
		var ids, configuration, buildSteals, plugin, pluginConfigurations, pluginifyOptions;

		configuration = options.configuration ?
			info.configurations[options.configuration] :
			_.find(info.configurations, function (item) {
				return item.isDefault;
			});

		if (options.plugin) {

			plugin = options.plugin;
			pluginConfigurations = info.modules[plugin].configurations;
			ids = plugin;

			opener(plugin, info.stealConfig, function (error, steals, opener) {

				var params = "";

				if (pluginConfigurations) {
					pluginConfigurations.forEach(function (configDependency) {
						params += utilities.getConfigParams(configDependency) + ",";
					});
				}

				params += "can";

				var content = '(' + steals[0].options.parsed + ')(' + params + ');';

				var banner = info.banner ? utilities.banner(info.banner, {
					url: options.url || '',
					info: info,
					pkg: info,
					ids: ids
				}) : '';

				content = utilities.prettify(content, info.beautify);

				callback(null, content, banner);
			});

		} else {
			ids = utilities.getIds(options.ids, info.modules);

			if (!configuration) {
				return callback(new Error('No configuration for: ' + options.configuration));
			}

			// From the list of ids get all our Steals
			buildSteals = utilities.getSteals(configuration.steals, ids);
			pluginifyOptions = _.extend({
				opener: configuration.steals.opener
			}, options.pluginify, info.pluginify, configuration.pluginify);

			pluginify.pluginifySteals(buildSteals, pluginifyOptions, function (error, str) {
				if (error) {
					return callback(error);
				}

				var banner = info.banner ? utilities.banner(info.banner, {
					url: options.url || '',
					info: info,
					pkg: info,
					ids: ids
				}) : '';
				var content = utilities.prettify(str, info.beautify);

				callback(null, content, banner);
			});
		}
	}
}

/**
 * Load the download builder information which will contain:
 *
 * - `package.json`
 * - `builder.json`
 * - All Steal dependencies (and file contents) for every available download builder configuration
 *
 * This will be used throughout most of the download builder.
 * The callback data will be `undefined` if any of the JSON configuration files
 * isn't available.
 *
 * @param {String|Object} options The resource path or a more detailed configuration.
 * @param {Function} callback Callback with the information object
 * @returns {*}
 */
var builder = function (options, callback) {
	var settings = typeof options === 'string' ? { path: options } : options;
	var filePath = settings.path;
	var builder = path.join(filePath, '/builder.json');
	var pkg = path.join(filePath, '/package.json');

	if (!(fs.existsSync(builder) && fs.existsSync(pkg))) {
		return callback(null, null);
	}

	var stealConfig = _.extend({
		root: path.dirname(filePath)
	}, options.steal);

	var info = _.extend({
		path: filePath,
		configurations: {},
		stealConfig: stealConfig
	}, require(builder), require(pkg));

	// Load Steals with parent folder of the current path as the root
	utilities.loadSteals(info, stealConfig, function (error, steals) {
		if (error) {
			return callback(error);
		} else {

			_.each(steals, function (stls, name) {
				info.configurations[name].steals = stls;
			});

			callback(null, info, function (options, cb) {
				utilities.build(options, info, cb);
			});
		}
	});
};

_.extend(builder, utilities);

module.exports = builder;
<commit_msg>Clean plugins.<commit_after>var _ = require('underscore');
var async = require('async');
var path = require('path');
var fs = require('fs');
var opener = require('./open');
var pluginify = require('./pluginify');
var steal = require('../steal');
var beautify = require('js-beautify');

var utilities = {
	/**
	 * Creates a banner from a template.
	 *
	 * @param {String} banner The banner template
	 * @param {Object} options The options to pass to the banner template
	 */
	banner: function (banner, options) {
		return banner ? _.template(banner, options) : '';
	},

	/**
	 * Prettify a source code string using the optional
	 * options. Removes all multiline comments and
	 * multi-newlines and semicolons.
	 * Beautifies the code with the `beautify` settings.
	 *
	 * @param {String} str The source code to beautify
	 * @param {Object} options The options containing the
	 * optional `banner` template and `beautify` settings.
	 * @returns {String} The prettified source code.
	 */
	prettify: function (str, options) {
		options = options || {};

		// Strip out multiline comments and clean up some other things
		// And run jsBeautifier
		var content = beautify(str.replace(/\/\*([\s\S]*?)\*\//gim, '')
			.replace(/\/\/(\s*)\n/gim, '')
			.replace(/;[\s]*;/gim, '')
			.replace(/(\/\/.*)\n[\s]*;/gi, '')
			.replace(/(\n){3,}/gim, '\n\n'), options);

		return content;
	},

	/**
	 * From a list of Steal objects return every object matching the
	 * id from the given list.
	 *
	 * @param {Array} steals The list of Steals
	 * @param {Array<String>} ids The list of ids
	 * @returns {Array} The list of Steal objects matching the id
	 */
	getSteals: function (steals, ids) {
		var result = [];
		ids.forEach(function (name) {
			var id = steals.opener.id(name).toString();
			var stl = _.find(steals, function (current) {
				return current.options.id.toString() === id;
			});
			if (!stl) {
				throw new Error('Module ' + name + ' is not available.');
			}
			result.push(stl);
		});
		return result;
	},

	/**
	 * Converts a single module name or a list of modules.
	 * Returns a list of the `builder.json` default module names
	 * if no modules have been given.
	 *
	 * @param {Object|String|null} modules The single module
	 * name, a list of modules names or nothing.
	 * @param {Object} builderModules The module list from the
	 * `builder.json` definition
	 * @returns {Array} The list of module ids
	 */
	getIds: function (modules, builderModules) {
		if (!modules || !modules.length) {
			var ids = [];
			_.each(builderModules, function (value, name) {
				if (value.type === "core") {
					ids.push(name);
				}
			});
			return ids;
		}
		return _.isArray(modules) ? modules : [modules];
	},

	/**
	 * Converts a single module name or a list of modules.
	 * Returns a list of the `builder.json` default module names
	 * if no modules have been given.
	 *
	 * @param {Object|String|null} modules The single module
	 * name, a list of modules names or nothing.
	 * @param {Object} builderModules The module list from the
	 * `builder.json` definition
	 * @returns {Array} The list of module ids
	 */
	getPluginIds: function (modules, builderModules) {
		if (!modules || !modules.length) {
			var ids = [];
			_.each(builderModules, function (value, name) {
				if (value.type === "plugin") {
					ids.push(name);
				}
			});
			return ids;
		}
		return _.isArray(modules) ? modules : [modules];
	},

	/**
	 * Get all dependencies for a given Steal id.
	 * Only include visible modules.
	 *
	 * @param {String} id The Steal id string to check for.
	 * @param {Array} steals A list of Steal dependencies to check in
	 * @param {Array} [visibleModules] A list of visible module ids.
	 * If given only these modules will be included in the dependencies.
	 * @returns {Array} A list of ids.
	 */
	getDependencies: function (id, steals, visibleModules) {
		id = steal.id(id).toString();

		var result = [];
		var stl = _.find(steals, function (curStl) {
			return id === curStl.options.id.toString();
		});

		if (stl) {
			var visibles = visibleModules ? _.map(visibleModules, function (mod) {
				return steal.id(mod).toString();
			}) : null;
			opener.visit([stl], function (stl) {
				var currentId = stl.options.id.toString();
				if (steal.id(id).toString() !== currentId &&
					(!visibles || visibles.indexOf(currentId) !== -1)) {
					result.push(currentId);
				}
			});
		}

		return result;
	},

	/**
	 * Load all Steal dependencies for each configuration and add them to the
	 * download builder information object.
	 *
	 * @param {Object} info The download builder information object
	 * @param {String} root The root path to use
	 * @param {Function} callback Callback to call when done loading
	 */
	loadSteals: function (info, config, callback) {
		var configurations = info.configurations;
		var modules = _.keys(info.modules);
		var stealers = _.map(configurations, function (configuration, configName) {
			return function (callback) {
				var stealConfig = utilities.deepExtend(configuration.steal || {}, config);
				opener(modules, stealConfig, function (error, configSteals, opener) {
					callback(error, configName, _.uniq(configSteals, function (stl) {
						return stl.options && stl.options.id.toString();
					}), opener);
				});
			}
		});

		async.series(stealers, function (error, args) {
			if (error) {
				return callback(error);
			}

			var steals = {};
			// Callback from above: [ error, configName, steals, opener ]
			_.each(args, function (rets) {
				var name = rets[0];
				var configSteals = rets[1];
				// We need to store the cloned opener Steal so that we can
				// use it for getting ids with the correct mappings
				configSteals.opener = rets[2];
				steals[name] = configSteals;
			});
			callback(null, steals);
		});
	},

	/**
	 * A small deep extend helper.
	 *
	 * @param {Object} destination The object to recursively extend into
	 * @param {Object} source The object to extend from
	 * @returns {Object} The destination object
	 */
	deepExtend: function (destination, source) {
		for (var property in source) {
			if (source[property] && source[property].constructor &&
				source[property].constructor === Object) {
				destination[property] = destination[property] || {};
				utilities.deepExtend(destination[property], source[property]);
			} else {
				destination[property] = source[property];
			}
		}
		return destination;
	},

	/**
	 * Gets params for plugin function wrappers based on configuration dependencies in builder.json
	 * @param configuration
	 * @returns {String} params to add to function wrapper
	 */
	getConfigParams: function (configuration) {
		var configMap = {
			jquery: "jQuery"
		};

		return configMap[configuration];
	},

	/**
	 * Build with a given configuration.
	 *
	 * @param {{}} options The options to use
	 * @option {Array} ids An array of ids to build (`isDefault` modules will be used if empty)
	 * @option {String} url The source URL to put in the banner
	 * @option {String} configuration The configuration name to use (uses default configuration if not available)
	 * @param {Object} info The builder configuration information as returned by `loadInfo`
	 * @param {Function} callback The callback to call with the source output
	 * @returns {*}
	 */
	build: function (options, info, callback) {
		// Grab our ids from the `modules`
		var ids, configuration, buildSteals, plugin, pluginConfigurations, pluginifyOptions;

		configuration = options.configuration ?
			info.configurations[options.configuration] :
			_.find(info.configurations, function (item) {
				return item.isDefault;
			});

		if (options.plugin) {

			plugin = options.plugin;
			pluginConfigurations = info.modules[plugin].configurations;
			ids = plugin;

			opener(plugin, info.stealConfig, function (error, steals, opener) {

				var params = "";

				if (pluginConfigurations) {
					pluginConfigurations.forEach(function (configDependency) {
						params += utilities.getConfigParams(configDependency) + ",";
					});
				}

				params += "can";

				var content = '(' + steals[0].options.parsed + ')(' + params + ');';

				var banner = info.banner ? utilities.banner(info.banner, {
					url: options.url || '',
					info: info,
					pkg: info,
					ids: ids
				}) : '';

				content = opener.clean(utilities.prettify(content, info.beautify), options);

				callback(null, content, banner);
			});

		} else {
			ids = utilities.getIds(options.ids, info.modules);

			if (!configuration) {
				return callback(new Error('No configuration for: ' + options.configuration));
			}

			// From the list of ids get all our Steals
			buildSteals = utilities.getSteals(configuration.steals, ids);
			pluginifyOptions = _.extend({
				opener: configuration.steals.opener
			}, options.pluginify, info.pluginify, configuration.pluginify);

			pluginify.pluginifySteals(buildSteals, pluginifyOptions, function (error, str) {
				if (error) {
					return callback(error);
				}

				var banner = info.banner ? utilities.banner(info.banner, {
					url: options.url || '',
					info: info,
					pkg: info,
					ids: ids
				}) : '';
				var content = utilities.prettify(str, info.beautify);

				callback(null, content, banner);
			});
		}
	}
}

/**
 * Load the download builder information which will contain:
 *
 * - `package.json`
 * - `builder.json`
 * - All Steal dependencies (and file contents) for every available download builder configuration
 *
 * This will be used throughout most of the download builder.
 * The callback data will be `undefined` if any of the JSON configuration files
 * isn't available.
 *
 * @param {String|Object} options The resource path or a more detailed configuration.
 * @param {Function} callback Callback with the information object
 * @returns {*}
 */
var builder = function (options, callback) {
	var settings = typeof options === 'string' ? { path: options } : options;
	var filePath = settings.path;
	var builder = path.join(filePath, '/builder.json');
	var pkg = path.join(filePath, '/package.json');

	if (!(fs.existsSync(builder) && fs.existsSync(pkg))) {
		return callback(null, null);
	}

	var stealConfig = _.extend({
		root: path.dirname(filePath)
	}, options.steal);

	var info = _.extend({
		path: filePath,
		configurations: {},
		stealConfig: stealConfig
	}, require(builder), require(pkg));

	// Load Steals with parent folder of the current path as the root
	utilities.loadSteals(info, stealConfig, function (error, steals) {
		if (error) {
			return callback(error);
		} else {

			_.each(steals, function (stls, name) {
				info.configurations[name].steals = stls;
			});

			callback(null, info, function (options, cb) {
				utilities.build(options, info, cb);
			});
		}
	});
};

_.extend(builder, utilities);

module.exports = builder;
<|endoftext|>


<commit_before>'use strict';

var Datastore = require('nedb'),
    Parser = require('./parser.js'),
    events = require('events'),
    watcher = require('./watcher.js'),
    fs = require('fs'),
    db = new Datastore({ filename: 'db/bundles.db', autoload: true }),
    Q = require('q'),
    util = require('util'),
    log = require('../logger');

// Using a unique constraint with the bundle name
db.ensureIndex({ fieldName: 'name', unique: true }, function (err) {});

// Used in the "watcher" listener
var self = {};

function Bundles() {
    log.trace("[lib/bundles/index.js] Loading bundles");
    events.EventEmitter.call(this);
    self = this;

    // Do an initial scan of the bundles dir
    var bundlesDir = fs.readdirSync('bundles/');
    var discoveredBundles = [];

    bundlesDir.forEach(function(bundleFolderName) {
        var bundle = Parser.parse(bundleFolderName);

        if (bundle !== null) {
            log.trace("[lib/bundles/index.js] Discovered bundle %s", bundle.name);
            discoveredBundles.push(bundle);
        }
    });

    // Remove any entries that are no longer present on disk,
    // add/update the rest
    self.all()
        .then(function (currentBundles) {
            currentBundles.forEach(function(bundle) {
                var bundleName = bundle.name;
                var bundleDiscovered = false;

                discoveredBundles.forEach(function (discoveredBundle) {
                    if (discoveredBundle.name === bundleName) {
                        bundleDiscovered = true;
                    }
                });

                if (!bundleDiscovered) {
                    log.info("[lib/bundle/index.js] %s's nodecg.json was not discovered, assuming the bundle has been deleted or moved", bundle.name);
                    self.remove(bundle.name);
                }
            });

            var promises = [];
            discoveredBundles.forEach(function (bundle) {
                promises.push(self.add(bundle));
            });

            Q.all(promises)
                .then(function() {
                    // I am calling self.all from within a self.all callback what is wrong with me
                    return self.all();
                })
                .then(function(bundles) {
                    self.emit('allLoaded', bundles);
                });
        }, function (reason) {
            log.info("[lib/bundle/index.js] Failed bundle DB init integrity scan", reason);
        });
}

util.inherits(Bundles, events.EventEmitter);

Bundles.prototype.all = function() {
    // Find all bundles in the datastore
    var deferred = Q.defer();
    db.find({}, function (err, docs) {
        if (err) {
            deferred.reject(new Error(err));
        } else {
            deferred.resolve(docs);
        }
    });
    return deferred.promise;
};

Bundles.prototype.find = function(name) {
    // Find a bundle by name
    var deferred = Q.defer();
    db.findOne({ name: name }, function (err, doc) {
        if (err || doc === null) {
            deferred.reject(new Error(err));
        } else {
            deferred.resolve(doc);
        }
    });
    return deferred.promise;
};

Bundles.prototype.add = function(bundle) {
    // If the parser rejects a bundle for some reason (such as a NodeCG version mismatch), it will be null
    if(!bundle) {
        return;
    }

    // Upserting the bundle
    var deferred = Q.defer();
    db.update({ name: bundle.name }, bundle, { upsert: true }, function (err, numReplaced, upsert) {
        if (err) {
            deferred.reject(new Error(err));
        } else {
            deferred.resolve(numReplaced);
            if (upsert) {
                log.info("[lib/bundle/index.js] Added %s to DB", bundle.name);
            } else {
                log.info("[lib/bundle/index.js] Updated %s's DB entry", bundle.name);
            }
        }
    });
    return deferred.promise;
};

Bundles.prototype.remove = function(bundleName) {
    // options set to {} since the default for multi is false
    db.remove({ name: bundleName }, {}, function (err, numRemoved) {
        log.info("[lib/bundle/index.js] %s has been removed from the DB", bundleName);
    });
};

watcher.on('bundleChanged', function (name) {
    var bundle = Parser.parse(name);

    // If nodecg.json is missing from the bundle dir, bundle will equal false
    if (bundle) {
        log.info("[lib/bundle/index.js] %s was changed, and has been reloaded from disk", name);
        self.add(bundle);
    } else {
        log.info("[lib/bundle/index.js] %s's nodecg.json can no longer be found on disk, assuming the bundle has been deleted or moved", name);
        self.remove(name);
    }
});

module.exports = new Bundles();
<commit_msg>Create bundles folder if it doesn't exist, fixes #43<commit_after>'use strict';

var Datastore = require('nedb'),
    Parser = require('./parser.js'),
    events = require('events'),
    watcher = require('./watcher.js'),
    fs = require('fs'),
    db = new Datastore({ filename: 'db/bundles.db', autoload: true }),
    Q = require('q'),
    util = require('util'),
    log = require('../logger');

// Using a unique constraint with the bundle name
db.ensureIndex({ fieldName: 'name', unique: true }, function (err) {});

// Used in the "watcher" listener
var self = {};

function Bundles() {
    log.trace("[lib/bundles/index.js] Loading bundles");
    events.EventEmitter.call(this);
    self = this;

    // Do an initial scan of the bundles dir
    if (!fs.existsSync('bundles/')) {
        fs.mkdirSync('bundles');
    }

    var bundlesDir = fs.readdirSync('bundles/');
    var discoveredBundles = [];

    bundlesDir.forEach(function(bundleFolderName) {
        var bundle = Parser.parse(bundleFolderName);

        if (bundle !== null) {
            log.trace("[lib/bundles/index.js] Discovered bundle %s", bundle.name);
            discoveredBundles.push(bundle);
        }
    });

    // Remove any entries that are no longer present on disk,
    // add/update the rest
    self.all()
        .then(function (currentBundles) {
            currentBundles.forEach(function(bundle) {
                var bundleName = bundle.name;
                var bundleDiscovered = false;

                discoveredBundles.forEach(function (discoveredBundle) {
                    if (discoveredBundle.name === bundleName) {
                        bundleDiscovered = true;
                    }
                });

                if (!bundleDiscovered) {
                    log.info("[lib/bundle/index.js] %s's nodecg.json was not discovered, assuming the bundle has been deleted or moved", bundle.name);
                    self.remove(bundle.name);
                }
            });

            var promises = [];
            discoveredBundles.forEach(function (bundle) {
                promises.push(self.add(bundle));
            });

            Q.all(promises)
                .then(function() {
                    // I am calling self.all from within a self.all callback what is wrong with me
                    return self.all();
                })
                .then(function(bundles) {
                    self.emit('allLoaded', bundles);
                });
        }, function (reason) {
            log.info("[lib/bundle/index.js] Failed bundle DB init integrity scan", reason);
        });
}

util.inherits(Bundles, events.EventEmitter);

Bundles.prototype.all = function() {
    // Find all bundles in the datastore
    var deferred = Q.defer();
    db.find({}, function (err, docs) {
        if (err) {
            deferred.reject(new Error(err));
        } else {
            deferred.resolve(docs);
        }
    });
    return deferred.promise;
};

Bundles.prototype.find = function(name) {
    // Find a bundle by name
    var deferred = Q.defer();
    db.findOne({ name: name }, function (err, doc) {
        if (err || doc === null) {
            deferred.reject(new Error(err));
        } else {
            deferred.resolve(doc);
        }
    });
    return deferred.promise;
};

Bundles.prototype.add = function(bundle) {
    // If the parser rejects a bundle for some reason (such as a NodeCG version mismatch), it will be null
    if(!bundle) {
        return;
    }

    // Upserting the bundle
    var deferred = Q.defer();
    db.update({ name: bundle.name }, bundle, { upsert: true }, function (err, numReplaced, upsert) {
        if (err) {
            deferred.reject(new Error(err));
        } else {
            deferred.resolve(numReplaced);
            if (upsert) {
                log.info("[lib/bundle/index.js] Added %s to DB", bundle.name);
            } else {
                log.info("[lib/bundle/index.js] Updated %s's DB entry", bundle.name);
            }
        }
    });
    return deferred.promise;
};

Bundles.prototype.remove = function(bundleName) {
    // options set to {} since the default for multi is false
    db.remove({ name: bundleName }, {}, function (err, numRemoved) {
        log.info("[lib/bundle/index.js] %s has been removed from the DB", bundleName);
    });
};

watcher.on('bundleChanged', function (name) {
    var bundle = Parser.parse(name);

    // If nodecg.json is missing from the bundle dir, bundle will equal false
    if (bundle) {
        log.info("[lib/bundle/index.js] %s was changed, and has been reloaded from disk", name);
        self.add(bundle);
    } else {
        log.info("[lib/bundle/index.js] %s's nodecg.json can no longer be found on disk, assuming the bundle has been deleted or moved", name);
        self.remove(name);
    }
});

module.exports = new Bundles();
<|endoftext|>