diff --git a/lib/axios.js b/lib/axios.js index ed1f519..8142437 100644 --- a/lib/axios.js +++ b/lib/axios.js @@ -3,6 +3,7 @@ var utils = require('./utils'); var bind = require('./helpers/bind'); var Axios = require('./core/Axios'); +var mergeConfig = require('./core/mergeConfig'); var defaults = require('./defaults'); /** @@ -32,7 +33,7 @@ axios.Axios = Axios; // Factory for creating new instances axios.create = function create(instanceConfig) { - return createInstance(utils.merge(defaults, instanceConfig)); + return createInstance(mergeConfig(axios.defaults, instanceConfig)); }; // Expose Cancel & CancelToken diff --git a/lib/core/Axios.js b/lib/core/Axios.js index f6eca92..8407a96 100644 --- a/lib/core/Axios.js +++ b/lib/core/Axios.js @@ -1,9 +1,9 @@ 'use strict'; -var defaults = require('./../defaults'); var utils = require('./../utils'); var InterceptorManager = require('./InterceptorManager'); var dispatchRequest = require('./dispatchRequest'); +var mergeConfig = require('./mergeConfig'); /** * Create a new instance of Axios @@ -27,13 +27,14 @@ Axios.prototype.request = function request(config) { /*eslint no-param-reassign:0*/ // Allow for axios('example/url'[, config]) a la fetch API if (typeof config === 'string') { - config = utils.merge({ - url: arguments[0] - }, arguments[1]); + config = arguments[1] || {}; + config.url = arguments[0]; + } else { + config = config || {}; } - config = utils.merge(defaults, this.defaults, config); - config.method = config.method.toLowerCase(); + config = mergeConfig(this.defaults, config); + config.method = config.method ? config.method.toLowerCase() : 'get'; // Hook up interceptors middleware var chain = [dispatchRequest, undefined]; diff --git a/lib/core/mergeConfig.js b/lib/core/mergeConfig.js new file mode 100644 index 0000000..6097a3e --- /dev/null +++ b/lib/core/mergeConfig.js @@ -0,0 +1,51 @@ +'use strict'; + +var utils = require('../utils'); + +/** + * Config-specific merge-function which creates a new config-object + * by merging two configuration objects together. + * + * @param {Object} config1 + * @param {Object} config2 + * @returns {Object} New object resulting from merging config2 to config1 + */ +module.exports = function mergeConfig(config1, config2) { + // eslint-disable-next-line no-param-reassign + config2 = config2 || {}; + var config = {}; + + utils.forEach(['url', 'method', 'params', 'data'], function valueFromConfig2(prop) { + if (typeof config2[prop] !== 'undefined') { + config[prop] = config2[prop]; + } + }); + + utils.forEach(['headers', 'auth', 'proxy'], function mergeDeepProperties(prop) { + if (utils.isObject(config2[prop])) { + config[prop] = utils.deepMerge(config1[prop], config2[prop]); + } else if (typeof config2[prop] !== 'undefined') { + config[prop] = config2[prop]; + } else if (utils.isObject(config1[prop])) { + config[prop] = utils.deepMerge(config1[prop]); + } else if (typeof config1[prop] !== 'undefined') { + config[prop] = config1[prop]; + } + }); + + utils.forEach([ + 'baseURL', 'transformRequest', 'transformResponse', 'paramsSerializer', + 'timeout', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName', + 'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', 'maxContentLength', + 'validateStatus', 'maxRedirects', 'httpAgent', 'httpsAgent', 'cancelToken', + 'socketPath' + ], function defaultToConfig2(prop) { + if (typeof config2[prop] !== 'undefined') { + config[prop] = config2[prop]; + } else if (typeof config1[prop] !== 'undefined') { + config[prop] = config1[prop]; + } + }); + + return config; +}; diff --git a/lib/defaults.js b/lib/defaults.js index e1bc1ca..65a25d5 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -26,7 +26,6 @@ function getDefaultAdapter() { } var defaults = { - method: 'get', adapter: getDefaultAdapter(), transformRequest: [function transformRequest(data, headers) { diff --git a/lib/utils.js b/lib/utils.js index b3fd865..aca5f4e 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -260,6 +260,32 @@ function merge(/* obj1, obj2, obj3, ... */) { return result; } +/** + * Function equal to merge with the difference being that no reference + * to original objects is kept. + * + * @see merge + * @param {Object} obj1 Object to merge + * @returns {Object} Result of all merge properties + */ +function deepMerge(/* obj1, obj2, obj3, ... */) { + var result = {}; + function assignValue(val, key) { + if (typeof result[key] === 'object' && typeof val === 'object') { + result[key] = deepMerge(result[key], val); + } else if (typeof val === 'object') { + result[key] = deepMerge({}, val); + } else { + result[key] = val; + } + } + + for (var i = 0, l = arguments.length; i < l; i++) { + forEach(arguments[i], assignValue); + } + return result; +} + /** * Extends object a by mutably adding to it the properties of object b. * @@ -298,6 +324,7 @@ module.exports = { isStandardBrowserEnv: isStandardBrowserEnv, forEach: forEach, merge: merge, + deepMerge: deepMerge, extend: extend, trim: trim }; diff --git a/test/specs/core/mergeConfig.spec.js b/test/specs/core/mergeConfig.spec.js new file mode 100644 index 0000000..bf6d209 --- /dev/null +++ b/test/specs/core/mergeConfig.spec.js @@ -0,0 +1,69 @@ +var defaults = require('../../../lib/defaults'); +var mergeConfig = require('../../../lib/core/mergeConfig'); + +describe('core::mergeConfig', function() { + it('should accept undefined for second argument', function() { + expect(mergeConfig(defaults, undefined)).toEqual(defaults); + }); + + it('should accept an object for second argument', function() { + expect(mergeConfig(defaults, {})).toEqual(defaults); + }); + + it('should not leave references', function() { + var merged = mergeConfig(defaults, {}); + expect(merged).not.toBe(defaults); + expect(merged.headers).not.toBe(defaults.headers); + }); + + it('should allow setting request options', function() { + var config = { + url: '__sample url__', + method: '__sample method__', + params: '__sample params__', + data: { foo: true } + }; + var merged = mergeConfig(defaults, config); + expect(merged.url).toEqual(config.url); + expect(merged.method).toEqual(config.method); + expect(merged.params).toEqual(config.params); + expect(merged.data).toEqual(config.data); + }); + + it('should not inherit request options', function() { + var localDefaults = { + url: '__sample url__', + method: '__sample method__', + params: '__sample params__', + data: { foo: true } + }; + var merged = mergeConfig(localDefaults, {}); + expect(merged.url).toEqual(undefined); + expect(merged.method).toEqual(undefined); + expect(merged.params).toEqual(undefined); + expect(merged.data).toEqual(undefined); + }); + + it('should merge auth, headers, proxy with defaults', function() { + expect(mergeConfig({ auth: undefined }, { auth: { user: 'foo', pass: 'test' } })).toEqual({ + auth: { user: 'foo', pass: 'test' } + }); + expect(mergeConfig({ auth: { user: 'foo', pass: 'test' } }, { auth: { pass: 'foobar' } })).toEqual({ + auth: { user: 'foo', pass: 'foobar' } + }); + }); + + it('should overwrite auth, headers, proxy with a non-object value', function() { + expect(mergeConfig({ auth: { user: 'foo', pass: 'test' } }, { auth: false })).toEqual({ + auth: false + }); + expect(mergeConfig({ auth: { user: 'foo', pass: 'test' } }, { auth: null })).toEqual({ + auth: null + }); + }); + + it('should allow setting other options', function() { + var merged = mergeConfig(defaults, { timeout: 123 }); + expect(merged.timeout).toEqual(123); + }); +}); diff --git a/test/specs/defaults.spec.js b/test/specs/defaults.spec.js index de19b6a..c8ee72e 100644 --- a/test/specs/defaults.spec.js +++ b/test/specs/defaults.spec.js @@ -148,14 +148,14 @@ describe('defaults', function () { }); }); - it('should be used by custom instance if set after instance created', function (done) { + it('should not be used by custom instance if set after instance created', function (done) { var instance = axios.create(); axios.defaults.baseURL = 'http://example.org/'; instance.get('/foo'); getAjaxRequest().then(function (request) { - expect(request.url).toBe('http://example.org/foo'); + expect(request.url).toBe('/foo'); done(); }); }); diff --git a/test/specs/options.spec.js b/test/specs/options.spec.js index 6520b8a..762b851 100644 --- a/test/specs/options.spec.js +++ b/test/specs/options.spec.js @@ -81,4 +81,32 @@ describe('options', function () { done(); }); }); + + it('should change only the baseURL of the specified instance', function() { + var instance1 = axios.create(); + var instance2 = axios.create(); + + instance1.defaults.baseURL = 'http://instance1.example.com/'; + + expect(instance2.defaults.baseURL).not.toBe('http://instance1.example.com/'); + }); + + it('should change only the headers of the specified instance', function() { + var instance1 = axios.create(); + var instance2 = axios.create(); + + instance1.defaults.headers.common.Authorization = 'faketoken'; + instance2.defaults.headers.common.Authorization = 'differentfaketoken'; + + instance1.defaults.headers.common['Content-Type'] = 'application/xml'; + instance2.defaults.headers.common['Content-Type'] = 'application/x-www-form-urlencoded'; + + expect(axios.defaults.headers.common.Authorization).toBe(undefined); + expect(instance1.defaults.headers.common.Authorization).toBe('faketoken'); + expect(instance2.defaults.headers.common.Authorization).toBe('differentfaketoken'); + + expect(axios.defaults.headers.common['Content-Type']).toBe(undefined); + expect(instance1.defaults.headers.common['Content-Type']).toBe('application/xml'); + expect(instance2.defaults.headers.common['Content-Type']).toBe('application/x-www-form-urlencoded'); + }); }); diff --git a/test/specs/utils/deepMerge.spec.js b/test/specs/utils/deepMerge.spec.js new file mode 100644 index 0000000..d879593 --- /dev/null +++ b/test/specs/utils/deepMerge.spec.js @@ -0,0 +1,66 @@ +var deepMerge = require('../../../lib/utils').deepMerge; + +describe('utils::deepMerge', function () { + it('should be immutable', function () { + var a = {}; + var b = {foo: 123}; + var c = {bar: 456}; + + deepMerge(a, b, c); + + expect(typeof a.foo).toEqual('undefined'); + expect(typeof a.bar).toEqual('undefined'); + expect(typeof b.bar).toEqual('undefined'); + expect(typeof c.foo).toEqual('undefined'); + }); + + it('should deepMerge properties', function () { + var a = {foo: 123}; + var b = {bar: 456}; + var c = {foo: 789}; + var d = deepMerge(a, b, c); + + expect(d.foo).toEqual(789); + expect(d.bar).toEqual(456); + }); + + it('should deepMerge recursively', function () { + var a = {foo: {bar: 123}}; + var b = {foo: {baz: 456}, bar: {qux: 789}}; + + expect(deepMerge(a, b)).toEqual({ + foo: { + bar: 123, + baz: 456 + }, + bar: { + qux: 789 + } + }); + }); + + it('should remove all references from nested objects', function () { + var a = {foo: {bar: 123}}; + var b = {}; + var d = deepMerge(a, b); + + expect(d).toEqual({ + foo: { + bar: 123 + } + }); + + expect(d.foo).not.toBe(a.foo); + }); + + it('handles null and undefined arguments', function () { + expect(deepMerge(undefined, undefined)).toEqual({}); + expect(deepMerge(undefined, {foo: 123})).toEqual({foo: 123}); + expect(deepMerge({foo: 123}, undefined)).toEqual({foo: 123}); + + expect(deepMerge(null, null)).toEqual({}); + expect(deepMerge(null, {foo: 123})).toEqual({foo: 123}); + expect(deepMerge({foo: 123}, null)).toEqual({foo: 123}); + }); +}); +