<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");
        }
        return value;
      },
      getName : function () {
        return { union : this.types };
      }
    }
  });
  types.T_Union = T_Union;

  var T_Map = Class({
    isa : Type,
    has : {
      innerType : null
    },
    methods : {
      BUILD : function (innerType) {
        return {
          innerType : innerType
        };
      },
      check : function (actualType, value, em) {
        // Maps.
        var m = {};
        for (var p in value) {
          var option = new Options({ type : this.innerType });
          m[p] = option.parse(value[p], false);
          em.addSubObj(p, option._getErrorMessage());
        }
        return m;
      }
    }
  });
  types.T_Map = T_Map;

  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));
        }
        return value;
      },
      _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);
        }
        return 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) {
        return value;
      },
      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) {
        if (!(value instanceof Array)) {
          em.add("", typeFail([this.elementType], value));
          return value;
        }

        var subOptions = [];
        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());
        }
        return vs;
      },
      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) {
        this.definition = {};
        if (!(definition instanceof Object)) {
          throw new Error("Options:BUILD: definition must be a hash.");
        }
        // > Throw if both defval and defvalfunc are specified
        if ("defaultValue" in definition) {
          this.definition.required = false;
          this.definition.defaultValue = definition.defaultValue;
        } else if ("defaultValueFunc" in definition) {
          this.definition.required = false;
          this.definition.defaultValueFunc = definition.defaultValueFunc;
        } else if ("required" in definition) {
          this.definition.required = !!definition.required;
        } else {
          this.definition.required = true;
        }

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

        if (definition.union) {
          this.definition.type = new T_Union(definition.union);
        } else if (definition.enumerable) {
          this.definition.type = new T_Enumerable(definition.enumerable);
        } else if (definition.type instanceof Array) {
          this.definition.type = new T_Array(definition.type[0]);
        } else if (definition.type === "mixed") {
          this.definition.type = new T_Mixed();
        } else if (definition.type instanceof Function) {
          this.definition.type = new T_Instance(definition.type);
        } else if ("map" in definition) {
          this.definition.type = new T_Map(definition.type);
        } else {
          this.definition.type = definition.type;
        }
      },
      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 && options !== null && options !== undefined) {
          var actualType = Options.typeof(options);
          options = this.definition.type.check(actualType, options, this.errorMessage);
          if (this.errorMessage.has() && throwErrors) {
            throw new Error(this.errorMessage.getErrorString());
          }
        } else if (this.definition.type instanceof Object && options instanceof Object
                   && options !== null && options !== undefined) {
          // Hashes.

          var h = {};
          // All properties in the definition.
          for (var p in this.definition.type) if (this.definition.type.hasOwnProperty(p)) {
            if (!(p in options) && (this.definition.type[p].required || !("required" in this.definition.type[p]))
                && !("defaultValue" in this.definition.type[p])
                && !("defaultValueFunc" in this.definition.type[p])) {
              this.errorMessage.add(p, "Missing property");
            } else {
              var opts = new Options(this.definition.type[p], false);
              h[p] = opts.parse(options[p], false);
              if (opts.hasErrors()) {
                this.errorMessage.addSubObj(p, opts._getErrorMessage());
              }
            }
          }
          // 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 && options !== null) {
          // Standard types: string, number, boolean.
        } else if (this.definition.required === false && (options === null || options === undefined)) {
          if ("defaultValue" in this.definition) {
            options = this.definition.defaultValue;
            return this.parse(options);
          } else if ("defaultValueFunc" in this.definition) {
            options = this.definition.defaultValueFunc();
            return this.parse(options);
          } 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");
        }
        return value;
      },
      getName : function () {
        return { union : this.types };
      }
    }
  });
  types.T_Union = T_Union;

  var T_Map = Class({
    isa : Type,
    has : {
      innerType : null
    },
    methods : {
      BUILD : function (innerType) {
        return {
          innerType : innerType
        };
      },
      check : function (actualType, value, em) {
        // Maps.
        var m = {};
        for (var p in value) {
          var option = new Options({ type : this.innerType });
          m[p] = option.parse(value[p], false);
          em.addSubObj(p, option._getErrorMessage());
        }
        return m;
      }
    }
  });
  types.T_Map = T_Map;

  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));
        }
        return value;
      },
      _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);
        }
        return 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) {
        return value;
      },
      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) {
        if (!(value instanceof Array)) {
          em.add("", typeFail([this.elementType], value));
          return value;
        }

        var subOptions = [];
        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());
        }
        return vs;
      },
      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) {
        this.definition = {};
        if (!(definition instanceof Object)) {
          throw new Error("Options:BUILD: definition must be a hash.");
        }
        // > Throw if both defval and defvalfunc are specified
        if ("defaultValue" in definition) {
          this.definition.required = false;
          this.definition.defaultValue = definition.defaultValue;
        } else if ("defaultValueFunc" in definition) {
          this.definition.required = false;
          this.definition.defaultValueFunc = definition.defaultValueFunc;
        } else if ("required" in definition) {
          this.definition.required = !!definition.required;
        } else {
          this.definition.required = true;
        }

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

        if (definition.union) {
          this.definition.type = new T_Union(definition.union);
        } else if (definition.enumerable) {
          this.definition.type = new T_Enumerable(definition.enumerable);
        } else if (definition.type instanceof Array) {
          this.definition.type = new T_Array(definition.type[0]);
        } else if (definition.type === "mixed") {
          this.definition.type = new T_Mixed();
        } else if (definition.type instanceof Function) {
          this.definition.type = new T_Instance(definition.type);
        } else if ("map" in definition) {
          this.definition.type = new T_Map(definition.type);
        } else {
          this.definition.type = definition.type;
        }
      },
      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 && options !== null && options !== undefined) {
          var actualType = Options.typeof(options);
          options = this.definition.type.check(actualType, options, this.errorMessage);
          if (this.errorMessage.has() && throwErrors) {
            throw new Error(this.errorMessage.getErrorString());
          }
        } else if (this.definition.type instanceof Object && options instanceof Object
                   && options !== null && options !== undefined) {
          // Hashes.

          var h = {};
          // All properties in the definition.
          for (var p in this.definition.type) if (this.definition.type.hasOwnProperty(p)) {
            if (!(p in options) && (this.definition.type[p].required || !("required" in this.definition.type[p]))
                && !("defaultValue" in this.definition.type[p])
                && !("defaultValueFunc" in this.definition.type[p])) {
              this.errorMessage.add(p, "Missing property");
            } else {
              var opts = new Options(this.definition.type[p], false);
              h[p] = opts.parse(options[p], false);
              if (opts.hasErrors()) {
                this.errorMessage.addSubObj(p, opts._getErrorMessage());
              }
            }
          }
          // 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 && options !== null) {
          // Standard types: string, number, boolean.
        } else if (this.definition.required === false && (options === null || options === undefined)) {
          if ("defaultValue" in this.definition) {
            options = this.definition.defaultValue;
            return this.parse(options);
          } else if ("defaultValueFunc" in this.definition) {
            options = this.definition.defaultValueFunc();
            return this.parse(options);
          } else {
            // .
          }
        } 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>import { createAction } from 'redux-actions'

import { routeTo } from './ui'
import { secureFetch } from '../util/middleware'
import { isNewUser } from '../util/user'

// Middleware API paths.
const API_MONITORTRIP_PATH = '/api/secure/monitoredtrip'
const API_OTPUSER_PATH = '/api/secure/user'
const API_OTPUSER_VERIFYSMS_PATH = '/verify_sms'

const setCurrentUser = createAction('SET_CURRENT_USER')
const setCurrentUserMonitoredTrips = createAction('SET_CURRENT_USER_MONITORED_TRIPS')
const setLastPhoneSmsRequest = createAction('SET_LAST_PHONE_SMS_REQUEST')
export const setPathBeforeSignIn = createAction('SET_PATH_BEFORE_SIGNIN')

