openapi-typesafe-fetch 1.0.0-rc.7 → 1.0.0-rc.9

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/CHANGELOG.md CHANGED
@@ -2,6 +2,51 @@
2
2
 
3
3
  This package is a fork of `openapi-fetch`. The entries above `0.17.0` are releases of openapi-typesafe-fetch; `0.17.0` and the entries below it are the upstream history up to the fork point (upstream commit `d46a319a`), with `openapi-fetch` versions.
4
4
 
5
+ ## 1.0.0-rc.9
6
+
7
+ ### Patch Changes
8
+
9
+ - [#112](https://github.com/openapi-typesafe/openapi-typesafe/pull/112) [`05886b5`](https://github.com/openapi-typesafe/openapi-typesafe/commit/05886b5b8fdbb832f58abd12b9f8c741b0c73ad2) Thanks [@fedorello](https://github.com/fedorello)! - An error body is parsed as JSON only when its `Content-Type` is a JSON media type (`json` or a
10
+ `+json` suffix), or when the response has no `Content-Type` and the body is JSON. Any other error
11
+ body, such as `text/plain` or HTML, is returned as text, as its `string` type says. A `text/plain`
12
+ error body `429`, `true`, `0` or `null` came back as a number, a boolean or `null`, and with `0`
13
+ or `null` the check `if (error)` was skipped. openapi-fetch parses every error body, so this
14
+ differs from it.
15
+
16
+ - [#112](https://github.com/openapi-typesafe/openapi-typesafe/pull/112) [`05886b5`](https://github.com/openapi-typesafe/openapi-typesafe/commit/05886b5b8fdbb832f58abd12b9f8c741b0c73ad2) Thanks [@fedorello](https://github.com/fedorello)! - Code that is generic in the path compiles again. `SuccessResponse`, `ErrorResponse`,
17
+ `ErrorResponseJSON` and the `data` and `error` of `FetchResponse` for `Paths[Path]` gave the whole
18
+ `responses` map next to the response bodies, so assigning them to the bodies failed with TS2322.
19
+ They give the bodies now. This also fixes the error type of hook wrappers generic in the path.
20
+
21
+ - [#112](https://github.com/openapi-typesafe/openapi-typesafe/pull/112) [`05886b5`](https://github.com/openapi-typesafe/openapi-typesafe/commit/05886b5b8fdbb832f58abd12b9f8c741b0c73ad2) Thanks [@fedorello](https://github.com/fedorello)! - Every optional option of `createClient()` and of a request accepts `undefined` in its type, not
22
+ only the members of `querySerializer`: `baseUrl`, `fetch`, `Request`, `FormData`,
23
+ `querySerializer`, `bodySerializer`, `pathSerializer`, `headers`, `requestInitExt`, `parseAs`,
24
+ `middleware` and `params`. The client already counted `undefined` as left out, so a wrapper that
25
+ passes its own optional options on now compiles with `exactOptionalPropertyTypes`.
26
+
27
+ ## 1.0.0-rc.8
28
+
29
+ ### Patch Changes
30
+
31
+ - [#103](https://github.com/openapi-typesafe/openapi-typesafe/pull/103) [`091b864`](https://github.com/openapi-typesafe/openapi-typesafe/commit/091b864cdb6dd9e1ff5810d4d44cebee8a01a721) Thanks [@fedorello](https://github.com/fedorello)! - `createClient<any>()` types `data` and `error` as `any` again; they were `never`, so
32
+ `if (error)` made the whole result `never`. `SuccessResponse<any>` and `ErrorResponse<any>` are
33
+ `any` too.
34
+
35
+ - [#103](https://github.com/openapi-typesafe/openapi-typesafe/pull/103) [`091b864`](https://github.com/openapi-typesafe/openapi-typesafe/commit/091b864cdb6dd9e1ff5810d4d44cebee8a01a721) Thanks [@fedorello](https://github.com/fedorello)! - Response types follow what the client returns. `ErrorResponseJSON`, `SuccessResponseJSON` and the
36
+ `data` and `error` of a client made with ``createClient<paths, `${string}/json`>()`` keep a
37
+ JSON body of any JSON media type: an `application/problem+json` error, an
38
+ `application/vnd.api+json` success and `application/json; charset=utf-8` bodies were left out,
39
+ so the error could be `never` or miss a status. An error that is not JSON, such as the HTML page
40
+ of a proxy, is typed `string`, the text the client returns, instead of being left out.
41
+
42
+ - [#103](https://github.com/openapi-typesafe/openapi-typesafe/pull/103) [`091b864`](https://github.com/openapi-typesafe/openapi-typesafe/commit/091b864cdb6dd9e1ff5810d4d44cebee8a01a721) Thanks [@fedorello](https://github.com/fedorello)! - Every `querySerializer` option accepts `undefined` in its type, so a wrapper that passes its own
43
+ optional option on (`{ array: { explode: options.explode } }`) compiles with
44
+ `exactOptionalPropertyTypes`.
45
+
46
+ - [#103](https://github.com/openapi-typesafe/openapi-typesafe/pull/103) [`091b864`](https://github.com/openapi-typesafe/openapi-typesafe/commit/091b864cdb6dd9e1ff5810d4d44cebee8a01a721) Thanks [@fedorello](https://github.com/fedorello)! - `serializeArrayParam` and `serializeObjectParam` with a nested value in a path style (`simple`,
47
+ `label`, `matrix`) name `pathSerializer()` in their error, also inside your own `pathSerializer`,
48
+ instead of `querySerializer()`.
49
+
5
50
  ## 1.0.0-rc.7
6
51
 
7
52
  ### Patch Changes
@@ -199,31 +199,66 @@ type OperationRequestBodyContent<T> = FilterKeys<OperationRequestBodyMediaConten
199
199
  * `default` is left to the errors, as before.
200
200
  */
201
201
  type SuccessStatus<T> = [SuccessStatusKey<T>] extends [never] ? Extract<keyof T, "default"> : SuccessStatusKey<T>;
202
+ /**
203
+ * `true` for `any`: a conditional type on `any` takes both branches, so `any` is checked first.
204
+ * The response types check it inside `T extends unknown`: TypeScript relates a conditional type
205
+ * on a type parameter through the one with the parameter's constraint put in, while one on
206
+ * `IsAny<T>` relates through both branches, which for a generic path added the whole map.
207
+ */
208
+ type IsAny<T> = 0 extends 1 & T ? true : false;
202
209
  /**
203
210
  * Return all 2XX responses from a Response Object Map, or `default` if it declares no 2XX one. For
204
211
  * a union of maps (a call with a union of paths), the union of each map's responses: `keyof` of a
205
212
  * union is only the shared statuses, so one map of them lost the body of every path that did not
206
- * share its statuses, and a `default`-only path typed the data `undefined` (upstream).
213
+ * share its statuses, and a `default`-only path typed the data `undefined` (upstream). An `any`
214
+ * map (`createClient<any>()`) gives `any`: no key of `keyof any` is a status, which gave `never`.
215
+ */
216
+ type SuccessResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? IsAny<T> extends true ? T : ResponseBodies<T, Media, SuccessStatus<T>, never> : never;
217
+ /**
218
+ * Return all 1XX, 3XX, 4XX, 5XX and `default` responses from a Response Object Map; for a union
219
+ * of maps, the union of each map's responses, as `SuccessResponse`. An `any` map gives `any`.
207
220
  */
208
- type SuccessResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, SuccessStatus<T>> : never;
221
+ type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? IsAny<T> extends true ? T : ResponseBodies<T, Media, ErrorStatusKey<T>, string> : never;
209
222
  /**
210
- * The bodies of the responses `ResponseCode` in the media types `Media`. A response with a
211
- * `content` map but no media type in `Media` adds nothing: it gave its whole map, so the error of
212
- * `ErrorResponseJSON` for a `text/html` 500 was `{ "text/html": string }`, a body no response has.
213
- * A response without `content` keeps its `content` type (`undefined` for `content?: never`).
223
+ * The bodies of the responses `ResponseCode` as the client returns them (`ResponseBody`): each
224
+ * success one as `SuccessResponse` gives it, each other one as `ErrorResponse` does.
214
225
  */
215
- type GetResponseContent<T extends Record<string | number, any>, Media extends MediaType = MediaType, ResponseCode extends keyof T = keyof T> = ResponseCode extends keyof T ? {
216
- [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? FilterKeys<T[K]["content"], Media> : K extends keyof T ? T[K]["content"] : never;
226
+ type GetResponseContent<T extends Record<string | number, any>, Media extends MediaType = MediaType, ResponseCode extends keyof T = keyof T> = ResponseCode extends SuccessStatus<T> ? ResponseBodies<T, Media, ResponseCode, never> : ResponseBodies<T, Media, ResponseCode, string>;
227
+ /**
228
+ * The bodies of the responses `ResponseCode`. A response without `content` keeps its `content`
229
+ * type (`undefined` for `content?: never`). `OtherBody` is the class's body that is not JSON: it
230
+ * is passed in, not read from the status, so the result for a generic map stays a type that
231
+ * TypeScript relates to the one of another client (ADR-030).
232
+ */
233
+ type ResponseBodies<T extends Record<string | number, any>, Media, ResponseCode extends keyof T, OtherBody> = ResponseCode extends keyof T ? {
234
+ [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? ResponseBody<T[K]["content"], Media, OtherBody> : K extends keyof T ? T[K]["content"] : never;
217
235
  }[ResponseCode] : never;
218
236
  /**
219
- * Return all 1XX, 3XX, 4XX, 5XX and `default` responses from a Response Object Map; for a union
220
- * of maps, the union of each map's responses, as `SuccessResponse`.
237
+ * The body of one response with the content map `Content`, as the client returns it. `Media`
238
+ * picks among its media types, as an `Accept` header does, and each one picked gives its schema.
239
+ * A response with none of them can still arrive (a proxy's HTML 500), so it gives what the client
240
+ * makes of its bodies: a JSON body gives its schema, any other body `OtherBody`, a `string` for an
241
+ * error (the client returns the text) and nothing for a success (the default `parseAs: "json"`
242
+ * rejects it; another `parseAs` types the data by itself). `Media` is read only in
243
+ * `keyof Content & Media`, as in `FilterKeys`, so TypeScript still relates the results of two
244
+ * clients with different `Media` in generic code (ADR-030).
221
245
  */
222
- type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, ErrorStatusKey<T>> : never;
246
+ type ResponseBody<Content, Media, OtherBody> = ([keyof Content & Media] extends [never] ? JSONLike<Content> : Content[keyof Content & Media]) | ([keyof Content & Media] extends [never] ? false extends {
247
+ [Key in keyof Content]: IsJSONMediaType<Key>;
248
+ }[keyof Content] ? OtherBody : never : never);
249
+ /**
250
+ * The media types that the client parses as JSON, as a content map writes them: `json` or a
251
+ * `+json` suffix (RFC 6838), with or without parameters, as the client's `JSON_MEDIA_TYPE_RE`, and
252
+ * the wildcards for any type and any `application` type, which may hold JSON. Not `x-ndjson` or
253
+ * `json-seq`, whose bodies are lines of JSON.
254
+ */
255
+ type JSONMediaType = `${string}/${"" | `${string}+`}json` | `${string}/${"" | `${string}+`}json${";" | " " | "\t"}${string}` | "*/*" | "application/*";
256
+ /** Whether the client parses a body of the media type `Key` as JSON, in any letter case. */
257
+ type IsJSONMediaType<Key> = Key extends string ? Lowercase<Key> extends JSONMediaType ? true : false : false;
223
258
  /** Return first JSON-like 2XX response from a path + HTTP method */
224
- type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
259
+ type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
225
260
  /** Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method */
226
- type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
261
+ type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
227
262
  /** Return JSON-like request body from a path + HTTP method */
228
263
  type RequestBodyJSON<PathMethod> = JSONLike<FilterKeys<OperationRequestBody<PathMethod>, "content">>;
229
264
  /** Find first match of multiple keys */
@@ -240,10 +275,13 @@ type GetValueWithDefault<Obj, KeyPattern, Default> = Obj extends unknown ? Filte
240
275
  */
241
276
  type MediaType = `${string}/${string}`;
242
277
  /**
243
- * Return any media type containing "json" (works for "application/json",
244
- * "application/vnd.api+json", "application/vnd.oai.openapi+json")
278
+ * The values of the JSON media types of a content map, as the client parses them: `json` or a
279
+ * `+json` suffix, with or without parameters ("application/json", "application/vnd.api+json",
280
+ * "application/json; charset=utf-8").
245
281
  */
246
- type JSONLike<T> = FilterKeys<T, `${string}/json`>;
282
+ type JSONLike<T> = {
283
+ [Key in keyof T]: IsJSONMediaType<Key> extends true ? T[Key] : never;
284
+ }[keyof T];
247
285
  /**
248
286
  * Filter objects that have required keys
249
287
  * @deprecated Use `RequiredKeysOf` instead
@@ -199,31 +199,66 @@ type OperationRequestBodyContent<T> = FilterKeys<OperationRequestBodyMediaConten
199
199
  * `default` is left to the errors, as before.
200
200
  */
201
201
  type SuccessStatus<T> = [SuccessStatusKey<T>] extends [never] ? Extract<keyof T, "default"> : SuccessStatusKey<T>;
202
+ /**
203
+ * `true` for `any`: a conditional type on `any` takes both branches, so `any` is checked first.
204
+ * The response types check it inside `T extends unknown`: TypeScript relates a conditional type
205
+ * on a type parameter through the one with the parameter's constraint put in, while one on
206
+ * `IsAny<T>` relates through both branches, which for a generic path added the whole map.
207
+ */
208
+ type IsAny<T> = 0 extends 1 & T ? true : false;
202
209
  /**
203
210
  * Return all 2XX responses from a Response Object Map, or `default` if it declares no 2XX one. For
204
211
  * a union of maps (a call with a union of paths), the union of each map's responses: `keyof` of a
205
212
  * union is only the shared statuses, so one map of them lost the body of every path that did not
206
- * share its statuses, and a `default`-only path typed the data `undefined` (upstream).
213
+ * share its statuses, and a `default`-only path typed the data `undefined` (upstream). An `any`
214
+ * map (`createClient<any>()`) gives `any`: no key of `keyof any` is a status, which gave `never`.
215
+ */
216
+ type SuccessResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? IsAny<T> extends true ? T : ResponseBodies<T, Media, SuccessStatus<T>, never> : never;
217
+ /**
218
+ * Return all 1XX, 3XX, 4XX, 5XX and `default` responses from a Response Object Map; for a union
219
+ * of maps, the union of each map's responses, as `SuccessResponse`. An `any` map gives `any`.
207
220
  */
208
- type SuccessResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, SuccessStatus<T>> : never;
221
+ type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? IsAny<T> extends true ? T : ResponseBodies<T, Media, ErrorStatusKey<T>, string> : never;
209
222
  /**
210
- * The bodies of the responses `ResponseCode` in the media types `Media`. A response with a
211
- * `content` map but no media type in `Media` adds nothing: it gave its whole map, so the error of
212
- * `ErrorResponseJSON` for a `text/html` 500 was `{ "text/html": string }`, a body no response has.
213
- * A response without `content` keeps its `content` type (`undefined` for `content?: never`).
223
+ * The bodies of the responses `ResponseCode` as the client returns them (`ResponseBody`): each
224
+ * success one as `SuccessResponse` gives it, each other one as `ErrorResponse` does.
214
225
  */
215
- type GetResponseContent<T extends Record<string | number, any>, Media extends MediaType = MediaType, ResponseCode extends keyof T = keyof T> = ResponseCode extends keyof T ? {
216
- [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? FilterKeys<T[K]["content"], Media> : K extends keyof T ? T[K]["content"] : never;
226
+ type GetResponseContent<T extends Record<string | number, any>, Media extends MediaType = MediaType, ResponseCode extends keyof T = keyof T> = ResponseCode extends SuccessStatus<T> ? ResponseBodies<T, Media, ResponseCode, never> : ResponseBodies<T, Media, ResponseCode, string>;
227
+ /**
228
+ * The bodies of the responses `ResponseCode`. A response without `content` keeps its `content`
229
+ * type (`undefined` for `content?: never`). `OtherBody` is the class's body that is not JSON: it
230
+ * is passed in, not read from the status, so the result for a generic map stays a type that
231
+ * TypeScript relates to the one of another client (ADR-030).
232
+ */
233
+ type ResponseBodies<T extends Record<string | number, any>, Media, ResponseCode extends keyof T, OtherBody> = ResponseCode extends keyof T ? {
234
+ [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? ResponseBody<T[K]["content"], Media, OtherBody> : K extends keyof T ? T[K]["content"] : never;
217
235
  }[ResponseCode] : never;
218
236
  /**
219
- * Return all 1XX, 3XX, 4XX, 5XX and `default` responses from a Response Object Map; for a union
220
- * of maps, the union of each map's responses, as `SuccessResponse`.
237
+ * The body of one response with the content map `Content`, as the client returns it. `Media`
238
+ * picks among its media types, as an `Accept` header does, and each one picked gives its schema.
239
+ * A response with none of them can still arrive (a proxy's HTML 500), so it gives what the client
240
+ * makes of its bodies: a JSON body gives its schema, any other body `OtherBody`, a `string` for an
241
+ * error (the client returns the text) and nothing for a success (the default `parseAs: "json"`
242
+ * rejects it; another `parseAs` types the data by itself). `Media` is read only in
243
+ * `keyof Content & Media`, as in `FilterKeys`, so TypeScript still relates the results of two
244
+ * clients with different `Media` in generic code (ADR-030).
221
245
  */
222
- type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, ErrorStatusKey<T>> : never;
246
+ type ResponseBody<Content, Media, OtherBody> = ([keyof Content & Media] extends [never] ? JSONLike<Content> : Content[keyof Content & Media]) | ([keyof Content & Media] extends [never] ? false extends {
247
+ [Key in keyof Content]: IsJSONMediaType<Key>;
248
+ }[keyof Content] ? OtherBody : never : never);
249
+ /**
250
+ * The media types that the client parses as JSON, as a content map writes them: `json` or a
251
+ * `+json` suffix (RFC 6838), with or without parameters, as the client's `JSON_MEDIA_TYPE_RE`, and
252
+ * the wildcards for any type and any `application` type, which may hold JSON. Not `x-ndjson` or
253
+ * `json-seq`, whose bodies are lines of JSON.
254
+ */
255
+ type JSONMediaType = `${string}/${"" | `${string}+`}json` | `${string}/${"" | `${string}+`}json${";" | " " | "\t"}${string}` | "*/*" | "application/*";
256
+ /** Whether the client parses a body of the media type `Key` as JSON, in any letter case. */
257
+ type IsJSONMediaType<Key> = Key extends string ? Lowercase<Key> extends JSONMediaType ? true : false : false;
223
258
  /** Return first JSON-like 2XX response from a path + HTTP method */
224
- type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
259
+ type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
225
260
  /** Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method */
226
- type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
261
+ type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
227
262
  /** Return JSON-like request body from a path + HTTP method */
228
263
  type RequestBodyJSON<PathMethod> = JSONLike<FilterKeys<OperationRequestBody<PathMethod>, "content">>;
229
264
  /** Find first match of multiple keys */
@@ -240,10 +275,13 @@ type GetValueWithDefault<Obj, KeyPattern, Default> = Obj extends unknown ? Filte
240
275
  */
241
276
  type MediaType = `${string}/${string}`;
242
277
  /**
243
- * Return any media type containing "json" (works for "application/json",
244
- * "application/vnd.api+json", "application/vnd.oai.openapi+json")
278
+ * The values of the JSON media types of a content map, as the client parses them: `json` or a
279
+ * `+json` suffix, with or without parameters ("application/json", "application/vnd.api+json",
280
+ * "application/json; charset=utf-8").
245
281
  */
246
- type JSONLike<T> = FilterKeys<T, `${string}/json`>;
282
+ type JSONLike<T> = {
283
+ [Key in keyof T]: IsJSONMediaType<Key> extends true ? T[Key] : never;
284
+ }[keyof T];
247
285
  /**
248
286
  * Filter objects that have required keys
249
287
  * @deprecated Use `RequiredKeysOf` instead
package/dist/helpers.d.ts CHANGED
@@ -199,31 +199,66 @@ type OperationRequestBodyContent<T> = FilterKeys<OperationRequestBodyMediaConten
199
199
  * `default` is left to the errors, as before.
200
200
  */
201
201
  type SuccessStatus<T> = [SuccessStatusKey<T>] extends [never] ? Extract<keyof T, "default"> : SuccessStatusKey<T>;
202
+ /**
203
+ * `true` for `any`: a conditional type on `any` takes both branches, so `any` is checked first.
204
+ * The response types check it inside `T extends unknown`: TypeScript relates a conditional type
205
+ * on a type parameter through the one with the parameter's constraint put in, while one on
206
+ * `IsAny<T>` relates through both branches, which for a generic path added the whole map.
207
+ */
208
+ type IsAny<T> = 0 extends 1 & T ? true : false;
202
209
  /**
203
210
  * Return all 2XX responses from a Response Object Map, or `default` if it declares no 2XX one. For
204
211
  * a union of maps (a call with a union of paths), the union of each map's responses: `keyof` of a
205
212
  * union is only the shared statuses, so one map of them lost the body of every path that did not
206
- * share its statuses, and a `default`-only path typed the data `undefined` (upstream).
213
+ * share its statuses, and a `default`-only path typed the data `undefined` (upstream). An `any`
214
+ * map (`createClient<any>()`) gives `any`: no key of `keyof any` is a status, which gave `never`.
215
+ */
216
+ type SuccessResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? IsAny<T> extends true ? T : ResponseBodies<T, Media, SuccessStatus<T>, never> : never;
217
+ /**
218
+ * Return all 1XX, 3XX, 4XX, 5XX and `default` responses from a Response Object Map; for a union
219
+ * of maps, the union of each map's responses, as `SuccessResponse`. An `any` map gives `any`.
207
220
  */
208
- type SuccessResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, SuccessStatus<T>> : never;
221
+ type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? IsAny<T> extends true ? T : ResponseBodies<T, Media, ErrorStatusKey<T>, string> : never;
209
222
  /**
210
- * The bodies of the responses `ResponseCode` in the media types `Media`. A response with a
211
- * `content` map but no media type in `Media` adds nothing: it gave its whole map, so the error of
212
- * `ErrorResponseJSON` for a `text/html` 500 was `{ "text/html": string }`, a body no response has.
213
- * A response without `content` keeps its `content` type (`undefined` for `content?: never`).
223
+ * The bodies of the responses `ResponseCode` as the client returns them (`ResponseBody`): each
224
+ * success one as `SuccessResponse` gives it, each other one as `ErrorResponse` does.
214
225
  */
215
- type GetResponseContent<T extends Record<string | number, any>, Media extends MediaType = MediaType, ResponseCode extends keyof T = keyof T> = ResponseCode extends keyof T ? {
216
- [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? FilterKeys<T[K]["content"], Media> : K extends keyof T ? T[K]["content"] : never;
226
+ type GetResponseContent<T extends Record<string | number, any>, Media extends MediaType = MediaType, ResponseCode extends keyof T = keyof T> = ResponseCode extends SuccessStatus<T> ? ResponseBodies<T, Media, ResponseCode, never> : ResponseBodies<T, Media, ResponseCode, string>;
227
+ /**
228
+ * The bodies of the responses `ResponseCode`. A response without `content` keeps its `content`
229
+ * type (`undefined` for `content?: never`). `OtherBody` is the class's body that is not JSON: it
230
+ * is passed in, not read from the status, so the result for a generic map stays a type that
231
+ * TypeScript relates to the one of another client (ADR-030).
232
+ */
233
+ type ResponseBodies<T extends Record<string | number, any>, Media, ResponseCode extends keyof T, OtherBody> = ResponseCode extends keyof T ? {
234
+ [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? ResponseBody<T[K]["content"], Media, OtherBody> : K extends keyof T ? T[K]["content"] : never;
217
235
  }[ResponseCode] : never;
218
236
  /**
219
- * Return all 1XX, 3XX, 4XX, 5XX and `default` responses from a Response Object Map; for a union
220
- * of maps, the union of each map's responses, as `SuccessResponse`.
237
+ * The body of one response with the content map `Content`, as the client returns it. `Media`
238
+ * picks among its media types, as an `Accept` header does, and each one picked gives its schema.
239
+ * A response with none of them can still arrive (a proxy's HTML 500), so it gives what the client
240
+ * makes of its bodies: a JSON body gives its schema, any other body `OtherBody`, a `string` for an
241
+ * error (the client returns the text) and nothing for a success (the default `parseAs: "json"`
242
+ * rejects it; another `parseAs` types the data by itself). `Media` is read only in
243
+ * `keyof Content & Media`, as in `FilterKeys`, so TypeScript still relates the results of two
244
+ * clients with different `Media` in generic code (ADR-030).
221
245
  */
222
- type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, ErrorStatusKey<T>> : never;
246
+ type ResponseBody<Content, Media, OtherBody> = ([keyof Content & Media] extends [never] ? JSONLike<Content> : Content[keyof Content & Media]) | ([keyof Content & Media] extends [never] ? false extends {
247
+ [Key in keyof Content]: IsJSONMediaType<Key>;
248
+ }[keyof Content] ? OtherBody : never : never);
249
+ /**
250
+ * The media types that the client parses as JSON, as a content map writes them: `json` or a
251
+ * `+json` suffix (RFC 6838), with or without parameters, as the client's `JSON_MEDIA_TYPE_RE`, and
252
+ * the wildcards for any type and any `application` type, which may hold JSON. Not `x-ndjson` or
253
+ * `json-seq`, whose bodies are lines of JSON.
254
+ */
255
+ type JSONMediaType = `${string}/${"" | `${string}+`}json` | `${string}/${"" | `${string}+`}json${";" | " " | "\t"}${string}` | "*/*" | "application/*";
256
+ /** Whether the client parses a body of the media type `Key` as JSON, in any letter case. */
257
+ type IsJSONMediaType<Key> = Key extends string ? Lowercase<Key> extends JSONMediaType ? true : false : false;
223
258
  /** Return first JSON-like 2XX response from a path + HTTP method */
224
- type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
259
+ type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
225
260
  /** Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method */
226
- type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
261
+ type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
227
262
  /** Return JSON-like request body from a path + HTTP method */
228
263
  type RequestBodyJSON<PathMethod> = JSONLike<FilterKeys<OperationRequestBody<PathMethod>, "content">>;
229
264
  /** Find first match of multiple keys */
@@ -240,10 +275,13 @@ type GetValueWithDefault<Obj, KeyPattern, Default> = Obj extends unknown ? Filte
240
275
  */
241
276
  type MediaType = `${string}/${string}`;
242
277
  /**
243
- * Return any media type containing "json" (works for "application/json",
244
- * "application/vnd.api+json", "application/vnd.oai.openapi+json")
278
+ * The values of the JSON media types of a content map, as the client parses them: `json` or a
279
+ * `+json` suffix, with or without parameters ("application/json", "application/vnd.api+json",
280
+ * "application/json; charset=utf-8").
245
281
  */
246
- type JSONLike<T> = FilterKeys<T, `${string}/json`>;
282
+ type JSONLike<T> = {
283
+ [Key in keyof T]: IsJSONMediaType<Key> extends true ? T[Key] : never;
284
+ }[keyof T];
247
285
  /**
248
286
  * Filter objects that have required keys
249
287
  * @deprecated Use `RequiredKeysOf` instead
package/dist/index.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ const CONTENT_TYPE = "Content-Type";
5
6
  const PATH_PARAM_RE = /\{[^{}]+\}/g;
6
7
  const DOT_SEGMENT_RE = /^\.\.?$/;
7
8
  const QUERY_OR_FRAGMENT_RE = /[?#]/;
@@ -83,7 +84,7 @@ function createClient(clientOptions) {
83
84
  // with no body, we should not to set Content-Type
84
85
  requestBody === void 0 || // a raw body (FormData, Blob, ...) gets its Content-Type from the platform, or none
85
86
  isRawBody(requestBody) ? {} : {
86
- "Content-Type": "application/json"
87
+ [CONTENT_TYPE]: "application/json"
87
88
  },
88
89
  baseHeaders,
89
90
  headers,
@@ -342,8 +343,8 @@ function createPathBasedClient(clientOptions) {
342
343
  return wrapAsPathBasedClient(createClient(clientOptions));
343
344
  }
344
345
  function dropFormContentType(headers, isClientBuilt) {
345
- if (isClientBuilt || !BOUNDARY_PARAMETER_RE.test(headers.get("Content-Type"))) {
346
- headers.delete("Content-Type");
346
+ if (isClientBuilt || !BOUNDARY_PARAMETER_RE.test(headers.get(CONTENT_TYPE))) {
347
+ headers.delete(CONTENT_TYPE);
347
348
  }
348
349
  }
349
350
  function isHeaders(value) {
@@ -359,7 +360,7 @@ function parseJSON(raw, response) {
359
360
  try {
360
361
  return JSON.parse(raw);
361
362
  } catch {
362
- const contentType = response.headers.get("Content-Type");
363
+ const contentType = response.headers.get(CONTENT_TYPE);
363
364
  const hint = TEXT_MEDIA_TYPE_RE.test(contentType) ? 'set `parseAs: "text"`' : "set `parseAs`";
364
365
  const subject = `Response ${response.status} (${contentType ?? "no Content-Type"})`;
365
366
  throw new SyntaxError(`${subject} is not JSON (${raw.length} characters); ${hint}`);
@@ -394,6 +395,10 @@ async function readErrorBody(response) {
394
395
  if (!raw) {
395
396
  return void 0;
396
397
  }
398
+ const contentType = response.headers.get(CONTENT_TYPE);
399
+ if (contentType !== null && !JSON_MEDIA_TYPE_RE.test(contentType)) {
400
+ return raw;
401
+ }
397
402
  try {
398
403
  return JSON.parse(raw);
399
404
  } catch {
@@ -440,6 +445,13 @@ function checkFlatMembers(value, message) {
440
445
  throw new Error(message);
441
446
  }
442
447
  }
448
+ const PATH_STYLE_JOINERS = { simple: ",", label: ".", matrix: ";" };
449
+ function checkFlatParam(value, style) {
450
+ checkFlatMembers(
451
+ value,
452
+ PATH_STYLE_JOINERS[style] ? PATH_NESTED_VALUE_ERROR : QUERY_NESTED_VALUE_ERROR
453
+ );
454
+ }
443
455
  function encodeValue(value, allowReserved) {
444
456
  if (isNestedValue(value)) {
445
457
  throw new Error(QUERY_NESTED_VALUE_ERROR);
@@ -454,7 +466,7 @@ function serializePrimitiveParam(name, value, options) {
454
466
  return `${name}=${encodeValue(value, options?.allowReserved)}`;
455
467
  }
456
468
  function joinExploded(values, style) {
457
- const joiner = { simple: ",", label: ".", matrix: ";" }[style] || "&";
469
+ const joiner = PATH_STYLE_JOINERS[style] || "&";
458
470
  const final = values.join(joiner);
459
471
  return style === "label" || style === "matrix" ? `${joiner}${final}` : final;
460
472
  }
@@ -491,6 +503,7 @@ function serializeObjectParam(name, value, options) {
491
503
  if (!value || typeof value !== "object" || !Object.keys(value).length) {
492
504
  return "";
493
505
  }
506
+ checkFlatParam(value, options.style);
494
507
  if (options.style !== "deepObject" && options.explode === false) {
495
508
  return serializeUnexplodedObject(name, value, options);
496
509
  }
@@ -530,6 +543,7 @@ function serializeArrayParam(name, value, options) {
530
543
  if (!Array.isArray(value) || !value.length) {
531
544
  return "";
532
545
  }
546
+ checkFlatParam(value, options.style);
533
547
  if (options.explode === false) {
534
548
  return serializeUnexplodedArray(name, value, options);
535
549
  }
@@ -620,11 +634,9 @@ function defaultPathSerializer(pathname, pathParams) {
620
634
  }
621
635
  function serializePathParam(name, value, options) {
622
636
  if (Array.isArray(value)) {
623
- checkFlatMembers(value, PATH_NESTED_VALUE_ERROR);
624
637
  return serializeArrayParam(name, value, options);
625
638
  }
626
639
  if (typeof value === "object" && !isDate(value)) {
627
- checkFlatMembers(value, PATH_NESTED_VALUE_ERROR);
628
640
  return serializeObjectParam(name, value, options);
629
641
  }
630
642
  if (options.style === "matrix") {
@@ -724,7 +736,7 @@ function defaultBodySerializer(body, headers) {
724
736
  if (isRawBody(body)) {
725
737
  return body;
726
738
  }
727
- const contentType = headers && (isHeaders(headers) ? headers : mergeHeaders(headers)).get("Content-Type");
739
+ const contentType = headers && (isHeaders(headers) ? headers : mergeHeaders(headers)).get(CONTENT_TYPE);
728
740
  if (FORM_MEDIA_TYPE_RE.test(contentType)) {
729
741
  return formBody(body);
730
742
  }