Source: lib/api.js

/*jshint laxcomma:true, smarttabs: true, node: true, unused: false, esnext: true */
'use strict';
/**
 * Serves as name space for a collection of resources. i.e., api/v1, api/v2, etc
 * @module tastypie/lib/api
 * @author Eric Satterwhite
 * @requires util
 * @requires boom
 * @requires debug
 * @requires mout/lang/isObject
 * @requires tastypie/lib/class
 * @requires tastypie/lib/class/options
 * @example
 var api = new Api('api/v1')

 api.add('test', new tatsypie.Resource() )
 // GET /api/v1/test?format=json
 **/
var  Class           = require( './class' ) // this is Class
   , util            = require('util')
   , joi             = require('joi')
   , Boom            = require('boom')
   , isObject        = require('mout/lang/isObject')
   , has             = require('mout/object/hasOwn')
   , debug           = require('debug')('tastypie:api')
   , Options         = require( './class/options' )
   , pkg             = require('../package.json')
   , mime            = require('./mime')
   , endsWithSlash   = /\/$/
   , trailingSlashes = /^\/|\/$/g
   , Api
   , optionValidator
   ;

optionValidator = joi.object().keys({
    serializer:joi.object().optional()
    ,select:joi.string().optional()
}).unknown()

function onPostAuth( request, reply ){
    let serializer = this.options.serializer;
    request.info.api_name = this.basepath;

    // if it is a format hapi deals with, it has already parsed it
    if(!this.baseexp.test( request.path ) || !request.payload || isObject( request.payload ) ){
        return reply.continue();
    }

    // if it is a string, lets try to deserialize it
    serializer.deserialize( request.payload, request.headers['content-type'], function( err, data ){
      if( data ){
        request.payload = data;
        return reply.continue( );
      }
      reply( err );
    });
}

function onPreResponse( request, reply ){
    var response = request.response
      , query    = request.query
      , serializer = this.options.serializer
      , format
      , fmt
      , ct
      ;

    fmt  = request.query && request.query.format;
    ct   = this.options.serializer.convertFormat( fmt );

    if( fmt && !ct ){
        return reply( Boom.unsupportedMediaType(`unsupported serialization format ${fmt}`) );
    }

    format = (fmt && ct) ? ct : mime.determine( request, serializer.types );
    debug('requested format %s', format);
    if( has(query,'callback') && !( has(query, 'format') ) || response.isBoom ){
      return reply.continue();
    }

    serializer.serialize( response.source, format,  function( err, content ){

      if( err ){
        return reply( err );
      }
      
      reply( content )
        .code( response.statusCode )
        .type( format || serializer.options.defaultFormat );
    });

}

/**
 * Provides namespaces for collections of resource ( api/v1, api/v2, etc )
 * @constructor
 * @alias module:tastypie/lib/api
 * @param {Object} options api configuration options
 * @param {?module:tastypie/lib/serializer} [serilaizer=null] A serializer instance to handle serialization / deserializion during the lifecycle of the request - rather than at the resource level.
 * @example var api = new Api('api/v1');
x.use('fake', new Resource() )
 */
Api = new Class( /** @lends module:NAME.Api.prototype */{
    mixin: Options
    
    ,options:{
        DEFAULT_LIMIT:20
        ,MAX_LIMIT:500
        , serializer: null
        , select: null
    }

    ,constructor: function( path,  options ){

        let results = optionValidator.validate( options );

        if( results.error ){
            throw results.error;
        }
        this.setOptions( options );
        this.basepath  = this.normalizePath( path );
        this.baseexp   = new RegExp( "^" + this.basepath );
        this.api_cache = {};
        this.url_cache = [];
        this.pending   = [];
        this.register  = this.plug.bind(this);
        
        this.register.attributes = {
            name:'tastypie'
            ,version:pkg.version
            ,multiple: true
        };

    }

    , plug: function( plugin, options, next ){
        var that = this;
        this.plg = this.options.select ? plugin.select( this.options.select ) : plugin;
        for( let key in this.api_cache ){
            if( this.api_cache.hasOwnProperty( key ) ){
                debug('registering plugin %s', key );
                this._register(key, this.api_cache[ key ] );
            }
        }

        this.plg.expose('name', pkg.name);
        this.plg.expose('version', pkg.version);
        
        debug("serializer %s", !!this.options.serializer, this.basepath );

        // FIXME: I don't like this...
        if( this.options.serializer  ){
            debug("Loading serialization plugin for %s", this.basepath );
            debug('adding server serializer for %s', this.basepath);
            this.plg.ext('onPostAuth',onPostAuth.bind( this ));
            this.plg.ext('onPreResponse', onPreResponse.bind( this ));
        }

        this.plg.route({
            path: this.basepath
            ,method:'GET'
            ,handler: function( request, reply ){
                var data = {}, current;
                for( let name in that.api_cache ){
                    if( that.api_cache.hasOwnProperty( name ) ){
                        current = that.api_cache[ name ];

                        data[ name ] = {};
                        for( let r of current.routes ){
                            let plg = r.config.plugins.tastypie;
                            if( !plg.name ){
                                return;
                            }
                            data[name][plg.name] = r.path;
                        }
                    }
                }
                reply( data );
            }
        });
        this.registed = Date.now();
        return next();
    }

    , _register: function( name, resource ){
        this.plg.route( resource.routes );
    }
    /**
     * Mounts a resource on a route prefix 
     * @method module:tastypie/lib/api#use
     * @param {String} [prefix] The route prefix in addition the the API prefix 
     * @param {module:tastypie/lib/resource} resource A resource instance to mount at the prfix path
     */ 
     ,use: function(/*prefix, resource*/){
        var prefix, resource;
        if( arguments.length === 1){
            resource = arguments[0];
            prefix   = arguments[0].options.name || '';
            prefix   = prefix.replace(endsWithSlash,'');
        } else {
            resource = arguments[1];
            prefix   = arguments[0];
            prefix   = prefix.replace(endsWithSlash,'');
        }
        this.api_cache[ prefix ] = resource;
        resource.api = this;
        resource.prefix = prefix;
        if( this.plg ){
            this._register( prefix, resource );
        } 

        this.url_cache = [];
        return this;
     }

    /**
     * Normalizes a uri path by stripping trailing or adding leading slashed acordingly
     * @protected
     * @method module:tastypie/lib/api#normalizePath
     * @param {String} path The path to normalize
     * @returns {String} The newly formated path
     */ 
     ,normalizePath: function( path ){
        return util.format( 
            '/%s'
            , path.replace(trailingSlashes,'')
        );
     }

});

// defines a quick url look up per API object
// it is cached on first call until another
// resource is registered

/**
 * @readonly
 * @name urls
 * @instance
 * @memberof module:tastypie/lib/api
 * @property {Object} urls An object containing all know methods registed to the api instance
 **/
Object.defineProperties(Api.prototype,{
    urls:{
        get: function(){
            var routes;
            if( !this.url_cache.length ){
                routes = this.plg && this.plg.table()[0].table || [];
                var baseexp = this.baseexp;
                
                this.url_cache = routes
                    .map(function( r ){
                        return r.path;
                    })
                    .filter(function( r ){
                        return baseexp.test( r );
                    });
            }
            return this.url_cache;
        }
    }
});

module.exports = Api;