function createNewUser (auth0User) {
  return {
    auth0UserId: auth0User.sub,
    email: auth0User.email,
    hasConsentedToTerms: false, // User must agree to terms.
    notificationChannel: 'email',
    phoneNumber: '',
    savedLocations: [],
    storeTripHistory: false // User must opt in.
  }
}

/**
 * Extracts accessToken, loggedInUser from the redux state,
 * and apiBaseUrl, apiKey from the middleware configuration.
 * If the middleware configuration does not exist, throws an error.
 */
function getMiddlewareVariables (state) {
  const { otp, user } = state
  const { otp_middleware: otpMiddleware = null } = otp.config.persistence

  if (otpMiddleware) {
    const { accessToken, loggedInUser } = user
    const { apiBaseUrl, apiKey } = otpMiddleware
    return {
      accessToken,
      apiBaseUrl,
      apiKey,
      loggedInUser
    }
  } else {
    throw new Error('This action requires config.yml#persistence#otp_middleware to be defined.')
  }
}

/**
 * Fetches the saved/monitored trips for a user.
 * We use the accessToken to fetch the data regardless of
 * whether the process to populate state.user is completed or not.
 */
export function fetchUserMonitoredTrips (accessToken) {
  return async function (dispatch, getState) {
    const { apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
    const requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}`

    const { data: trips, status } = await secureFetch(requestUrl, accessToken, apiKey, 'GET')
    if (status === 'success') {
      dispatch(setCurrentUserMonitoredTrips(trips.data))
    }
  }
}

/**
 * Fetches user preferences to state.user,
 * or set initial values under state.user if no user has been loaded.
 */
export function fetchOrInitializeUser (auth) {
  return async function (dispatch, getState) {
    const { apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
    const { accessToken, user: authUser } = auth
    const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/fromtoken`

    const { data: user, status } = await secureFetch(requestUrl, accessToken, apiKey)

    // Beware! On AWS API gateway, if a user is not found in the middleware
    // (e.g. they just created their Auth0 password but have not completed the account setup form yet),
    // the call above will return, for example:
    // {
    //    status: 'success',
    //    data: {
    //      "result": "ERR",
    //      "message": "No user with id=000000 found.",
    //      "code": 404,
    //      "detail": null
    //    }
    // }
    //
    // The same call to a middleware instance that is not behind an API gateway
    // will return:
    // {
    //    status: 'error',
    //    message: 'Error get-ing user...'
    // }
    // TODO: Improve AWS response.

    const isNewAccount = status === 'error' || (user && user.result === 'ERR')
    if (!isNewAccount) {
      // Load user's monitored trips before setting the user state.
      await dispatch(fetchUserMonitoredTrips(accessToken))

      dispatch(setCurrentUser({ accessToken, user }))
    } else {
      dispatch(setCurrentUser({ accessToken, user: createNewUser(authUser) }))
    }
  }
}

/**
 * Updates (or creates) a user entry in the middleware,
 * then, if that was successful, updates the redux state with that user.
 * @param userData the user entry to persist.
 * @param silentOnSuccess true to suppress the confirmation if the operation is successful (e.g. immediately after user accepts the terms).
 */
export function createOrUpdateUser (userData, silentOnSuccess = false) {
  return async function (dispatch, getState) {
    const { accessToken, apiBaseUrl, apiKey, loggedInUser } = getMiddlewareVariables(getState())
    const { id } = userData // Middleware ID, NOT auth0 (or similar) id.
    let requestUrl, method

    // Determine URL and method to use.
    if (isNewUser(loggedInUser)) {
      requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}`
      method = 'POST'
    } else {
      requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/${id}`
      method = 'PUT'
    }

    const { data, message, status } = await secureFetch(requestUrl, accessToken, apiKey, method, {
      body: JSON.stringify(userData)
    })

    // TODO: improve the UI feedback messages for this.
    if (status === 'success' && data) {
      if (!silentOnSuccess) {
        alert('Your preferences have been saved.')
      }

      // Update application state with the user entry as saved
      // (as returned) by the middleware.
      dispatch(setCurrentUser({ accessToken, user: data }))
    } else {
      alert(`An error was encountered:\n${JSON.stringify(message)}`)
    }
  }
}

/**
 * Updates a logged-in user's monitored trip,
 * then, if that was successful, alerts (optional)
 * and refreshes the redux monitoredTrips with the updated trip.
 */
export function createOrUpdateUserMonitoredTrip (tripData, isNew, silentOnSuccess) {
  return async function (dispatch, getState) {
    const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
    const { id } = tripData
    let requestUrl, method

    // Determine URL and method to use.
    if (isNew) {
      requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}`
      method = 'POST'
    } else {
      requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}/${id}`
      method = 'PUT'
    }

    const { data, message, status } = await secureFetch(requestUrl, accessToken, apiKey, method, {
      body: JSON.stringify(tripData)
    })

    // TODO: improve the UI feedback messages for this.
    if (status === 'success' && data) {
      if (!silentOnSuccess) {
        alert('Your preferences have been saved.')
      }

      // Reload user's monitored trips after add/update.
      await dispatch(fetchUserMonitoredTrips(accessToken))

      // Finally, navigate to the saved trips page.
      dispatch(routeTo('/savedtrips'))
    } else {
      alert(`An error was encountered:\n${JSON.stringify(message)}`)
    }
  }
}

/**
 * Deletes a logged-in user's monitored trip,
 * then, if that was successful, refreshes the redux monitoredTrips state.
 */
export function deleteUserMonitoredTrip (tripId) {
  return async function (dispatch, getState) {
    const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
    const requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}/${tripId}`

    const { message, status } = await secureFetch(requestUrl, accessToken, apiKey, 'DELETE')
    if (status === 'success') {
      // Reload user's monitored trips after deletion.
      await dispatch(fetchUserMonitoredTrips(accessToken))
    } else {
      alert(`An error was encountered:\n${JSON.stringify(message)}`)
    }
  }
}

/**
 * Requests a verification code via SMS for the logged-in user.
 */
export function requestPhoneVerificationSms (newPhoneNumber) {
  return async function (dispatch, getState) {
    const state = getState()
    const { number, timestamp } = state.user.lastPhoneSmsRequest
    const now = new Date()

    // Request a new verification code if we are requesting a different number.
    // or enough time has ellapsed since the last request (1 minute?).
    // TODO: Should throttling be handled in the middleware?
    if (number !== newPhoneNumber || timestamp + 60000 >= now) {
      const { accessToken, apiBaseUrl, apiKey, loggedInUser } = getMiddlewareVariables(state)
      const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/${loggedInUser.id}${API_OTPUSER_VERIFYSMS_PATH}/${encodeURIComponent(newPhoneNumber)}`

      const { message, status } = await secureFetch(requestUrl, accessToken, apiKey, 'GET')

      dispatch(setLastPhoneSmsRequest({
        number: newPhoneNumber,
        status,
        timestamp: now
      }))

      if (status === 'success') {
        // Refetch user and update application state with new phone number and verification status.
        // (This also refetches the user's monitored trip, and that's ok.)
        await dispatch(fetchOrInitializeUser({ accessToken }))
      } else {
        alert(`An error was encountered:\n${JSON.stringify(message)}`)
      }
    } else {
      // Alert user if they have been throttled.
      // TODO: improve this alert.
      alert('A verification SMS was sent to the indicated phone number less than a minute ago. Please try again in a few moments.')
    }
  }
}

/**
 * Validate the phone number verification code for the logged-in user.
 */
