openapi-typesafe-fetch 1.0.0-rc.6 → 1.0.0-rc.8

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,46 @@
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.8
6
+
7
+ ### Patch Changes
8
+
9
+ - [#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
10
+ `if (error)` made the whole result `never`. `SuccessResponse<any>` and `ErrorResponse<any>` are
11
+ `any` too.
12
+
13
+ - [#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
14
+ `data` and `error` of a client made with ``createClient<paths, `${string}/json`>()`` keep a
15
+ JSON body of any JSON media type: an `application/problem+json` error, an
16
+ `application/vnd.api+json` success and `application/json; charset=utf-8` bodies were left out,
17
+ so the error could be `never` or miss a status. An error that is not JSON, such as the HTML page
18
+ of a proxy, is typed `string`, the text the client returns, instead of being left out.
19
+
20
+ - [#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
21
+ optional option on (`{ array: { explode: options.explode } }`) compiles with
22
+ `exactOptionalPropertyTypes`.
23
+
24
+ - [#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`,
25
+ `label`, `matrix`) name `pathSerializer()` in their error, also inside your own `pathSerializer`,
26
+ instead of `querySerializer()`.
27
+
28
+ ## 1.0.0-rc.7
29
+
30
+ ### Patch Changes
31
+
32
+ - [#96](https://github.com/openapi-typesafe/openapi-typesafe/pull/96) [`171fa71`](https://github.com/openapi-typesafe/openapi-typesafe/commit/171fa71311107e4e26a9ae7a62b431662fb13de5) Thanks [@fedorello](https://github.com/fedorello)! - `SuccessResponseJSON`, `ErrorResponseJSON` and the `data` and `error` of a client made with
33
+ ``createClient<paths, `${string}/json`>()`` leave out a response that has no JSON media type. It
34
+ added its whole `content` map, so a `text/html` 500 typed the error as `{ "text/html": string }`,
35
+ a value the client never returns.
36
+
37
+ - [#96](https://github.com/openapi-typesafe/openapi-typesafe/pull/96) [`171fa71`](https://github.com/openapi-typesafe/openapi-typesafe/commit/171fa71311107e4e26a9ae7a62b431662fb13de5) Thanks [@fedorello](https://github.com/fedorello)! - A nested array or object in a path parameter (`{ id: [[1]] }`) now fails with an error that names
38
+ `pathSerializer()`, the option that sends such values. It told you to write a `querySerializer()`,
39
+ which does not apply to the path.
40
+
41
+ - [#96](https://github.com/openapi-typesafe/openapi-typesafe/pull/96) [`171fa71`](https://github.com/openapi-typesafe/openapi-typesafe/commit/171fa71311107e4e26a9ae7a62b431662fb13de5) Thanks [@fedorello](https://github.com/fedorello)! - A request's `querySerializer` option set to `undefined` (`{ array: { explode: undefined } }`, as a
42
+ wrapper passes its own optional option on) now takes the client's value, as an option left out
43
+ does. It replaced the client's value, so the default style was sent.
44
+
5
45
  ## 1.0.0-rc.6
6
46
 
7
47
  ### Patch Changes
@@ -199,25 +199,61 @@ 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
+ /** `true` for `any`: a conditional type on `any` takes both branches, so `any` is checked first. */
203
+ type IsAny<T> = 0 extends 1 & T ? true : false;
202
204
  /**
203
205
  * Return all 2XX responses from a Response Object Map, or `default` if it declares no 2XX one. For
204
206
  * a union of maps (a call with a union of paths), the union of each map's responses: `keyof` of a
205
207
  * 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).
208
+ * share its statuses, and a `default`-only path typed the data `undefined` (upstream). An `any`
209
+ * map (`createClient<any>()`) gives `any`: no key of `keyof any` is a status, which gave `never`.
207
210
  */
208
- type SuccessResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, SuccessStatus<T>> : never;
209
- type GetResponseContent<T extends Record<string | number, any>, Media extends MediaType = MediaType, ResponseCode extends keyof T = keyof T> = ResponseCode extends keyof T ? {
210
- [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? FilterKeys<T[K]["content"], Media> extends never ? T[K]["content"] : FilterKeys<T[K]["content"], Media> : K extends keyof T ? T[K]["content"] : never;
211
- }[ResponseCode] : never;
211
+ type SuccessResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = IsAny<T> extends true ? T : T extends unknown ? ResponseBodies<T, Media, SuccessStatus<T>, never> : never;
212
212
  /**
213
213
  * Return all 1XX, 3XX, 4XX, 5XX and `default` responses from a Response Object Map; for a union
214
- * of maps, the union of each map's responses, as `SuccessResponse`.
214
+ * of maps, the union of each map's responses, as `SuccessResponse`. An `any` map gives `any`.
215
+ */
216
+ type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = IsAny<T> extends true ? T : T extends unknown ? ResponseBodies<T, Media, ErrorStatusKey<T>, string> : never;
217
+ /**
218
+ * The bodies of the responses `ResponseCode` as the client returns them (`ResponseBody`): each
219
+ * success one as `SuccessResponse` gives it, each other one as `ErrorResponse` does.
215
220
  */
216
- type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, ErrorStatusKey<T>> : never;
221
+ 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>;
222
+ /**
223
+ * The bodies of the responses `ResponseCode`. A response without `content` keeps its `content`
224
+ * type (`undefined` for `content?: never`). `OtherBody` is the class's body that is not JSON: it
225
+ * is passed in, not read from the status, so the result for a generic map stays a type that
226
+ * TypeScript relates to the one of another client (ADR-030).
227
+ */
228
+ type ResponseBodies<T extends Record<string | number, any>, Media, ResponseCode extends keyof T, OtherBody> = ResponseCode extends keyof T ? {
229
+ [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? ResponseBody<T[K]["content"], Media, OtherBody> : K extends keyof T ? T[K]["content"] : never;
230
+ }[ResponseCode] : never;
231
+ /**
232
+ * The body of one response with the content map `Content`, as the client returns it. `Media`
233
+ * picks among its media types, as an `Accept` header does, and each one picked gives its schema.
234
+ * A response with none of them can still arrive (a proxy's HTML 500), so it gives what the client
235
+ * makes of its bodies: a JSON body gives its schema, any other body `OtherBody`, a `string` for an
236
+ * error (the client returns the text) and nothing for a success (the default `parseAs: "json"`
237
+ * rejects it; another `parseAs` types the data by itself). `Media` is read only in
238
+ * `keyof Content & Media`, as in `FilterKeys`, so TypeScript still relates the results of two
239
+ * clients with different `Media` in generic code (ADR-030).
240
+ */
241
+ type ResponseBody<Content, Media, OtherBody> = ([keyof Content & Media] extends [never] ? JSONLike<Content> : Content[keyof Content & Media]) | ([keyof Content & Media] extends [never] ? false extends {
242
+ [Key in keyof Content]: IsJSONMediaType<Key>;
243
+ }[keyof Content] ? OtherBody : never : never);
244
+ /**
245
+ * The media types that the client parses as JSON, as a content map writes them: `json` or a
246
+ * `+json` suffix (RFC 6838), with or without parameters, as the client's `JSON_MEDIA_TYPE_RE`, and
247
+ * the wildcards for any type and any `application` type, which may hold JSON. Not `x-ndjson` or
248
+ * `json-seq`, whose bodies are lines of JSON.
249
+ */
250
+ type JSONMediaType = `${string}/${"" | `${string}+`}json` | `${string}/${"" | `${string}+`}json${";" | " " | "\t"}${string}` | "*/*" | "application/*";
251
+ /** Whether the client parses a body of the media type `Key` as JSON, in any letter case. */
252
+ type IsJSONMediaType<Key> = Key extends string ? Lowercase<Key> extends JSONMediaType ? true : false : false;
217
253
  /** Return first JSON-like 2XX response from a path + HTTP method */
218
- type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
254
+ type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
219
255
  /** Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method */
220
- type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
256
+ type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
221
257
  /** Return JSON-like request body from a path + HTTP method */
222
258
  type RequestBodyJSON<PathMethod> = JSONLike<FilterKeys<OperationRequestBody<PathMethod>, "content">>;
223
259
  /** Find first match of multiple keys */
@@ -234,10 +270,13 @@ type GetValueWithDefault<Obj, KeyPattern, Default> = Obj extends unknown ? Filte
234
270
  */
235
271
  type MediaType = `${string}/${string}`;
236
272
  /**
237
- * Return any media type containing "json" (works for "application/json",
238
- * "application/vnd.api+json", "application/vnd.oai.openapi+json")
273
+ * The values of the JSON media types of a content map, as the client parses them: `json` or a
274
+ * `+json` suffix, with or without parameters ("application/json", "application/vnd.api+json",
275
+ * "application/json; charset=utf-8").
239
276
  */
240
- type JSONLike<T> = FilterKeys<T, `${string}/json`>;
277
+ type JSONLike<T> = {
278
+ [Key in keyof T]: IsJSONMediaType<Key> extends true ? T[Key] : never;
279
+ }[keyof T];
241
280
  /**
242
281
  * Filter objects that have required keys
243
282
  * @deprecated Use `RequiredKeysOf` instead
@@ -199,25 +199,61 @@ 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
+ /** `true` for `any`: a conditional type on `any` takes both branches, so `any` is checked first. */
203
+ type IsAny<T> = 0 extends 1 & T ? true : false;
202
204
  /**
203
205
  * Return all 2XX responses from a Response Object Map, or `default` if it declares no 2XX one. For
204
206
  * a union of maps (a call with a union of paths), the union of each map's responses: `keyof` of a
205
207
  * 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).
208
+ * share its statuses, and a `default`-only path typed the data `undefined` (upstream). An `any`
209
+ * map (`createClient<any>()`) gives `any`: no key of `keyof any` is a status, which gave `never`.
207
210
  */
208
- type SuccessResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, SuccessStatus<T>> : never;
209
- type GetResponseContent<T extends Record<string | number, any>, Media extends MediaType = MediaType, ResponseCode extends keyof T = keyof T> = ResponseCode extends keyof T ? {
210
- [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? FilterKeys<T[K]["content"], Media> extends never ? T[K]["content"] : FilterKeys<T[K]["content"], Media> : K extends keyof T ? T[K]["content"] : never;
211
- }[ResponseCode] : never;
211
+ type SuccessResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = IsAny<T> extends true ? T : T extends unknown ? ResponseBodies<T, Media, SuccessStatus<T>, never> : never;
212
212
  /**
213
213
  * Return all 1XX, 3XX, 4XX, 5XX and `default` responses from a Response Object Map; for a union
214
- * of maps, the union of each map's responses, as `SuccessResponse`.
214
+ * of maps, the union of each map's responses, as `SuccessResponse`. An `any` map gives `any`.
215
+ */
216
+ type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = IsAny<T> extends true ? T : T extends unknown ? ResponseBodies<T, Media, ErrorStatusKey<T>, string> : never;
217
+ /**
218
+ * The bodies of the responses `ResponseCode` as the client returns them (`ResponseBody`): each
219
+ * success one as `SuccessResponse` gives it, each other one as `ErrorResponse` does.
215
220
  */
216
- type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, ErrorStatusKey<T>> : never;
221
+ 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>;
222
+ /**
223
+ * The bodies of the responses `ResponseCode`. A response without `content` keeps its `content`
224
+ * type (`undefined` for `content?: never`). `OtherBody` is the class's body that is not JSON: it
225
+ * is passed in, not read from the status, so the result for a generic map stays a type that
226
+ * TypeScript relates to the one of another client (ADR-030).
227
+ */
228
+ type ResponseBodies<T extends Record<string | number, any>, Media, ResponseCode extends keyof T, OtherBody> = ResponseCode extends keyof T ? {
229
+ [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? ResponseBody<T[K]["content"], Media, OtherBody> : K extends keyof T ? T[K]["content"] : never;
230
+ }[ResponseCode] : never;
231
+ /**
232
+ * The body of one response with the content map `Content`, as the client returns it. `Media`
233
+ * picks among its media types, as an `Accept` header does, and each one picked gives its schema.
234
+ * A response with none of them can still arrive (a proxy's HTML 500), so it gives what the client
235
+ * makes of its bodies: a JSON body gives its schema, any other body `OtherBody`, a `string` for an
236
+ * error (the client returns the text) and nothing for a success (the default `parseAs: "json"`
237
+ * rejects it; another `parseAs` types the data by itself). `Media` is read only in
238
+ * `keyof Content & Media`, as in `FilterKeys`, so TypeScript still relates the results of two
239
+ * clients with different `Media` in generic code (ADR-030).
240
+ */
241
+ type ResponseBody<Content, Media, OtherBody> = ([keyof Content & Media] extends [never] ? JSONLike<Content> : Content[keyof Content & Media]) | ([keyof Content & Media] extends [never] ? false extends {
242
+ [Key in keyof Content]: IsJSONMediaType<Key>;
243
+ }[keyof Content] ? OtherBody : never : never);
244
+ /**
245
+ * The media types that the client parses as JSON, as a content map writes them: `json` or a
246
+ * `+json` suffix (RFC 6838), with or without parameters, as the client's `JSON_MEDIA_TYPE_RE`, and
247
+ * the wildcards for any type and any `application` type, which may hold JSON. Not `x-ndjson` or
248
+ * `json-seq`, whose bodies are lines of JSON.
249
+ */
250
+ type JSONMediaType = `${string}/${"" | `${string}+`}json` | `${string}/${"" | `${string}+`}json${";" | " " | "\t"}${string}` | "*/*" | "application/*";
251
+ /** Whether the client parses a body of the media type `Key` as JSON, in any letter case. */
252
+ type IsJSONMediaType<Key> = Key extends string ? Lowercase<Key> extends JSONMediaType ? true : false : false;
217
253
  /** Return first JSON-like 2XX response from a path + HTTP method */
218
- type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
254
+ type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
219
255
  /** Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method */
220
- type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
256
+ type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
221
257
  /** Return JSON-like request body from a path + HTTP method */
222
258
  type RequestBodyJSON<PathMethod> = JSONLike<FilterKeys<OperationRequestBody<PathMethod>, "content">>;
223
259
  /** Find first match of multiple keys */
@@ -234,10 +270,13 @@ type GetValueWithDefault<Obj, KeyPattern, Default> = Obj extends unknown ? Filte
234
270
  */
235
271
  type MediaType = `${string}/${string}`;
236
272
  /**
237
- * Return any media type containing "json" (works for "application/json",
238
- * "application/vnd.api+json", "application/vnd.oai.openapi+json")
273
+ * The values of the JSON media types of a content map, as the client parses them: `json` or a
274
+ * `+json` suffix, with or without parameters ("application/json", "application/vnd.api+json",
275
+ * "application/json; charset=utf-8").
239
276
  */
240
- type JSONLike<T> = FilterKeys<T, `${string}/json`>;
277
+ type JSONLike<T> = {
278
+ [Key in keyof T]: IsJSONMediaType<Key> extends true ? T[Key] : never;
279
+ }[keyof T];
241
280
  /**
242
281
  * Filter objects that have required keys
243
282
  * @deprecated Use `RequiredKeysOf` instead
package/dist/helpers.d.ts CHANGED
@@ -199,25 +199,61 @@ 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
+ /** `true` for `any`: a conditional type on `any` takes both branches, so `any` is checked first. */
203
+ type IsAny<T> = 0 extends 1 & T ? true : false;
202
204
  /**
203
205
  * Return all 2XX responses from a Response Object Map, or `default` if it declares no 2XX one. For
204
206
  * a union of maps (a call with a union of paths), the union of each map's responses: `keyof` of a
205
207
  * 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).
208
+ * share its statuses, and a `default`-only path typed the data `undefined` (upstream). An `any`
209
+ * map (`createClient<any>()`) gives `any`: no key of `keyof any` is a status, which gave `never`.
207
210
  */
208
- type SuccessResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, SuccessStatus<T>> : never;
209
- type GetResponseContent<T extends Record<string | number, any>, Media extends MediaType = MediaType, ResponseCode extends keyof T = keyof T> = ResponseCode extends keyof T ? {
210
- [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? FilterKeys<T[K]["content"], Media> extends never ? T[K]["content"] : FilterKeys<T[K]["content"], Media> : K extends keyof T ? T[K]["content"] : never;
211
- }[ResponseCode] : never;
211
+ type SuccessResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = IsAny<T> extends true ? T : T extends unknown ? ResponseBodies<T, Media, SuccessStatus<T>, never> : never;
212
212
  /**
213
213
  * Return all 1XX, 3XX, 4XX, 5XX and `default` responses from a Response Object Map; for a union
214
- * of maps, the union of each map's responses, as `SuccessResponse`.
214
+ * of maps, the union of each map's responses, as `SuccessResponse`. An `any` map gives `any`.
215
+ */
216
+ type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = IsAny<T> extends true ? T : T extends unknown ? ResponseBodies<T, Media, ErrorStatusKey<T>, string> : never;
217
+ /**
218
+ * The bodies of the responses `ResponseCode` as the client returns them (`ResponseBody`): each
219
+ * success one as `SuccessResponse` gives it, each other one as `ErrorResponse` does.
215
220
  */
216
- type ErrorResponse<T extends Record<string | number, any>, Media extends MediaType = MediaType> = T extends unknown ? GetResponseContent<T, Media, ErrorStatusKey<T>> : never;
221
+ 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>;
222
+ /**
223
+ * The bodies of the responses `ResponseCode`. A response without `content` keeps its `content`
224
+ * type (`undefined` for `content?: never`). `OtherBody` is the class's body that is not JSON: it
225
+ * is passed in, not read from the status, so the result for a generic map stays a type that
226
+ * TypeScript relates to the one of another client (ADR-030).
227
+ */
228
+ type ResponseBodies<T extends Record<string | number, any>, Media, ResponseCode extends keyof T, OtherBody> = ResponseCode extends keyof T ? {
229
+ [K in ResponseCode]: T[K]["content"] extends Record<string, any> ? ResponseBody<T[K]["content"], Media, OtherBody> : K extends keyof T ? T[K]["content"] : never;
230
+ }[ResponseCode] : never;
231
+ /**
232
+ * The body of one response with the content map `Content`, as the client returns it. `Media`
233
+ * picks among its media types, as an `Accept` header does, and each one picked gives its schema.
234
+ * A response with none of them can still arrive (a proxy's HTML 500), so it gives what the client
235
+ * makes of its bodies: a JSON body gives its schema, any other body `OtherBody`, a `string` for an
236
+ * error (the client returns the text) and nothing for a success (the default `parseAs: "json"`
237
+ * rejects it; another `parseAs` types the data by itself). `Media` is read only in
238
+ * `keyof Content & Media`, as in `FilterKeys`, so TypeScript still relates the results of two
239
+ * clients with different `Media` in generic code (ADR-030).
240
+ */
241
+ type ResponseBody<Content, Media, OtherBody> = ([keyof Content & Media] extends [never] ? JSONLike<Content> : Content[keyof Content & Media]) | ([keyof Content & Media] extends [never] ? false extends {
242
+ [Key in keyof Content]: IsJSONMediaType<Key>;
243
+ }[keyof Content] ? OtherBody : never : never);
244
+ /**
245
+ * The media types that the client parses as JSON, as a content map writes them: `json` or a
246
+ * `+json` suffix (RFC 6838), with or without parameters, as the client's `JSON_MEDIA_TYPE_RE`, and
247
+ * the wildcards for any type and any `application` type, which may hold JSON. Not `x-ndjson` or
248
+ * `json-seq`, whose bodies are lines of JSON.
249
+ */
250
+ type JSONMediaType = `${string}/${"" | `${string}+`}json` | `${string}/${"" | `${string}+`}json${";" | " " | "\t"}${string}` | "*/*" | "application/*";
251
+ /** Whether the client parses a body of the media type `Key` as JSON, in any letter case. */
252
+ type IsJSONMediaType<Key> = Key extends string ? Lowercase<Key> extends JSONMediaType ? true : false : false;
217
253
  /** Return first JSON-like 2XX response from a path + HTTP method */
218
- type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
254
+ type SuccessResponseJSON<PathMethod extends Record<string | number, any>> = SuccessResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
219
255
  /** Return JSON-like 1XX, 3XX, 4XX, 5XX and `default` responses from a path + HTTP method */
220
- type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, `${string}/json`>;
256
+ type ErrorResponseJSON<PathMethod extends Record<string | number, any>> = ErrorResponse<ResponseObjectMap<PathMethod>, JSONMediaType>;
221
257
  /** Return JSON-like request body from a path + HTTP method */
222
258
  type RequestBodyJSON<PathMethod> = JSONLike<FilterKeys<OperationRequestBody<PathMethod>, "content">>;
223
259
  /** Find first match of multiple keys */
@@ -234,10 +270,13 @@ type GetValueWithDefault<Obj, KeyPattern, Default> = Obj extends unknown ? Filte
234
270
  */
235
271
  type MediaType = `${string}/${string}`;
236
272
  /**
237
- * Return any media type containing "json" (works for "application/json",
238
- * "application/vnd.api+json", "application/vnd.oai.openapi+json")
273
+ * The values of the JSON media types of a content map, as the client parses them: `json` or a
274
+ * `+json` suffix, with or without parameters ("application/json", "application/vnd.api+json",
275
+ * "application/json; charset=utf-8").
239
276
  */
240
- type JSONLike<T> = FilterKeys<T, `${string}/json`>;
277
+ type JSONLike<T> = {
278
+ [Key in keyof T]: IsJSONMediaType<Key> extends true ? T[Key] : never;
279
+ }[keyof T];
241
280
  /**
242
281
  * Filter objects that have required keys
243
282
  * @deprecated Use `RequiredKeysOf` instead
package/dist/index.cjs CHANGED
@@ -415,9 +415,7 @@ function headerParamValue(value) {
415
415
  if (value === null || typeof value !== "object" || isDate(value)) {
416
416
  return valueText(value);
417
417
  }
418
- if (Object.values(value).some((member) => isNestedValue(member))) {
419
- throw new Error(`${NESTED_VALUE_ERROR} in header parameters.`);
420
- }
418
+ checkFlatMembers(value, HEADER_NESTED_VALUE_ERROR);
421
419
  const serialize = Array.isArray(value) ? serializeArrayParam : serializeObjectParam;
422
420
  return serialize("", value, HEADER_PARAM_STYLE) || void 0;
423
421
  }
@@ -429,14 +427,29 @@ function serializeHeaderParams(header) {
429
427
  return Object.fromEntries(entries);
430
428
  }
431
429
  const NESTED_VALUE_ERROR = "Deeply-nested arrays/objects aren\u2019t supported";
430
+ const HEADER_NESTED_VALUE_ERROR = `${NESTED_VALUE_ERROR} in header parameters.`;
431
+ const PATH_HOOK_HINT = "Provide your own `pathSerializer()` to handle these.";
432
+ const PATH_NESTED_VALUE_ERROR = `${NESTED_VALUE_ERROR} in path parameters. ${PATH_HOOK_HINT}`;
433
+ const QUERY_HOOK_HINT = "Provide your own `querySerializer()` to handle these.";
434
+ const QUERY_NESTED_VALUE_ERROR = `${NESTED_VALUE_ERROR}. ${QUERY_HOOK_HINT}`;
432
435
  function isNestedValue(value) {
433
436
  return Boolean(value) && typeof value === "object" && !isDate(value);
434
437
  }
438
+ function checkFlatMembers(value, message) {
439
+ if (Object.values(value).some((member) => isNestedValue(member))) {
440
+ throw new Error(message);
441
+ }
442
+ }
443
+ const PATH_STYLE_JOINERS = { simple: ",", label: ".", matrix: ";" };
444
+ function checkFlatParam(value, style) {
445
+ checkFlatMembers(
446
+ value,
447
+ PATH_STYLE_JOINERS[style] ? PATH_NESTED_VALUE_ERROR : QUERY_NESTED_VALUE_ERROR
448
+ );
449
+ }
435
450
  function encodeValue(value, allowReserved) {
436
451
  if (isNestedValue(value)) {
437
- throw new Error(
438
- `${NESTED_VALUE_ERROR}. Provide your own \`querySerializer()\` to handle these.`
439
- );
452
+ throw new Error(QUERY_NESTED_VALUE_ERROR);
440
453
  }
441
454
  const text = valueText(value);
442
455
  return allowReserved === true ? text : encodeURIComponent(text);
@@ -448,7 +461,7 @@ function serializePrimitiveParam(name, value, options) {
448
461
  return `${name}=${encodeValue(value, options?.allowReserved)}`;
449
462
  }
450
463
  function joinExploded(values, style) {
451
- const joiner = { simple: ",", label: ".", matrix: ";" }[style] || "&";
464
+ const joiner = PATH_STYLE_JOINERS[style] || "&";
452
465
  const final = values.join(joiner);
453
466
  return style === "label" || style === "matrix" ? `${joiner}${final}` : final;
454
467
  }
@@ -485,6 +498,7 @@ function serializeObjectParam(name, value, options) {
485
498
  if (!value || typeof value !== "object" || !Object.keys(value).length) {
486
499
  return "";
487
500
  }
501
+ checkFlatParam(value, options.style);
488
502
  if (options.style !== "deepObject" && options.explode === false) {
489
503
  return serializeUnexplodedObject(name, value, options);
490
504
  }
@@ -524,6 +538,7 @@ function serializeArrayParam(name, value, options) {
524
538
  if (!Array.isArray(value) || !value.length) {
525
539
  return "";
526
540
  }
541
+ checkFlatParam(value, options.style);
527
542
  if (options.explode === false) {
528
543
  return serializeUnexplodedArray(name, value, options);
529
544
  }
@@ -573,11 +588,15 @@ function mergeQueryOptions(clientOptions, requestOptions) {
573
588
  const base = typeof clientOptions === "function" ? {} : clientOptions ?? {};
574
589
  return {
575
590
  ...base,
576
- ...requestOptions,
577
- array: { ...base.array, ...requestOptions.array },
578
- object: { ...base.object, ...requestOptions.object }
591
+ ...definedOptions(requestOptions),
592
+ array: { ...base.array, ...definedOptions(requestOptions.array) },
593
+ object: { ...base.object, ...definedOptions(requestOptions.object) }
579
594
  };
580
595
  }
596
+ function definedOptions(options) {
597
+ const entries = Object.entries(options ?? {}).filter(([, value]) => value !== void 0);
598
+ return Object.fromEntries(entries);
599
+ }
581
600
  function defaultPathSerializer(pathname, pathParams) {
582
601
  const matches = pathname.match(PATH_PARAM_RE);
583
602
  if (!matches) {
@@ -602,22 +621,24 @@ function defaultPathSerializer(pathname, pathParams) {
602
621
  if (!pathParams || pathParams[name] === void 0 || pathParams[name] === null) {
603
622
  continue;
604
623
  }
605
- const value = pathParams[name];
606
- let serialized;
607
- if (Array.isArray(value)) {
608
- serialized = serializeArrayParam(name, value, { style, explode });
609
- } else if (typeof value === "object" && !isDate(value)) {
610
- serialized = serializeObjectParam(name, value, { style, explode });
611
- } else if (style === "matrix") {
612
- serialized = `;${serializePrimitiveParam(name, value)}`;
613
- } else {
614
- serialized = style === "label" ? `.${encodeValue(value)}` : encodeValue(value);
615
- }
624
+ const serialized = serializePathParam(name, pathParams[name], { style, explode });
616
625
  nextURL = nextURL.replace(match, () => serialized);
617
626
  }
618
627
  checkSerializedPath(pathname, nextURL);
619
628
  return nextURL;
620
629
  }
630
+ function serializePathParam(name, value, options) {
631
+ if (Array.isArray(value)) {
632
+ return serializeArrayParam(name, value, options);
633
+ }
634
+ if (typeof value === "object" && !isDate(value)) {
635
+ return serializeObjectParam(name, value, options);
636
+ }
637
+ if (options.style === "matrix") {
638
+ return `;${serializePrimitiveParam(name, value)}`;
639
+ }
640
+ return options.style === "label" ? `.${encodeValue(value)}` : encodeValue(value);
641
+ }
621
642
  function checkSerializedPath(template, url) {
622
643
  const fixed = template.slice(0, template.search(PATH_PARAM_RE));
623
644
  const path = url.split(QUERY_OR_FRAGMENT_RE, 1)[0];