Skip to content

Commit df03745

Browse files
authored
Merge pull request #2221 from hey-api/fix/axios-client-prefer-json
fix(parser): prefer JSON media type
2 parents 5d85d29 + e335e1e commit df03745

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2561
-57
lines changed

.changeset/rude-shirts-decide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hey-api/openapi-ts': patch
3+
---
4+
5+
fix(parser): prefer JSON media type

packages/openapi-ts-tests/test/3.0.x.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,18 @@ describe(`OpenAPI ${version}`, () => {
159159
}),
160160
description: 'handles binary content',
161161
},
162+
{
163+
config: createConfig({
164+
input: 'content-types.yaml',
165+
output: 'content-types',
166+
plugins: [
167+
'@hey-api/client-axios',
168+
'@hey-api/typescript',
169+
'@hey-api/sdk',
170+
],
171+
}),
172+
description: 'handles content types',
173+
},
162174
{
163175
config: createConfig({
164176
input: 'discriminator-all-of.yaml',

packages/openapi-ts-tests/test/3.1.x.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,18 @@ describe(`OpenAPI ${version}`, () => {
166166
}),
167167
description: 'handles binary content',
168168
},
169+
{
170+
config: createConfig({
171+
input: 'content-types.yaml',
172+
output: 'content-types',
173+
plugins: [
174+
'@hey-api/client-axios',
175+
'@hey-api/typescript',
176+
'@hey-api/sdk',
177+
],
178+
}),
179+
description: 'handles content types',
180+
},
169181
{
170182
config: createConfig({
171183
input: 'discriminator-all-of.yaml',
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// This file is auto-generated by @hey-api/openapi-ts
2+
3+
import type { ClientOptions } from './types.gen';
4+
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from './client';
5+
6+
/**
7+
* The `createClientConfig()` function will be called on client initialization
8+
* and the returned object will become the client's initial configuration.
9+
*
10+
* You may want to initialize your client this way instead of calling
11+
* `setConfig()`. This is useful for example if you're using Next.js
12+
* to ensure your client always has the correct values.
13+
*/
14+
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> = (override?: Config<DefaultClientOptions & T>) => Config<Required<DefaultClientOptions> & T>;
15+
16+
export const client = createClient(createConfig<ClientOptions>());
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import type { AxiosError, RawAxiosRequestHeaders } from 'axios';
2+
import axios from 'axios';
3+
4+
import type { Client, Config } from './types';
5+
import {
6+
buildUrl,
7+
createConfig,
8+
mergeConfigs,
9+
mergeHeaders,
10+
setAuthParams,
11+
} from './utils';
12+
13+
export const createClient = (config: Config = {}): Client => {
14+
let _config = mergeConfigs(createConfig(), config);
15+
16+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
17+
const { auth, ...configWithoutAuth } = _config;
18+
const instance = axios.create(configWithoutAuth);
19+
20+
const getConfig = (): Config => ({ ..._config });
21+
22+
const setConfig = (config: Config): Config => {
23+
_config = mergeConfigs(_config, config);
24+
instance.defaults = {
25+
...instance.defaults,
26+
..._config,
27+
// @ts-expect-error
28+
headers: mergeHeaders(instance.defaults.headers, _config.headers),
29+
};
30+
return getConfig();
31+
};
32+
33+
// @ts-expect-error
34+
const request: Client['request'] = async (options) => {
35+
const opts = {
36+
..._config,
37+
...options,
38+
axios: options.axios ?? _config.axios ?? instance,
39+
headers: mergeHeaders(_config.headers, options.headers),
40+
};
41+
42+
if (opts.security) {
43+
await setAuthParams({
44+
...opts,
45+
security: opts.security,
46+
});
47+
}
48+
49+
if (opts.body && opts.bodySerializer) {
50+
opts.body = opts.bodySerializer(opts.body);
51+
}
52+
53+
const url = buildUrl(opts);
54+
55+
try {
56+
// assign Axios here for consistency with fetch
57+
const _axios = opts.axios!;
58+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
59+
const { auth, ...optsWithoutAuth } = opts;
60+
const response = await _axios({
61+
...optsWithoutAuth,
62+
baseURL: opts.baseURL as string,
63+
data: opts.body,
64+
headers: opts.headers as RawAxiosRequestHeaders,
65+
// let `paramsSerializer()` handle query params if it exists
66+
params: opts.paramsSerializer ? opts.query : undefined,
67+
url,
68+
});
69+
70+
let { data } = response;
71+
72+
if (opts.responseType === 'json') {
73+
if (opts.responseValidator) {
74+
await opts.responseValidator(data);
75+
}
76+
77+
if (opts.responseTransformer) {
78+
data = await opts.responseTransformer(data);
79+
}
80+
}
81+
82+
return {
83+
...response,
84+
data: data ?? {},
85+
};
86+
} catch (error) {
87+
const e = error as AxiosError;
88+
if (opts.throwOnError) {
89+
throw e;
90+
}
91+
// @ts-expect-error
92+
e.error = e.response?.data ?? {};
93+
return e;
94+
}
95+
};
96+
97+
return {
98+
buildUrl,
99+
delete: (options) => request({ ...options, method: 'DELETE' }),
100+
get: (options) => request({ ...options, method: 'GET' }),
101+
getConfig,
102+
head: (options) => request({ ...options, method: 'HEAD' }),
103+
instance,
104+
options: (options) => request({ ...options, method: 'OPTIONS' }),
105+
patch: (options) => request({ ...options, method: 'PATCH' }),
106+
post: (options) => request({ ...options, method: 'POST' }),
107+
put: (options) => request({ ...options, method: 'PUT' }),
108+
request,
109+
setConfig,
110+
} as Client;
111+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export type { Auth } from '../core/auth';
2+
export type { QuerySerializerOptions } from '../core/bodySerializer';
3+
export {
4+
formDataBodySerializer,
5+
jsonBodySerializer,
6+
urlSearchParamsBodySerializer,
7+
} from '../core/bodySerializer';
8+
export { buildClientParams } from '../core/params';
9+
export { createClient } from './client';
10+
export type {
11+
Client,
12+
ClientOptions,
13+
Config,
14+
CreateClientConfig,
15+
Options,
16+
OptionsLegacyParser,
17+
RequestOptions,
18+
RequestResult,
19+
TDataShape,
20+
} from './types';
21+
export { createConfig } from './utils';
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import type {
2+
AxiosError,
3+
AxiosInstance,
4+
AxiosResponse,
5+
AxiosStatic,
6+
CreateAxiosDefaults,
7+
} from 'axios';
8+
9+
import type { Auth } from '../core/auth';
10+
import type {
11+
Client as CoreClient,
12+
Config as CoreConfig,
13+
} from '../core/types';
14+
15+
export interface Config<T extends ClientOptions = ClientOptions>
16+
extends Omit<CreateAxiosDefaults, 'auth' | 'baseURL' | 'headers' | 'method'>,
17+
CoreConfig {
18+
/**
19+
* Axios implementation. You can use this option to provide a custom
20+
* Axios instance.
21+
*
22+
* @default axios
23+
*/
24+
axios?: AxiosStatic;
25+
/**
26+
* Base URL for all requests made by this client.
27+
*/
28+
baseURL?: T['baseURL'];
29+
/**
30+
* An object containing any HTTP headers that you want to pre-populate your
31+
* `Headers` object with.
32+
*
33+
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
34+
*/
35+
headers?:
36+
| CreateAxiosDefaults['headers']
37+
| Record<
38+
string,
39+
| string
40+
| number
41+
| boolean
42+
| (string | number | boolean)[]
43+
| null
44+
| undefined
45+
| unknown
46+
>;
47+
/**
48+
* Throw an error instead of returning it in the response?
49+
*
50+
* @default false
51+
*/
52+
throwOnError?: T['throwOnError'];
53+
}
54+
55+
export interface RequestOptions<
56+
ThrowOnError extends boolean = boolean,
57+
Url extends string = string,
58+
> extends Config<{
59+
throwOnError: ThrowOnError;
60+
}> {
61+
/**
62+
* Any body that you want to add to your request.
63+
*
64+
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
65+
*/
66+
body?: unknown;
67+
path?: Record<string, unknown>;
68+
query?: Record<string, unknown>;
69+
/**
70+
* Security mechanism(s) to use for the request.
71+
*/
72+
security?: ReadonlyArray<Auth>;
73+
url: Url;
74+
}
75+
76+
export type RequestResult<
77+
TData = unknown,
78+
TError = unknown,
79+
ThrowOnError extends boolean = boolean,
80+
> = ThrowOnError extends true
81+
? Promise<
82+
AxiosResponse<
83+
TData extends Record<string, unknown> ? TData[keyof TData] : TData
84+
>
85+
>
86+
: Promise<
87+
| (AxiosResponse<
88+
TData extends Record<string, unknown> ? TData[keyof TData] : TData
89+
> & { error: undefined })
90+
| (AxiosError<
91+
TError extends Record<string, unknown> ? TError[keyof TError] : TError
92+
> & {
93+
data: undefined;
94+
error: TError extends Record<string, unknown>
95+
? TError[keyof TError]
96+
: TError;
97+
})
98+
>;
99+
100+
export interface ClientOptions {
101+
baseURL?: string;
102+
throwOnError?: boolean;
103+
}
104+
105+
type MethodFn = <
106+
TData = unknown,
107+
TError = unknown,
108+
ThrowOnError extends boolean = false,
109+
>(
110+
options: Omit<RequestOptions<ThrowOnError>, 'method'>,
111+
) => RequestResult<TData, TError, ThrowOnError>;
112+
113+
type RequestFn = <
114+
TData = unknown,
115+
TError = unknown,
116+
ThrowOnError extends boolean = false,
117+
>(
118+
options: Omit<RequestOptions<ThrowOnError>, 'method'> &
119+
Pick<Required<RequestOptions<ThrowOnError>>, 'method'>,
120+
) => RequestResult<TData, TError, ThrowOnError>;
121+
122+
type BuildUrlFn = <
123+
TData extends {
124+
body?: unknown;
125+
path?: Record<string, unknown>;
126+
query?: Record<string, unknown>;
127+
url: string;
128+
},
129+
>(
130+
options: Pick<TData, 'url'> & Omit<Options<TData>, 'axios'>,
131+
) => string;
132+
133+
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & {
134+
instance: AxiosInstance;
135+
};
136+
137+
/**
138+
* The `createClientConfig()` function will be called on client initialization
139+
* and the returned object will become the client's initial configuration.
140+
*
141+
* You may want to initialize your client this way instead of calling
142+
* `setConfig()`. This is useful for example if you're using Next.js
143+
* to ensure your client always has the correct values.
144+
*/
145+
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
146+
override?: Config<ClientOptions & T>,
147+
) => Config<Required<ClientOptions> & T>;
148+
149+
export interface TDataShape {
150+
body?: unknown;
151+
headers?: unknown;
152+
path?: unknown;
153+
query?: unknown;
154+
url: string;
155+
}
156+
157+
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
158+
159+
export type Options<
160+
TData extends TDataShape = TDataShape,
161+
ThrowOnError extends boolean = boolean,
162+
> = OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'path' | 'query' | 'url'> &
163+
Omit<TData, 'url'>;
164+
165+
export type OptionsLegacyParser<
166+
TData = unknown,
167+
ThrowOnError extends boolean = boolean,
168+
> = TData extends { body?: any }
169+
? TData extends { headers?: any }
170+
? OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'headers' | 'url'> & TData
171+
: OmitKeys<RequestOptions<ThrowOnError>, 'body' | 'url'> &
172+
TData &
173+
Pick<RequestOptions<ThrowOnError>, 'headers'>
174+
: TData extends { headers?: any }
175+
? OmitKeys<RequestOptions<ThrowOnError>, 'headers' | 'url'> &
176+
TData &
177+
Pick<RequestOptions<ThrowOnError>, 'body'>
178+
: OmitKeys<RequestOptions<ThrowOnError>, 'url'> & TData;

0 commit comments

Comments
 (0)