export function verifyPhoneNumber (code) {
  return async function (dispatch, getState) {
    const { accessToken, apiBaseUrl, apiKey, loggedInUser } = getMiddlewareVariables(getState())
    const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/${loggedInUser.id}${API_OTPUSER_VERIFYSMS_PATH}/${code}`

    const { data, status } = await secureFetch(requestUrl, accessToken, apiKey, 'POST')

    // If the check is successful, status in the returned data will be "approved".
    if (status === 'success' && data) {
      if (data.status === 'approved') {
        // Refetch user and update application state with new phone number and verification status.
        // (This also refetches the user's monitored trip, and that's ok.)
        dispatch(fetchOrInitializeUser({ accessToken }))
      } else {
        // Otherwise, the user entered a wrong/incorrect code.
        alert('The code you entered is invalid. Please try again.')
      }
    } else {
      // This happens when an error occurs on backend side, especially
      // when the code has expired (or was cancelled by Twilio after too many attempts).
      alert(`Your phone could not be verified. Perhaps the code you entered has expired. Please request a new code and try again.`)
    }
  }
}
<commit_msg>fix(actions/user): Fix sms throttling.<commit_after>import { createAction } from 'redux-actions'

import { routeTo } from './ui'
import { secureFetch } from '../util/middleware'
import { isNewUser } from '../util/user'

// Middleware API paths.
const API_MONITORTRIP_PATH = '/api/secure/monitoredtrip'
const API_OTPUSER_PATH = '/api/secure/user'
const API_OTPUSER_VERIFYSMS_PATH = '/verify_sms'

const setCurrentUser = createAction('SET_CURRENT_USER')
const setCurrentUserMonitoredTrips = createAction('SET_CURRENT_USER_MONITORED_TRIPS')
const setLastPhoneSmsRequest = createAction('SET_LAST_PHONE_SMS_REQUEST')
export const setPathBeforeSignIn = createAction('SET_PATH_BEFORE_SIGNIN')

function createNewUser (auth0User) {
  return {
    auth0UserId: auth0User.sub,
    email: auth0User.email,
    hasConsentedToTerms: false, // User must agree to terms.
    notificationChannel: 'email',
    phoneNumber: '',
    savedLocations: [],
    storeTripHistory: false // User must opt in.
  }
}

/**
 * Extracts accessToken, loggedInUser from the redux state,
 * and apiBaseUrl, apiKey from the middleware configuration.
 * If the middleware configuration does not exist, throws an error.
 */
function getMiddlewareVariables (state) {
  const { otp, user } = state
  const { otp_middleware: otpMiddleware = null } = otp.config.persistence

  if (otpMiddleware) {
    const { accessToken, loggedInUser } = user
    const { apiBaseUrl, apiKey } = otpMiddleware
    return {
      accessToken,
      apiBaseUrl,
      apiKey,
      loggedInUser
    }
  } else {
    throw new Error('This action requires config.yml#persistence#otp_middleware to be defined.')
  }
}

/**
 * Fetches the saved/monitored trips for a user.
 * We use the accessToken to fetch the data regardless of
 * whether the process to populate state.user is completed or not.
 */
export function fetchUserMonitoredTrips (accessToken) {
  return async function (dispatch, getState) {
    const { apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
    const requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}`

    const { data: trips, status } = await secureFetch(requestUrl, accessToken, apiKey, 'GET')
    if (status === 'success') {
      dispatch(setCurrentUserMonitoredTrips(trips.data))
    }
  }
}

/**
 * Fetches user preferences to state.user,
 * or set initial values under state.user if no user has been loaded.
 */
export function fetchOrInitializeUser (auth) {
  return async function (dispatch, getState) {
    const { apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
    const { accessToken, user: authUser } = auth
    const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/fromtoken`

    const { data: user, status } = await secureFetch(requestUrl, accessToken, apiKey)

    // Beware! On AWS API gateway, if a user is not found in the middleware
    // (e.g. they just created their Auth0 password but have not completed the account setup form yet),
    // the call above will return, for example:
    // {
    //    status: 'success',
    //    data: {
    //      "result": "ERR",
    //      "message": "No user with id=000000 found.",
    //      "code": 404,
    //      "detail": null
    //    }
    // }
    //
    // The same call to a middleware instance that is not behind an API gateway
    // will return:
    // {
    //    status: 'error',
    //    message: 'Error get-ing user...'
    // }
    // TODO: Improve AWS response.

    const isNewAccount = status === 'error' || (user && user.result === 'ERR')
    if (!isNewAccount) {
      // Load user's monitored trips before setting the user state.
      await dispatch(fetchUserMonitoredTrips(accessToken))

      dispatch(setCurrentUser({ accessToken, user }))
    } else {
      dispatch(setCurrentUser({ accessToken, user: createNewUser(authUser) }))
    }
  }
}

/**
 * Updates (or creates) a user entry in the middleware,
 * then, if that was successful, updates the redux state with that user.
 * @param userData the user entry to persist.
 * @param silentOnSuccess true to suppress the confirmation if the operation is successful (e.g. immediately after user accepts the terms).
 */
export function createOrUpdateUser (userData, silentOnSuccess = false) {
  return async function (dispatch, getState) {
    const { accessToken, apiBaseUrl, apiKey, loggedInUser } = getMiddlewareVariables(getState())
    const { id } = userData // Middleware ID, NOT auth0 (or similar) id.
    let requestUrl, method

    // Determine URL and method to use.
    if (isNewUser(loggedInUser)) {
      requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}`
      method = 'POST'
    } else {
      requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/${id}`
      method = 'PUT'
    }

    const { data, message, status } = await secureFetch(requestUrl, accessToken, apiKey, method, {
      body: JSON.stringify(userData)
    })

    // TODO: improve the UI feedback messages for this.
    if (status === 'success' && data) {
      if (!silentOnSuccess) {
        alert('Your preferences have been saved.')
      }

      // Update application state with the user entry as saved
      // (as returned) by the middleware.
      dispatch(setCurrentUser({ accessToken, user: data }))
    } else {
      alert(`An error was encountered:\n${JSON.stringify(message)}`)
    }
  }
}

/**
 * Updates a logged-in user's monitored trip,
 * then, if that was successful, alerts (optional)
 * and refreshes the redux monitoredTrips with the updated trip.
 */
export function createOrUpdateUserMonitoredTrip (tripData, isNew, silentOnSuccess) {
  return async function (dispatch, getState) {
    const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
    const { id } = tripData
    let requestUrl, method

    // Determine URL and method to use.
    if (isNew) {
      requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}`
      method = 'POST'
    } else {
      requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}/${id}`
      method = 'PUT'
    }

    const { data, message, status } = await secureFetch(requestUrl, accessToken, apiKey, method, {
      body: JSON.stringify(tripData)
    })

    // TODO: improve the UI feedback messages for this.
    if (status === 'success' && data) {
      if (!silentOnSuccess) {
        alert('Your preferences have been saved.')
      }

      // Reload user's monitored trips after add/update.
      await dispatch(fetchUserMonitoredTrips(accessToken))

      // Finally, navigate to the saved trips page.
      dispatch(routeTo('/savedtrips'))
    } else {
      alert(`An error was encountered:\n${JSON.stringify(message)}`)
    }
  }
}

/**
 * Deletes a logged-in user's monitored trip,
 * then, if that was successful, refreshes the redux monitoredTrips state.
 */
export function deleteUserMonitoredTrip (tripId) {
  return async function (dispatch, getState) {
    const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState())
    const requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}/${tripId}`

    const { message, status } = await secureFetch(requestUrl, accessToken, apiKey, 'DELETE')
    if (status === 'success') {
      // Reload user's monitored trips after deletion.
      await dispatch(fetchUserMonitoredTrips(accessToken))
    } else {
      alert(`An error was encountered:\n${JSON.stringify(message)}`)
    }
  }
}

/**
 * Requests a verification code via SMS for the logged-in user.
 */
