atsid/circuits-js

View on GitHub
js/ServiceMethod.js

Summary

Maintainability
C
1 day
Test Coverage
"use strict";
/**
 * @class circuits.ServiceMethod
 * A wrapper class for service method invocations. An instance of this class is created
 * and attached to a circuits.Service for each method on the service.
 */
define([
    "./log",
    "./util",
    "./declare"
], function (
    logger,
    Util,
    declare
) {

    var util = new Util(),
        module = declare(null, {
            /**
             * @constructor
             * @param methodName {String} - the name of the service method wrapped by this instance.
             * @param reader {circuits.ServiceDescriptorReader} - the reader used to obtain information about the service.
             * @param provider {circuits.DataProvider} - the object providing low-level data access.
             */
            constructor: function (methodName, reader, provider) {
                this.name = methodName;
                this.reader = reader;
                this.provider = provider;
                this.responsePayloadName = reader.getResponsePayloadName(this.name);
                this.requestPayloadName = reader.getRequestPayloadName(this.name);
                this.transport = reader.getMethodTransport(this.name);
                this.smdMethod = reader.getMethod(this.name);
            },

            /**
             * Call this service method with the provided params and plugins.
             * Note: that this is not generally expected to be called directly by developers - see the Service
             * class for an example of how it is wrapped up in a service-specific named method.
             * @param {Object} - the parameters to the service call, these are service-call specific as defined by the SMD this
             *  service is based on. There is special handling for request payloads (e.g. for POST's and PUT's):
             *  - If params.payload exists it is used as the initial payload, otherwise params is used.
             *  - If the initial payload does not have the structure expected by the type defined in the SMD, it is coerced
             *      into that structure before processing.
             * @param {hash by type of arrays of circuits.Plugin's} - the plugins that should be applied to this invocation.
             */
            invoke: function (params, plugins) {
                logger.debug("Calling service method: " + this.name + " with params", params);

                var that = this,
                    provider = this.provider,
                    method = this.transport,
                    url = this.reader.getServiceUrl(this.name, params),
                    jsonpCallbackParam = this.reader.getJsonpCallbackParameter(),
                    smdReturn = this.reader.getResponseSchema(this.name),
                    payloadParamDef = this.reader.getRequestPayloadParam(this.name),
                    headers,
                    requestPayload = params.payload || params,
                    newParams = util.mixin({}, params),
                    requestParams,
                    responseType = (smdReturn.type === "any" ? "blob" : "json"),
                    timeout = this.reader.getMethodTimeout(this.name),
                    providerHandler = function (statusCode, data, ioArgs) {
                        var status = statusCode.toString(),
                            payload = (statusCode < 400) ? that.processResponse(statusCode, data, plugins, ioArgs) : data,
                            total = 0;

                        //add the total from the payload if it is an array response. putting it in the request object, as that is where http status, etc. is stored.
                        newParams.request.total = data && data.total;

                        newParams.response = data;

                        //links should be sitting alonside the actual payload, like the total.
                        newParams.links = data && data.links;

                        total = data && data.total; //XXX: temporary transitional leaving it here. some projects expect it as the third arg in callback below

                        util.executePluginChain(plugins.handler, function (plugin) {
                            var regex = new RegExp(plugin.statusPattern);
                            newParams.plugin = plugin;
                            if (regex.test(status)) {
                                plugin.fn.call(plugin.scope || plugin, payload, newParams, total);
                            }
                        });
                    },

                    providerProgress = function (event) {
                        util.executePluginChain(plugins.progress, function (plugin) {
                            plugin.fn.call(plugin.scope || plugin, event.lengthComputable, event.loaded, event.total);
                        });
                    };

                //modify the URL with any plugins
                util.executePluginChain(plugins.url, function (plugin) {
                    url = plugin.fn.call(plugin.scope || plugin, url, that);
                });

                // Use a plugged-in provider if there is one
                if (plugins.provider && plugins.provider.length > 0) {
                    provider = plugins.provider[plugins.provider.length - 1];
                    provider = provider.fn.call(provider.scope || provider, this);
                }


                // add Content-Type header if there is a payload and it has an enctype.
                if (payloadParamDef && payloadParamDef.enctype) {
                    headers = {"Content-Type": payloadParamDef.enctype};
                    // it's a regular payload so process it.
                } else {
                    requestPayload = this.processRequest(requestPayload, plugins);
                }

                logger.debug("Calling method [" + this.name + "] with URL: " + url);

                requestParams = {
                    url: url,
                    headers: headers,
                    payload: requestPayload,
                    handler: providerHandler,
                    onprogress: providerProgress,
                    asynchronous: params.asynchronous,
                    timeout: timeout || "none",
                    dontExecute: true
                };

                if (jsonpCallbackParam) {
                    requestParams.jsonpCallbackParam = jsonpCallbackParam;
                }

                newParams.request = provider[provider.httpMethodMap[method].method](requestParams);


                if (smdReturn.type === "any") {
                    newParams.request.url = url;
                    newParams.request.mediaType = this.smdMethod.contentType || "";
                    logger.debug("Setting request for returnType=any  " + newParams.request);
                } else {
                    newParams.request.execute();
                }

                return newParams.request;
            },

            /**
             * An internal method used by invoke to massage and perform plugin processing on
             * a request payload. The method attempts to coerce the payload into the type specified
             * in the smd and then runs the 'request' and 'write' plugins returning the new payload.
             *
             * @param {Object} payload - the request data passed to the service method by the client.
             * @returns {Object} newPayload - the new or modified payload.
             */
            processRequest: function (payload, plugins, ioArgs) {
                var writePayload = payload || "", intermediate, that = this;

                // Very simiplistic payload type coercion
                if (this.requestPayloadName && !payload[this.requestPayloadName]) {
                    payload = {};
                    payload[this.requestPayloadName] = writePayload;
                }

                // Allow request plugins to execute
                util.executePluginChain(plugins.request, function (plugin) {
                    intermediate = plugin.fn.call(plugin.scope || plugin, payload, that, ioArgs);
                    payload = intermediate || payload;
                });

                writePayload = (this.requestPayloadName && payload[this.requestPayloadName]) || payload;

                // Allow write plugins to execute
                util.executePluginChain(plugins.write, function (plugin) {
                    if (util.isArray(writePayload)) {
                        writePayload.forEach(function (item, idx) {
                            intermediate = plugin.fn.call(plugin.scope || plugin, item, that);
                            writePayload[idx] = intermediate || writePayload[idx];
                        }, that);
                    } else {
                        intermediate = plugin.fn.call(plugin.scope || plugin, writePayload, that);
                        writePayload = intermediate || writePayload;
                    }
                });

                if (this.requestPayloadName) {
                    payload[this.requestPayloadName] = writePayload;
                } else {
                    payload = writePayload;
                }

                return payload;
            },

            /**
             * An internal method used by the handler method generated by invoke to perform plugin processing on the
             * response data.
             * @param statusCode {Number} The status code from the request
             * @param data {Object} The response data returned by the provider
             * @param plugins {Array of circuits.Plugin} The merged plugins for the invocation
             * @param ioArgs {Object} The object used to generate the request
             */
            processResponse: function (statusCode, data, plugins, ioArgs) {
                var isList = this.reader.isListResponse(this.name),
                //TODO: "any" is JSONSchema default if no type is defined. this should come through a model though so we aren't tacking it on everywhere
                    returnType = this.reader.getResponseSchema(this.name).type || "any", //this could fail if there is no "returns" block on the method
                    payload,
                    that = this,
                    intermediate,
                    successfulResponsePattern = '(2|3)\\d\\d';

                //only auto-process responses if not JSONSchema "null" primitive type
                if (returnType && returnType !== "null") {
                    if (typeof (data) === "object") {
                        //first we apply any global response plugins to the raw data
                        //warning: if the plugin doesn't write it back out with the correct payload name,
                        //it will cause an issue below
                        util.executePluginChain(plugins.response, function (plugin) {
                            // filter plugins on statusPattern
                            var statusPattern = plugin.statusPattern || successfulResponsePattern,
                                regex = new RegExp(statusPattern);
                            if (regex.test(statusCode)) {
                                intermediate = plugin.fn.call(plugin.scope || plugin, data, that, ioArgs);
                                data = intermediate || data;
                            }
                        });
                    }

                    payload = data;

                    if (this.responsePayloadName && data && data[this.responsePayloadName]) {

                        payload = data[this.responsePayloadName];

                        logger.debug("Extracting payload for [" + this.name + "] from [" + this.payloadName + "] property", payload);
                    }


                    //apply any read plugins supplied, after receiving the server results
                    util.executePluginChain(plugins.read, function (plugin) {
                        // filter plugins on statusPattern
                        var statusPattern = plugin.statusPattern || successfulResponsePattern,
                            regex = new RegExp(statusPattern);
                        if (regex.test(statusCode)) {
                            if (isList) {
                                payload.some(function (item, idx) {
                                    intermediate = plugin.fn.call(plugin.scope || plugin, item, that);
                                    payload[idx] = intermediate || payload[idx];
                                }, that);
                            } else {
                                intermediate = plugin.fn.call(plugin.scope || plugin, payload, that);
                                payload = intermediate || payload;
                            }
                        }
                    });

                }


                this.data = payload; //hold on to a copy for future use (could be undefined of course)

                return payload;
            }

        });
    return module;
});