Show:
/**
 * Copyright 2016, Yahoo! Inc.
 * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
 */

import Ember from 'ember';
import flatten from '../utils/flatten';
import cycleBreaker from '../utils/cycle-breaker';

const {
  get,
  set,
  RSVP,
  computed,
  isEmpty,
  isArray,
  isNone,
  A: emberArray
} = Ember;

const A = emberArray();

function callable(method) {
  return function (collection) {
    return A[method].apply(collection, arguments);
  };
}

const uniq = callable('uniq');
const compact = callable('compact');

/**
 * @module Validations
 * @class ResultCollection
 */
export default Ember.Object.extend({

  /**
   * A set of all validator {{#crossLink "Result"}}{{/crossLink}} objects for this specific attribute
   * @property content
   * @type {Ember.Array}
   */
  content: null,

  /**
   * The attribute that this collection belongs to
   * @property attribute
   * @type {String}
   */
  attribute: '',

  init() {
    this._super(...arguments);
    set(this, 'content', emberArray(get(this, 'content')));
  },

  /**
   * ```javascript
   * // Examples
   * get(user, 'validations.isInvalid')
   * get(user, 'validations.attrs.username.isInvalid')
   * ```
   *
   * @property isInvalid
   * @readOnly
   * @type {Ember.ComputedProperty | Boolean}
   */
  isInvalid: computed.not('isValid'),

  /**
   * ```javascript
   * // Examples
   * get(user, 'validations.isValid')
   * get(user, 'validations.attrs.username.isValid')
   * ```
   *
   * @property isValid
   * @default true
   * @readOnly
   * @type {Ember.ComputedProperty | Boolean}
   */
  isValid: computed('content.@each.isValid', cycleBreaker(function () {
    return get(this, 'content').isEvery('isValid', true);
  }, true)),

  /**
   * This property is toggled only if there is an async validation
   *
   * ```javascript
   * // Examples
   * get(user, 'validations.isValidating')
   * get(user, 'validations.attrs.username.isValidating')
   * ```
   *
   * @property isValidating
   * @default false
   * @readOnly
   * @type {Ember.ComputedProperty | Boolean}
   */
  isValidating: computed('content.@each.isValidating', cycleBreaker(function () {
    return !get(this, 'content').isEvery('isValidating', false);
  }, false)),

  /**
   * Will be true only if isValid is `true` and isValidating is `false`
   *
   * ```javascript
   * // Examples
   * get(user, 'validations.isTruelyValid')
   * get(user, 'validations.attrs.username.isTruelyValid')
   * ```
   *
   * @property isTruelyValid
   * @default true
   * @readOnly
   * @type {Ember.ComputedProperty | Boolean}
   */
  isTruelyValid: computed('content.@each.isTruelyValid', cycleBreaker(function () {
    return get(this, 'content').isEvery('isTruelyValid', true);
  }, true)),

  /**
   * Will be true is the attribute in question is not `null` or `undefined`. If the object being
   * validated is an Ember Data Model and you have a `defaultValue` specified, then it will use that for comparison.
   *
   * ```javascript
   * // Examples
   * // 'username' : DS.attr('string', { defaultValue: 'johndoe' })
   * get(user, 'validations.isDirty')
   * get(user, 'validations.attrs.username.isDirty')
   * ```
   *
   * @property isDirty
   * @default false
   * @readOnly
   * @type {Ember.ComputedProperty | Boolean}
   */
  isDirty: computed('content.@each.isDirty', cycleBreaker(function () {
    return !get(this, 'content').isEvery('isDirty', false);
  }, false)),

  /**
   * Will be `true` only if a validation returns a promise
   *
   * ```javascript
   * // Examples
   * get(user, 'validations.isAsync')
   * get(user, 'validations.attrs.username.isAsync')
   * ```
   *
   * @property isAsync
   * @default false
   * @readOnly
   * @type {Ember.ComputedProperty | Boolean}
   */
  isAsync: computed('content.@each.isAsync', cycleBreaker(function () {
    return !get(this, 'content').isEvery('isAsync', false);
  }, false)),

  /**
   * A collection of all error messages on the object in question
   *
   * ```javascript
   * // Examples
   * get(user, 'validations.messages')
   * get(user, 'validations.attrs.username.messages')
   * ```
   *
   * @property messages
   * @readOnly
   * @type {Ember.ComputedProperty | Array}
   */
  messages: computed('content.@each.messages', cycleBreaker(function () {
    const messages = flatten(get(this, 'content').getEach('messages'));

    return uniq(compact(messages));
  })),

  /**
   * An alias to the first message in the messages collection.
   *
   * ```javascript
   * // Example
   * get(user, 'validations.message')
   * get(user, 'validations.attrs.username.message')
   * ```
   *
   * @property message
   * @readOnly
   * @type {Ember.ComputedProperty | String}
   */
  message: computed('messages.[]', cycleBreaker(function () {
    return get(this, 'messages.0');
  })),

  /**
   * A collection of all {{#crossLink "Error"}}Errors{{/crossLink}} on the object in question.
   * Each error object includes the error message and it's associated attribute name.
   *
   * ```javascript
   * // Example
   * get(user, 'validations.errors')
   * get(user, 'validations.attrs.username.errors')
   * ```
   *
   * @property errors
   * @readOnly
   * @type {Ember.ComputedProperty | Array}
   */
  errors: computed('attribute', 'content.@each.errors', cycleBreaker(function () {
    const attribute = get(this, 'attribute');
    let errors = flatten(get(this, 'content').getEach('errors'));

    errors = uniq(compact(errors));
    errors.forEach(e => {
      if(e.get('attribute') !== attribute) {
        e.set('parentAttribute', attribute);
      }
    });

    return errors;
  })),

  /**
   * An alias to the first {{#crossLink "Error"}}{{/crossLink}} in the errors collection.
   *
   * ```javascript
   * // Example
   * get(user, 'validations.error')
   * get(user, 'validations.attrs.username.error')
   * ```
   *
   * @property error
   * @readOnly
   * @type {Ember.ComputedProperty | Error}
   */
  error: computed('errors.[]', cycleBreaker(function () {
    return get(this, 'errors.0');
  })),

  /**
   * All built options of the validators associated with the results in this collection grouped by validator type
   *
   * ```javascript
   * // Given the following validators
   * {
   *   username: [
   *     validator('presence', true),
   *     validator('length', { max: 15 }),
   *     validator('format', { regex: /foo/ }),
   *     validator('format', { regex: /bar/ }),
   *   ]
   * }
   * ```
   *
   * ```js
   * get(user, 'validations.attrs.username.options')
   * ```
   *
   * The above will return the following
   * ```js
   * {
   *   'presence': { presence: true},
   *   'length': { max: 15 },
   *   'regex': [{ regex: /foo/ }, { regex: /bar/ }]
   * }
   * ```
   *
   * @property options
   * @readOnly
   * @type {Ember.ComputedProperty | Object}
   */
  options: computed('content.[]', function () {
    return this._groupValidatorOptions();
  }),

  /**
   * @property _promise
   * @async
   * @private
   * @type {Ember.ComputedProperty | Promise}
   */
  _promise: computed('content.@each._promise', cycleBreaker(function () {
    const promises = get(this, 'content').getEach('_promise');

    if (!isEmpty(promises)) {
      return RSVP.all(compact(flatten(promises)));
    }
  })),

  /**
   * @property value
   * @type {Ember.ComputedProperty}
   * @private
   */
  value: computed('isAsync', cycleBreaker(function () {
    return get(this, 'isAsync') ? get(this, '_promise') : this;
  })),

  /**
   * Used by the `options` property to create a hash from the `content` that is grouped by validator type.
   * If there is more than 1 of a type, it groups it into an array of option objects.
   *
   * @method  _groupValidatorOptions
   * @return  {Object}
   * @private
   */
  _groupValidatorOptions() {
    const validators = get(this, 'content').getEach('_validator');

    return validators.reduce((options, v) => {
      if (isNone(v) || isNone(get(v, '_type'))) {
        return options;
      }

      const type = get(v, '_type');
      const vOpts = get(v, 'options');

      if (options[type]) {
        if (isArray(options[type])) {
          options[type].push(vOpts);
        } else {
          options[type] = [options[type], vOpts];
        }
      } else {
        options[type] = vOpts;
      }
      return options;
    }, {});
  }
});