Skip to content

Commit 43e3e69

Browse files
committed
Make reviver and parse dates options work together. Add new functional helpers. Update readme.
1 parent 72bb8e7 commit 43e3e69

File tree

6 files changed

+184
-33
lines changed

6 files changed

+184
-33
lines changed

deno.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
},
1212
"imports": {
1313
"@deno/dnt": "jsr:@deno/dnt@^0.41.3",
14-
"@std/assert": "jsr:@std/assert@^1.0.9",
15-
"@std/path": "jsr:@std/path@^1.0.8"
14+
"@std/assert": "jsr:@std/assert@^1.0.10",
15+
"@std/path": "jsr:@std/path@^1.0.8",
16+
"zod": "npm:zod@^3.24.1"
1617
},
1718
"exclude": ["npm"]
1819
}

deno.lock

+13-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

readme.md

+14-14
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
FetchClient is a library that makes it easier to use the fetch API for JSON APIs. It provides the following features:
55

6-
* Makes fetch easier to use for JSON APIs
7-
* Automatic model validation
8-
* Caching
9-
* Middleware
10-
* Problem Details support
6+
* [Makes fetch easier to use for JSON APIs](#typed-response)
7+
* [Automatic model validation](#model-validator)
8+
* [Caching](#caching)
9+
* [Middleware](#middleware)
10+
* [Problem Details](https://www.rfc-editor.org/rfc/rfc9457.html) support
1111
* Option to parse dates in responses
1212

1313
## Install
@@ -22,7 +22,7 @@ npm install --save @exceptionless/fetchclient
2222

2323
## Usage
2424

25-
Get a typed JSON response:
25+
### Typed Response
2626

2727
```ts
2828
import { FetchClient } from '@exceptionless/fetchclient';
@@ -39,23 +39,23 @@ const response = await client.getJSON<Products>(
3939
const products = response.data;
4040
```
4141

42-
Get a typed JSON response using a function:
42+
### Typed Response Using a Function
4343

4444
```ts
45-
import { useFetchClient } from '@exceptionless/fetchclient';
45+
import { getJSON } from '@exceptionless/fetchclient';
4646

4747
type Products = {
4848
products: Array<{ id: number; name: string }>;
4949
};
5050

51-
const response = await useFetchClient().getJSON<Products>(
51+
const response = await getJSON<Products>(
5252
`https://dummyjson.com/products/search?q=iphone&limit=10`,
5353
);
5454

5555
const products = response.data;
5656
```
5757

58-
Use a model validator:
58+
### Model Validator
5959

6060
```ts
6161
import { FetchClient, setModelValidator } from '@exceptionless/fetchclient';
@@ -89,7 +89,7 @@ if (!response.ok) {
8989
}
9090
```
9191

92-
Use caching:
92+
### Caching
9393

9494
```ts
9595
import { FetchClient } from '@exceptionless/fetchclient';
@@ -109,7 +109,7 @@ const response = await client.getJSON<Todo>(
109109
client.cache.delete(["todos", "1"]);
110110
```
111111

112-
Use middleware:
112+
### Middleware
113113

114114
```ts
115115
import { FetchClient, useMiddleware } from '@exceptionless/fetchclient';
@@ -139,7 +139,7 @@ Also, take a look at the tests:
139139
Run tests:
140140

141141
```shell
142-
deno test --allow-net
142+
deno run test
143143
```
144144

145145
Lint code:
@@ -157,7 +157,7 @@ deno fmt
157157
Type check code:
158158

159159
```shell
160-
deno check scripts/*.ts *.ts src/*.ts
160+
deno run check
161161
```
162162

163163
## License

src/DefaultHelpers.ts

+82-2
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,99 @@ import {
55
defaultInstance as defaultProvider,
66
type FetchClientProvider,
77
} from "./FetchClientProvider.ts";
8+
import type { FetchClientResponse } from "./FetchClientResponse.ts";
89
import type { ProblemDetails } from "./ProblemDetails.ts";
9-
import type { RequestOptions } from "./RequestOptions.ts";
10+
import type { GetRequestOptions, RequestOptions } from "./RequestOptions.ts";
1011

1112
let getCurrentProviderFunc: () => FetchClientProvider | null = () => null;
1213

1314
/**
14-
* Gets a FetchClient instance.
15+
* Gets a FetchClient instance from the current provider.
1516
* @returns The FetchClient instance.
1617
*/
1718
export function useFetchClient(options?: FetchClientOptions): FetchClient {
1819
return getCurrentProvider().getFetchClient(options);
1920
}
2021

22+
/**
23+
* Sends a GET request to the specified URL using the default client and provider and returns the response as JSON.
24+
* @param url - The URL to send the GET request to.
25+
* @param options - Optional request options.
26+
* @returns A promise that resolves to the response as JSON.
27+
*/
28+
export function getJSON<T>(
29+
url: string,
30+
options?: GetRequestOptions,
31+
): Promise<FetchClientResponse<T>> {
32+
return useFetchClient().getJSON(url, options);
33+
}
34+
35+
/**
36+
* Sends a POST request with JSON payload using the default client and provider to the specified URL.
37+
*
38+
* @template T - The type of the response data.
39+
* @param {string} url - The URL to send the request to.
40+
* @param {object | string | FormData} [body] - The JSON payload or form data to send with the request.
41+
* @param {RequestOptions} [options] - Additional options for the request.
42+
* @returns {Promise<FetchClientResponse<T>>} - A promise that resolves to the response data.
43+
*/
44+
export function postJSON<T>(
45+
url: string,
46+
body?: object | string | FormData,
47+
options?: RequestOptions,
48+
): Promise<FetchClientResponse<T>> {
49+
return useFetchClient().postJSON(url, body, options);
50+
}
51+
52+
/**
53+
* Sends a PUT request with JSON payload using the default client and provider to the specified URL.
54+
*
55+
* @template T - The type of the response data.
56+
* @param {string} url - The URL to send the request to.
57+
* @param {object | string} [body] - The JSON payload to send with the request.
58+
* @param {RequestOptions} [options] - Additional options for the request.
59+
* @returns {Promise<FetchClientResponse<T>>} - A promise that resolves to the response data.
60+
*/
61+
export function putJSON<T>(
62+
url: string,
63+
body?: object | string,
64+
options?: RequestOptions,
65+
): Promise<FetchClientResponse<T>> {
66+
return useFetchClient().putJSON(url, body, options);
67+
}
68+
69+
/**
70+
* Sends a PATCH request with JSON payload using the default client and provider to the specified URL.
71+
*
72+
* @template T - The type of the response data.
73+
* @param {string} url - The URL to send the request to.
74+
* @param {object | string} [body] - The JSON payload to send with the request.
75+
* @param {RequestOptions} [options] - Additional options for the request.
76+
* @returns {Promise<FetchClientResponse<T>>} - A promise that resolves to the response data.
77+
*/
78+
export function patchJSON<T>(
79+
url: string,
80+
body?: object | string,
81+
options?: RequestOptions,
82+
): Promise<FetchClientResponse<T>> {
83+
return useFetchClient().patchJSON(url, body, options);
84+
}
85+
86+
/**
87+
* Sends a DELETE request with JSON payload using the default client and provider to the specified URL.
88+
*
89+
* @template T - The type of the response data.
90+
* @param {string} url - The URL to send the request to.
91+
* @param {RequestOptions} [options] - Additional options for the request.
92+
* @returns {Promise<FetchClientResponse<T>>} - A promise that resolves to the response data.
93+
*/
94+
export function deleteJSON<T>(
95+
url: string,
96+
options?: RequestOptions,
97+
): Promise<FetchClientResponse<T>> {
98+
return useFetchClient().deleteJSON(url, options);
99+
}
100+
21101
/**
22102
* Gets the current FetchClientProvider.
23103
* @returns The current FetchClientProvider.

src/FetchClient.test.ts

+49-2
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import { assert, assertEquals, assertFalse, assertRejects } from "@std/assert";
22
import {
33
FetchClient,
44
type FetchClientContext,
5+
getJSON,
56
ProblemDetails,
67
setBaseUrl,
78
useFetchClient,
89
} from "../mod.ts";
910
import { FetchClientProvider } from "./FetchClientProvider.ts";
10-
import { z, type ZodTypeAny } from "https://deno.land/x/zod@v3.23.8/mod.ts";
11+
import { z, type ZodTypeAny } from "zod";
1112

1213
export const TodoSchema = z.object({
1314
userId: z.number(),
@@ -32,7 +33,7 @@ Deno.test("can getJSON", async () => {
3233
});
3334

3435
Deno.test("can use function", async () => {
35-
const res = await useFetchClient().getJSON<Products>(
36+
const res = await getJSON<Products>(
3637
`https://dummyjson.com/products/search?q=iphone&limit=10`,
3738
);
3839

@@ -662,6 +663,52 @@ Deno.test("can use reviver", async () => {
662663
assert(res.data.completedTime instanceof Date);
663664
});
664665

666+
Deno.test("can parse dates and use reviver", async () => {
667+
const provider = new FetchClientProvider();
668+
const fakeFetch = (): Promise<Response> =>
669+
new Promise((resolve) => {
670+
const data = JSON.stringify({
671+
userId: 1,
672+
id: 1,
673+
title: "A random title",
674+
completed: false,
675+
completedTime: "2021-01-01T00:00:00.000Z",
676+
});
677+
resolve(new Response(data));
678+
});
679+
680+
provider.fetch = fakeFetch;
681+
682+
const api = provider.getFetchClient();
683+
684+
let res = await api.getJSON<Todo>(
685+
`https://jsonplaceholder.typicode.com/todos/1`,
686+
);
687+
688+
assertEquals(res.status, 200);
689+
assert(res.data);
690+
assertEquals(res.data.title, "A random title");
691+
assertFalse(res.data.completedTime instanceof Date);
692+
693+
res = await api.getJSON<Todo>(
694+
`https://jsonplaceholder.typicode.com/todos/1`,
695+
{
696+
shouldParseDates: true,
697+
reviver: (key: string, value: unknown) => {
698+
if (key === "title") {
699+
return "revived";
700+
}
701+
return value;
702+
},
703+
},
704+
);
705+
706+
assertEquals(res.status, 200);
707+
assert(res.data);
708+
assertEquals(res.data.title, "revived");
709+
assert(res.data.completedTime instanceof Date);
710+
});
711+
665712
Deno.test("can use kitchen sink", async () => {
666713
let called = false;
667714
let optionsCalled = false;

src/FetchClient.ts

+23-7
Original file line numberDiff line numberDiff line change
@@ -520,13 +520,11 @@ export class FetchClient {
520520
): Promise<FetchClientResponse<T>> {
521521
let data = null;
522522
try {
523-
if (options.reviver) {
523+
if (options.reviver || options.shouldParseDates) {
524524
const body = await response.text();
525-
data = JSON.parse(body, options.reviver);
526-
} else if (options.shouldParseDates) {
527-
// TODO: Combine reviver and shouldParseDates into a single function
528-
const body = await response.text();
529-
data = JSON.parse(body, this.parseDates);
525+
data = JSON.parse(body, (key, value) => {
526+
return this.reviveJsonValue(options, key, value);
527+
});
530528
} else {
531529
data = await response.json();
532530
}
@@ -554,7 +552,25 @@ export class FetchClient {
554552
return jsonResponse;
555553
}
556554

557-
private parseDates(this: unknown, _key: string, value: unknown): unknown {
555+
private reviveJsonValue(
556+
options: RequestOptions,
557+
key: string,
558+
value: unknown,
559+
): unknown {
560+
let revivedValued = value;
561+
562+
if (options.reviver) {
563+
revivedValued = options.reviver.call(this, key, revivedValued);
564+
}
565+
566+
if (options.shouldParseDates) {
567+
revivedValued = this.tryParseDate(key, revivedValued);
568+
}
569+
570+
return revivedValued;
571+
}
572+
573+
private tryParseDate(_key: string, value: unknown): unknown {
558574
if (typeof value !== "string") {
559575
return value;
560576
}

0 commit comments

Comments
 (0)