Skip to content

Commit d369b08

Browse files
jstewmonsindresorhus
authored andcommittedJul 31, 2018
Make got.mergeOptions() behavior more obvious and document its behavior (#538)
1 parent 6d654fa commit d369b08

9 files changed

+110
-49
lines changed
 

‎advanced-creation.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Configure a new `got` instance with the provided settings.<br>
1111

1212
##### [options](readme.md#options)
1313

14-
To inherit from parent, set it as `got.defaults.options` or use [`got.assignOptions(defaults.options, options)`](readme.md#gotassignoptionsparentoptions-newoptions).<br>
14+
To inherit from parent, set it as `got.defaults.options` or use [`got.mergeOptions(defaults.options, options)`](readme.md#gotmergeOptionsparentoptions-newoptions).<br>
1515
**Note**: Avoid using [object spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals) as it doesn't work recursively.
1616

1717
##### methods
@@ -54,7 +54,7 @@ const settings = {
5454
return next(options);
5555
},
5656
methods: got.defaults.methods,
57-
options: got.assignOptions(got.defaults.options, {
57+
options: got.mergeOptions(got.defaults.options, {
5858
json: true
5959
})
6060
};
@@ -99,7 +99,7 @@ const unchangedGot = got.create(defaults);
9999
const settings = {
100100
handler: got.defaults.handler,
101101
methods: got.defaults.methods,
102-
options: got.assignOptions(got.defaults.options, {
102+
options: got.mergeOptions(got.defaults.options, {
103103
headers: {
104104
unicorn: 'rainbow'
105105
}

‎package.json

-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
"cacheable-request": "^4.0.1",
3939
"decompress-response": "^3.3.0",
4040
"duplexer3": "^0.1.4",
41-
"extend": "^3.0.1",
4241
"get-stream": "^3.0.0",
4342
"mimic-response": "^1.0.0",
4443
"p-cancelable": "^0.5.0",

‎readme.md

+19-9
Original file line numberDiff line numberDiff line change
@@ -358,10 +358,8 @@ Sets `options.method` to the method name and makes a request.
358358

359359
#### got.extend([options])
360360

361-
Configure a new `got` instance with default `options` and (optionally) a custom `baseUrl`:
361+
Configure a new `got` instance with default `options`. `options` are merged with the extended instance's `defaults.options` as described in [`got.mergeOptions`](#gotmergeoptionsparentoptions-newoptions).
362362

363-
**Note:** You can extend another extended instance. `got.defaults` provides settings used by that instance.<br>
364-
Check out the [unchanged default values](source/index.js).
365363

366364
```js
367365
const client = got.extend({
@@ -405,16 +403,28 @@ client.get('/demo');
405403

406404
*Need more control over the behavior of Got? Check out the [`got.create()`](advanced-creation.md).*
407405

408-
#### got.assignOptions(parentOptions, newOptions)
406+
**Both `got.extend(options)` and `got.create(options)` will freeze the instance's default options. For `got.extend()`, the instance's default options are the result of `got.mergeOptions`, which effectively copies plain `Object` and `Array` values. Therefore, you should treat objects passed to these methods as immutable.**
409407

410-
Extends parent options. Avoid using [object spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals) as it doesn't work recursively:
408+
#### got.mergeOptions(parentOptions, newOptions)
409+
410+
Extends parent options. The options objects are deeply merged to a new object. The value of each property is determined as follows:
411+
412+
- If the new value is `undefined` the parent value is preserved.
413+
- If the parent value is an instance of `URL` and the new value is a `string` or `URL`, a new URL instance is created, using the parent value as the base: `new URL(new, parent)`.
414+
- If the new value is an `Array`, the new value is recursively merged into an empty array (the source value is discarded). `undefined` elements in the source array are not assigned during the merge, so the resulting array will have an empty item where the source array had an `undefined` item.
415+
- If the new value is a plain `Object`
416+
- If the parent value is a plain `Object`, both values are merged recursively into a new `Object`.
417+
- Otherwise, only the new value is merged recursively into a new `Object`.
418+
- Otherwise, the new value is assigned to the property.
419+
420+
Avoid using [object spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals) as it doesn't work recursively:
411421

412422
```js
413-
const a = {headers: {cat: 'meow'}};
414-
const b = {headers: {dog: 'woof'}};
423+
const a = {headers: {cat: 'meow', habitat: ['house', 'alley']}};
424+
const b = {headers: {cow: 'moo', habitat: ['barn']}};
415425

416-
{...a, ...b} // => {headers: {dog: 'woof'}}
417-
got.assignOptions(a, b) // => {headers: {cat: 'meow', dog: 'woof'}}
426+
{...a, ...b} // => {headers: {cow: 'moo'}}
427+
got.mergeOptions(a, b) // => {headers: {cat: 'meow', cow: 'moo', habitat: ['barn']}}
418428
```
419429

420430
## Errors

‎source/assign-options.js

-27
This file was deleted.

‎source/create.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22
const errors = require('./errors');
3-
const assignOptions = require('./assign-options');
3+
const mergeOptions = require('./merge-options');
44
const asStream = require('./as-stream');
55
const asPromise = require('./as-promise');
66
const normalizeArguments = require('./normalize-arguments');
@@ -15,7 +15,7 @@ const create = defaults => {
1515

1616
function got(url, options) {
1717
try {
18-
options = assignOptions(defaults.options, options);
18+
options = mergeOptions(defaults.options, options);
1919
return defaults.handler(normalizeArguments(url, options, defaults), next);
2020
} catch (error) {
2121
return Promise.reject(error);
@@ -24,13 +24,13 @@ const create = defaults => {
2424

2525
got.create = create;
2626
got.extend = (options = {}) => create({
27-
options: assignOptions(defaults.options, options),
27+
options: mergeOptions(defaults.options, options),
2828
methods: defaults.methods,
2929
handler: defaults.handler
3030
});
3131

3232
got.stream = (url, options) => {
33-
options = assignOptions(defaults.options, options);
33+
options = mergeOptions(defaults.options, options);
3434
options.stream = true;
3535
return defaults.handler(normalizeArguments(url, options, defaults), next);
3636
};
@@ -40,7 +40,7 @@ const create = defaults => {
4040
got.stream[method] = (url, options) => got.stream(url, {...options, method});
4141
}
4242

43-
Object.assign(got, {...errors, assignOptions});
43+
Object.assign(got, {...errors, mergeOptions});
4444
Object.defineProperty(got, 'defaults', {
4545
value: deepFreeze(defaults),
4646
writable: false,

‎source/merge-options.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const {URL} = require('url');
2+
const is = require('@sindresorhus/is');
3+
4+
module.exports = (defaults, options = {}) => {
5+
return merge({}, defaults, options);
6+
};
7+
8+
function merge(target, ...sources) {
9+
for (const source of sources) {
10+
const sourceIter = is.array(source) ?
11+
source.entries() :
12+
Object.entries(source);
13+
for (const [key, sourceValue] of sourceIter) {
14+
const targetValue = target[key];
15+
if (is.undefined(sourceValue)) {
16+
continue;
17+
}
18+
if (is.array(sourceValue)) {
19+
target[key] = merge(new Array(sourceValue.length), sourceValue);
20+
} else if (is.urlInstance(targetValue) && (
21+
is.urlInstance(sourceValue) || is.string(sourceValue)
22+
)) {
23+
target[key] = new URL(sourceValue, targetValue);
24+
} else if (is.plainObject(sourceValue)) {
25+
if (is.plainObject(targetValue)) {
26+
target[key] = merge({}, targetValue, sourceValue);
27+
} else {
28+
target[key] = merge({}, sourceValue);
29+
}
30+
} else {
31+
target[key] = sourceValue;
32+
}
33+
}
34+
}
35+
return target;
36+
}

‎source/normalize-arguments.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,17 @@ module.exports = (url, options, defaults) => {
6767
options.headers.accept = 'application/json';
6868
}
6969

70+
const {headers} = options;
71+
for (const [key, value] of Object.entries(headers)) {
72+
if (is.nullOrUndefined(value)) {
73+
delete headers[key];
74+
}
75+
}
76+
7077
const {body} = options;
7178
if (is.nullOrUndefined(body)) {
7279
options.method = (options.method || 'GET').toUpperCase();
7380
} else {
74-
const {headers} = options;
7581
const isObject = is.object(body) && !Buffer.isBuffer(body) && !is.nodeStream(body);
7682
if (!is.nodeStream(body) && !is.string(body) && !is.buffer(body) && !(options.form || options.json)) {
7783
throw new TypeError('The `body` option must be a stream.Readable, string or Buffer');

‎test/create.js

+38-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {URL} from 'url';
12
import test from 'ava';
23
import got from '../source';
34
import {createServer} from './helpers/server';
@@ -76,6 +77,42 @@ test('custom headers (extend)', async t => {
7677
t.is(headers.unicorn, 'rainbow');
7778
});
7879

80+
test('extend overwrites arrays', t => {
81+
const statusCodes = [408];
82+
const a = got.extend({retry: {statusCodes}});
83+
t.deepEqual(a.defaults.options.retry.statusCodes, statusCodes);
84+
t.not(a.defaults.options.retry.statusCodes, statusCodes);
85+
});
86+
87+
test('extend overwrites null', t => {
88+
const statusCodes = null;
89+
const a = got.extend({retry: {statusCodes}});
90+
t.is(a.defaults.options.retry.statusCodes, statusCodes);
91+
});
92+
93+
test('extend ignores source values set to undefined', t => {
94+
const a = got.extend({
95+
headers: {foo: undefined, 'user-agent': undefined}
96+
});
97+
const b = a.extend({headers: {foo: undefined}});
98+
t.deepEqual(
99+
b.defaults.options.headers,
100+
got.defaults.options.headers
101+
);
102+
});
103+
104+
test('extend merges URL instances', t => {
105+
const a = got.extend({baseUrl: new URL('https://example.com')});
106+
const b = a.extend({baseUrl: '/foo'});
107+
t.is(b.defaults.options.baseUrl.toString(), 'https://example.com/foo');
108+
});
109+
110+
test('extend ignores object values set to undefined (root keys)', t => {
111+
t.true(Reflect.has(got.defaults.options, 'headers'));
112+
const a = got.extend({headers: undefined});
113+
t.deepEqual(a.defaults.options, got.defaults.options);
114+
});
115+
79116
test('create', async t => {
80117
const instance = got.create({
81118
options: {},
@@ -105,7 +142,7 @@ test('no tampering with defaults', t => {
105142
const instance = got.create({
106143
handler: got.defaults.handler,
107144
methods: got.defaults.methods,
108-
options: got.assignOptions(got.defaults.options, {
145+
options: got.mergeOptions(got.defaults.options, {
109146
baseUrl: 'example'
110147
})
111148
});

‎test/headers.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,9 @@ test('remove null value headers', async t => {
145145
test('remove undefined value headers', async t => {
146146
const {body} = await got(s.url, {
147147
headers: {
148-
'user-agent': undefined
148+
foo: undefined
149149
}
150150
});
151151
const headers = JSON.parse(body);
152-
t.false(Reflect.has(headers, 'user-agent'));
152+
t.false(Reflect.has(headers, 'foo'));
153153
});

0 commit comments

Comments
 (0)
Please sign in to comment.