Skip to content

Commit

Permalink
Simplify wrapping got (#503)
Browse files Browse the repository at this point in the history
This renames `got.create()` to `got.extend()` and adds a more powerful `got.create()` method.
  • Loading branch information
szmarczak authored and sindresorhus committed Jul 8, 2018
1 parent 75dc4c2 commit bc41a49
Show file tree
Hide file tree
Showing 8 changed files with 342 additions and 56 deletions.
116 changes: 116 additions & 0 deletions advanced-creation.md
@@ -0,0 +1,116 @@
# Advanced creation

> Make calling REST APIs easier by creating niche-specific `got` instances.
#### got.create(settings)

Example: [gh-got](https://github.com/sindresorhus/gh-got/blob/master/index.js)

Configure a new `got` instance with the provided settings.<br>
**Note:** In contrast to `got.extend()`, this method has no defaults.

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

To inherit from parent, set it as `got.defaults.options` or use [object spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals).

##### methods

Type: `Object`

Array of supported request methods.

To inherit from parent, set it as `got.defaults.methods`.

##### handler

Type: `Function`<br>
Default: `undefined`

Function making additional changes to the request.

To inherit from parent, set it as `got.defaults.handler`.<br>
To use the default handler, just omit specifying this.

###### [url](readme.md#url)

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

###### next()

Normalizes arguments and returns a `Promise` or a `Stream` depending on [`options.stream`](readme.md#stream).

```js
const settings = {
handler: (url, options, next) => {
if (options.stream) {
// It's a Stream
// We can perform stream-specific actions on it
return next(url, options)
.on('request', request => setTimeout(() => request.abort(), 50));
}

// It's a Promise
return next(url, options);
},
methods: got.defaults.methods,
options: {
...got.defaults.options,
json: true
}
};

const jsonGot = got.create(settings);
```

```js
const defaults = {
handler: (url, options, next) => {
return next(url, options);
},
methods: [
'get',
'post',
'put',
'patch',
'head',
'delete'
],
options: {
retries: 2,
cache: false,
decompress: true,
useElectronNet: false,
throwHttpErrors: true,
headers: {
'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`
}
}
};

// Same as:
const defaults = {
handler: got.defaults.handler,
methods: got.defaults.methods,
options: got.defaults.options
};

const unchangedGot = got.create(defaults);
```

```js
const settings = {
handler: got.defaults.handler,
methods: got.defaults.methods,
options: {
...got.defaults.options,
headers: {
unicorn: 'rainbow'
}
}
};

const unicorn = got.create(settings);

// Same as:
const unicorn = got.extend({headers: {unicorn: 'rainbow'}});
```
83 changes: 76 additions & 7 deletions readme.md
Expand Up @@ -34,6 +34,7 @@ Created because [`request`](https://github.com/request/request) is bloated *(sev
- [JSON mode](#json)
- [WHATWG URL support](#url)
- [Electron support](#useelectronnet)
- [Advanced creation](advanced-creation.md)


## Install
Expand Down Expand Up @@ -105,6 +106,17 @@ Type: `Object`

Any of the [`https.request`](https://nodejs.org/api/https.html#https_https_request_options_callback) options.

###### baseUrl

Type: `string` `Object`

When specified, `url` will be prepended by `baseUrl`.<br>
If you specify an absolute URL, it will skip the `baseUrl`.

Very useful when used with `got.extend()` to create niche-specific `got` instances.

Can be a string or a [WHATWG `URL`](https://nodejs.org/api/url.html#url_class_url).

###### headers

Type: `Object`<br>
Expand Down Expand Up @@ -235,6 +247,8 @@ If this is disabled, requests that encounter an error status code will be resolv

#### got.stream(url, [options])

Sets `options.stream` to `true`.

`stream` method will return Duplex stream with additional events:

##### .on('request', request)
Expand Down Expand Up @@ -300,25 +314,56 @@ If it's not possible to retrieve the body size (can happen when streaming), `tot

Sets `options.method` to the method name and makes a request.

#### Instances
### Instances

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

Configure a new `got` instance with default `options` and (optionally) a custom `baseUrl`:

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

Configure a new `got` instance with default `options`:
```js
const client = got.extend({
baseUrl: 'https://example.com',
headers: {
'x-unicorn': 'rainbow'
}
});

client.get('/demo');

/* HTTP Request =>
* GET /demo HTTP/1.1
* Host: example.com
* x-unicorn: rainbow
*/
```

```js
(async () => {
const client = got.create({headers: {'x-foo': 'bar'}});
const {headers} = await client.get('httpbin.org/headers', {json: true}).body;
const client = got.extend({
baseUrl: 'httpbin.org',
headers: {
'x-foo': 'bar'
}
});
const {headers} = (await client.get('/headers', {json: true})).body;
//=> headers['x-foo'] === 'bar'

const jsonClient = client.create({json: true, headers: {'x-baz': 'qux'}});
const {headers: headers2} = await jsonClient.get('httpbin.org/headers').body;
const jsonClient = client.extend({
json: true,
headers: {
'x-baz': 'qux'
}
});
const {headers: headers2} = (await jsonClient.get('/headers')).body;
//=> headers2['x-foo'] === 'bar'
//=> headers2['x-baz'] === 'qux'
})();
```

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

