"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.Schema = void 0;

var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));

var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));

var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));

var _tsFns = require("ts-fns");

var _factory = require("./factory.js");

var _index = require("./ty/index.js");

var _meta = require("./meta.js");

var _utils = require("./shared/utils.js");

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }

var Schema = /*#__PURE__*/function () {
  function Schema(metas) {
    var _this = this;

    (0, _classCallCheck2.default)(this, Schema);

    var use = function use(metas) {
      (0, _tsFns.each)(metas, function (meta, key) {
        if (!meta) {
          return;
        }

        if (_this[key]) {
          return;
        } // only metas will be used


        var value = (0, _tsFns.isInstanceOf)(meta, _meta.Meta) ? meta : (0, _tsFns.isInheritedOf)(meta, _meta.Meta) ? new meta() : null;

        if (!value) {
          return;
        }

        (0, _tsFns.define)(_this, key, {
          value: value,
          enumerable: true
        }); // bugfix: should put after define, because two metas may referer to each other
        // which cause die loop

        if (value.deps && (0, _tsFns.isFunction)(meta.deps)) {
          var deps = meta.deps();
          use(deps);
        }
      });
    };

    use(metas);
  }

  (0, _createClass2.default)(Schema, [{
    key: "has",
    value: function has(key) {
      return !!this[key];
    }
    /**
     * get default value by using `default` option
     * @param {*} key
     */

  }, {
    key: "getDefault",
    value: function getDefault(key, context) {
      var meta = this[key];
      var defaultValue = meta.default;

      if ((0, _tsFns.isFunction)(defaultValue)) {
        return defaultValue.call(context, key);
      } else if ((0, _utils.isMemoRef)(defaultValue)) {
        var _context$$$memories$f;

        return context === null || context === void 0 ? void 0 : (_context$$$memories$f = context.$$memories.find(function (item) {
          return item.getter = defaultValue.getter;
        })) === null || _context$$$memories$f === void 0 ? void 0 : _context$$$memories$f.value;
      } else if ((0, _tsFns.isObject)(defaultValue) || (0, _tsFns.isArray)(defaultValue)) {
        return (0, _tsFns.clone)(defaultValue);
      } else {
        return defaultValue;
      }
    }
    /**
     * get message for some attr
     * @param {*} key
     * @param {*} attr
     * @param {*} context
     */

  }, {
    key: "$message",
    value: function $message(key, attr, context) {
      var _this2 = this;

      return function (givenMessage) {
        var meta = _this2[key];

        if (!meta) {
          return '';
        }

        var node = meta[attr];

        if (!node) {
          return '';
        }

        var defualtMessage = (0, _tsFns.interpolate)(Schema.defualtMessages[attr] || "{key} ".concat(attr, " error."), {
          key: key
        });
        var handle = meta.catch;
        var finalMessage = defualtMessage;

        if ((0, _tsFns.isString)(givenMessage)) {
          finalMessage = givenMessage;
        } // required: true
        else if ((0, _tsFns.isBoolean)(node)) {
          finalMessage = defualtMessage;
        } // required: 'should be required.'
        else if ((0, _tsFns.isString)(node)) {
          finalMessage = node;
        } // required() { return true }, function to return boolean
        else if ((0, _tsFns.isFunction)(node)) {
          finalMessage = defualtMessage;
        } // required: { ... }
        else if ((0, _tsFns.isObject)(node) && node.message) {
          var message = node.message; // required: { message() { return 'xxx' } }

          if ((0, _tsFns.isFunction)(message)) {
            finalMessage = _this2._trydo(function () {
              return message.call(context, attr, key);
            }, function (error) {
              return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || defualtMessage;
            }, {
              key: key,
              attr: 'message'
            });
          } // required: { message: 'required' }
          else if ((0, _tsFns.isString)(message)) {
            finalMessage = message;
          }
        } // interpolate the inner message


        finalMessage = (0, _tsFns.interpolate)(finalMessage, meta);
        return finalMessage;
      };
    }
    /**
     * get determine result for some attr
     * @param {*} key
     * @param {*} attr
     * @param {*} context
     */

  }, {
    key: "$decide",
    value: function $decide(attr, key, value, context) {
      var _this3 = this;

      return function (fallback) {
        var meta = _this3[key];

        var callback = function callback() {
          return (0, _tsFns.isFunction)(fallback) ? fallback.call(context, value, key) : fallback;
        };

        if (!meta) {
          return callback();
        }

        if (!(0, _tsFns.inObject)(attr, meta)) {
          return callback();
        }

        var node = meta[attr];
        var handle = meta.catch;
        /**
         * node is a object like: {
         *   determine: true,
         *   message: '{key} should be required',
         * }
         */

        if ((0, _tsFns.isObject)(node)) {
          var determine = node.determine;

          if ((0, _tsFns.isFunction)(determine)) {
            return _this3._trydo(function () {
              return determine.call(context, value, key);
            }, function (error) {
              return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || callback();
            }, {
              key: key,
              attr: attr
            });
          } else {
            return determine;
          }
        }
        /**
         * node is a function, use as a getter
         */


        if ((0, _tsFns.isFunction)(node)) {
          return _this3._trydo(function () {
            return node.call(context, value, key);
          }, function (error) {
            return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || fallback;
          }, {
            key: key,
            attr: attr
          });
        }
        /**
         * node is a normal value
         */


        return node;
      };
    }
  }, {
    key: "required",
    value: function required(key, value, context) {
      return this.$decide('required', key, value, context)(false);
    }
  }, {
    key: "readonly",
    value: function readonly(key, value, context) {
      return this.$decide('readonly', key, value, context)(false);
    }
  }, {
    key: "disabled",
    value: function disabled(key, value, context) {
      if (!this.available(key, value, context)) {
        return true;
      }

      return this.$decide('disabled', key, value, context)(false);
    }
  }, {
    key: "hidden",
    value: function hidden(key, value, context) {
      if (!this.available(key, value, context)) {
        return true;
      }

      return this.$decide('hidden', key, value, context)(false);
    }
  }, {
    key: "available",
    value: function available(key, value, context) {
      return this.$decide('available', key, value, context)(true);
    }
  }, {
    key: "empty",
    value: function empty(key, value, context) {
      var meta = this[key];
      var empty = meta.empty,
          handle = meta.catch;

      if (!empty) {
        return (0, _tsFns.isEmpty)(value);
      }

      return this._trydo(function () {
        return empty.call(context, value, key);
      }, function (error) {
        return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || (0, _tsFns.isEmpty)(value);
      }, {
        key: key,
        attr: 'empty'
      });
    }
  }, {
    key: "get",
    value: function get(key, value, context) {
      var meta = this[key];

      if (!meta) {
        return value;
      }

      var getter = meta.getter,
          handle = meta.catch;

      if ((0, _tsFns.isFunction)(getter)) {
        var coming = this._trydo(function () {
          return getter.call(context, value, key);
        }, function (error) {
          return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || value;
        }, {
          key: key,
          attr: 'getter'
        });

        return coming;
      } else {
        return value;
      }
    }
  }, {
    key: "format",
    value: function format(key, value, context) {
      var meta = this[key];

      if (!meta) {
        return (0, _tsFns.isNone)(value) ? '' : value + '';
      }

      var formatter = meta.formatter,
          handle = meta.catch;
      var text = (0, _tsFns.isNone)(value) ? '' : value + '';

      if ((0, _tsFns.isFunction)(formatter)) {
        var coming = this._trydo(function () {
          return formatter.call(context, value, key);
        }, function (error) {
          return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || text;
        }, {
          key: key,
          attr: 'formatter'
        });

        return (0, _tsFns.isNone)(coming) ? '' : coming + '';
      } else {
        return text;
      }
    }
    /**
     * get new value by given value
     * @param {*} key
     * @param {*} value
     * @param {*} context
     */

  }, {
    key: "$set",
    value: function $set(key, value, context) {
      var meta = this[key];

      if (!meta) {
        return value;
      }

      var setter = meta.setter,
          handle = meta.catch;

      if ((0, _tsFns.isFunction)(setter)) {
        value = this._trydo(function () {
          return setter.call(context, value, key);
        }, function (error) {
          return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || value;
        }, {
          key: key,
          attr: 'setter'
        });
      }

      return value;
    } // type checking

  }, {
    key: "check",
    value: function check(key, value, context) {
      var meta = this[key];

      if (!meta) {
        return;
      }

      var type = meta.type,
          message = meta.message;

      if (!type) {
        return;
      }

      var target = {}; // make rule works

      if (!(0, _tsFns.isUndefined)(value)) {
        target[key] = value;
      }

      var error = (0, _tsFns.isInstanceOf)(type, _index.Rule) ? _index.Ty.catch(target).by((0, _defineProperty2.default)({}, key, type)) : _index.Ty.catch(value).by(type);

      if (error) {
        var e = {
          key: key,
          action: 'check',
          value: value,
          type: true,
          error: error,
          message: this.$message(key, 'type', context)(message)
        };

        this._catch(key, e, context);

        return error;
      }
    }
    /**
     * get new value, with `readonly` checking
     * @param {*} key
     * @param {*} next
     * @param {*} prev
     * @param {*} context
     */

  }, {
    key: "set",
    value: function set(key, next, prev, context) {
      var meta = this[key];

      if (!meta) {
        return next;
      }

      var readonly = this.readonly(key, prev, context);

      if (readonly) {
        var e = {
          key: key,
          action: 'set',
          next: next,
          prev: prev,
          readonly: true,
          message: this.$message(key, 'readonly', context)(readonly)
        };

        this._catch(key, e, context);

        return prev;
      }

      var value = this.$set(key, next, context); // run checking even though force=false, so that we can catch the error

      var error = this.check(key, value, context);

      if (meta.force && error) {
        return prev;
      }

      return value;
    }
    /**
     * create a function to only validate the rules which passed into validators
     * @param {*} key
     * @param {*} value
     * @param {*} context
     * @returns {function}
     */

  }, {
    key: "$validate",
    value: function $validate(key, value, context) {
      var _this4 = this;

      var meta = this[key];
      var handle = meta.catch,
          _meta$validators = meta.validators,
          validators = _meta$validators === void 0 ? [] : _meta$validators;

      var validate = function validate(validator, index, dontTry) {
        var determine = validator.determine,
            validate = validator.validate,
            message = validator.message,
            name = validator.name;

        if ((0, _tsFns.isBoolean)(determine) && !determine) {
          return;
        }

        if ((0, _tsFns.isFunction)(determine)) {
          var bool = _this4._trydo(function () {
            return determine.call(context, value, key);
          }, function (error) {
            return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || false;
          }, {
            key: key,
            attr: 'validators[' + index + '].determine'
          }, dontTry);

          if (!bool) {
            return;
          }
        }

        var res = _this4._trydo(function () {
          return validate.call(context, value, key);
        }, function (error) {
          return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || true;
        }, {
          key: key,
          attr: 'validators[' + index + '].validate'
        }, dontTry); // private: if the validate result is an array, it may the submodel return the validate error directly


        if ((0, _tsFns.isArray)(res)) {
          return res;
        } // true, 'xxx', {}, 1, 2, 3... are allowed
        // if a dev want to use custom message, he can return an Error


        if (res && !(0, _tsFns.isInstanceOf)(res, Error)) {
          return;
        }

        var msg = // if validate return an error, use error message as
        (0, _tsFns.isInstanceOf)(res, Error) ? res.message : (0, _tsFns.isString)(message) ? message : '';

        if ((0, _tsFns.isFunction)(message)) {
          msg = _this4._trydo(function () {
            return message.call(context, value, key, res);
          }, function (error) {
            return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || msg || "".concat(key, " did not pass validators[").concat(index, "]");
          }, {
            key: key,
            attr: 'validators[' + index + '].message'
          }, dontTry);
        } // interpolate with meta, so that we can provide more info


        msg = (0, _tsFns.interpolate)(msg, _objectSpread({
          key: key,
          value: value
        }, meta));
        var error = {
          key: key,
          value: value,
          at: index,
          validator: name || validator,
          message: msg
        };
        return error;
      };

      var runOne = function runOne(dontTry, validators, i, errors) {
        var validator = validators[i];

        if (!validator) {
          return;
        }

        var isBreak = validator.break,
            isAsync = validator.async; // ignore async validators

        if (isAsync) {
          return;
        }

        var error = validate(validator, i, dontTry);

        if ((0, _tsFns.isArray)(error)) {
          var errs = error.filter(function (item) {
            return !!item && (0, _typeof2.default)(item) === 'object' && item.message;
          });
          errors.push.apply(errors, (0, _toConsumableArray2.default)(errs));
          return isBreak;
        } else if (error) {
          errors.push(error);
          return isBreak;
        }
      };

      var validateByRange = function validateByRange(dontTry, validators, _ref) {
        var _ref2 = (0, _slicedToArray2.default)(_ref, 2),
            _ref2$ = _ref2[0],
            start = _ref2$ === void 0 ? 0 : _ref2$,
            _ref2$2 = _ref2[1],
            end = _ref2$2 === void 0 ? validators.length - 1 : _ref2$2;

        var errors = [];

        for (var i = start; i <= end; i++) {
          var isBreak = runOne(dontTry, validators, i, errors);

          if (isBreak) {
            break;
          }
        }

        return errors;
      };

      var validateByIndexes = function validateByIndexes(dontTry, validators) {
        var errors = [];

        for (var i = 0, len = arguments.length <= 2 ? 0 : arguments.length - 2; i < len; i++) {
          var index = i + 2 < 2 || arguments.length <= i + 2 ? undefined : arguments[i + 2];
          var isBreak = runOne(dontTry, validators, index, errors);

          if (isBreak) {
            break;
          }
        }

        return errors;
      };

      return function () {
        // ignore if disabled
        if (_this4.disabled(key, value, context)) {
          return [];
        } // if user did not fill the value, and the field is not required, there is no need to get error


        if (_this4.empty(key, value, context) && !_this4.required(key, value, context)) {
          return [];
        }

        for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
          args[_key] = arguments[_key];
        }

        var first = args[0]; // array, pass custom valiators which is not in schema, i.e. schema.$validate('some', value, model)([{ validate: v => v > 0, message: 'should > 0' }])

        if ((0, _tsFns.isArray)(first) && first[0] && (0, _typeof2.default)(first[0]) === 'object') {
          return validateByRange(1, first, []);
        } // array, i.e. schema.$validate('some', value, model)([2, 6])
        else if ((0, _tsFns.isArray)(first)) {
          return validateByRange(0, validators, first);
        } // number, i.e. schema.$validate('some', value, model)(1, 2, 4, 6)
        else if ((0, _tsFns.isNumber)(first)) {
          return validateByIndexes.apply(void 0, [0, validators].concat(args));
        } else {
          return [];
        }
      };
    }
    /**
     * validate type and vaidators
     * @param {*} key
     * @param {*} value
     * @param {*} context
     */

  }, {
    key: "validate",
    value: function validate(key, value, context) {
      var meta = this[key];
      var errors = [];

      if (!meta) {
        var e = {
          action: 'validate',
          key: key,
          value: value,
          message: "".concat(key, " is not existing in schema.")
        };

        this._catch(key, e, context);

        return errors;
      }

      var errs = this.$validate(key, value, context)([]);
      errors.push.apply(errors, (0, _toConsumableArray2.default)(errs));
      return errors;
    }
  }, {
    key: "$validateAsync",
    value: function $validateAsync(key, value, context) {
      var _this5 = this;

      var meta = this[key];
      var _meta$validators2 = meta.validators,
          validators = _meta$validators2 === void 0 ? [] : _meta$validators2;

      function asyncx(fn) {
        return function () {
          try {
            for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
              args[_key2] = arguments[_key2];
            }

            return Promise.resolve(fn.call.apply(fn, [this].concat(args)));
          } catch (e) {
            return Promise.reject(e);
          }
        };
      }

      var validate = function validate(dontTry, validator, index) {
        var determine = validator.determine,
            validate = validator.validate,
            message = validator.message,
            name = validator.name;

        if ((0, _tsFns.isBoolean)(determine) && !determine) {
          return Promise.resolve();
        }

        var test = function test() {
          return asyncx(determine).call(context, value, key).catch(function (error) {
            if (dontTry) {
              throw error;
            }

            var e = {
              action: '$validateAsync',
              key: key,
              value: value,
              error: error,
              attr: 'validators[' + index + '].determine'
            };

            _this5._catch(key, e, context);

            return false;
          });
        };

        var check = function check() {
          return asyncx(validate).call(context, value, key).catch(function (error) {
            if (dontTry) {
              throw error;
            }

            var e = {
              action: '$validateAsync',
              key: key,
              value: value,
              error: error,
              attr: 'validators[' + index + '].validate'
            };

            _this5._catch(key, e, context);

            return true;
          });
        };

        var say = function say(res) {
          return asyncx(message).call(context, value, key, res).catch(function (error) {
            if (dontTry) {
              throw error;
            }

            var e = {
              action: '$validateAsync',
              key: key,
              value: value,
              error: error,
              attr: 'validators[' + index + '].message'
            };

            _this5._catch(key, e, context);
          });
        };

        var gen = function gen(msg) {
          // interpolate with meta, so that we can provide more info
          var message = (0, _tsFns.interpolate)(msg, _objectSpread({
            key: key,
            value: value
          }, meta));
          var error = {
            key: key,
            value: value,
            at: index,
            validator: name || validator,
            message: message
          };
          return error;
        };

        var create = function create(res) {
          var msg = (0, _tsFns.isString)(message) ? message // if validate return an error, use error message as
          : (0, _tsFns.isInstanceOf)(res, Error) ? res.message : '';

          if ((0, _tsFns.isFunction)(message)) {
            return say(res).then(function (o) {
              msg = o || msg;
              return gen(msg);
            });
          } else {
            return gen(msg);
          }
        };

        var run = function run() {
          return check().then(function (res) {
            // private: if the validate result is an array, it may the submodel return the validate error directly
            if ((0, _tsFns.isArray)(res)) {
              return res;
            } // true, 'xxx', {}, 1, 2, 3... are allowed
            // if a dev want to use custom message, he can return an Error


            if (res && !(0, _tsFns.isInstanceOf)(res, Error)) {
              return;
            }

            return create(res);
          });
        };

        if ((0, _tsFns.isFunction)(determine)) {
          return test().then(function (bool) {
            if (bool) {
              return run();
            }
          });
        }

        return run();
      };

      var pipe = function pipe(dontTry, validators) {
        var errors = [];
        var i = 0;

        var through = function through() {
          var validator = validators[i];

          if (!validator) {
            return Promise.resolve();
          }

          var isBreak = validator.break;
          return validate(dontTry, validator, i).then(function (error) {
            if (error) {
              if ((0, _tsFns.isArray)(error)) {
                var errs = error.filter(function (item) {
                  return !!item && (0, _typeof2.default)(item) === 'object' && item.message;
                });
                errors.push.apply(errors, (0, _toConsumableArray2.default)(errs));
              } else {
                errors.push(error);
              }

              if (isBreak) {
                return Promise.resolve();
              }
            }

            i++;
            return through();
          });
        };

        return through().then(function () {
          return errors;
        });
      };

      var validateByRange = function validateByRange(dontTry, validators, _ref3) {
        var _ref4 = (0, _slicedToArray2.default)(_ref3, 2),
            _ref4$ = _ref4[0],
            start = _ref4$ === void 0 ? 0 : _ref4$,
            _ref4$2 = _ref4[1],
            end = _ref4$2 === void 0 ? validators.length - 1 : _ref4$2;

        var items = [];

        for (var i = start; i <= end; i++) {
          var validator = validators[i];

          if (validator) {
            items.push(validator);
          }
        }

        return pipe(dontTry, items);
      };

      var validateByIndexes = function validateByIndexes(dontTry, validators) {
        var items = [];

        for (var i = 0, len = arguments.length <= 2 ? 0 : arguments.length - 2; i < len; i++) {
          var index = i + 2 < 2 || arguments.length <= i + 2 ? undefined : arguments[i + 2];
          var validator = validators[index];

          if (validator) {
            items.push(validator);
          }
        }

        return pipe(dontTry, items);
      };

      return function () {
        // ignore if disabled
        if (_this5.disabled(key, value, context)) {
          return Promise.resolve([]);
        } // if user did not fill the value, and the field is not required, there is no need to get error


        if (_this5.empty(key, value, context) && !_this5.required(key, value, context)) {
          return Promise.resolve([]);
        }

        for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
          args[_key3] = arguments[_key3];
        }

        var first = args[0]; // array, pass custom valiators which is not in schema, i.e. schema.$validate('some', value, model)([{ validate: v => v > 0, message: 'should > 0' }])

        if ((0, _tsFns.isArray)(first) && first[0] && (0, _typeof2.default)(first[0]) === 'object') {
          return validateByRange(1, first, []);
        } // array, i.e. schema.$validate('some', value, model)([2, 6])
        else if ((0, _tsFns.isArray)(first)) {
          return validateByRange(0, validators, first);
        } // number, i.e. schema.$validate('some', value, model)(1, 2, 4, 6)
        else if ((0, _tsFns.isNumber)(first)) {
          return validateByIndexes.apply(void 0, [0, validators].concat(args));
        } else {
          return Promise.resolve([]);
        }
      };
    }
  }, {
    key: "validateAsync",
    value: function validateAsync(key, value, context) {
      var meta = this[key];

      if (!meta) {
        var e = {
          action: 'validate',
          key: key,
          value: value,
          message: "".concat(key, " is not existing in schema.")
        };

        this._catch(key, e, context);

        return Promise.resolve([]);
      }

      return this.$validateAsync(key, value, context)([]);
    }
  }, {
    key: "$parse",
    value: function $parse(key, data, context) {
      var meta = this[key];
      var dataValue = data[key];

      if (!meta) {
        return dataValue;
      }

      var handle = meta.catch,
          create = meta.create,
          saveAs = meta.saveAs,
          save = meta.save,
          asset = meta.asset,
          force = meta.force;
      var dataKey = asset ? (0, _tsFns.isFunction)(asset) ? asset(data, key) : asset : key;
      var defaultValue = this.getDefault(key, context);
      var assetValue = data[dataKey]; // use asset first, then fallback to key

      var value = (0, _tsFns.isUndefined)(assetValue) ? dataValue : assetValue;
      var coming = (0, _tsFns.isUndefined)(value) ? defaultValue : value; // use create to create value

      if ((0, _tsFns.isFunction)(create)) {
        var fromData = (0, _tsFns.decideby)(function () {
          if (!(0, _tsFns.isUndefined)(value)) {
            return data;
          }

          var fromJson = _objectSpread({}, data);

          if ((0, _tsFns.isFunction)(saveAs)) {
            var res = saveAs.call(context, defaultValue, key, fromJson);
            (0, _utils.patchObj)(fromJson, res);
          }

          if ((0, _tsFns.isFunction)(save)) {
            fromJson[dataKey] = save.call(context, defaultValue, key, fromJson);
          }

          if (!(0, _tsFns.inObject)(dataKey, fromJson)) {
            fromJson[dataKey] = defaultValue;
          }

          return fromJson;
        });
        var usedValue = (0, _tsFns.decideby)(function () {
          if (!(0, _tsFns.isUndefined)(value)) {
            return value;
          }

          if ((0, _tsFns.isInstanceOf)(meta, _factory.FactoryMeta)) {
            return defaultValue;
          }

          return fromData[dataKey];
        });
        coming = this._trydo(function () {
          return create.call(context, usedValue, key, fromData);
        }, function (error) {
          return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || coming;
        }, {
          key: key,
          attr: 'create'
        });
      }

      if ((0, _tsFns.isUndefined)(coming) || force && this.check(key, coming, context)) {
        coming = defaultValue;
      }

      return coming;
    }
    /**
     * parse data by passed data with `create` option, you'd better to call ensure to after parse to make sure your data is fix with type
     * @param {*} json
     * @param {*} context
     */

  }, {
    key: "parse",
    value: function parse(json, context) {
      var _this6 = this;

      var output = {};
      var keys = Object.keys(this);
      keys.forEach(function (key) {
        var coming = _this6.$parse(key, json, context);

        output[key] = coming;
      });
      return output;
    }
    /**
     * generate required props automaticly by given data,
     * like parse, but only use the given properties to generate thin data and patch less properties
     * @param {*} json
     * @param {*} context
     * @returns
     */

  }, {
    key: "discover",
    value: function discover(json, context) {
      var _this7 = this;

      var output = {};
      var deps = [];
      var proxy = new Proxy(json, {
        get: function get(_, key) {
          deps.push(key);
          return json[key];
        }
      });
      (0, _tsFns.each)(this, function (meta, key) {
        var asset = meta.asset,
            handle = meta.catch,
            create = meta.create,
            force = meta.force;
        var dataKey = asset ? (0, _tsFns.isFunction)(asset) ? asset(json, key) : asset : key;
        var hasCreate = (0, _tsFns.isFunction)(create);
        var hasNoGivenData = (0, _tsFns.isUndefined)(json[dataKey]);
        /**
         * because the given json may generate outside with unknow data,
         * for example:
         *
         * const { name, age } = data
         * some.patch({ name, age }) // here name and age properties are existing, but may be undefined
         *
         * so we use isUndefined to check whether the property is existing,
         * if the property is undefined, we treat it as not existing
         */
        // certain asset key required

        if (asset && hasNoGivenData) {
          return;
        } // without create (means will not consume other properties) and key
        else if (!hasCreate && hasNoGivenData) {
          return;
        }

        var value = proxy[dataKey];

        var defaultValue = _this7.getDefault(key, context);

        var coming = (0, _tsFns.isUndefined)(value) ? defaultValue : value;

        if (hasCreate) {
          deps.length = 0; // clear deps at first

          coming = _this7._trydo(function () {
            return create.call(context, coming, key, proxy);
          }, function (error) {
            return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || coming;
          }, {
            key: key,
            attr: 'create'
          }); // only check when coming is undefined

          if ((0, _tsFns.isUndefined)(coming)) {
            // if the given json is not fit for current create required,
            // it means this field should not generate
            if (!deps.length || !deps.some(function (dep) {
              return !(0, _tsFns.isUndefined)(json[dep]);
            })) {
              deps.length = 0; // clear deps

              return;
            }
          }

          deps.length = 0; // clear deps
        }

        if ((0, _tsFns.isUndefined)(coming) || force && _this7.check(key, coming, context)) {
          coming = defaultValue;
        }

        output[key] = coming;
      });
      return output;
    }
    /**
     * export to get output data
     * @param {*} data
     * @param {*} context
     */

  }, {
    key: "export",
    value: function _export(data, context) {
      var _this8 = this;

      var patch = {};
      var output = {};
      (0, _tsFns.each)(this, function (meta, key) {
        var drop = meta.drop,
            map = meta.map,
            mapAs = meta.mapAs,
            handle = meta.catch,
            _meta$to = meta.to,
            to = _meta$to === void 0 ? key : _meta$to;
        var value = data[key];

        if (_this8.disabled(key, value, context)) {
          return;
        }

        if ((0, _tsFns.isFunction)(mapAs)) {
          var res = _this8._trydo(function () {
            return mapAs.call(context, value, key, data, output);
          }, function (error) {
            return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || {};
          }, {
            key: key,
            attr: 'mapAs'
          });

          if (!(0, _tsFns.isUndefined)(res)) {
            (0, _utils.patchObj)(patch, res);
          }
        }

        if ((0, _tsFns.isBoolean)(drop) && drop) {
          return;
        }

        if ((0, _tsFns.isFunction)(drop)) {
          var bool = _this8._trydo(function () {
            return drop.call(context, value, key, data);
          }, function (error) {
            return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || false;
          }, {
            key: key,
            attr: 'drop'
          });

          if (bool) {
            return;
          }
        }

        if ((0, _tsFns.isFunction)(map)) {
          var _res = _this8._trydo(function () {
            return map.call(context, value, key, data);
          }, function (error) {
            return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || value;
          }, {
            key: key,
            attr: 'map'
          }); // can return nothing, and modify output inside


          if (!(0, _tsFns.isUndefined)(_res)) {
            (0, _tsFns.assign)(output, to, _res);
          }
        } else {
          (0, _tsFns.assign)(output, to, value);
        }
      });
      var result = Object.assign(output, patch);
      return result;
    }
    /**
     * export a json to backup model
     * @param {*} data
     * @param {*} context
     */

  }, {
    key: "record",
    value: function record(data, context) {
      var _this9 = this;

      var output = {};
      (0, _tsFns.each)(this, function (meta, key) {
        var save = meta.save,
            saveAs = meta.saveAs,
            handle = meta.catch,
            asset = meta.asset;
        var value = data[key];
        var dataKey = asset ? (0, _tsFns.isFunction)(asset) ? asset(data, key) : asset : key;

        var defaultValue = _this9.getDefault(key, context);

        if ((0, _tsFns.isFunction)(saveAs)) {
          var res = _this9._trydo(function () {
            return saveAs.call(context, value, key, data, output);
          }, function (error) {
            return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || {};
          }, {
            key: key,
            attr: 'saveAs'
          });

          if ((0, _tsFns.isObject)(res)) {
            (0, _utils.patchObj)(output, res);
          }
        }

        if ((0, _tsFns.isFunction)(save)) {
          var _res2 = _this9._trydo(function () {
            return save.call(context, value, key, data);
          }, function (error) {
            return (0, _tsFns.isFunction)(handle) && handle.call(context, error, key) || defaultValue;
          }, {
            key: key,
            attr: 'save'
          });

          output[dataKey] = _res2;
        }

        if (!(0, _tsFns.inObject)(dataKey, output)) {
          output[dataKey] = value;
        }
      });
      return output;
    }
  }, {
    key: "_trydo",
    value: function _trydo(fn, fallback, basic, force) {
      if (force) {
        return fn();
      }

      try {
        return fn();
      } catch (error) {
        var err = _objectSpread(_objectSpread({}, basic), {}, {
          error: error
        });

        var e = this.onError(err) || err;
        return fallback(e);
      }
    }
  }, {
    key: "_catch",
    value: function _catch(key, e, context) {
      this.onError(e);
      var meta = this[key];

      if (!meta) {
        return;
      }

      var handle = meta.catch;

      if ((0, _tsFns.isFunction)(handle)) {
        handle.call(context, e, key);
      }
    }
  }, {
    key: "onError",
    value: function onError() {}
  }]);
  return Schema;
}();

exports.Schema = Schema;
(0, _defineProperty2.default)(Schema, "defualtMessages", {
  type: "{key} does not match type.",
  readonly: "{key} can not be set new value because of readonly.",
  disabled: "{key} can not be set new value because of disabled."
});