Source: singleton.js

function SingletonFactory(Base, REGEX) {
  /**
  Base model that represents a single object.
  @class Singleton
  @extends Base
  @prop {boolean} $dirty=false  - If instance has been modified since initilization or the last save, equals `true`; else `false`
  @prop {boolean} $busy         - If instance is currently in the middle of an API call, equals `true`; else `false`
  @prop {boolean} $loaded       - If instance has been loaded or instantiated with data, equals `true`; else `false`
  @prop {string}  $type         - The type of model the instance is
  */
  var Singleton = function () {};

  /**
  Singleton field cofiguration definition.
  @typedef {FieldConfig}
  @type {object}
  */

  /**
   * Helper functions
   */
  function cap(str) {
    return str.charAt(0).toLowerCase() + str.slice(1).replace(/_([a-z])/g, function ( v, l ) {
      return l.toUpperCase();
    });
  }
  function label(str) {
    return str.charAt(0).toUpperCase() + str.slice(1)
      .replace(/[a-z]([A-Z])/g, function (v, l, os) { // Handle "camelCase" => "Camel Case"
        return str.charAt(os + 1) + ' ' + l.toUpperCase();
      })
      .replace(/_([a-z])/g, function (v, l) { // Handle "underscore_case" => "Underscore Case"
        return ' ' + l.toUpperCase();
      });
  }
  function setError(field, key, value) {
    /*jshint validthis:true */
    var self = this;
    self.$errors[field] = self.$errors[field] || {};
    self.$errors[field][key] = value;
    self[field].$errors[key] = value;
  }
  function validate(val, fieldConfig) {
    /*jshint validthis:true */
    /*jshint laxbreak:true */
    var self = this,
        ret = true,
        matches, limit, equals;

    // setError(... true) === In error state
    // setError(... false) === In valid state
    // ret === false if ever setError(... true)

    // required
    if ( fieldConfig.required === true || ( m_isFunction(fieldConfig.required) === true && fieldConfig.required.call(self, val) === true ) ) {
      if ( m_isUndefined(val) || m_isNull(val) || val.length === 0 ) {
        setError.call(self, fieldConfig.methodName, 'required', true );
        ret = false;
      } else {
        setError.call(self, fieldConfig.methodName, 'required', false );
      }
    }

    if ( m_isUndefined(val) === false && m_isNull(val) === false ) {
    // START DEFINED-ONLY CHECKS

      // type
      if ( indexOf(['st','nu','ob','ar','bo','dt'], fieldConfig.type ) > -1 ) {
        setError.call(self, fieldConfig.methodName, 'type', false );
        if ( ( fieldConfig.type === 'st' && !m_isString(val) )
          || ( fieldConfig.type === 'nu' && !m_isNumber(val) )
          || ( fieldConfig.type === 'ob' && !m_isObject(val) )
          || ( fieldConfig.type === 'ar' && !m_isArray(val) )
          || ( fieldConfig.type === 'bo' && !m_isBoolean(val) )
          || ( fieldConfig.type === 'dt' && !m_isDate(new Date(val)) )
        ) {
          setError.call(self, fieldConfig.methodName, 'type', true );
          ret = false;
        }
      }

      // min/max
      if ( m_isNumber(fieldConfig.min) || ( fieldConfig.type === 'dt' && m_isDate(fieldConfig.min) ) ) {
        if ( ( fieldConfig.type === 'st' || fieldConfig.type === 'ar' ) && val.length >= fieldConfig.min ) {
          setError.call(self, fieldConfig.methodName, 'min', false );
        } else if ( ( !fieldConfig.type || fieldConfig.type === 'nu' ) && parseFloat( val ) >= fieldConfig.min ) {
          setError.call(self, fieldConfig.methodName, 'min', false );
        } else if ( fieldConfig.type === 'dt' && new Date( val ) >= new Date(fieldConfig.min) ) {
          setError.call(self, fieldConfig.methodName, 'min', false );
        } else {
          setError.call(self, fieldConfig.methodName, 'min', true );
          ret = false;
        }
      }
      if ( m_isNumber(fieldConfig.max) || ( fieldConfig.type === 'dt' && m_isDate(fieldConfig.max) ) ) {
        if ( ( fieldConfig.type === 'st' || fieldConfig.type === 'ar' ) && val.length <= fieldConfig.max ) {
          setError.call(self, fieldConfig.methodName, 'max', false );
        } else if ( ( !fieldConfig.type || fieldConfig.type === 'nu' ) && parseFloat( val ) <= fieldConfig.max ) {
          setError.call(self, fieldConfig.methodName, 'max', false );
        } else if ( fieldConfig.type === 'dt' && new Date( val ) <= new Date(fieldConfig.max) ) {
          setError.call(self, fieldConfig.methodName, 'max', false );
        } else {
          setError.call(self, fieldConfig.methodName, 'max', true );
          ret = false;
        }
      }

      // equals
      if ( m_isString(fieldConfig.equals) ) {
        if ( m_isFunction(self[fieldConfig.equals]) && m_equals(self[fieldConfig.equals](), val) ) {
          setError.call(self, fieldConfig.methodName, 'equals', false );
          setError.call(self, fieldConfig.equals, 'equals', false );
          self.trigger('validated.' + fieldConfig.equals, false);
        } else {
          setError.call(self, fieldConfig.methodName, 'equals', true );
          setError.call(self, fieldConfig.equals, 'equals', true );
          self.trigger('validated.' + fieldConfig.equals, true);
          ret = false;
        }
      } else if ( m_isArray(fieldConfig.equals) ) {
        equals = false;
        m_forEach(fieldConfig.equals, function (target) {
          if ( m_isFunction(self[target]) ) {
            if ( m_equals(self[target](), val) ) {
              equals = true;
              setError.call(self, target, 'equals', false );
              self.trigger('validated.' + target, false);
            } else {
              setError.call(self, target, 'equals', true );
              self.trigger('validated.' + target, true);
            }
          }
        });
        setError.call(self, fieldConfig.methodName, 'equals', !equals );
        ret = ret && equals;
      }

    // END DEFINED-ONLY CHECKS
    }

    // matches
    if ( m_isRegEx(fieldConfig.matches) ) {
      matches = fieldConfig.matches.test(val) || ( m_isUndefined(val) || m_isNull(val) || val.length === 0 );
      setError.call(self, fieldConfig.methodName, 'matches', !matches );
      ret = matches && ret;
    }

    // limit
    if ( m_isUndefined(fieldConfig.limit) === false && m_isNull(fieldConfig.limit) === false ) {
      if ( m_isUndefined(val) || m_isNull(val) || val.length === 0 ) {
        setError.call(self, fieldConfig.methodName, 'limit', false );
      } else if ( m_isArray(fieldConfig.limit) || m_isObject(fieldConfig.limit) ) {
        limit = false;
        m_forEach(fieldConfig.limit, function (lim) {
          if ( m_isObject( lim ) === true && !m_isNull(lim.value) && !m_isUndefined(lim.value) ) {
            limit = limit || m_equals(lim.value, val);
          } else {
            limit = limit || m_equals(lim, val);
          }
        });
        // limit === true when a match was found, so invert for setError
        setError.call(self, fieldConfig.methodName, 'limit', !limit );
        ret = limit && ret;
      } else if ( m_isString(fieldConfig.limit) || m_isNumber(fieldConfig.limit) || m_isBoolean(fieldConfig.limit) ) {
        limit = m_equals(fieldConfig.limit, val);
        setError.call(self, fieldConfig.methodName, 'limit', !limit );
        ret = limit && ret;
      }
    }

    return ret;
  }

  Singleton = Base.extend(
    /** @lends Singleton.prototype */
    {
      $type: 'Singleton',
      $preExtend: function (properties) {
        if ( m_isObject(this.fields) ) {
          properties.fields = merge( {}, this.fields, properties.fields );
          m_forEach(properties.fields, function (value, key) {
            if ( value === false ) {
              delete properties.fields[key];
            }
          });
        }
        return properties;
      },
      /**
      Instantiates the Singleton by setting up all the field getter/setters.
      @override
      */
      init: function (data, forClone) {
        /*jshint unused:false */
        var self = this._super.apply(this, arguments);
        self.$$merged = self.$$data = data || {};
        self.$$setData = {};
        self.$loaded = data ? true : false;
        self.$dirty = false;
        self.$pristine = true;
        self.$busy = false;
        self.$valid = true;
        self.$invalid = false;
        self.$$fieldConfig = false;

        return self.each(function (fieldConfig) {
          if ( fieldConfig.getter !== undefined && !m_isFunction(fieldConfig.getter) ) {
            throw new Error('Singleton Init Error: "getter" must be undefined/null or a function');
          }
          if ( fieldConfig.setter !== undefined && !m_isFunction(fieldConfig.setter) ) {
            throw new Error('Singleton Init Error: "setter" must be undefined/null or a function');
          }
          function getter() {
            var ret,
                field = fieldConfig.key;
            //console.log('getter: ' + (fieldConfig.key ? fieldConfig.key : key) + ' = ' + self.get()[ fieldConfig.key ? fieldConfig.key : key ] );
            if ( fieldConfig.$$getterCacheSet === true ) {
              return fieldConfig.$$getterCache;
            }
            if ( fieldConfig.getter ) {
              ret = fieldConfig.getter.call(self, fieldConfig);
              if ( ret === undefined ) {
                return ret;
              }
            } else {
              field = field.split( '.' );
              ret = self.get()[ field.shift() ];
              while ( field.length > 0 ) {
                if ( m_isObject( ret ) === false ) {
                  return null;
                }
                ret = ret[ field.shift() ];
              }
              if ( ( ret === null || ret === undefined ) && fieldConfig.default !== undefined ) {
                ret = fieldConfig.default;
              }
              if ( m_isFunction(fieldConfig.mutateGet) === true ) {
                ret = fieldConfig.mutateGet.call(self, ret, fieldConfig);
              }
            }
            fieldConfig.$$getterCacheSet = true;
            fieldConfig.$$getterCache = ret;
            return ret;
          }
          function setter(val) {
            var field = fieldConfig.key,
                f, target;
            //console.log('setter: ' + (fieldConfig.key ? fieldConfig.key : key) + ' = ' + val );
            if ( fieldConfig.readonly === true ) {
              throw new Error(fieldConfig.methodName + ' is read-only.' );
            }
            self.$$merged = false;
            self.$dirty = true;
            self.$pristine = false;
            self.$loaded = true;
            fieldConfig.$$getterCacheSet = false;
            delete fieldConfig.$$getterCache;
            if ( fieldConfig.setter ) {
              fieldConfig.setter.call(self, val, fieldConfig);
              return self;
            }
            field = field.split( '.' );
            target = self.$$setData;
            while ( field.length > 0 ) {
              f = field.shift();
              target = target[ f ] = m_isObject( target[ f ] ) === true ? target[ f ] : {};
            }
            if ( m_isFunction(fieldConfig.mutateSet) === true ) {
              val = fieldConfig.mutateSet.call(self, val, fieldConfig);
            }
            target[ field[ 0 ] ] = val;
            return self;
          }
          /**
          @typedef SingletonField
          @type {function}
          @arg [val] - If provided, will be used as the field's new value. If not provided, method acts as a getter
          @prop {object} $errors - Contains details about any error states on the field
          @prop {object} $config - Contains the configuration for this field
          @prop {SingletonFieldValidator} valid - Validates the field's value against the field definition
          @returns {Singleton} `this` 
          */
          /**
          @typedef SingletonFieldValidator
          @type {function}
          @returns {boolean} `true` if the value is currently valid; else `false`
          */
          self[ fieldConfig.methodName ] = function (val) {
            if ( arguments.length ) {
              return setter.call(self[ fieldConfig.methodName ], val);
            }
            return getter.call(self[ fieldConfig.methodName ]);
          };
          self[ fieldConfig.methodName ].$label = fieldConfig.label || label(fieldConfig.configKey);
          fieldConfig.label = self[fieldConfig.methodName].$label;
          self[ fieldConfig.methodName ].$errors = {};
          self[ fieldConfig.methodName ].$parent = self;
          self[ fieldConfig.methodName ].$config = fieldConfig;
          self[ fieldConfig.methodName ].valid = function ( val ) {
            var ret = true,
                i = 0;
            if ( arguments.length === 0 ) {
              val = self[ fieldConfig.methodName ]();
            }
            if ( m_isFunction( fieldConfig.validator ) ) {
              ret = fieldConfig.validator.call(self, val, fieldConfig);
              setError(self, fieldConfig.methodName, 'validator', !ret);
            }
            ret = validate.call(self, val, fieldConfig) && ret;
            if (ret === false) {
              self.$valid = ret;
            } else if (self.$valid === false) {
              // Set $valid state to null to prevent endless loop if first field is valid
              self.$valid = null;
              for(; i<self.$$fieldConfig.length; i++) {
                self.$valid = self[ self.$$fieldConfig[i].methodName ].valid();
                if (self.$valid === false) {
                  break;
                }
              }
            }
            self.$invalid = !self.$valid;
            self.trigger('validated.' + fieldConfig.methodName, ret);
            return ret;
          };
        }); 
      },
      /**
      Method to retrieve all the current and pending data ($$data extended by $$setData) for the instance.
      @returns {object}
      */
      get: function () {
        var self = this;
        // Use a static variable as a cache
        if (self.$$merged !== false) {
          return self.$$merged;
        }
        self.$$merged = merge({}, self.$$data, self.$$setData);
        return self.$$merged;
      },
      /**
      Method to set the pending data ($$setData) for the instance. Also sets `this.$loaded = true`.
      @arg {object} val - The pending data to set on the instance
      @returns {Singleton} `this`
      */
      set: function (val) {
        var self = this;
        self.$$merged = false;
        self.$dirty = true;
        self.$pristine = false;
        self.$$setData = m_copy(val);
        self.$loaded = self.$loaded || objectKeys(val).length > 0;
        self.clearCache();
        return self;
      },
      /**
      Clears any instance and field cache that is currently present.
      @returns {Singleton} `this`
      */
      clearCache: function () {
        var self = this;
        if (self.$$merged !== false) {
          self.$$merged = false;
          self.each(function (fieldConfig) {
            fieldConfig.$$getterCacheSet = false;
            delete fieldConfig.$$getterCache;
          });
        }
        return self;
      },
      /**
      Triggers `cb` for each field on the model.
      @arg {Singleton~eachCB} cb - Method to call for each field
      @returns {Singleton} `this`
      */
      /**
      Callback for Singleton.each.
      @callback Singleton~eachCB
      @param {FieldConfig} fieldConfig
      @this Singleton
      */
      each: function (cb) {
        var self = this;
        if ( m_isObject(self.fields) && self.$$fieldConfig === false ) {
          self.$$fieldConfig = [];
          m_forEach(self.fields, function (field, key) {
            var fieldConfig = m_isFunction(field) ? field.apply(self, arguments) : m_isObject(field) ? m_copy(field) : {};
            fieldConfig.key = fieldConfig.key || key;
            fieldConfig.configKey = key;
            fieldConfig.methodName = fieldConfig.methodName || cap(key);
            self.$$fieldConfig.push(fieldConfig);
          });
        }
        if ( m_isArray(self.$$fieldConfig) === true && self.$$fieldConfig.length > 0 ) {
          m_forEach(self.$$fieldConfig, function (fieldConfig) {
            cb.call(self, fieldConfig);
          });
        }
        return self;
      },
      /**
      Pulls fields out of the mode and returns them as an object. Can pas in a function to use a dynamic pick list.
      @arg {array|Singleton~pickCB} fields - List of fields to include OR function to call for each field to dynamically determine whether to include it or not
      @returns {object}
      */
      /**
      Callback for Singleton.pick.
      @callback Singleton~pickCB
      @param {FieldConfig} fieldConfig
      @param {key} fieldConfig.key
      @this {Singleton} `this`
      */
      pick: function (fields) {
        var self = this,
            ret = {},
            _fields = [];
        if ( m_isArray(self.$$fieldConfig) === true && self.$$fieldConfig.length > 0 ) {
          if (m_isFunction(fields)) {
            m_forEach(self.$$fieldConfig, function (config) {
              if ( fields.call(self, config) === true ) {
                _fields.push(config.key);
              }
            });
          } else {
            _fields = fields;
          };
          ret = pick(self.get(), _fields);
        }
        return ret;
      },
      /**
      Validates each field and returns whether the model is valid or not.
      @returns {boolean}
      */
      validate: function () {
        var self = this;
        self.each(function (fieldConfig) {
          self[ fieldConfig.methodName ].valid();
        });
        self.trigger('validated', self.$valid);
        return self.$valid;
      },
      /**
      Clears any pending data that may exist.
      @returns {Singleton} `this`
      */
      cancel: function () {
        var self = this;
        if (self.$dirty) {
          self.$dirty = false;
          self.$pristine = true;
          self.clearCache();
          self.$$setData = {};
        }
        return self;
      },
      /**
      Merges the current and pending data (or sets the current data and removes the pending data).
      @arg {object} [data] - If provided, is used as the finalized data. If not, `this.get()` is used
      @returns {Singleton} `this`
      */
      finalize: function (data) {
        var self = this;
        if ( data || self.$dirty ) {
          self.$dirty = false;
          self.$pristine = true;
          self.$$data = data || self.get();
          self.$$setData = {};
          self.trigger('finalized', data);
        }
        return self;
      },
      /**
      Clones $$setData and other properties.
      @overrides
      */
      clone: function () {
        var self = this,
            ret = self._super.apply(self, arguments);
        ret.$$data = m_copy(self.$$data);
        if ( objectKeys(self.$$setData).length > 0 ) {
          ret.set(self.$$setData);
        }
        ret.$loaded = self.$loaded;
        ret.$parent = self.$parent;
        return ret;
      },
      /**
      Sets `this.$loaded = true`, deletes `this.$busy`, and clears any instance cache that may exist.
      @overrides
      */
      resolve: function () {
        var self = this;
        self.$loaded = true;
        delete self.$busy;
        self.clearCache();
        return self._super.apply(self, arguments);
      },
      /**
      Sets `this.$loaded = true`, deletes `this.$busy`, and clears any instance cache that may exist.
      @overrides
      */
      reject: function () {
        var self = this;
        self.$loaded = true;
        delete self.$busy;
        self.clearCache();
        return self._super.apply(self, arguments);
      },
      
      /**
      Re-runs the last `read` call or, if never called, calls `read`.
      @returns {Singleton} `this`
      */
      refresh: function () {
        var self = this;
        if (self.$$lastReadData) {
          return self.read(self.$$lastReadData);
        }
        return self.read();
      },

      /**
      Success callback passed into a service.
      @arg data - The data resulting from a sucessful service call
      @callback Singleton~successCallback
      */
      /**
      Fail callback passed into a service.
      @arg data - The data resulting from an erroring service call
      @callback Singleton~failCallback
      */
      /**
      Service to read (GET) the data for this instance. Services should return `false` if they are currently invalid.
      @arg data - Data to be used during the read
      @arg {Singleton~successCallback} Success callback for the service
      @arg {Singleton~failCallback} Failure callback for the service
      @abstract
      @returns {boolean}
      */
      readService: false,
      /**
      Uses the readService (if defined) to attempt to retrieve the data for the instance. Will finalize the instance.
      @arg [data] - Data to be provided to the readService
      @returns {Singleton} `this`
      */
      read: function (data, idx) {
        var self = this,
          ret;

        if (self.$busy === true) {
          self.always(function() {
            self.read(data, idx);
          });
          idx = self.unfinalize();
          return self;
        } else {
          idx = idx || self.unfinalize();
        }

        if (m_isFunction(self.readService)) {
          self.$busy = true;
          self.$$lastReadData = data || {};
          ret = self.readService(
            data,
            function (data) {
              delete self.$errors.read;
              self.finalize(data);
              self.resolve(idx);
              self.trigger('read', data);
            },
            function (data) {
              self.$errors.read = data;
              self.reject(idx);
            }
          );
          if (ret === false) {
            self.$errors.read = true;
            self.reject(idx);
          }
        } else {
          self.$errors.read = true;
          self.reject(idx);
        }
        return self;
      },
      /**
      Service to update (PUT) the data for this instance. Services should return `false` if they are currently invalid.
      @arg [data] - Data to be used during the update
      @arg {Singleton~successCallback} Success callback for the service
      @arg {Singleton~failCallback} Failure callback for the service
      @abstract
      @returns {boolean}
      */
      updateService: false,
      /**
      Uses the updateService (if defined) to attempt to update the data for the instance. Will finalize the instance upon success.
      @arg [data] - Data to be provided to the updateService. Defaults to an object contining all the fields who's "updateable" config is not false
      @returns {Singleton} `this`
      */
      update: function (data, idx) {
        var self = this,
          ret;

        if (self.$busy === true) {
          self.always(function() {
            self.update(data, idx);
          });
          idx = self.unfinalize();
          return self;
        } else {
          idx = idx || self.unfinalize();
        }

        if (m_isFunction(self.updateService)) {
          self.$busy = true;
          if (arguments.length === 0) {
            if (self.$dirty === true) {
              data = self.pick(function (fieldConfig) {
                if (fieldConfig.updateable !== false) {
                  return true;
                }
              });
            }
          }
          if (objectKeys(data).length === 0) {
            delete self.$errors.update;
            return self.resolve(idx);
          }
          ret = self.updateService(
            data,
            function (data) {
              delete self.$errors.update;
              self.finalize(data);
              self.resolve(idx);
              self.trigger('updated', data);
            },
            function (data) {
              self.$errors.update = data;
              self.reject(idx);
            }
          );
          if (ret === false) {
            self.$errors.update = true;
            self.reject(idx);
          }
        } else {
          self.$errors.update = true;
          self.reject(idx);
        }
        return self;
      },
      /**
      Service to change (PATCH) the data for this instance. Services should return `false` if they are currently invalid.
      @arg data - Data to be used during the change
      @arg {Singleton~successCallback} Success callback for the service
      @arg {Singleton~failCallback} Failure callback for the service
      @abstract
      @returns {boolean}
      */
      changeService: false,
      /**
      Uses the changeService (if defined) to attempt to change the data for the instance. Will finalize the instance upon success.
      @arg [data=this.$$setData] - Data to be provided to the changeService. Defaults to an object contining all the fields who's value has changed and who's "updateable" config is not false
      @returns {Singleton} `this`
      */
      change: function (data, idx) {
        var self = this,
            fields,
            ret;

        if (self.$busy === true) {
          self.always(function() {
            self.change(data, idx);
          });
          idx = self.unfinalize();
          return self;
        } else {
          idx = idx || self.unfinalize();
        }

        if (m_isFunction(self.changeService)) {
          self.$busy = true;
          if (arguments.length === 0) {
            if (self.$dirty === true) {
              if ( m_isArray(self.$$fieldConfig) === true && self.$$fieldConfig.length > 0 ) {
                fields = pick(self.$$fieldConfig, function (fieldConfig) {
                  if (fieldConfig.updateable !== false) {
                    return fieldConfig.key;
                  }
                }, self);
                data = pick(self.$$setData, fields);
              }
            }
          }
          if (objectKeys(data).length === 0) {
            delete self.$errors.change;
            return self.resolve(idx);
          }
          ret = self.changeService(
            data,
            function (data) {
              delete self.$errors.change;
              self.finalize(data);
              self.resolve(idx);
              self.trigger('changed', data);
            },
            function (data) {
              self.$errors.change = data;
              self.reject(idx);
            }
          );
          if (ret === false) {
            self.$errors.change = true;
            self.reject(idx);
          }
        } else {
          self.$errors.change = true;
          self.reject(idx);
        }
        return self;
      },
      /**
      Service to upload data for this instance. Services should return `false` if they are currently invalid.
      @arg data - Data to be used during the upload
      @arg {Singleton~successCallback} Success callback for the service
      @arg {Singleton~failCallback} Failure callback for the service
      @abstract
      @returns {boolean}
      */
      uploadService: false,
      /**
      Uses the uploadService (if defined) to attempt to upload data for the instance. Will finalize the instance.
      @arg [data] - Data to be provided to the uploadService
      @returns {Singleton} `this`
      */
      upload: function (data, idx) {
        var self = this,
          ret;

        if (self.$busy === true) {
          self.always(function() {
            self.upload(data, idx);
          });
          idx = self.unfinalize();
          return self;
        } else {
          idx = idx || self.unfinalize();
        }

        if (m_isFunction(self.uploadService)) {
          self.$busy = true;
          ret = self.uploadService(
            data,
            function (data) {
              delete self.$errors.upload;
              self.finalize(data);
              self.resolve(idx);
              self.trigger('uploaded', data);
            },
            function (data) {
              self.$errors.upload = data;
              self.reject(idx);
            }
          );
          if (ret === false) {
            self.$errors.upload = true;
            self.reject(idx);
          }
        } else {
          self.$errors.upload = true;
          self.reject(idx);
        }
        return self;
      },
      /**
      Service to create (POST) data for this instance. Services should return `false` if they are currently invalid.
      @arg data - Data to be used during the creation
      @arg {Singleton~successCallback} Success callback for the service
      @arg {Singleton~failCallback} Failure callback for the service
      @abstract
      @returns {boolean}
      */
      createService: false,
      /**
      Uses the createService (if defined) to attempt to create data for the instance. Will finalize the instance.
      @arg [data] - Data to be provided to the createService. Defaults to an object contining all the fields who's "createable" config is not false
      @returns {Singleton} `this`
      */
      create: function (data, idx) {
        var self = this,
          ret;

        if (self.$busy === true) {
          self.always(function() {
            self.create(data, idx);
          });
          idx = self.unfinalize();
          return self;
        } else {
          idx = idx || self.unfinalize();
        }

        if (m_isFunction(self.createService)) {
          self.$busy = true;
          if (arguments.length === 0) {
            if (self.$dirty === true) {
              data = self.pick(function (fieldConfig) {
                if (fieldConfig.createable !== false) {
                  return fieldConfig.key;
                }
              });
            }
          }
          if (objectKeys(data).length === 0) {
            delete self.$errors.create;
            return self.resolve(idx);
          }
          ret = self.createService(
            data,
            function (data) {
              delete self.$errors.create;
              self.finalize(data);
              self.resolve(idx);
              self.trigger('created', data);
            },
            function (data) {
              self.$errors.create = data;
              self.reject(idx);
            }
          );
          if (ret === false) {
            self.$errors.create = true;
            self.reject(idx);
          }
        } else {
          self.$errors.create = true;
          self.reject(idx);
        }
        return self;
      },
      /**
      Service to remove (DELETE) this instance. Services should return `false` if they are currently invalid.
      @arg data - Data to be used during the deletion
      @arg {Singleton~successCallback} Success callback for the service
      @arg {Singleton~failCallback} Failure callback for the service
      @abstract
      @returns {boolean}
      */
      deleteService: false,
      /**
      Uses the deleteService (if defined) to attempt to create data for the instance. Will finalize the instance.
      @arg [data] - Data to be provided to the deleteService
      @returns {Singleton} `this`
      */
      delete: function (data, idx) {
        var self = this,
          ret;

        if (self.$busy === true) {
          self.always(function() {
            self.delete(data, idx);
          });
          idx = self.unfinalize();
          return self;
        } else {
          idx = idx || self.unfinalize();
        }

        if (m_isFunction(self.deleteService)) {
          self.$busy = true;
          ret = self.deleteService(
            data,
            function (data) {
              delete self.$errors.delete;
              self.finalize(data || {});
              self.resolve(idx);
              self.trigger('deleted', data);
            },
            function (data) {
              self.$errors.delete = data;
              self.reject(idx);
            }
          );
          if (ret === false) {
            self.$errors.delete = true;
            self.reject(idx);
          }
        } else {
          self.$errors.delete = true;
          self.reject(idx);
        }
        return self;
      }
    }
  );
  /**
   * Return the constructor function
   */
  return Singleton;
}
angular.module( 'angular-m' )
  .factory( 'Singleton', ['Base', 'REGEX', SingletonFactory ] );