## Errors

Expand Down Expand Up @@ -676,6 +721,30 @@ got('sindresorhus.com', {

Bear in mind, if you send an `if-modified-since` header and receive a `304 Not Modified` response, the body will be empty. It's your responsibility to cache and retrieve the body contents.

### Custom endpoints

Use `got.extend()` to make it nicer to work with REST APIs. Especially if you use the `baseUrl` option.

**Note:** Not to be confused with [`got.create()`](advanced-creation.md), which has no defaults.

```js
const got = require('got');
const pkg = require('./package.json');

const custom = got.extend({
baseUrl: 'example.com',
json: true,
headers: {
'user-agent': `my-module/${pkg.version} (https://github.com/username/my-module)`
}
});

// Use `custom` exactly how you use `got`
(async () => {
const list = await custom('/v1/users/list');
})();
```


## Related

Expand Down
2 changes: 0 additions & 2 deletions source/as-stream.js
Expand Up @@ -6,8 +6,6 @@ const requestAsEventEmitter = require('./request-as-event-emitter');
const {HTTPError, ReadError} = require('./errors');

module.exports = options => {
options.stream = true;

const input = new PassThrough();
const output = new PassThrough();
const proxy = duplexer3(input, output);
Expand Down
17 changes: 17 additions & 0 deletions source/assign-options.js
@@ -0,0 +1,17 @@
const extend = require('extend');
const is = require('@sindresorhus/is');

module.exports = (defaults, options = {}) => {
const opts = extend(true, {}, defaults, options);

if (Reflect.has(options, 'headers')) {
for (const [key, value] of Object.entries(options.headers)) {
if (is.nullOrUndefined(value)) {
delete opts.headers[key];
continue;
}
}
}

return opts;
};
73 changes: 38 additions & 35 deletions source/create.js
@@ -1,64 +1,67 @@
'use strict';
const extend = require('extend');
const is = require('@sindresorhus/is');
const URLGlobal = typeof URL === 'undefined' ? require('url').URL : URL; // TODO: Use the `URL` global when targeting Node.js 10
const errors = require('./errors');
const assignOptions = require('./assign-options');
const asStream = require('./as-stream');
const asPromise = require('./as-promise');
const errors = require('./errors');
const normalizeArguments = require('./normalize-arguments');
const deepFreeze = require('./deep-freeze');

const assignOptions = (defaults, options = {}) => {
const opts = extend(true, {}, defaults, options);
const next = (path, options) => {
let url = path;

if (Reflect.has(options, 'headers')) {
for (const [key, value] of Object.entries(options.headers)) {
if (is.nullOrUndefined(value)) {
delete opts.headers[key];
continue;
}
}
if (options.baseUrl) {
url = new URLGlobal(path, options.baseUrl);
}

options = normalizeArguments(url, options);

if (options.stream) {
return asStream(options);
}

return opts;
return asPromise(options);
};

const create = (defaults = {}) => {
const create = defaults => {
if (!defaults.handler) {
defaults.handler = next;
}

function got(url, options) {
try {
options = assignOptions(defaults, options);
const normalizedArgs = normalizeArguments(url, options);

if (normalizedArgs.stream) {
return asStream(normalizedArgs);
}

return asPromise(normalizedArgs);
options = assignOptions(defaults.options, options);
return defaults.handler(url, options, next);
} catch (error) {
return Promise.reject(error);
}
}

got.create = (options = {}) => create(assignOptions(defaults, options));
got.create = create;
got.extend = (options = {}) => create({
options: assignOptions(defaults.options, options),
methods: defaults.methods,
handler: defaults.handler
});

got.stream = (url, options) => {
options = assignOptions(defaults, options);
return asStream(normalizeArguments(url, options));
options = assignOptions(defaults.options, options);
options.stream = true;
return defaults.handler(url, options, next);
};

const methods = [
'get',
'post',
'put',
'patch',
'head',
'delete'
];

for (const method of methods) {
for (const method of defaults.methods) {
got[method] = (url, options) => got(url, {...options, method});
got.stream[method] = (url, options) => got.stream(url, {...options, method});
}

Object.assign(got, errors);
Object.defineProperty(got, 'defaults', {
value: deepFreeze(defaults),
writable: false,
enumerable: true,
configurable: true
});

return got;
};
Expand Down
12 changes: 12 additions & 0 deletions source/deep-freeze.js
@@ -0,0 +1,12 @@
'use strict';
const is = require('@sindresorhus/is');

module.exports = function deepFreeze(obj) {
for (const [key, value] of Object.entries(obj)) {
if (is.object(value)) {
deepFreeze(obj[key]);
}
}

return Object.freeze(obj);
};

0 comments on commit bc41a49

Please sign in to comment.