Skip to content

Commit 1c75332

Browse files
committed
feat: Add setHeaders and makeIterableHeaders functions
1 parent de71e17 commit 1c75332

File tree

4 files changed

+417
-11
lines changed

4 files changed

+417
-11
lines changed

README.md

Lines changed: 125 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This package:
66

77
+ Uses the modern, standardized `fetch` function.
88
+ Does **not** throw on non-OK HTTP responses.
9-
+ Allows to fully type all possible HTTP responses depending on the HTTP status code.
9+
+ **Allows to fully type all possible HTTP responses depending on the HTTP status code.**
1010

1111
## Does a Non-OK Status Code Warrant an Error?
1212

@@ -35,16 +35,20 @@ npm i dr-fetch
3535
### Create Custom Fetch Function
3636

3737
This is optional and only needed if you need to do something before or after fetching. By far the most common task to
38-
do is to add an authorization header to every call.
38+
do is to add the `authorization` header and the `accept` header to every call.
3939

4040
```typescript
4141
// myFetch.ts
4242
import { obtainToken } from './magical-auth-stuff.js';
43+
import { setHeaders } from 'dr-fetch';
4344

4445
export function myFetch(url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) {
4546
const token = obtainToken();
46-
// Add token to request headers. Not shown because it depends on whether init was given, whether init.headers is
47-
// a POJO or not, etc. TypeScript will guide you through the possibilities.
47+
// Make sure there's an object where headers can be added:
48+
init ??= {};
49+
// With setHeaders(), you can add headers to 'init' with a map, an array of tuples, a Headers
50+
// object or a POJO object.
51+
setHeaders(init, { 'Accept': 'application/json', 'Authorization': `Bearer ${token}`});
4852
// Finally, do fetch.
4953
return fetch(url, init);
5054
}
@@ -55,28 +59,34 @@ Think of this custom function as the place where you do interceptions (if you ar
5559
### Create Fetcher Object
5660

5761
```typescript
62+
// fetcher.ts
5863
import { DrFetch } from "dr-fetch";
5964
import { myFetch } from "./myFetch.js";
6065