export function requestPhoneVerificationSms (newPhoneNumber) {
  return async function (dispatch, getState) {
    const state = getState()
    const { number, timestamp } = state.user.lastPhoneSmsRequest
    const now = new Date()

    // Request a new verification code if we are requesting a different number.
    // or enough time has ellapsed since the last request (1 minute?).
    // TODO: Should throttling be handled in the middleware?
    if (number !== newPhoneNumber || (now - timestamp) >= 60000) {
      const { accessToken, apiBaseUrl, apiKey, loggedInUser } = getMiddlewareVariables(state)
      const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/${loggedInUser.id}${API_OTPUSER_VERIFYSMS_PATH}/${encodeURIComponent(newPhoneNumber)}`

      const { message, status } = await secureFetch(requestUrl, accessToken, apiKey, 'GET')

      dispatch(setLastPhoneSmsRequest({
        number: newPhoneNumber,
        status,
        timestamp: now
      }))

      if (status === 'success') {
        // Refetch user and update application state with new phone number and verification status.
        // (This also refetches the user's monitored trip, and that's ok.)
        await dispatch(fetchOrInitializeUser({ accessToken }))
      } else {
        alert(`An error was encountered:\n${JSON.stringify(message)}`)
      }
    } else {
      // Alert user if they have been throttled.
      // TODO: improve this alert.
      alert('A verification SMS was sent to the indicated phone number less than a minute ago. Please try again in a few moments.')
    }
  }
}

/**
 * Validate the phone number verification code for the logged-in user.
 */
export function verifyPhoneNumber (code) {
  return async function (dispatch, getState) {
    const { accessToken, apiBaseUrl, apiKey, loggedInUser } = getMiddlewareVariables(getState())
    const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/${loggedInUser.id}${API_OTPUSER_VERIFYSMS_PATH}/${code}`

    const { data, status } = await secureFetch(requestUrl, accessToken, apiKey, 'POST')

    // If the check is successful, status in the returned data will be "approved".
    if (status === 'success' && data) {
      if (data.status === 'approved') {
        // Refetch user and update application state with new phone number and verification status.
        // (This also refetches the user's monitored trip, and that's ok.)
        dispatch(fetchOrInitializeUser({ accessToken }))
      } else {
        // Otherwise, the user entered a wrong/incorrect code.
        alert('The code you entered is invalid. Please try again.')
      }
    } else {
      // This happens when an error occurs on backend side, especially
      // when the code has expired (or was cancelled by Twilio after too many attempts).
      alert(`Your phone could not be verified. Perhaps the code you entered has expired. Please request a new code and try again.`)
    }
  }
}
<|endoftext|>


<commit_before>var API = exports.API = function(model) {
  this.model = model
}

API.prototype.request = function(/*method, obj, success */) {
  // retrieve arguments
  var args    = Array.prototype.slice.call(arguments)
    , method  = args.shift()
    , success = args.pop()
    , obj, view, key, id

  if (method === 'list') {
    view = args.shift()
    key  = args.shift()
  } else {
    if (args.length === 2)
      id = args.shift()
    else
      id = args[0].id
    obj  = args.shift()
  }

  // build ajax request
  var params = {
    type:     method === 'list' ? 'get' : method,
    dataType: 'json',
    url:      '/_api/' + this.model._type.toLowerCase(),
    success:  function(data) { success(null, data) },
    error:    function(xhr)  { success({ status: xhr.status, message: xhr.statusText }, null)}
  }

  // compose url
  if (method === 'list') {
    if (view) {
      params.url += '?view=' + view
      if (key) params.url += '&key=' + key
    }
  }
  else if (method != 'post') {
    params.url += '/' + encodeURIComponent(id)
  }

  // add data
  if (method == 'post' || method == 'put') {
    params.contentType = 'application/json'
    params.data = JSON.stringify(obj instanceof this.model ? obj.toJSON() : obj)
  }

  // Fire in the hole
  $.ajax(params)
}

API.prototype.createModel = function(data) {
  var instance = new this.model(data)
  instance.isNew = false
  return instance
}

API.prototype.mergeModel = function(instance, data) {
  Object.keys(data).forEach(function(key) {
    if (instance.hasOwnProperty(key) && instance[key] !== data[key])
      instance[key] = data[key]
  })
}

API.prototype.get = function(id, callback) {
  var that = this
  this.request('get', id, function(err, data) {
    if (err) return callback(err, null)
    callback(null, that.createModel(data))
  })
}

API.prototype.list = function(/* view, key, callback */) {
  var that = this
    , args = Array.prototype.slice.call(arguments)
    , callback = args.pop()
  this.request.apply(this, ['list'].concat(args, function(err, data) {
    if (err) return callback(err, null)
    if (!data || !Array.isArray(data)) return callback(null, data || null)
    callback(null, data.map(function(row) {
      return that.createModel(row)
    }))
  }))
}

API.prototype.put = function(/*instance, callback*/) {
  var args = Array.prototype.slice.call(arguments)
    , callback = args.pop()
    , that = this
  this.request.apply(this, ['put'].concat(args, function(err, data) {
    if (err) return callback(err, null)
    var instance
    if (typeof args[0] === 'object') {
      instance = args[0]
      that.mergeModel(instance, data)
    } else {
      instance = data
    }
    instance.isNew = false
    callback(null, instance)
  }))
}

API.prototype.post = function(instance, callback) {
  var that = this
  this.request('post', instance, function(err, data) {
    if (err) return callback(err, null)
    if (!(instance instanceof that.model))
      instance = that.createModel(instance)
    that.mergeModel(instance, data)
    instance.isNew = false
    callback(null, instance)
  })
}

API.prototype.delete = function(instance, callback) {
  var args = Array.prototype.slice.call(arguments)
    , callback = args.pop()
  this.request.apply(this, ['delete'].concat(args, function(err, data) {
    if (err) return callback(err, null)
    callback(null)
  }))
}

exports.initialize = function(model, opts, callback) {
  var api = new API(model)
  return api
}
<commit_msg>Move Server-Side Validation Errors into Client-Side $errors Object<commit_after>var API = exports.API = function(model) {
  this.model = model
}

API.prototype.request = function(/*method, obj, success */) {
  // retrieve arguments
  var args    = Array.prototype.slice.call(arguments)
    , method  = args.shift()
    , success = args.pop()
    , obj, view, key, id

  if (method === 'list') {
    view = args.shift()
    key  = args.shift()
  } else {
    if (args.length === 2)
      id = args.shift()
    else
      id = args[0].id
    obj  = args.shift()
  }

  // build ajax request
  var params = {
    type:     method === 'list' ? 'get' : method,
    dataType: 'json',
    url:      '/_api/' + this.model._type.toLowerCase(),
    success:  function(data) { success(null, data) },
    error:    function(xhr)  {
      if (xhr.responseText) {
        var err = JSON.parse(xhr.responseText)
        Object.keys(err.details).forEach(function(key) {
          obj.$errors[key] = err.details[key]
          obj.emit('changed.' + key)
        })
        obj.$errors = err.details
      }
      success({ status: xhr.status, message: xhr.statusText }, null)
    }
  }

  // compose url
  if (method === 'list') {
    if (view) {
      params.url += '?view=' + view
      if (key) params.url += '&key=' + key
    }
  }
  else if (method != 'post') {
    params.url += '/' + encodeURIComponent(id)
  }

  // add data
  if (method == 'post' || method == 'put') {
    params.contentType = 'application/json'
    params.data = JSON.stringify(obj instanceof this.model ? obj.toJSON() : obj)
  }

  // Fire in the hole
  $.ajax(params)
}

API.prototype.createModel = function(data) {
  var instance = new this.model(data)
  instance.isNew = false
  return instance
}

API.prototype.mergeModel = function(instance, data) {
  Object.keys(data).forEach(function(key) {
    if (instance.hasOwnProperty(key) && instance[key] !== data[key])
      instance[key] = data[key]
  })
}

API.prototype.get = function(id, callback) {
  var that = this
  this.request('get', id, function(err, data) {
    if (err) return callback(err, null)
    callback(null, that.createModel(data))
  })
}

API.prototype.list = function(/* view, key, callback */) {
  var that = this
    , args = Array.prototype.slice.call(arguments)
    , callback = args.pop()
  this.request.apply(this, ['list'].concat(args, function(err, data) {
    if (err) return callback(err, null)
    if (!data || !Array.isArray(data)) return callback(null, data || null)
    callback(null, data.map(function(row) {
      return that.createModel(row)
    }))
  }))
}

API.prototype.put = function(/*instance, callback*/) {
  var args = Array.prototype.slice.call(arguments)
    , callback = args.pop()
    , that = this
  this.request.apply(this, ['put'].concat(args, function(err, data) {
    if (err) return callback(err, null)
    var instance
    if (typeof args[0] === 'object') {
      instance = args[0]
      that.mergeModel(instance, data)
    } else {
      instance = data
    }
    instance.isNew = false
    callback(null, instance)
  }))
}

API.prototype.post = function(instance, callback) {
  var that = this
  this.request('post', instance, function(err, data) {
    if (err) return callback(err, null)
    if (!(instance instanceof that.model))
      instance = that.createModel(instance)
    that.mergeModel(instance, data)
    instance.isNew = false
    callback(null, instance)
  })
}

API.prototype.delete = function(instance, callback) {
  var args = Array.prototype.slice.call(arguments)
    , callback = args.pop()
  this.request.apply(this, ['delete'].concat(args, function(err, data) {
    if (err) return callback(err, null)
    callback(null)
  }))
}

exports.initialize = function(model, opts, callback) {
  var api = new API(model)
  return api
}
<|endoftext|>


<commit_before>#!/usr/bin/env node

var program = require('commander'),
    fs = require('fs'),
    path = require('path'),
    common = require('./lib/common.js'),
    xhn = require('../xhn');

var generator = common.generator,
    method = common.method,
    relations = common.relations,
    resource = common.resource,
    resources = common.resources;

var seperator = path.sep,
    folder = '.' + seperator + 'api' + seperator,
    file = 'api.json';

var api = resources((fs.existsSync(folder + file)) ? JSON.parse(fs.readFileSync(folder + file, 'utf8')) : undefined),
    affordances = api.getAffordances(true),
    rels = relations(),
    apigen = generator()
        .setAffordances(affordances)
        .setDelegate(function (resource, method, status, affordances) {
            var json = !!program.json,
                quiet = !program.debug,
                hfactors = xhn.affordances,
                control = xhn.hc,
                stringify = xhn.stringify;

            var nonWordCharacters = /\W+/g,
                underscore = '_',
                statusString = status.toString(),
                methodString = method._method.toString(),
                resourceString = resource._uri.toString(),
                normalizedStatus = statusString.toLowerCase().replace(nonWordCharacters, underscore),
                normalizedMethod = methodString.toLowerCase().replace(nonWordCharacters, underscore),
                normalizedResource = resourceString.toLowerCase().replace(nonWordCharacters, underscore),
                statusPath = folder + normalizedStatus + seperator,
                methodPath = statusPath + normalizedMethod + seperator,
                resourcePath = methodPath + normalizedResource + seperator,
                extension = ((json) ? 'json' : 'xml'),
                file = 'response.' + extension;

            if (!quiet) {
                console.log('Will save to file: ' + resourcePath + file);
                console.log('Serialization format: ' + extension);
            }

            var hypermedia = hfactors(statusString);

            if (!quiet) {
                console.log('Affordances:');
                console.log(affordances);
            }

            function finish() {
                try {
                    if (!fs.existsSync(folder)) {
                        fs.mkdirSync(folder);
                    }
                    if (!fs.existsSync(statusPath)) {
                        fs.mkdirSync(statusPath);
                    }
                    if (!fs.existsSync(methodPath)) {
                        fs.mkdirSync(methodPath);
                    }
                    if (!fs.existsSync(resourcePath)) {
                        fs.mkdirSync(resourcePath);
                    }
                    fs.writeFileSync(resourcePath + file, stringify(hypermedia));
                } catch (e) {
                    console.log('Attempting to create the Affordance file at its default path threw an exception.');
                    console.log(e);
                }
                apigen.generate(quiet);
            }

            function chooseAction() {
                console.log('Please choose an action possibility. Choose done to serialize to disk.');
                program.choose(affordances, function (action) {
                    if (action === 0) {
                        finish();
                    } else {
                        action = affordances[action];
                        affordances = affordances.filter(function (element) {
                            return element !== action;
                        });

                        if (!quiet) {
                            console.log('Affordances:');
                            console.log(affordances);
                        }

                        var method = action._method,
                            resource = action._resource,
                            arguments = action._arguments,
                            id = action._id;

                        var hfactor = control();

                        hypermedia.addAffordance(hfactor);
                        hfactor._method = method;
                        hfactor._uri = resource;

                        if (!!arguments) {
                            hfactor._metadata.addField('Content-Type', arguments);
                        }
                        if (!!id) {
                            hfactor._id = id;
                        }
                        if (!quiet) {
                            console.log('You chose: ' + action);
                            console.log('Affordance Method:');
                            console.log(method);
                            console.log('Affordance Resource:');
                            console.log(resource);
                            console.log('Affordance Arguments:');
                            console.log(arguments);
                            console.log('Affordance ID:');
                            console.log(id);
                        }

                        program.prompt('Enter a link relation: ', function (relation) {
                            hfactor._relation = relation;
                            if (!id) {
                                program.prompt('Enter a link identifier: ', function (id) {
                                    hfactor._id = id;
                                    chooseAction();
                                });
                            } else {
                                chooseAction();
                            }
                        });
                    }
                });
            }

            function start() {
                console.log('Generating: ' + method._method + ' --> ' + resource._uri + ' --> ' + status);
                chooseAction();
            }

            start();
        });

program
    .command('put [res] [met]')
    .description('Create or Update a Resource or Method within your API specification.')
    .action(function (res, met) {
        var quiet = !program.debug,
            ret = (typeof program.returns === 'string') ? program.returns : null,
            arg = (typeof program.arguments === 'string') ? program.arguments : null,
            id = (typeof program.id === 'string') ? program.id : null,
            ids = api.getIds(true);

        if (!quiet) {
            console.log('Resource:');
            console.log(res);
            console.log('Method:');
            console.log(met);
            console.log('Method arguments:');
            console.log(arg);
            console.log('Method returns:');
            console.log(ret);
            console.log('Method id:');
            console.log(id);
            console.log('Existing Method ids:');
            console.dir(ids);
        }

        if (!!met) {
            res = api.getResource(res);

            if (!!ret) {
                try {
                    ret = JSON.parse('[' + ret + ']');
                    ret = ret.filter(function (element, position) {
                        return ret.indexOf(element) === position;
                    });
                } catch (e) {
                    console.log('A valid, comma-delimited set of status codes must be entered for your transfer protocol. ' +
                        'An (HTTP) example: 200,300,400,500');
                    return;
                }
            }
            if (!!res) {
                if (!!id) {
                    var pair = api.getResourceMethodPair(id),
                        same = !!pair && (pair[0] === res) && (pair[1] === res.getMethod(met));
                    if (!!pair && !same) {
                        console.log('A valid, globally unique method ID should be entered.');
                        return;
                    }
                }

                res.putMethod(method(met, arg, ret, id));
                if (!quiet) {
                    ids = api.getIds(true);
                    console.log('Updated Method ids:');
                    console.dir(ids);
                }
            } else {
                console.log('A valid, existent resource from your API should be entered. An example: /uri');
            }
        } else if (!!res) {
            api.putResource(resource(res));
        }
    });

program
    .command('get [res] [met]')
    .description('Retrieve an existing Resource or Method within your API specification.')
    .action(function (res, met) {
        var quiet = !program.debug,
            id = (typeof program.id === 'string') ? program.id : null,
            ids = api.getIds(true);

        if (!quiet) {
            console.log('Resource:');
            console.log(res);
            console.log('Method:');
            console.log(met);
            console.log('Method id:');
            console.log(id);
            console.log('Existing Method ids:');
            console.dir(ids);
        }
        if (!!met) {
            res = api.getResource(res);

            if (!!res) {
                console.log(JSON.stringify(res.getMethod(met), null, '    '));
            } else {
                console.log('A valid, existent resource from your API should be entered. An example: /uri');
            }
        } else if (!!res) {
            console.log(JSON.stringify(api.getResource(res), null, '    '));
        } else if (!!id) {
            console.log(JSON.stringify(api.getMethod(id), null, '    '));
        } else {
            console.log(JSON.stringify(api, null, '    '));
        }
    });

program
    .command('del [res] [met]')
    .description('Delete an existing Resource or Method within your API specification.')
    .action(function (res, met) {
        var quiet = !program.debug,
            id = (typeof program.id === 'string') ? program.id : null,
            ids = api.getIds(true);

        if (!quiet) {
            console.log('Resource:');
            console.log(res);
            console.log('Method:');
            console.log(met);
            console.log('Method id:');
            console.log(id);
            console.log('Existing Method ids:');
            console.dir(ids);
        }
        if (!!met) {
            res = api.getResource(res);

            if (!!res) {
                res.deleteMethod(met);
            } else {
                console.log('A valid, existent resource from your API should be entered. An example: /uri');
            }
        } else if (!!res) {
            api.deleteResource(res);
        } else if (!!id) {
            api.deleteMethod(id);
        }
        if (!quiet) {
            ids = api.getIds(true);
            console.log('Updated Method ids:');
            console.dir(ids);
        }
    });

program
    .command('gen [res] [met]')
    .description('Generate a hypermedia response for any possible affordance within your API specification.')
    .action(function (res, met) {
        var quiet = !program.debug,
            ret = (typeof program.returns === 'string') ? program.returns : null,
            id = (typeof program.id === 'string') ? program.id : null,
            ids = api.getIds(true);

        if (!!program.json) {
            xhn.plugin(xhn.NavAL_JSON);
            xhn.prettify('json');
        } else {
            xhn.plugin(xhn.NavAL_XML);
            xhn.prettify('xml');
        }

        xhn.quiet(quiet);

        if (!quiet) {
            console.log('Resource:');
            console.log(res);
            console.log('Method:');
            console.log(met);
            console.log('Method returns:');
            console.log(ret);
            console.log('Method id:');
            console.log(id);
            console.log('Existing Method ids:');
            console.dir(ids);
        }

        if (!!ret) {
            try {
                ret = JSON.parse('[' + ret + ']');
                ret = ret.filter(function (element, position) {
                    return ret.indexOf(element) === position;
                });
            } catch (e) {
                console.log('A valid, comma-delimited set of status codes must be entered for your transfer protocol. ' +
                    'An (HTTP) example: 200,300,400,500');
                return;
            }
        }

        if (!!met) {
            res = api.getResource(res);

            if (!!res) {
                met = res.getMethod(met);

                if (!!met) {
                    apigen
                        .setResource(res)
                        .setMethod(met)
                        .setStatusCodes(ret)
                        .generate(quiet);
                } else {
                    console.log('A valid, existent method for your resource should be entered. An example: DELETE');
                }
            } else {
                console.log('A valid, existent resource from your API should be entered. An example: /uri');
            }
        } else if (!!res) {
            res = api.getResource(res);

            if (!!res) {
                apigen
                    .setResource(res)
                    .generate(quiet);
            } else {
                console.log('A valid, existent resource from your API should be entered. An example: /uri');
            }
        } else if (!!id) {
            var pair = api.getResourceMethodPair(id);

            if (!!pair) {
                apigen
                    .setResource(pair[0])
                    .setMethod(pair[1])
                    .setStatusCodes(ret)
                    .generate(quiet);
            } else {
                console.log('A valid, existent id from your API should be entered. Ids are optional. So make sure it is defined.');
            }
        } else {
            apigen
                .setResources(api)
                .generate(quiet);
        }
    });

program
    .command('ls')
    .description('List all affordances available within your API specification.')
    .action(function () {
        api.getAffordances(false);
    });

program
    .version('0.0.1')
    .option('-a, --arguments [arg]', 'Use this flag when putting a method to specify a media type argument that method may accept.')
    .option('-d, --debug', 'Use this flag when you would like to see debug logs in the console.')
    .option('-i, --id [id]', 'Use this flag when putting a method to specify a unique identifier for that method.')
    .option('-j, --json', 'Use this flag when generating to ensure the output is json.')
    .option('-r, --returns [ret]', 'Use this flag when putting or generating to specify a comma-separated set of status codes.')
    .option('-x, --xml', 'Use this flag when generating to ensure the output is xml.')
    .parse(process.argv);

if (!fs.existsSync(folder)) {
    fs.mkdirSync(folder);
}

fs.writeFileSync(folder + file, JSON.stringify(api, null, '    '));
<commit_msg>Changed the folder from '.' (CWD) to the NodeJS module-global __dirname to guarantee that, regardless where index.js is invoked from, the api folder will be created within the same folder as index.js.<commit_after>#!/usr/bin/env node

var program = require('commander'),
    fs = require('fs'),
    path = require('path'),
    common = require('./lib/common.js'),
    xhn = require('../xhn');

var generator = common.generator,
    method = common.method,
    relations = common.relations,
    resource = common.resource,
    resources = common.resources;

var seperator = path.sep,
    folder = __dirname + seperator + 'api' + seperator,
    file = 'api.json';

var api = resources((fs.existsSync(folder + file)) ? JSON.parse(fs.readFileSync(folder + file, 'utf8')) : undefined),
    affordances = api.getAffordances(true),
    rels = relations(),
    apigen = generator()
        .setAffordances(affordances)
        .setDelegate(function (resource, method, status, affordances) {
            var json = !!program.json,
                quiet = !program.debug,
                hfactors = xhn.affordances,
                control = xhn.hc,
                stringify = xhn.stringify;

            var nonWordCharacters = /\W+/g,
                underscore = '_',
                statusString = status.toString(),
                methodString = method._method.toString(),
                resourceString = resource._uri.toString(),
                normalizedStatus = statusString.toLowerCase().replace(nonWordCharacters, underscore),
                normalizedMethod = methodString.toLowerCase().replace(nonWordCharacters, underscore),
                normalizedResource = resourceString.toLowerCase().replace(nonWordCharacters, underscore),
                statusPath = folder + normalizedStatus + seperator,
                methodPath = statusPath + normalizedMethod + seperator,
                resourcePath = methodPath + normalizedResource + seperator,
                extension = ((json) ? 'json' : 'xml'),
                file = 'response.' + extension;

            if (!quiet) {
                console.log('Will save to file: ' + resourcePath + file);
                console.log('Serialization format: ' + extension);
            }

            var hypermedia = hfactors(statusString);

            if (!quiet) {
                console.log('Affordances:');
                console.log(affordances);
            }

            function finish() {
                try {
                    if (!fs.existsSync(folder)) {
                        fs.mkdirSync(folder);
                    }
                    if (!fs.existsSync(statusPath)) {
                        fs.mkdirSync(statusPath);
                    }
                    if (!fs.existsSync(methodPath)) {
                        fs.mkdirSync(methodPath);
                    }
                    if (!fs.existsSync(resourcePath)) {
                        fs.mkdirSync(resourcePath);
                    }
                    fs.writeFileSync(resourcePath + file, stringify(hypermedia));
                } catch (e) {
                    console.log('Attempting to create the Affordance file at its default path threw an exception.');
                    console.log(e);
                }
                apigen.generate(quiet);
            }

            function chooseAction() {
                console.log('Please choose an action possibility. Choose done to serialize to disk.');
                program.choose(affordances, function (action) {
                    if (action === 0) {
                        finish();
                    } else {
                        action = affordances[action];
                        affordances = affordances.filter(function (element) {
                            return element !== action;
                        });

                        if (!quiet) {
                            console.log('Affordances:');
                            console.log(affordances);
                        }

                        var method = action._method,
                            resource = action._resource,
                            arguments = action._arguments,
                            id = action._id;

                        var hfactor = control();

                        hypermedia.addAffordance(hfactor);
                        hfactor._method = method;
                        hfactor._uri = resource;

                        if (!!arguments) {
                            hfactor._metadata.addField('Content-Type', arguments);
                        }
                        if (!!id) {
                            hfactor._id = id;
                        }
                        if (!quiet) {
                            console.log('You chose: ' + action);
                            console.log('Affordance Method:');
                            console.log(method);
                            console.log('Affordance Resource:');
                            console.log(resource);
                            console.log('Affordance Arguments:');
                            console.log(arguments);
                            console.log('Affordance ID:');
                            console.log(id);
                        }

                        program.prompt('Enter a link relation: ', function (relation) {
                            hfactor._relation = relation;
                            if (!id) {
                                program.prompt('Enter a link identifier: ', function (id) {
                                    hfactor._id = id;
                                    chooseAction();
                                });
                            } else {
                                chooseAction();
                            }
                        });
                    }
                });
            }

            function start() {
                console.log('Generating: ' + method._method + ' --> ' + resource._uri + ' --> ' + status);
                chooseAction();
            }

            start();
        });

program
    .command('put [res] [met]')
    .description('Create or Update a Resource or Method within your API specification.')
    .action(function (res, met) {
        var quiet = !program.debug,
            ret = (typeof program.returns === 'string') ? program.returns : null,
            arg = (typeof program.arguments === 'string') ? program.arguments : null,
            id = (typeof program.id === 'string') ? program.id : null,
            ids = api.getIds(true);

        if (!quiet) {
            console.log('Resource:');
            console.log(res);
            console.log('Method:');
            console.log(met);
            console.log('Method arguments:');
            console.log(arg);
            console.log('Method returns:');
            console.log(ret);
            console.log('Method id:');
            console.log(id);
            console.log('Existing Method ids:');
            console.dir(ids);
        }

        if (!!met) {
            res = api.getResource(res);

            if (!!ret) {
                try {
                    ret = JSON.parse('[' + ret + ']');
                    ret = ret.filter(function (element, position) {
                        return ret.indexOf(element) === position;
                    });
                } catch (e) {
                    console.log('A valid, comma-delimited set of status codes must be entered for your transfer protocol. ' +
                        'An (HTTP) example: 200,300,400,500');
                    return;
                }
            }
            if (!!res) {
                if (!!id) {
                    var pair = api.getResourceMethodPair(id),
                        same = !!pair && (pair[0] === res) && (pair[1] === res.getMethod(met));
                    if (!!pair && !same) {
                        console.log('A valid, globally unique method ID should be entered.');
                        return;
                    }
                }

                res.putMethod(method(met, arg, ret, id));
                if (!quiet) {
                    ids = api.getIds(true);
                    console.log('Updated Method ids:');
                    console.dir(ids);
                }
            } else {
                console.log('A valid, existent resource from your API should be entered. An example: /uri');
            }
        } else if (!!res) {
            api.putResource(resource(res));
        }
    });

program
    .command('get [res] [met]')
    .description('Retrieve an existing Resource or Method within your API specification.')
    .action(function (res, met) {
        var quiet = !program.debug,
            id = (typeof program.id === 'string') ? program.id : null,
            ids = api.getIds(true);

        if (!quiet) {
            console.log('Resource:');
            console.log(res);
            console.log('Method:');
            console.log(met);
            console.log('Method id:');
            console.log(id);
            console.log('Existing Method ids:');
            console.dir(ids);
        }
        if (!!met) {
            res = api.getResource(res);

            if (!!res) {
                console.log(JSON.stringify(res.getMethod(met), null, '    '));
            } else {
                console.log('A valid, existent resource from your API should be entered. An example: /uri');
            }
        } else if (!!res) {
            console.log(JSON.stringify(api.getResource(res), null, '    '));
        } else if (!!id) {
            console.log(JSON.stringify(api.getMethod(id), null, '    '));
        } else {
            console.log(JSON.stringify(api, null, '    '));
        }
    });

program
    .command('del [res] [met]')
    .description('Delete an existing Resource or Method within your API specification.')
    .action(function (res, met) {
        var quiet = !program.debug,
            id = (typeof program.id === 'string') ? program.id : null,
            ids = api.getIds(true);

        if (!quiet) {
            console.log('Resource:');
            console.log(res);
            console.log('Method:');
            console.log(met);
            console.log('Method id:');
            console.log(id);
            console.log('Existing Method ids:');
            console.dir(ids);
        }
        if (!!met) {
            res = api.getResource(res);

            if (!!res) {
                res.deleteMethod(met);
            } else {
                console.log('A valid, existent resource from your API should be entered. An example: /uri');
            }
        } else if (!!res) {
            api.deleteResource(res);
        } else if (!!id) {
            api.deleteMethod(id);
        }
        if (!quiet) {
            ids = api.getIds(true);
            console.log('Updated Method ids:');
            console.dir(ids);
        }
    });

program
    .command('gen [res] [met]')
    .description('Generate a hypermedia response for any possible affordance within your API specification.')
    .action(function (res, met) {
        var quiet = !program.debug,
            ret = (typeof program.returns === 'string') ? program.returns : null,
            id = (typeof program.id === 'string') ? program.id : null,
            ids = api.getIds(true);

        if (!!program.json) {
            xhn.plugin(xhn.NavAL_JSON);
            xhn.prettify('json');
        } else {
            xhn.plugin(xhn.NavAL_XML);
            xhn.prettify('xml');
        }

        xhn.quiet(quiet);

        if (!quiet) {
            console.log('Resource:');
            console.log(res);
            console.log('Method:');
            console.log(met);
            console.log('Method returns:');
            console.log(ret);
            console.log('Method id:');
            console.log(id);
            console.log('Existing Method ids:');
            console.dir(ids);
        }

        if (!!ret) {
            try {
                ret = JSON.parse('[' + ret + ']');
                ret = ret.filter(function (element, position) {
                    return ret.indexOf(element) === position;
                });
            } catch (e) {
                console.log('A valid, comma-delimited set of status codes must be entered for your transfer protocol. ' +
                    'An (HTTP) example: 200,300,400,500');
                return;
            }
        }

        if (!!met) {
            res = api.getResource(res);

            if (!!res) {
                met = res.getMethod(met);

                if (!!met) {
                    apigen
                        .setResource(res)
                        .setMethod(met)
                        .setStatusCodes(ret)
                        .generate(quiet);
                } else {
                    console.log('A valid, existent method for your resource should be entered. An example: DELETE');
                }
            } else {
                console.log('A valid, existent resource from your API should be entered. An example: /uri');
            }
        } else if (!!res) {
            res = api.getResource(res);

            if (!!res) {
                apigen
                    .setResource(res)
                    .generate(quiet);
            } else {
                console.log('A valid, existent resource from your API should be entered. An example: /uri');
            }
        } else if (!!id) {
            var pair = api.getResourceMethodPair(id);

            if (!!pair) {
                apigen
                    .setResource(pair[0])
                    .setMethod(pair[1])
                    .setStatusCodes(ret)
                    .generate(quiet);
            } else {
                console.log('A valid, existent id from your API should be entered. Ids are optional. So make sure it is defined.');
            }
        } else {
            apigen
                .setResources(api)
                .generate(quiet);
        }
    });

program
    .command('ls')
    .description('List all affordances available within your API specification.')
    .action(function () {
        api.getAffordances(false);
    });

program
    .version('0.0.1')
    .option('-a, --arguments [arg]', 'Use this flag when putting a method to specify a media type argument that method may accept.')
    .option('-d, --debug', 'Use this flag when you would like to see debug logs in the console.')
    .option('-i, --id [id]', 'Use this flag when putting a method to specify a unique identifier for that method.')
    .option('-j, --json', 'Use this flag when generating to ensure the output is json.')
    .option('-r, --returns [ret]', 'Use this flag when putting or generating to specify a comma-separated set of status codes.')
    .option('-x, --xml', 'Use this flag when generating to ensure the output is xml.')
    .parse(process.argv);

if (!fs.existsSync(folder)) {
    fs.mkdirSync(folder);
}

fs.writeFileSync(folder + file, JSON.stringify(api, null, '    '));
<|endoftext|>


<commit_before>import wd from 'wd';
import assert from 'assert';

export default function test(config) {
  let browser = wd.remote('localhost', config.port);
  let adb = config.adb;

  let log = (msg) => {
    if(config.logger) config.logger(msg);
  }

  browser.on('status', (info) => {
    if(config.logger) config.logger(info);
  });

  browser.on('command', (meth, path, data) => {
    if(config.logger) config.logger(
      [meth, path, data].join(" ")
    );
  });

  let isApplicationRunning = (config) => {
    setTimeout( () => {
      log("Checking if ("+config.pkg+") application is running");
      if(adb.isAppRunning(config.pkg)) {
        adb.closeApp(config.pkg);
        browser.quit();
        config.cb(true);
      }else {
        browser.quit();
        config.cb(false, {error:"Application didnt open"});
      }
    }, 3000);
  }

  let error = (config, err) => {
    browser.quit();
    config.cb(false, {error: err});
  }

  let localTest = (config) => {
    browser.elementByLinkText('Open app', (err, el) => {
      log("Clicking on element");
      browser.clickElement(el, (err) => {
        isApplicationRunning(config);
      });
    });
  }

  let remoteTest = (config) => {
    log("Searching for intent url link");
    browser.elementsByTagName('a', (err, els) => {
      isIntentLink(els,
      // success
      (el) => {
        browser.execute(intentFixScript(config.intentURL), (err) => {
          if(err) {
            error(config, err.toString());
            return;
          }
          browser.clickElement(el, (err) => {
            if(err) {
              error(config, err.toString());
              return;
            }
            isApplicationRunning(config);
          });
        });
      },
      // error
      (err) => {
        error(config, err.toString());
      });
    });
  }

  let isIntentLink = (els, cb, errCb) => {
    var len = els.length;
    if(len == 0) {
      errCb("Intent url link not found");
      return;
    }
    let el = els.splice(0,1)[0];
    el.getAttribute("href", (err, attr) => {
      if(err) return errCb(err);
      if(attr == null) {
        isIntentLink(els, cb, errCb);
        return;
      }
      if(attr == config.intentURL) {
        cb(el);
        return;
      }else{
        isIntentLink(els, cb, errCb);
      }
    });
  }

  let intentFixScript = (intentURL) => {
    let script = 'var els=document.querySelectorAll(\'[href="{$intentURL}"]\'); for(var i in els){ try{ els[i].setAttribute("onclick","window.open(\'{$intentURL}\')");}catch(e){}}';
    return script.replace(/\{\$intentURL\}/ig, intentURL);
  }

  browser.init(
  {browserName:'Chrome',deviceName:'Android',platformName: 'Android'},
  () => {
    log("Opening url: "+config.url);
    browser.get(config.url, () => {
      if(config.local) {
        localTest(config);
      }else{
        remoteTest(config);
      }
    });
  });
}

<commit_msg>update android test, avoid executeScript to make the intent to work, use press in native contexts instead<commit_after>import wd from 'wd';
import assert from 'assert';

export default function test(config) {
  let browser = wd.remote('localhost', config.port);
  let adb = config.adb;
  let webViewXPath = '//android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.webkit.WebView[1]/android.view.View[1]';

  let log = (msg) => {
    if(config.logger) config.logger(msg);
  }

  browser.on('status', (info) => {
    if(config.logger) config.logger(info);
  });

  browser.on('command', (meth, path, data) => {
    if(config.logger) config.logger(
      [meth, path, data].join(" ")
    );
  });

  let isApplicationRunning = (config) => {
    setTimeout( () => {
      log("Checking if ("+config.pkg+") application is running");
      if(adb.isAppRunning(config.pkg)) {
        adb.closeApp(config.pkg);
        browser.quit();
        config.cb(true);
      }else {
        browser.quit();
        config.cb(false, {error:"Application didnt open"});
      }
    }, 3000);
  }

  let error = (config, err) => {
    browser.quit();
    config.cb(false, {error: err});
  }

  let localTest = (config) => {
    browser.elementByLinkText('Open app', (err, el) => {
      log("Clicking on element");
      browser.clickElement(el, (err) => {
        isApplicationRunning(config);
      });
    });
  }

  let pressAction = (driver, coords) => {
    let action = new wd.TouchAction(driver);
        action.press({x:coords.x,y:coords.y}).wait(10).release()
    return action;
  }

  let intentClick = (driver, el) => {
    let elCoords;
    // get element location
    driver.getLocation(el, (err, coords) => {
      if(err) return error(err);
      elCoords = coords;
      driver.context('NATIVE_APP', (err) => {
        if(err) return error(err);
        driver.elementByXPath(webViewXPath, (err, el) => {
          if(err) return error(err);
          driver.getLocation(el, (err, coords) => {
            if(err) return error(err);
            let action = pressAction(driver, {x: elCoords.x, y: (coords.y + elCoords.y)});
            action.perform( (err) => {
              if(err) return error(err);
              isApplicationRunning(config);
            });
          });
        });
      });
    });
  }

  let remoteTest = (config) => {
    log("Searching for intent url link");
    browser.elementsByTagName('a', (err, els) => {
      isIntentLink(els,
      // success
      (el) => {
        intentClick(browser, el);
      },
      // error
      (err) => {
        error(config, err.toString());
      });
    });
  }

  let isIntentLink = (els, cb, errCb) => {
    var len = els.length;
    if(len == 0) {
      errCb("Intent url link not found");
      return;
    }
    let el = els.splice(0,1)[0];
    el.getAttribute("href", (err, attr) => {
      if(err) return errCb(err);
      if(attr == null) {
        isIntentLink(els, cb, errCb);
        return;
      }
      if(attr == config.intentURL) {
        cb(el);
        return;
      }else{
        isIntentLink(els, cb, errCb);
      }
    });
  }

  let intentFixScript = (intentURL) => {
    let script = 'var els=document.querySelectorAll(\'[href="{$intentURL}"]\'); for(var i in els){ try{ els[i].setAttribute("onclick","window.open(\'{$intentURL}\')");}catch(e){}}';
    return script.replace(/\{\$intentURL\}/ig, intentURL);
  }

  browser.init(
  {browserName:'Chrome',deviceName:'Android',platformName: 'Android'},
  () => {
    log("Opening url: "+config.url);
    browser.get(config.url, () => {
      if(config.local) {
        localTest(config);
      }else{
        remoteTest(config);
      }
    });
  });
}

<|endoftext|>