@zimic/http 0.0.1-canary.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +16 -0
- package/README.md +230 -0
- package/dist/chunk-VHQRAQPQ.mjs +1371 -0
- package/dist/chunk-VHQRAQPQ.mjs.map +1 -0
- package/dist/chunk-VUDGONB5.js +1382 -0
- package/dist/chunk-VUDGONB5.js.map +1 -0
- package/dist/cli.js +116 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +109 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.ts +1306 -0
- package/dist/index.js +544 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +537 -0
- package/dist/index.mjs.map +1 -0
- package/dist/typegen.d.ts +86 -0
- package/dist/typegen.js +12 -0
- package/dist/typegen.js.map +1 -0
- package/dist/typegen.mjs +3 -0
- package/dist/typegen.mjs.map +1 -0
- package/index.d.ts +1 -0
- package/package.json +110 -0
- package/src/cli/cli.ts +92 -0
- package/src/cli/index.ts +4 -0
- package/src/cli/typegen/openapi.ts +24 -0
- package/src/formData/HttpFormData.ts +300 -0
- package/src/formData/types.ts +110 -0
- package/src/headers/HttpHeaders.ts +217 -0
- package/src/headers/types.ts +65 -0
- package/src/index.ts +55 -0
- package/src/pathParams/types.ts +67 -0
- package/src/searchParams/HttpSearchParams.ts +258 -0
- package/src/searchParams/types.ts +133 -0
- package/src/typegen/index.ts +12 -0
- package/src/typegen/namespace/TypegenNamespace.ts +18 -0
- package/src/typegen/openapi/generate.ts +168 -0
- package/src/typegen/openapi/transform/components.ts +481 -0
- package/src/typegen/openapi/transform/context.ts +67 -0
- package/src/typegen/openapi/transform/filters.ts +71 -0
- package/src/typegen/openapi/transform/imports.ts +15 -0
- package/src/typegen/openapi/transform/io.ts +86 -0
- package/src/typegen/openapi/transform/methods.ts +803 -0
- package/src/typegen/openapi/transform/operations.ts +120 -0
- package/src/typegen/openapi/transform/paths.ts +119 -0
- package/src/typegen/openapi/utils/types.ts +45 -0
- package/src/types/arrays.d.ts +4 -0
- package/src/types/json.ts +89 -0
- package/src/types/objects.d.ts +14 -0
- package/src/types/requests.ts +96 -0
- package/src/types/schema.ts +834 -0
- package/src/types/strings.d.ts +9 -0
- package/src/types/utils.ts +64 -0
- package/src/utils/console.ts +7 -0
- package/src/utils/data.ts +13 -0
- package/src/utils/files.ts +28 -0
- package/src/utils/imports.ts +12 -0
- package/src/utils/prettier.ts +13 -0
- package/src/utils/strings.ts +3 -0
- package/src/utils/time.ts +25 -0
- package/src/utils/urls.ts +52 -0
- package/typegen.d.ts +1 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { ArrayItemIfArray, ReplaceBy } from '@/types/utils';
|
|
2
|
+
import { fileEquals } from '@/utils/files';
|
|
3
|
+
|
|
4
|
+
import { HttpFormDataSchema, HttpFormDataSchemaName } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An extended HTTP form data object with a strictly-typed schema. Fully compatible with the built-in
|
|
8
|
+
* {@link https://developer.mozilla.org/docs/Web/API/FormData `FormData`} class.
|
|
9
|
+
*
|
|
10
|
+
* **IMPORTANT**: the input of `HttpFormData` and all of its internal types must be declared inline or as a type aliases
|
|
11
|
+
* (`type`). They cannot be interfaces.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* import { HttpFormData } from '@zimic/http';
|
|
15
|
+
*
|
|
16
|
+
* const formData = new HttpFormData<{
|
|
17
|
+
* files: File[];
|
|
18
|
+
* description?: string;
|
|
19
|
+
* }>();
|
|
20
|
+
*
|
|
21
|
+
* formData.append('file', new File(['content'], 'file.txt', { type: 'text/plain' }));
|
|
22
|
+
* formData.append('description', 'My file');
|
|
23
|
+
*
|
|
24
|
+
* const files = formData.getAll('file');
|
|
25
|
+
* console.log(files); // [File { name: 'file.txt', type: 'text/plain' }]
|
|
26
|
+
*
|
|
27
|
+
* const description = formData.get('description');
|
|
28
|
+
* console.log(description); // 'My file'
|
|
29
|
+
*
|
|
30
|
+
* @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐http#httpformdata `HttpFormData` API reference}
|
|
31
|
+
*/
|
|
32
|
+
class HttpFormData<Schema extends HttpFormDataSchema = HttpFormDataSchema> extends FormData {
|
|
33
|
+
/** @see {@link https://developer.mozilla.org/docs/Web/API/FormData/set MDN Reference} */
|
|
34
|
+
set<Name extends HttpFormDataSchemaName<Schema>>(
|
|
35
|
+
name: Name,
|
|
36
|
+
value: Exclude<ArrayItemIfArray<NonNullable<Schema[Name]>>, Blob>,
|
|
37
|
+
): void;
|
|
38
|
+
set<Name extends HttpFormDataSchemaName<Schema>>(
|
|
39
|
+
name: Name,
|
|
40
|
+
blob: Exclude<ArrayItemIfArray<NonNullable<Schema[Name]>>, string>,
|
|
41
|
+
fileName?: string,
|
|
42
|
+
): void;
|
|
43
|
+
set<Name extends HttpFormDataSchemaName<Schema>>(
|
|
44
|
+
name: Name,
|
|
45
|
+
blobOrValue: ArrayItemIfArray<NonNullable<Schema[Name]>>,
|
|
46
|
+
fileName?: string,
|
|
47
|
+
): void {
|
|
48
|
+
if (fileName === undefined) {
|
|
49
|
+
super.set(name, blobOrValue as Blob);
|
|
50
|
+
} else {
|
|
51
|
+
super.set(name, blobOrValue as Blob, fileName);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @see {@link https://developer.mozilla.org/docs/Web/API/FormData/append MDN Reference} */
|
|
56
|
+
append<Name extends HttpFormDataSchemaName<Schema>>(
|
|
57
|
+
name: Name,
|
|
58
|
+
value: Exclude<ArrayItemIfArray<NonNullable<Schema[Name]>>, Blob>,
|
|
59
|
+
): void;
|
|
60
|
+
append<Name extends HttpFormDataSchemaName<Schema>>(
|
|
61
|
+
name: Name,
|
|
62
|
+
blob: Exclude<ArrayItemIfArray<NonNullable<Schema[Name]>>, string>,
|
|
63
|
+
fileName?: string,
|
|
64
|
+
): void;
|
|
65
|
+
append<Name extends HttpFormDataSchemaName<Schema>>(
|
|
66
|
+
name: Name,
|
|
67
|
+
blobOrValue: ArrayItemIfArray<NonNullable<Schema[Name]>>,
|
|
68
|
+
fileName?: string,
|
|
69
|
+
): void {
|
|
70
|
+
if (fileName === undefined) {
|
|
71
|
+
super.append(name, blobOrValue as Blob);
|
|
72
|
+
} else {
|
|
73
|
+
super.append(name, blobOrValue as Blob, fileName);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the value of the entry associated to a key name.
|
|
79
|
+
*
|
|
80
|
+
* If the key might have multiple values, use {@link HttpFormData#getAll} instead.
|
|
81
|
+
*
|
|
82
|
+
* @param name The name of the key to get the value of.
|
|
83
|
+
* @returns The value associated with the key name, or `null` if the key does not exist.
|
|
84
|
+
* @see {@link https://developer.mozilla.org/docs/Web/API/FormData/get MDN Reference}
|
|
85
|
+
*/
|
|
86
|
+
get<Name extends HttpFormDataSchemaName.NonArray<Schema>>(
|
|
87
|
+
name: Name,
|
|
88
|
+
): ReplaceBy<ReplaceBy<ArrayItemIfArray<Schema[Name]>, undefined, null>, Blob, File> {
|
|
89
|
+
return super.get(name) as ReplaceBy<ReplaceBy<ArrayItemIfArray<Schema[Name]>, undefined, null>, Blob, File>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get all the values of the entry associated with a key name.
|
|
94
|
+
*
|
|
95
|
+
* If the key has at most a single value, use {@link HttpFormData#get} instead.
|
|
96
|
+
*
|
|
97
|
+
* @param name The name of the key to get the values of.
|
|
98
|
+
* @returns An array of values associated with the key name, or an empty array if the key does not exist.
|
|
99
|
+
* @see {@link https://developer.mozilla.org/docs/Web/API/FormData/getAll MDN Reference}
|
|
100
|
+
*/
|
|
101
|
+
getAll<Name extends HttpFormDataSchemaName.Array<Schema>>(
|
|
102
|
+
name: Name,
|
|
103
|
+
): ReplaceBy<ArrayItemIfArray<NonNullable<Schema[Name]>>, Blob, File>[] {
|
|
104
|
+
return super.getAll(name) as ReplaceBy<ArrayItemIfArray<NonNullable<Schema[Name]>>, Blob, File>[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @see {@link https://developer.mozilla.org/docs/Web/API/FormData/has MDN Reference} */
|
|
108
|
+
has<Name extends HttpFormDataSchemaName<Schema>>(name: Name): boolean {
|
|
109
|
+
return super.has(name);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @see {@link https://developer.mozilla.org/docs/Web/API/FormData/delete MDN Reference} */
|
|
113
|
+
delete<Name extends HttpFormDataSchemaName<Schema>>(name: Name): void {
|
|
114
|
+
super.delete(name);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
forEach<This extends HttpFormData<Schema>>(
|
|
118
|
+
callback: <Key extends HttpFormDataSchemaName<Schema>>(
|
|
119
|
+
value: ReplaceBy<ArrayItemIfArray<NonNullable<Schema[Key]>>, Blob, File>,
|
|
120
|
+
key: Key,
|
|
121
|
+
parent: HttpFormData<Schema>,
|
|
122
|
+
) => void,
|
|
123
|
+
thisArg?: This,
|
|
124
|
+
): void {
|
|
125
|
+
super.forEach(callback as (value: FormDataEntryValue, key: string, parent: FormData) => void, thisArg);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** @see {@link https://developer.mozilla.org/docs/Web/API/FormData/keys MDN Reference} */
|
|
129
|
+
keys(): FormDataIterator<HttpFormDataSchemaName<Schema>> {
|
|
130
|
+
return super.keys() as FormDataIterator<HttpFormDataSchemaName<Schema>>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @see {@link https://developer.mozilla.org/docs/Web/API/FormData/values MDN Reference} */
|
|
134
|
+
values(): FormDataIterator<
|
|
135
|
+
ReplaceBy<ArrayItemIfArray<NonNullable<Schema[HttpFormDataSchemaName<Schema>]>>, Blob, File>
|
|
136
|
+
> {
|
|
137
|
+
return super.values() as FormDataIterator<
|
|
138
|
+
ReplaceBy<ArrayItemIfArray<NonNullable<Schema[HttpFormDataSchemaName<Schema>]>>, Blob, File>
|
|
139
|
+
>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** @see {@link https://developer.mozilla.org/docs/Web/API/FormData/entries MDN Reference} */
|
|
143
|
+
entries(): FormDataIterator<
|
|
144
|
+
[
|
|
145
|
+
HttpFormDataSchemaName<Schema>,
|
|
146
|
+
ReplaceBy<ArrayItemIfArray<NonNullable<Schema[HttpFormDataSchemaName<Schema>]>>, Blob, File>,
|
|
147
|
+
]
|
|
148
|
+
> {
|
|
149
|
+
return super.entries() as FormDataIterator<
|
|
150
|
+
[
|
|
151
|
+
HttpFormDataSchemaName<Schema>,
|
|
152
|
+
ReplaceBy<ArrayItemIfArray<NonNullable<Schema[HttpFormDataSchemaName<Schema>]>>, Blob, File>,
|
|
153
|
+
]
|
|
154
|
+
>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
[Symbol.iterator](): FormDataIterator<
|
|
158
|
+
[
|
|
159
|
+
HttpFormDataSchemaName<Schema>,
|
|
160
|
+
ReplaceBy<ArrayItemIfArray<NonNullable<Schema[HttpFormDataSchemaName<Schema>]>>, Blob, File>,
|
|
161
|
+
]
|
|
162
|
+
> {
|
|
163
|
+
return super[Symbol.iterator]() as FormDataIterator<
|
|
164
|
+
[
|
|
165
|
+
HttpFormDataSchemaName<Schema>,
|
|
166
|
+
ReplaceBy<ArrayItemIfArray<NonNullable<Schema[HttpFormDataSchemaName<Schema>]>>, Blob, File>,
|
|
167
|
+
]
|
|
168
|
+
>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Checks if the data is equal to the other data. Equality is defined as having the same keys and values, regardless
|
|
173
|
+
* of the order of keys.
|
|
174
|
+
*
|
|
175
|
+
* @param otherData The other data to compare.
|
|
176
|
+
* @returns A promise that resolves with `true` if the data is equal to the other data, or `false` otherwise.
|
|
177
|
+
* Important: both form data might be read while comparing.
|
|
178
|
+
*/
|
|
179
|
+
async equals<OtherSchema extends Schema>(otherData: HttpFormData<OtherSchema>): Promise<boolean> {
|
|
180
|
+
for (const [otherKey, otherValue] of otherData.entries()) {
|
|
181
|
+
const values = super.getAll.call(this, otherKey);
|
|
182
|
+
|
|
183
|
+
const haveSameNumberOfValues = values.length === super.getAll.call(otherData, otherKey).length;
|
|
184
|
+
if (!haveSameNumberOfValues) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let valueExists = false;
|
|
189
|
+
|
|
190
|
+
for (const value of values) {
|
|
191
|
+
if (
|
|
192
|
+
value === otherValue ||
|
|
193
|
+
(value instanceof Blob && otherValue instanceof Blob && (await fileEquals(value, otherValue)))
|
|
194
|
+
) {
|
|
195
|
+
valueExists = true;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!valueExists) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const key of this.keys()) {
|
|
206
|
+
const otherHasKey = super.has.call(otherData, key);
|
|
207
|
+
if (!otherHasKey) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Checks if the data contains the other data. This method is less strict than {@link HttpFormData#equals} and only
|
|
217
|
+
* requires that all keys and values in the other data are present in this data.
|
|
218
|
+
*
|
|
219
|
+
* @param otherData The other data to compare.
|
|
220
|
+
* @returns A promise that resolves with `true` if this data contains the other data, or `false` otherwise. Important:
|
|
221
|
+
* both form data might be read while comparing.
|
|
222
|
+
*/
|
|
223
|
+
async contains<OtherSchema extends Schema>(otherData: HttpFormData<OtherSchema>): Promise<boolean> {
|
|
224
|
+
for (const [otherKey, otherValue] of otherData.entries()) {
|
|
225
|
+
const values = super.getAll.call(this, otherKey);
|
|
226
|
+
|
|
227
|
+
const haveCompatibleNumberOfValues = values.length >= super.getAll.call(otherData, otherKey).length;
|
|
228
|
+
if (!haveCompatibleNumberOfValues) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let valueExists = false;
|
|
233
|
+
|
|
234
|
+
for (const value of values) {
|
|
235
|
+
if (
|
|
236
|
+
value === otherValue ||
|
|
237
|
+
(typeof value === 'string' && typeof otherValue === 'string' && value === otherValue) ||
|
|
238
|
+
(value instanceof Blob && otherValue instanceof Blob && (await fileEquals(value, otherValue)))
|
|
239
|
+
) {
|
|
240
|
+
valueExists = true;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!valueExists) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Converts this form data into a plain object. This method is useful for serialization and debugging purposes.
|
|
255
|
+
*
|
|
256
|
+
* **NOTE**: If a key has multiple values, the object will contain an array of values for that key. If the key has
|
|
257
|
+
* only one value, the object will contain its value directly, without an array, regardless of how the value was
|
|
258
|
+
* initialized when creating the form data.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* const formData = new HttpFormData<{
|
|
262
|
+
* title: string;
|
|
263
|
+
* descriptions: string[];
|
|
264
|
+
* content: Blob;
|
|
265
|
+
* }>();
|
|
266
|
+
*
|
|
267
|
+
* formData.set('title', 'My title');
|
|
268
|
+
* formData.append('descriptions', 'Description 1');
|
|
269
|
+
* formData.append('descriptions', 'Description 2');
|
|
270
|
+
* formData.set('content', new Blob(['content'], { type: 'text/plain' }));
|
|
271
|
+
*
|
|
272
|
+
* const object = formData.toObject();
|
|
273
|
+
* console.log(object); // { title: 'My title', descriptions: ['Description 1', 'Description 2'], content: Blob { type: 'text/plain' } }
|
|
274
|
+
*
|
|
275
|
+
* @returns A plain object representation of this form data.
|
|
276
|
+
*/
|
|
277
|
+
toObject() {
|
|
278
|
+
const object = {} as Schema;
|
|
279
|
+
|
|
280
|
+
type SchemaValue = Schema[HttpFormDataSchemaName<Schema>];
|
|
281
|
+
|
|
282
|
+
for (const [key, value] of this.entries()) {
|
|
283
|
+
if (key in object) {
|
|
284
|
+
const existingValue = object[key];
|
|
285
|
+
|
|
286
|
+
if (Array.isArray<SchemaValue>(existingValue)) {
|
|
287
|
+
existingValue.push(value as SchemaValue);
|
|
288
|
+
} else {
|
|
289
|
+
object[key] = [existingValue, value] as SchemaValue;
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
object[key] = value as SchemaValue;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return object;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export default HttpFormData;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { ArrayKey, IfNever, NonArrayKey } from '@/types/utils';
|
|
2
|
+
|
|
3
|
+
/** A schema for strict HTTP form data. */
|
|
4
|
+
export interface HttpFormDataSchema {
|
|
5
|
+
[fieldName: string]: string | string[] | Blob | Blob[] | null | undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export namespace HttpFormDataSchema {
|
|
9
|
+
/** A schema for loose HTTP form data. Field values are not strictly typed. */
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
export type Loose = Record<string, any>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export namespace HttpFormDataSchemaName {
|
|
15
|
+
/** Extracts the names of the form data fields defined in a {@link HttpFormDataSchema} that are arrays. */
|
|
16
|
+
export type Array<Schema extends HttpFormDataSchema> = IfNever<Schema, never, ArrayKey<Schema> & string>;
|
|
17
|
+
|
|
18
|
+
/** Extracts the names of the form data fields defined in a {@link HttpFormDataSchema} that are not arrays. */
|
|
19
|
+
export type NonArray<Schema extends HttpFormDataSchema> = IfNever<Schema, never, NonArrayKey<Schema> & string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extracts the names of the form data fields defined in a {@link HttpFormDataSchema}. Each key is considered a field
|
|
24
|
+
* name. `HttpFormDataSchemaName.Array` can be used to extract the names of array form data fields, whereas
|
|
25
|
+
* `HttpFormDataSchemaName.NonArray` extracts the names of non-array form data fields.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* import { type HttpFormDataSchemaName } from '@zimic/http';
|
|
29
|
+
*
|
|
30
|
+
* type FormDataName = HttpFormDataSchemaName<{
|
|
31
|
+
* title: string;
|
|
32
|
+
* descriptions: string[];
|
|
33
|
+
* content: Blob;
|
|
34
|
+
* }>;
|
|
35
|
+
* // "title" | "descriptions" | "content"
|
|
36
|
+
*
|
|
37
|
+
* type ArrayFormDataName = HttpFormDataSchemaName.Array<{
|
|
38
|
+
* title: string;
|
|
39
|
+
* descriptions: string[];
|
|
40
|
+
* content: Blob;
|
|
41
|
+
* }>;
|
|
42
|
+
* // "descriptions"
|
|
43
|
+
*
|
|
44
|
+
* type NonArrayFormDataName = HttpFormDataSchemaName.NonArray<{
|
|
45
|
+
* title: string;
|
|
46
|
+
* descriptions: string[];
|
|
47
|
+
* content: Blob;
|
|
48
|
+
* }>;
|
|
49
|
+
* // "title" | "content"
|
|
50
|
+
*/
|
|
51
|
+
export type HttpFormDataSchemaName<Schema extends HttpFormDataSchema> = IfNever<Schema, never, keyof Schema & string>;
|
|
52
|
+
|
|
53
|
+
type PrimitiveHttpFormDataSerialized<Type> = Type extends HttpFormDataSchema[string]
|
|
54
|
+
? Type
|
|
55
|
+
: Type extends (infer ArrayItem)[]
|
|
56
|
+
? ArrayItem extends (infer _InternalArrayItem)[]
|
|
57
|
+
? never
|
|
58
|
+
: PrimitiveHttpFormDataSerialized<ArrayItem>[]
|
|
59
|
+
: Type extends number
|
|
60
|
+
? `${number}`
|
|
61
|
+
: Type extends boolean
|
|
62
|
+
? `${boolean}`
|
|
63
|
+
: never;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Recursively converts a schema to its {@link https://developer.mozilla.org/docs/Web/API/FormData FormData}-serialized
|
|
67
|
+
* version. Numbers and booleans are converted to `${number}` and `${boolean}` respectively, and not serializable values
|
|
68
|
+
* are excluded, such as functions and dates.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* import { type HttpFormDataSerialized } from '@zimic/http';
|
|
72
|
+
*
|
|
73
|
+
* type Schema = HttpFormDataSerialized<{
|
|
74
|
+
* contentTitle: string | null;
|
|
75
|
+
* contentSize: number;
|
|
76
|
+
* content: Blob;
|
|
77
|
+
* full?: boolean;
|
|
78
|
+
* date: Date;
|
|
79
|
+
* method: () => void;
|
|
80
|
+
* }>;
|
|
81
|
+
* // {
|
|
82
|
+
* // contentTitle: string | null;
|
|
83
|
+
* // contentSize? `${number}`;
|
|
84
|
+
* // content: Blob;
|
|
85
|
+
* // full?: "false" | "true";
|
|
86
|
+
* // }
|
|
87
|
+
*/
|
|
88
|
+
export type HttpFormDataSerialized<Type> = Type extends HttpFormDataSchema
|
|
89
|
+
? Type
|
|
90
|
+
: Type extends (infer _ArrayItem)[]
|
|
91
|
+
? never
|
|
92
|
+
: Type extends Date
|
|
93
|
+
? never
|
|
94
|
+
: Type extends (...parameters: never[]) => unknown
|
|
95
|
+
? never
|
|
96
|
+
: Type extends symbol
|
|
97
|
+
? never
|
|
98
|
+
: Type extends Map<infer _Key, infer _Value>
|
|
99
|
+
? never
|
|
100
|
+
: Type extends Set<infer _Value>
|
|
101
|
+
? never
|
|
102
|
+
: Type extends object
|
|
103
|
+
? {
|
|
104
|
+
[Key in keyof Type as IfNever<
|
|
105
|
+
PrimitiveHttpFormDataSerialized<Type[Key]>,
|
|
106
|
+
never,
|
|
107
|
+
Key
|
|
108
|
+
>]: PrimitiveHttpFormDataSerialized<Type[Key]>;
|
|
109
|
+
}
|
|
110
|
+
: never;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { Default, ReplaceBy } from '@/types/utils';
|
|
2
|
+
|
|
3
|
+
import { HttpHeadersSchema, HttpHeadersInit, HttpHeadersSchemaName } from './types';
|
|
4
|
+
|
|
5
|
+
function pickPrimitiveProperties<Schema extends HttpHeadersSchema>(schema: Schema) {
|
|
6
|
+
return Object.entries(schema).reduce<Record<string, string>>((accumulated, [key, value]) => {
|
|
7
|
+
if (value !== undefined) {
|
|
8
|
+
accumulated[key] = value;
|
|
9
|
+
}
|
|
10
|
+
return accumulated;
|
|
11
|
+
}, {});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* An extended HTTP headers object with a strictly-typed schema. Fully compatible with the built-in
|
|
16
|
+
* {@link https://developer.mozilla.org/docs/Web/API/Headers `Headers`} class.
|
|
17
|
+
*
|
|
18
|
+
* **IMPORTANT**: the input of `HttpHeaders` and all of its internal types must be declared inline or as a type aliases
|
|
19
|
+
* (`type`). They cannot be interfaces.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* import { HttpHeaders } from '@zimic/http';
|
|
23
|
+
*
|
|
24
|
+
* const headers = new HttpHeaders<{
|
|
25
|
+
* accept?: string;
|
|
26
|
+
* 'content-type'?: string;
|
|
27
|
+
* }>({
|
|
28
|
+
* accept: '*',
|
|
29
|
+
* 'content-type': 'application/json',
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* const contentType = headers.get('content-type');
|
|
33
|
+
* console.log(contentType); // 'application/json'
|
|
34
|
+
*
|
|
35
|
+
* @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐http#httpheaders `HttpHeaders` API reference}
|
|
36
|
+
*/
|
|
37
|
+
class HttpHeaders<Schema extends HttpHeadersSchema = HttpHeadersSchema> extends Headers {
|
|
38
|
+
constructor(init?: HttpHeadersInit<Schema>) {
|
|
39
|
+
if (init instanceof Headers || Array.isArray(init) || !init) {
|
|
40
|
+
super(init);
|
|
41
|
+
} else {
|
|
42
|
+
super(pickPrimitiveProperties(init));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @see {@link https://developer.mozilla.org/docs/Web/API/Headers/set MDN Reference} */
|
|
47
|
+
set<Name extends HttpHeadersSchemaName<Schema>>(name: Name, value: NonNullable<Schema[Name]>): void {
|
|
48
|
+
super.set(name, value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @see {@link https://developer.mozilla.org/docs/Web/API/Headers/append MDN Reference} */
|
|
52
|
+
append<Name extends HttpHeadersSchemaName<Schema>>(name: Name, value: NonNullable<Schema[Name]>): void {
|
|
53
|
+
super.append(name, value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @see {@link https://developer.mozilla.org/docs/Web/API/Headers/get MDN Reference} */
|
|
57
|
+
get<Name extends HttpHeadersSchemaName<Schema>>(name: Name): ReplaceBy<Schema[Name], undefined, null> {
|
|
58
|
+
return super.get(name) as ReplaceBy<Schema[Name], undefined, null>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @see {@link https://developer.mozilla.org/docs/Web/API/Headers/has MDN Reference} */
|
|
62
|
+
getSetCookie(): NonNullable<Default<Schema['Set-Cookie'], string>>[] {
|
|
63
|
+
return super.getSetCookie() as NonNullable<Default<Schema['Set-Cookie'], string>>[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @see {@link https://developer.mozilla.org/docs/Web/API/Headers/has MDN Reference} */
|
|
67
|
+
has<Name extends HttpHeadersSchemaName<Schema>>(name: Name): boolean {
|
|
68
|
+
return super.has(name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @see {@link https://developer.mozilla.org/docs/Web/API/Headers/delete MDN Reference} */
|
|
72
|
+
delete<Name extends HttpHeadersSchemaName<Schema>>(name: Name): void {
|
|
73
|
+
super.delete(name);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
forEach<This extends HttpHeaders<Schema>>(
|
|
77
|
+
callback: <Key extends HttpHeadersSchemaName<Schema>>(
|
|
78
|
+
value: NonNullable<Schema[Key]>,
|
|
79
|
+
key: Key,
|
|
80
|
+
parent: Headers,
|
|
81
|
+
) => void,
|
|
82
|
+
thisArg?: This,
|
|
83
|
+
): void {
|
|
84
|
+
super.forEach(callback as (value: string, key: string, parent: Headers) => void, thisArg);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @see {@link https://developer.mozilla.org/docs/Web/API/Headers/keys MDN Reference} */
|
|
88
|
+
keys(): HeadersIterator<HttpHeadersSchemaName<Schema>> {
|
|
89
|
+
return super.keys() as HeadersIterator<HttpHeadersSchemaName<Schema>>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @see {@link https://developer.mozilla.org/docs/Web/API/Headers/values MDN Reference} */
|
|
93
|
+
values(): HeadersIterator<NonNullable<Schema[HttpHeadersSchemaName<Schema>]>> {
|
|
94
|
+
return super.values() as HeadersIterator<NonNullable<Schema[HttpHeadersSchemaName<Schema>]>>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** @see {@link https://developer.mozilla.org/docs/Web/API/Headers/entries MDN Reference} */
|
|
98
|
+
entries(): HeadersIterator<[HttpHeadersSchemaName<Schema>, NonNullable<Schema[HttpHeadersSchemaName<Schema>]>]> {
|
|
99
|
+
return super.entries() as HeadersIterator<
|
|
100
|
+
[HttpHeadersSchemaName<Schema>, NonNullable<Schema[HttpHeadersSchemaName<Schema>]>]
|
|
101
|
+
>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
[Symbol.iterator](): HeadersIterator<
|
|
105
|
+
[HttpHeadersSchemaName<Schema>, NonNullable<Schema[HttpHeadersSchemaName<Schema>]>]
|
|
106
|
+
> {
|
|
107
|
+
return super[Symbol.iterator]() as HeadersIterator<
|
|
108
|
+
[HttpHeadersSchemaName<Schema>, NonNullable<Schema[HttpHeadersSchemaName<Schema>]>]
|
|
109
|
+
>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Checks if this headers object is equal to another set of headers. Equality is defined as having the same keys and
|
|
114
|
+
* values, regardless of the order of keys.
|
|
115
|
+
*
|
|
116
|
+
* @param otherHeaders The other headers object to compare against.
|
|
117
|
+
* @returns `true` if the headers are equal, `false` otherwise.
|
|
118
|
+
*/
|
|
119
|
+
equals<OtherSchema extends Schema>(otherHeaders: HttpHeaders<OtherSchema>): boolean {
|
|
120
|
+
for (const [key, otherValue] of otherHeaders.entries()) {
|
|
121
|
+
const value = super.get.call(this, key);
|
|
122
|
+
|
|
123
|
+
if (value === null) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const valueItems = this.splitHeaderValues(value);
|
|
128
|
+
const otherValueItems = this.splitHeaderValues(otherValue);
|
|
129
|
+
|
|
130
|
+
const haveCompatibleNumberOfValues = valueItems.length === otherValueItems.length;
|
|
131
|
+
if (!haveCompatibleNumberOfValues) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const otherValueItem of otherValueItems) {
|
|
136
|
+
if (!valueItems.includes(otherValueItem)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const key of this.keys()) {
|
|
143
|
+
const otherHasKey = super.has.call(otherHeaders, key);
|
|
144
|
+
if (!otherHasKey) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Checks if this headers object contains another set of headers. This method is less strict than
|
|
154
|
+
* {@link HttpHeaders#equals} and only requires that all keys and values in the other headers are present in these
|
|
155
|
+
* headers.
|
|
156
|
+
*
|
|
157
|
+
* @param otherHeaders The other headers object to compare against.
|
|
158
|
+
* @returns `true` if these headers contain the other headers, `false` otherwise.
|
|
159
|
+
*/
|
|
160
|
+
contains<OtherSchema extends Schema>(otherHeaders: HttpHeaders<OtherSchema>): boolean {
|
|
161
|
+
for (const [key, otherValue] of otherHeaders.entries()) {
|
|
162
|
+
const value = super.get.call(this, key);
|
|
163
|
+
|
|
164
|
+
if (value === null) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const valueItems = this.splitHeaderValues(value);
|
|
169
|
+
const otherValueItems = this.splitHeaderValues(otherValue);
|
|
170
|
+
|
|
171
|
+
const haveCompatibleNumberOfValues = valueItems.length >= otherValueItems.length;
|
|
172
|
+
if (!haveCompatibleNumberOfValues) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const otherValueItem of otherValueItems) {
|
|
177
|
+
if (!valueItems.includes(otherValueItem)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Converts these headers into a plain object. This method is useful for serialization and debugging purposes.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* const headers = new HttpHeaders({
|
|
191
|
+
* accept: 'application/json',
|
|
192
|
+
* 'content-type': 'application/json',
|
|
193
|
+
* });
|
|
194
|
+
* const object = headers.toObject();
|
|
195
|
+
* console.log(object); // { accept: 'application/json', 'content-type': 'application/json' }
|
|
196
|
+
*
|
|
197
|
+
* @returns A plain object representation of these headers.
|
|
198
|
+
*/
|
|
199
|
+
toObject(): Schema {
|
|
200
|
+
const object = {} as Schema;
|
|
201
|
+
|
|
202
|
+
for (const [key, value] of this.entries()) {
|
|
203
|
+
object[key] = value;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return object;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private splitHeaderValues(value: string) {
|
|
210
|
+
return value
|
|
211
|
+
.split(',')
|
|
212
|
+
.map((item) => item.trim())
|
|
213
|
+
.filter((item) => item.length > 0);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export default HttpHeaders;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { IfNever } from '@/types/utils';
|
|
2
|
+
|
|
3
|
+
import { HttpPathParamsSerialized } from '../pathParams/types';
|
|
4
|
+
import HttpHeaders from './HttpHeaders';
|
|
5
|
+
|
|
6
|
+
/** A schema for strict HTTP headers. */
|
|
7
|
+
export interface HttpHeadersSchema {
|
|
8
|
+
[headerName: string]: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export namespace HttpHeadersSchema {
|
|
12
|
+
/** A schema for loose HTTP headers. Header values are not strictly typed. */
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
export type Loose = Record<string, any>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A strict tuple representation of a {@link HttpHeadersSchema}. */
|
|
18
|
+
export type HttpHeadersSchemaTuple<Schema extends HttpHeadersSchema = HttpHeadersSchema> = {
|
|
19
|
+
[Key in keyof Schema & string]: [Key, NonNullable<Schema[Key]>];
|
|
20
|
+
}[keyof Schema & string];
|
|
21
|
+
|
|
22
|
+
/** An initialization value for {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐http#httpheaders `HttpHeaders`}. */
|
|
23
|
+
export type HttpHeadersInit<Schema extends HttpHeadersSchema = HttpHeadersSchema> =
|
|
24
|
+
| Headers
|
|
25
|
+
| Schema
|
|
26
|
+
| HttpHeaders<Schema>
|
|
27
|
+
| HttpHeadersSchemaTuple<Schema>[];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extracts the names of the headers defined in a {@link HttpHeadersSchema}. Each key is considered a header name.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* import { type HttpHeadersSchemaName } from '@zimic/http';
|
|
34
|
+
*
|
|
35
|
+
* type HeaderName = HttpHeadersSchemaName<{
|
|
36
|
+
* 'content-type': string;
|
|
37
|
+
* 'content-length'?: string;
|
|
38
|
+
* }>;
|
|
39
|
+
* // "content-type" | "content-length"
|
|
40
|
+
*/
|
|
41
|
+
export type HttpHeadersSchemaName<Schema extends HttpHeadersSchema> = IfNever<Schema, never, keyof Schema & string>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Recursively converts a schema to its
|
|
45
|
+
* {@link https://developer.mozilla.org/docs/Web/API/Headers HTTP headers}-serialized version. Numbers and booleans are
|
|
46
|
+
* converted to `${number}` and `${boolean}` respectively, null becomes undefined and not serializable values are
|
|
47
|
+
* excluded, such as functions and dates.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* import { type HttpHeadersSerialized } from '@zimic/http';
|
|
51
|
+
*
|
|
52
|
+
* type Params = HttpHeadersSerialized<{
|
|
53
|
+
* 'content-type': string;
|
|
54
|
+
* 'x-remaining-tries': number;
|
|
55
|
+
* 'x-full'?: boolean;
|
|
56
|
+
* 'x-date': Date;
|
|
57
|
+
* method: () => void;
|
|
58
|
+
* }>;
|
|
59
|
+
* // {
|
|
60
|
+
* // 'content-type': string;
|
|
61
|
+
* // 'x-remaining-tries': `${number}`;
|
|
62
|
+
* // 'x-full'?: "false" | "true";
|
|
63
|
+
* // }
|
|
64
|
+
*/
|
|
65
|
+
export type HttpHeadersSerialized<Type> = HttpPathParamsSerialized<Type>;
|