61-
const fetcher = new DrFetch(myFetch);
66+
export default new DrFetch(myFetch);
6267
// If you don't need a custom fetch function, just do:
63-
const fetcher = new DrFetch();
68+
export default new DrFetch();
6469
```
6570

6671
### Adding a Custom Body Parser
6772

73+
This step is also optional.
74+
6875
One can say that the `DrFetch` class comes with 2 basic body parsers:
6976

70-
1. JSON parser when the the value of the `coontent-type` response header is `application/json` or similar
77+
1. JSON parser when the value of the `coontent-type` response header is `application/json` or similar
7178
(`application/problem+json`, for instance).
7279
2. Text parser when the value of the `content-type` response header is `text/<something>`, such as `text/plain` or
7380
`text/csv`.
7481

75-
If your API sends a content type not included in any of the above two cases, use `DrFetch.withParser()` to add a custom
82+
If your API sends a content type not covered by any of the above two cases, use `DrFetch.withParser()` to add a custom
7683
parser for the content type you are expecting. The class allows for fluent syntax, so you can chain calls:
7784

7885
```typescript
79-
const fetcher = new DrFetch(myFetch)
86+
// fetcher.ts
87+
...
88+
89+
export default new DrFetch(myFetch)
8090
.withParser('custom/contentType', async (response) => {
8191
// Do what you must with the provided response object. In the end, you must return the parsed body.
8292
return finalBody;
@@ -94,6 +104,7 @@ This is the fun part where we can enumerate the various shapes of the body depen
94104

95105
```typescript
96106
import type { MyData } from "./my-datatypes.js";
107+
import fetcher from './fetcher.js';
97108

98109
const response = await fetcher
99110
.for<200, MyData[]>()
@@ -180,22 +191,125 @@ accepts, such as `FormData`), no headers are explicitly specified and therefore
180191
custom data-fetching function you provide) does in these cases.
181192

182193
```typescript
194+
import type { Todo } from './myTypes.js';
195+
183196
const newTodo = { text: 'I am new. Insert me!' };
184197
const response = await fetcher
185-
.for<200, { success: boolean; }>()
198+
.for<200, { success: true; entity: Todo; }>()
186199
.for<400, { errors: string[]; }>()
187200
.post('/api/todos', newTodo);
188201

189202
const newTodos = [{ text: 'I am new. Insert me!' }, { text: 'Me too!' }];
190203
const response = await fetcher
191-
.for<200, { success: boolean; }>()
204+
.for<200, { success: true; entities: Todo[]; }>()
192205
.for<400, { errors: string[]; }>()
193206
.post('/api/todos', newTodos);
194207
```
195208

196209
As stated, your custom fetch can be used to further customize the request because these shortcut functions will, in the
197210
end, call it.
198211

212+
## setHeader and makeIterableHeaders
213+
214+
> Since **v0.4.0**
215+
216+
These are two helper functions that assist you in writing custom data-fetching functions.
217+
218+
If you haven't realized, the `init` paramter in `fetch()` can have the headers specified in 3 different formats:
219+
220+
+ As a `Headers` object (an instance of the `Headers` class)
221+
+ As a POJO object, where the property key is the header name, and the property value is the header value
222+
+ As an array of tuples of type `[string, string]`, where the first element is the header name, and the second one is
223+
its value
224+
225+
To further complicate this, the POJO object also accepts an array of strings as property values for headers that accept
226+
multiple values.
227+
228+
So writing a formal custom fetch **without** `setHeaders()` looks like this:
229+
230+
```typescript
231+
export function myFetch(URL: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) {
232+
const acceptHdrKey = 'Accept';
233+
const acceptHdrValue = 'application/json';
234+
init ??= {};
235+
init.headers ??= new Headers();
236+
if (Array.isArray(init.headers)) {
237+
// Tuples, so push a tuple per desired header:
238+
init.headers.push([acceptHdrKey, acceptHdrValue]);
239+
}
240+
else if (init.headers instanceof Headers) {
241+
init.headers.set(acceptHdrKey, acceptHdrValue);
242+
}
243+
else {
244+
// POJO object, so add headers as properties of an object:
245+
init.headers[acceptHdrKey] = acceptHdrValue;
246+
}
247+
return fetch(url, init);
248+
}
249+
```
250+
251+
This would also get more complex if you account for multi-value headers. Now the same thing, using `setHeaders()`:
252+
253+
```typescript
254+
export function myFetch(URL: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) {
255+
init ??= {};
256+
setHeaders(init, [['Accept', 'application/json']]);
257+
// OR:
258+
setHeaders(init, new Map([['Accept', ['application/json', 'application/xml']]]));
259+
// OR:
260+
setHeaders(init, { 'Accept': ['application/json', 'application/xml'] });
261+
// OR:
262+
setHeaders(init, new Headers([['Accept', 'application/json']]));
263+
return fetch(url, init);
264+
}
265+
```
266+
267+
The difference is indeed pretty shocking. Also note that adding arrays of values doesn't increase the complexity of
268+
the code.
269+
270+
### makeIterableHeaders
271+
272+
This function is the magic trick that powers the `setHeaders` function, and is very handy for troubleshooting or unit
273+
testing because it can take a collection of HTTP header specifications in the form of a map, a Headers object, a POJO
274+
object or an array of tuples and return an iterator object that iterates through the definitions in the same way: A
275+
list of tuples.
276+
277+
```typescript
278+
const myHeaders1 = new Headers();
279+
myHeaders1.set('Accept', 'application/json');
280+
myHeaders1.set('Authorization', 'Bearer x');
281+
282+
const myHeaders2 = new Map();
283+
myHeaders2.set('Accept', 'application/json');
284+
myHeaders2.set('Authorization', 'Bearer x');
285+
286+
const myHeaders3 = {
287+
'Accept': 'application/json',
288+
'Authorization': 'Bearer x'
289+
};
290+
291+
const myHeaders4 = [
292+
['Accept', 'application/json'],
293+
['Authorization', 'Bearer x'],
294+
];
295+
296+
// The output of these is identical.
297+
console.log([...makeIterableHeaders(myHeaders1)]);
298+
console.log([...makeIterableHeaders(myHeaders2)]);
299+
console.log([...makeIterableHeaders(myHeaders3)]);
300+
console.log([...makeIterableHeaders(myHeaders4)]);
301+
```
302+
303+
This function is a **generator function**, so what returns is an iterator object. The two most helpful ways of using
304+
it are in `for..of` statements and spreading:
305+
306+
```typescript
307+
for (let [key, value] of makeIterableHeaders(myHeaders)) { ... }
308+
309+
// In unit-testing, perhaps:
310+
expect([...makeIterableHeaders(myHeaders)].length).to.equal(2);
311+
```
312+
199313
## Usage Without TypeScript (JavaScript Projects)
200314

201315
Why are you a weird fellow/gal? Anyway, prejudice aside, body typing will mean nothing to you, so forget about `for()`

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './DrFetch.js';
2+
export * from './setHeaders.js';
23
export type * from './types.js';
34

src/setHeaders.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* Defines all the possible data constructs that can be used to set HTTP headers in an 'init' configuration object.
3+
*/
4+
export type HeaderInput =
5+
Map<string, string | ReadonlyArray<string>> |
6+
[string, string][] |
7+
Record<string, string | ReadonlyArray<string>> |
8+
Headers;
9+
10+
function* headerTuplesGenerator(headers: [string, string][]) {
11+
yield* headers;
12+
}
13+
14+
function headerMapGenerator(headers: Map<string, string | ReadonlyArray<string>>) {
15+
return headers.entries();
16+
}
17+
18+
function* headersPojoGenerator(headers: Record<string, string | ReadonlyArray<string>>) {
19+
yield* Object.entries(headers);
20+
}
21+
22+
function headersClassGenerator(headers: Headers) {
23+
return headers.entries();
24+
}
25+
26+
/**
27+
* Creates an iterator object that can be used to examine the contents of the provided headers source.
28+
*
29+
* Useful for troubleshooting or unit testing, and used internally by `setHeaders` because it reduces the many possible
30+
* ways to specify headers into to one: Tuples. Because it is an iterator, it can:
31+
*
32+
* + Be used in `for..of` statements
33+
* + Be spreaded using the spred (`...`) operator in arrays and parameters
34+
* + Be used in other generators via `yield*`
35+
* + Be destructured (array destructuring)
36+
* @param headers The source of the headers to enumerate.
37+
* @returns An iterator object that will enumerate every header contained in the source in the form of a tuple
38+
* `[header, value]`.
39+
* @example
40+
* ```typescript
41+
* const myHeaders1 = new Headers();
42+
* myHeaders1.set('Accept', 'application/json');
43+
* myHeaders1.set('Authorization', 'Bearer x');
44+
*
45+
* const myHeaders2 = new Map();
46+
* myHeaders2.set('Accept', 'application/json');
47+
* myHeaders2.set('Authorization', 'Bearer x');
48+
*
49+
* const myHeaders3 = {
50+
* 'Accept': 'application/json',
51+
* 'Authorization': 'Bearer x'
52+
* };
53+
*
54+
* // The output of these is identical.
55+
* console.log([...makeIterableHeaders(myHeaders1)]);
56+
* console.log([...makeIterableHeaders(myHeaders2)]);
57+
* console.log([...makeIterableHeaders(myHeaders3)]);
58+
* ```
59+
*/
60+
export function makeIterableHeaders(headers: HeaderInput) {
61+
const iterator = Array.isArray(headers) ?
62+
headerTuplesGenerator(headers) :
63+
headers instanceof Map ?
64+
headerMapGenerator(headers) :
65+
headers instanceof Headers ?
66+
headersClassGenerator(headers) :
67+
headersPojoGenerator(headers)
68+
;
69+
return {
70+
[Symbol.iterator]() {
71+
return {
72+
next() {
73+
return iterator.next()
74+
}
75+
};
76+
}
77+
};
78+
}
79+
80+
function setTupleHeaders(headers: [string, string][], newHeaders: HeaderInput) {
81+
for (let [key, value] of makeIterableHeaders(newHeaders)) {
82+
headers.push([key, Array.isArray(value) ? value.join(', ') : value as string]);
83+
}
84+
}
85+
86+
function setHeadersInHeadersInstance(headers: Headers, newHeaders: HeaderInput) {
87+
for (let [key, value] of makeIterableHeaders(newHeaders)) {
88+
if (Array.isArray(value)) {
89+
for (let v of value) {
90+
headers.append(key, v);
91+
}
92+
}
93+
else {
94+
headers.set(key, value as string);
95+
}
96+
}
97+
}
98+
99+
function setPojoHeaders(headers: Record<string, string>, newHeaders: HeaderInput) {
100+
for (let [key, value] of makeIterableHeaders(newHeaders)) {
101+
headers[key] = Array.isArray(value) ? value.join(', ') : value as string;
102+
}
103+
}
104+
105+
export function setHeaders(init: Exclude<Parameters<typeof fetch>[1], undefined>, headers: HeaderInput) {
106+
if (!init) {
107+
throw new Error("The 'init' argument cannot be undefined.");
108+
}
109+
init.headers ??= new Headers();
110+
if (Array.isArray(init.headers)) {
111+
setTupleHeaders(init.headers, headers);
112+
}
113+
else if (init.headers instanceof Headers) {
114+
setHeadersInHeadersInstance(init.headers, headers);
115+
}
116+
else {
117+
setPojoHeaders(init.headers, headers);
118+
}
119+
}
120+
121+
const x = new Headers()

0 commit comments

Comments
 (0)