Source: lib/fields/api.js

/*jshint laxcomma: true, smarttabs: true, node: true, unused: true, esnext: true*/
'use strict';
/**
 * resource field definitions
 * @module tastypie/fields/api
 * @author Eric Satterwhite
 * @since 0.0.1
 * @requires events
 * @requires path
 * @requires util
 * @requires mout/lang/isFunction
 * @requires mout/lang/isString
 * @requires mout/lang/kindOf
 * @requires mout/date/strftime
 * @requires mout/object/get
 * @requires mout/object/hasOwn
 * @requires tastypie/lib/class
 * @requires tastypie/lib/class/options
 * @requires tastypie/lib/class/parent
 */

const events         = require( 'events' )
    , util           = require( 'util' )
    , boom           = require( 'boom' )
    , get            = require( 'mout/object/get' )
    , hasOwn         = require( 'mout/object/hasOwn' )
    , isFunction     = require( 'mout/lang/isFunction' )
    , isUndefined    = require( 'mout/lang/isUndefined' )
    , Class          = require( '../class' )
    , Options        = require( '../class/options' )
    , Parent         = require( '../class/parent' )
    , STRING         = 'string'
    , FUNCTION       = 'function'
    ;

let ApiField;

/**
 * Base field providing generic functionality around value serialization / deserialization
 * @constructor
 * @alias module:tastypie/fields/api
 * @param {Object} options
 * @param {Boolean} [options.readonly=false] Vales will be omitted during the hydration cycle
 * @param {Boolean} [options.nullable=false] true of the field should allow for a null vlaue
 * @param {?String|Function} [options.attribute=null] A name path or function to be used to extract values during dehydration
 * @param {String} [options.name=null] The name of the field. This is automatically set when added to a resource
 * @param {String} [options.help=A general no-op field] Help text to include in the schema defintion for this field
 * @param {!Mixed} [options.default=null] A default value for the field to fill in if none is provided
 * @param {Boolean} [options.exclude=false] If true the field values will be excluded from dehydration & response payloads
 * @mixes module:tastypie/lib/class/options
 * @mixes module:tastypie/lib/class/parent
 * @example var x = new tastypie/fields.ApiField();
 */
ApiField = new Class({
    inherits: events.EventEmitter
    , mixin:[ Parent, Options ]
    , options:{
        'readonly'  : false
      , 'nullable'  : false
      , 'attribute' : null
      , 'default'   : undefined
      , 'blank'     : false
      , 'name'      : null
      , 'exclude'   : false
      , help:'A general no op field'
    }

    , constructor: function( options ){
        this.setOptions( options );
    }

    /**
     * used to determine serialization type
     * @private
     * @method module:tastypie/fields/api#type
     * @return {String} internal data type
     **/
    , type: function( ){
        return 'string';
    }

    /**
     * returns a sample of the format expected and returned by a field
     * @method module:tastypie/fields/api#format
     * @return {String|undefined} The format expected for a particular field type
     **/
    ,format: function(){

    }

    /**
     * Converts data value into the serialization specific type.
     * @method module:tastypie/fields/api#convert
     * @param {Mixed} val value to convert
     * @returns {Mixed} value The converted field value
     **/
    , convert: function convert( val ){
        return val;
    }

    /**
     * Converts a serialized value in to a javascript object value
     * @method module:tastypie/fields/api#hydrate
     * @param {Bundle} bundle a bundle representing the current request
     **/
    , hydrate: function hydrate( bundle, cb ){
        var name = this.options.name
          , attr = this.options.attribute
          , obj  = bundle.object
          , value
          ;

        // if readonly, don't care!
        if( this.options.readonly ){
          return setImmediate(cb, null );
        }

        // if there is a value in data, return it.
        if( hasOwn(bundle.data, name ) ){
          return setImmediate(cb, null, bundle.data[ name ] );
        }

        if( value = get( bundle.data, attr ) ){
          return setImmediate(cb, null, value );
        }

        // if bundle data doesn't have a matching property.
        // look for one.
        // NOTE: we can't use hasOwnProperty here because we care inherited properties
        if( attr && attr in obj ){
          return setImmediate(cb, null, obj[ attr ] );
        } else if ( name in obj ){
          return setImmediate( cb, null, obj[ name ] );
        } else if( hasOwn(this.options, 'default') && !isUndefined( this.options.default ) ){
          return setImmediate( cb, null, isFunction( this.default ) ? this.default( bundle.data ) : this.default );
        } else if( this.options.nullable ){
          return setImmediate( cb, null, null );
        } else {
          let err = boom.create(
            400
          , util.format(
              "Field %s has no data, default value and is not nullable "
            , this.options.name
            )
          );
          return setImmediate(cb, err)
        }
    }

    /**
     * Converts a javascript object / value into something sutable for seriaation. ex. Date formatting
     * @method module:tastypie/fields/api#dehydrate
     * @param {Object} object Object to dehydrate before serialization
     * @param {Sting|Function} attribute a name path or function to use to retrieve a value from the object
     * @return {Mixed} value object value converted to its internal type
     **/
    , dehydrate: function dehydrate( obj, cb ){
        var current, attribute;
        attribute = this.options.attribute;

        if( typeof attribute === STRING ){
          current = get( obj, attribute );
          if( current == null ){
            if( this.options.hasOwnProperty( 'default' ) ){
                current = this.options.default;
            } else if( this.options.nullable ){
                current = null;
            }
          }
          current = typeof current === FUNCTION ? current( obj ) : current;
        } else if( typeof attribute === FUNCTION ){
          current = attribute();
        }
        return setImmediate(cb, null, this.convert( current ));
    }

    /**
     * Injects dynamic properties onto field instance for later use
     * @protected
     * @method module:tastypie/fields/api#augment
     * @param {Resource} resource the resource instance this field is attached to
     * @param {String} name The property name this field is associated to
     **/
    , augment: function augment( cls, name ){
        this.name = this.name || name;
        this.resource = this.resource || cls;
        return this;
    }
});

Object.defineProperties(ApiField.prototype,{
    default:{
        enumerable:false
        ,writeable:false
        ,get: function( ){
            return this.options.default;
        }
    }
});

module.exports = ApiField;