Skip to content

Commit 8a9b754

Browse files
Add support for custom transformAndCheckStatus transform, and forcing json transforms (#159)
* Add support for custom transformAndCheckStatus transform, and forcing json transforms * Checking new transform setup * Finalising new transform and updating transformAndCheckStatus to match * Adding exports and bumping version * Bumping more versions and fixing comments
1 parent 1c55f18 commit 8a9b754

File tree

8 files changed

+326
-84
lines changed

8 files changed

+326
-84
lines changed

packages/kerosene-test/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@kablamo/kerosene-test",
3-
"version": "0.0.20",
3+
"version": "0.0.21",
44
"repository": "https://github.com/KablamoOSS/kerosene/tree/master/packages/kerosene-test",
55
"bugs": {
66
"url": "https://github.com/KablamoOSS/kerosene/issues"
@@ -46,7 +46,7 @@
4646
"utils"
4747
],
4848
"dependencies": {
49-
"@kablamo/kerosene": "^0.0.47",
49+
"@kablamo/kerosene": "^0.0.48",
5050
"lodash": "^4.17.21",
5151
"sinon": "^19.0.2"
5252
},

packages/kerosene-ui/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@kablamo/kerosene-ui",
3-
"version": "0.0.47",
3+
"version": "0.0.48",
44
"repository": "https://github.com/KablamoOSS/kerosene/tree/master/packages/kerosene-ui",
55
"bugs": {
66
"url": "https://github.com/KablamoOSS/kerosene/issues"
@@ -35,12 +35,12 @@
3535
"node": ">=18.12.0"
3636
},
3737
"dependencies": {
38-
"@kablamo/kerosene": "^0.0.47",
38+
"@kablamo/kerosene": "^0.0.48",
3939
"lodash": "^4.17.21",
4040
"use-sync-external-store": "^1.4.0"
4141
},
4242
"devDependencies": {
43-
"@kablamo/kerosene-test": "^0.0.20",
43+
"@kablamo/kerosene-test": "^0.0.21",
4444
"@kablamo/rollup-plugin-resolve-externals": "^0.0.2",
4545
"@optimize-lodash/rollup-plugin": "^5.0.2",
4646
"@sinonjs/fake-timers": "^14.0.0",

packages/kerosene/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@kablamo/kerosene",
3-
"version": "0.0.47",
3+
"version": "0.0.48",
44
"repository": "https://github.com/KablamoOSS/kerosene/tree/master/packages/kerosene",
55
"bugs": {
66
"url": "https://github.com/KablamoOSS/kerosene/issues"

packages/kerosene/src/fetch/transform.spec.ts

Lines changed: 148 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,49 @@
11
import _contentType, { type ParsedMediaType } from "content-type";
22
import { when } from "jest-when";
33
import type { DeepPartial } from "../types";
4-
import transform from "./transform";
4+
import transform, { createTransform, transformDefaultJson } from "./transform";
55

66
jest.mock("content-type");
77
const contentType = _contentType as unknown as jest.Mocked<typeof _contentType>;
88

9+
const contentTypes = [
10+
{
11+
type: "application/json",
12+
method: "json",
13+
content: { some: "prop" },
14+
hasExplicitSupport: true,
15+
},
16+
{
17+
type: "text/plain",
18+
method: "text",
19+
content: "Text",
20+
hasExplicitSupport: true,
21+
},
22+
{
23+
type: "application/pdf",
24+
method: "blob",
25+
content: Symbol("Blob"),
26+
hasExplicitSupport: false,
27+
},
28+
{
29+
type: "application/zip",
30+
method: "blob",
31+
content: Symbol("Blob"),
32+
hasExplicitSupport: false,
33+
},
34+
{
35+
type: "application/octet-stream",
36+
method: "blob",
37+
content: Symbol("Blob"),
38+
hasExplicitSupport: false,
39+
},
40+
] as const satisfies {
41+
type: string;
42+
method: string;
43+
content: unknown;
44+
hasExplicitSupport: boolean;
45+
}[];
46+
947
describe("transform", () => {
1048
it("should resolve a 204 status as null", async () => {
1149
await expect(
@@ -28,33 +66,7 @@ describe("transform", () => {
2866
).resolves.toBe(text);
2967
});
3068

31-
[
32-
{
33-
type: "application/json",
34-
method: "json",
35-
content: { some: "prop" },
36-
},
37-
{
38-
type: "text/plain",
39-
method: "text",
40-
content: "Text",
41-
},
42-
{
43-
type: "application/pdf",
44-
method: "blob",
45-
content: Symbol("Blob"),
46-
},
47-
{
48-
type: "application/zip",
49-
method: "blob",
50-
content: Symbol("Blob"),
51-
},
52-
{
53-
type: "application/octet-stream",
54-
method: "blob",
55-
content: Symbol("Blob"),
56-
},
57-
].forEach(({ type, method, content }) => {
69+
contentTypes.forEach(({ type, method, content }) => {
5870
it(`should use ${method}() for ${type}`, async () => {
5971
const header = `${type}; charset=utf-8`;
6072
when(contentType.parse)
@@ -76,3 +88,111 @@ describe("transform", () => {
7688
});
7789
});
7890
});
91+
92+
describe("createTransform", () => {
93+
it("should resolve a 204 status as null", async () => {
94+
const customTransform = createTransform();
95+
await expect(
96+
customTransform({ status: 204 } as Partial<Response> as Response),
97+
).resolves.toBe(null);
98+
});
99+
100+
it("should resolve a response without a content type as the defaultTransform result", async () => {
101+
const result = "transformedValue";
102+
const customTransform = createTransform({
103+
defaultTransform: () => Promise.resolve(result),
104+
});
105+
await expect(
106+
customTransform({
107+
status: 200,
108+
headers: {
109+
get: jest.fn().mockReturnValue(null),
110+
},
111+
} as DeepPartial<Response> as Response),
112+
).resolves.toBe(result);
113+
});
114+
115+
contentTypes.forEach(({ type, method, content, hasExplicitSupport }) => {
116+
const result = "transformedValue";
117+
const customTransform = createTransform({
118+
defaultTransform: () => Promise.resolve(result),
119+
});
120+
121+
it(
122+
hasExplicitSupport
123+
? `should use ${method}() for ${type}`
124+
: `should use the provided transform for type ${type}`,
125+
async () => {
126+
const header = `${type}; charset=utf-8`;
127+
when(contentType.parse)
128+
.calledWith(header)
129+
.mockReturnValue({
130+
type,
131+
} as Partial<ParsedMediaType> as ParsedMediaType);
132+
const getHeaders = jest.fn();
133+
when(getHeaders).calledWith("Content-Type").mockReturnValue(header);
134+
await expect(
135+
customTransform({
136+
status: 200,
137+
headers: {
138+
get: getHeaders,
139+
},
140+
[method]: async () => content,
141+
} as DeepPartial<Response> as Response),
142+
).resolves.toEqual(hasExplicitSupport ? content : result);
143+
},
144+
);
145+
});
146+
});
147+
148+
describe("transformDefaultJson", () => {
149+
it("should resolve a 204 status as null", async () => {
150+
await expect(
151+
transformDefaultJson({
152+
status: 204,
153+
} as Partial<Response> as Response),
154+
).resolves.toBe(null);
155+
});
156+
157+
it("should resolve a response without a content type as the json result", async () => {
158+
const result = "transformedValue";
159+
await expect(
160+
transformDefaultJson({
161+
status: 200,
162+
headers: {
163+
get: jest.fn().mockReturnValue(null),
164+
},
165+
json: () => Promise.resolve(result),
166+
} as DeepPartial<Response> as Response),
167+
).resolves.toBe(result);
168+
});
169+
170+
contentTypes.forEach(({ type, method, content, hasExplicitSupport }) => {
171+
const result = { key: "value" };
172+
it(
173+
hasExplicitSupport
174+
? `should use ${method}() for ${type}`
175+
: `should use the json transform for type ${type}`,
176+
async () => {
177+
const header = `${type}; charset=utf-8`;
178+
when(contentType.parse)
179+
.calledWith(header)
180+
.mockReturnValue({
181+
type,
182+
} as Partial<ParsedMediaType> as ParsedMediaType);
183+
const getHeaders = jest.fn();
184+
when(getHeaders).calledWith("Content-Type").mockReturnValue(header);
185+
await expect(
186+
transformDefaultJson({
187+
status: 200,
188+
headers: {
189+
get: getHeaders,
190+
},
191+
json: () => Promise.resolve(result),
192+
[method]: async () => content,
193+
} as DeepPartial<Response> as Response),
194+
).resolves.toEqual(hasExplicitSupport ? content : result);
195+
},
196+
);
197+
});
198+
});
Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,57 @@
11
import { parse } from "content-type";
22

3+
export type CreateTransformOptions = {
4+
defaultTransform?: (res: Response) => Promise<unknown>;
5+
};
6+
37
/**
4-
* Takes a fetch response and attempts to transform the response automatically according to the status code and
5-
* Content-Type header (if provided)
6-
* @param response
8+
* Create a handler for parsing fetch responses, using the status code and Content-Type header if provided, and falling back to an option custom transform if not. This is only necessary if you want to provide a defaultTransform for `transform` different to the existing behaviour
9+
* @param options
10+
711
*/
8-
export default function transform(response: Response) {
9-
if (response.status === 204) {
10-
return Promise.resolve(null);
11-
}
12+
export function createTransform({
13+
defaultTransform,
14+
}: CreateTransformOptions = {}) {
15+
return (response: Response) => {
16+
if (response.status === 204) {
17+
return Promise.resolve(null);
18+
}
1219

13-
const contentType = response.headers.get("Content-Type");
20+
const contentType = response.headers.get("Content-Type");
1421

15-
if (!contentType) {
16-
return response.text();
17-
}
22+
if (!contentType) {
23+
return defaultTransform ? defaultTransform(response) : response.text();
24+
}
1825

19-
const { type } = parse(contentType);
26+
const { type } = parse(contentType);
2027

21-
switch (type) {
22-
case "application/json":
23-
return response.json();
28+
switch (type) {
29+
case "application/json":
30+
return response.json();
2431

25-
case "text/plain":
26-
return response.text();
32+
case "text/plain":
33+
return response.text();
2734

28-
default:
29-
return response.blob();
30-
}
35+
default:
36+
return defaultTransform ? defaultTransform(response) : response.blob();
37+
}
38+
};
3139
}
40+
41+
/**
42+
* Takes a fetch response and attempts to transform the response automatically according to the status code and
43+
* Content-Type header (if provided), falling back to the JSON response if a matching type is not found
44+
* @param response
45+
*/
46+
export const transformDefaultJson = createTransform({
47+
defaultTransform: (response) => response.json(),
48+
});
49+
50+
/**
51+
* Takes a fetch response and attempts to transform the response automatically according to the status code and
52+
* Content-Type header (if provided)
53+
* @param response
54+
*/
55+
const transform = createTransform();
56+
57+
export default transform;

0 commit comments

Comments
 (0)