Skip to content

Commit 2996fc9

Browse files
committed
fix(typescript-axios-slim): add observe response mode
1 parent 50ca85b commit 2996fc9

6 files changed

Lines changed: 73 additions & 13 deletions

File tree

docs/generators/typescript-axios-slim.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl
4949
|withSeparateModelsAndApi|Put the model and api in separate folders and in separate classes. This requires in addition a value for 'apiPackage' and 'modelPackage'| |false|
5050
|withoutPrefixEnums|Don't prefix enum names with class names| |false|
5151

52+
Generated API methods default to resolving payload data. Pass `observe: 'response'` in the request config to receive `AxiosResponse<T>` when you need headers or status metadata.
53+
5254
## IMPORT MAPPING
5355

5456
| Type/Alias | Imports |

modules/openapi-generator/src/main/resources/typescript-axios-slim/README.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This generator creates TypeScript/JavaScript client that utilizes [axios](https://github.com/axios/axios). The generated Node module can be used in the following environments:
44

5-
> `typescript-axios-slim` intentionally differs from `typescript-axios`: it removes `AxiosParamCreator`, `Fp`, and `Factory` layers, always uses a single request-parameter object per operation, and emits direct class methods with request-parameter schema validation.
5+
> `typescript-axios-slim` intentionally differs from `typescript-axios`: it removes `AxiosParamCreator`, `Fp`, and `Factory` layers, always uses a single request-parameter object per operation, and emits direct class methods with request-parameter schema validation. By default methods resolve payload data directly; pass `observe: 'response'` to receive full `AxiosResponse` values when you need headers or status metadata.
66

77
Environment
88
* Node.js

modules/openapi-generator/src/main/resources/typescript-axios-slim/api.mustache

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
{{^withSeparateModelsAndApi}}
77
import type { Configuration } from './configuration{{importFileExtension}}';
8-
import type { AxiosInstance, RawAxiosRequestConfig } from 'axios';
8+
import type { AxiosInstance, RawAxiosRequestConfig, AxiosResponse } from 'axios';
99
import globalAxios from 'axios';
1010
{{#withNodeImports}}
1111
// URLSearchParams not necessarily used
@@ -19,6 +19,7 @@ import * as v from 'valibot';
1919
// Some imports not used depending on template conditions
2020
// @ts-ignore
2121
import { DUMMY_BASE_URL, validateRequestParameters, withParams, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, replaceWithSerializableTypeIfNeeded{{#withAWSV4Signature}}, setAWS4SignatureInterceptor{{/withAWSV4Signature}} } from './common{{importFileExtension}}';
22+
import type { ObserveOptions, ResponseObserveOptions } from './common{{importFileExtension}}';
2223
import type { RequestArgs } from './base{{importFileExtension}}';
2324
// @ts-ignore
2425
import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base{{importFileExtension}}';

modules/openapi-generator/src/main/resources/typescript-axios-slim/apiInner.mustache

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
import type { Configuration } from '{{apiRelativeToRoot}}configuration{{importFileExtension}}';
8-
import type { AxiosInstance, RawAxiosRequestConfig } from 'axios';
8+
import type { AxiosInstance, RawAxiosRequestConfig, AxiosResponse } from 'axios';
99
import globalAxios from 'axios';
1010
{{#withNodeImports}}
1111
// URLSearchParams not necessarily used
@@ -19,6 +19,7 @@ import * as v from 'valibot';
1919
// Some imports not used depending on template conditions
2020
// @ts-ignore
2121
import { DUMMY_BASE_URL, validateRequestParameters, withParams, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, replaceWithSerializableTypeIfNeeded{{#withAWSV4Signature}}, setAWS4SignatureInterceptor{{/withAWSV4Signature}} } from '{{apiRelativeToRoot}}common{{importFileExtension}}';
22+
import type { ObserveOptions, ResponseObserveOptions } from '{{apiRelativeToRoot}}common{{importFileExtension}}';
2223
// @ts-ignore
2324
import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from '{{apiRelativeToRoot}}base{{importFileExtension}}';
2425
{{#imports}}
@@ -59,7 +60,8 @@ export interface {{classname}}Interface {
5960
* @deprecated{{/isDeprecated}}
6061
* @throws {RequiredError}
6162
*/
62-
{{nickname}}({{#allParams.0}}requestParameters{{^hasRequiredParams}}?{{/hasRequiredParams}}: {{classname}}{{operationIdCamelCase}}Request, {{/allParams.0}}options?: RawAxiosRequestConfig): Promise<{{{returnType}}}{{^returnType}}void{{/returnType}}>;
63+
{{nickname}}({{#allParams.0}}requestParameters{{^hasRequiredParams}}?{{/hasRequiredParams}}: {{classname}}{{operationIdCamelCase}}Request, {{/allParams.0}}options: ResponseObserveOptions): Promise<AxiosResponse<{{{returnType}}}{{^returnType}}void{{/returnType}}>>;
64+
{{nickname}}({{#allParams.0}}requestParameters{{^hasRequiredParams}}?{{/hasRequiredParams}}: {{classname}}{{operationIdCamelCase}}Request, {{/allParams.0}}options?: ObserveOptions): Promise<{{{returnType}}}{{^returnType}}void{{/returnType}}>;
6365

6466
{{/operation}}
6567
}
@@ -105,14 +107,17 @@ export class {{classname}} extends BaseAPI {
105107
* @deprecated{{/isDeprecated}}
106108
* @throws {RequiredError}
107109
*/
108-
public async {{nickname}}({{#allParams.0}}requestParameters: {{classname}}{{operationIdCamelCase}}Request{{^hasRequiredParams}} = {}{{/hasRequiredParams}}, {{/allParams.0}}options: RawAxiosRequestConfig = {}): Promise<{{{returnType}}}{{^returnType}}void{{/returnType}}> {
110+
public async {{nickname}}({{#allParams.0}}requestParameters: {{classname}}{{operationIdCamelCase}}Request{{^hasRequiredParams}} = {}{{/hasRequiredParams}}, {{/allParams.0}}options: ResponseObserveOptions): Promise<AxiosResponse<{{{returnType}}}{{^returnType}}void{{/returnType}}>>;
111+
public async {{nickname}}<TObserve extends ObserveOptions = ObserveOptions>({{#allParams.0}}requestParameters: {{classname}}{{operationIdCamelCase}}Request{{^hasRequiredParams}} = {}{{/hasRequiredParams}}, {{/allParams.0}}options: TObserve = {} as TObserve): Promise<TObserve extends ResponseObserveOptions ? AxiosResponse<{{{returnType}}}{{^returnType}}void{{/returnType}}> : {{{returnType}}}{{^returnType}}void{{/returnType}}> {
109112
{{#allParams.0}}
110113
validateRequestParameters('{{nickname}}', {{nickname}}RequestSchema, requestParameters);
111114
{{/allParams.0}}
112115
const localVarPath = {{#pathParams}}withParams(`{{{path}}}`, { {{#pathParams}}"{{baseName}}": requestParameters.{{paramName}}, {{/pathParams}} }){{/pathParams}}{{^pathParams}}`{{{path}}}`{{/pathParams}};
113116
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
114117
const baseOptions = this.configuration ? this.configuration.baseOptions : undefined;
115-
const localVarRequestOptions = { method: '{{httpMethod}}', ...baseOptions, ...options } as RawAxiosRequestConfig;
118+
const localVarObserve = options.observe ?? 'body';
119+
const { observe, ...axiosOptions } = options;
120+
const localVarRequestOptions = { method: '{{httpMethod}}', ...baseOptions, ...axiosOptions } as RawAxiosRequestConfig;
116121
const localVarHeaderParameter = {} as any;
117122
const localVarQueryParameter = {} as any;{{#vendorExtensions}}{{#hasFormParams}}
118123
const localVarFormParams = new {{^multipartFormData}}URLSearchParams(){{/multipartFormData}}{{#multipartFormData}}((this.configuration && this.configuration.formDataCtor) || FormData)(){{/multipartFormData}};{{/hasFormParams}}{{/vendorExtensions}}
@@ -299,8 +304,12 @@ export class {{classname}} extends BaseAPI {
299304
const effectiveBasePath = localVarOperationServerBasePath || this.basePath || BASE_PATH;
300305
localVarRequestOptions.url = (this.axios.defaults.baseURL ? '' : this.configuration?.basePath ?? effectiveBasePath) + toPathString(localVarUrlObj);
301306

302-
const localVarResponse = await this.axios.request<{{{returnType}}}{{^returnType}}void{{/returnType}}>(localVarRequestOptions);
303-
return localVarResponse.data;
307+
const localVarResponse = await this.axios.request<{{{returnType}}}{{^returnType}}void{{/returnType}}, AxiosResponse<{{{returnType}}}{{^returnType}}void{{/returnType}}>>(localVarRequestOptions);
308+
if (localVarObserve === 'response') {
309+
return localVarResponse as TObserve extends ResponseObserveOptions ? AxiosResponse<{{{returnType}}}{{^returnType}}void{{/returnType}}> : {{{returnType}}}{{^returnType}}void{{/returnType}};
310+
}
311+
312+
return localVarResponse.data as TObserve extends ResponseObserveOptions ? AxiosResponse<{{{returnType}}}{{^returnType}}void{{/returnType}}> : {{{returnType}}}{{^returnType}}void{{/returnType}};
304313
}
305314
{{^-last}}
306315

modules/openapi-generator/src/main/resources/typescript-axios-slim/common.mustache

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import type { Configuration } from "./configuration{{importFileExtension}}";
66
import type { RequestArgs } from "./base{{importFileExtension}}";
7-
import type { AxiosInstance, AxiosResponse } from 'axios';
7+
import type { AxiosInstance, AxiosResponse, RawAxiosRequestConfig } from 'axios';
88
import * as v from 'valibot';
99
{{#withAWSV4Signature}}
1010
import { aws4Interceptor } from "aws4-axios";
@@ -16,6 +16,11 @@ import { URL, URLSearchParams } from 'url';
1616

1717
export const DUMMY_BASE_URL = 'https://example.com'
1818

19+
export type Observe = 'body' | 'response';
20+
export type BodyObserveOptions = RawAxiosRequestConfig & { observe?: 'body' };
21+
export type ResponseObserveOptions = RawAxiosRequestConfig & { observe: 'response' };
22+
export type ObserveOptions = BodyObserveOptions | ResponseObserveOptions;
23+
1924
/**
2025
*
2126
* @throws {RequiredError}

modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/axios/TypeScriptAxiosSlimParityTest.java

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,24 @@ public void shouldReturnPayloadDataFromObjectOrientedMethods() throws Exception
125125
IdentitySurface slimSurface = generateIdentity("typescript-axios-slim", EDGE_CASE_SPEC, NO_CUSTOMIZER);
126126
String apiSource = String.join(" ", slimSurface.apiFiles.values());
127127

128-
assertTrue(apiSource.contains("): Promise<"), "Slim API methods should return Promise payload types");
128+
assertTrue(apiSource.contains("<TObserve extends ObserveOptions = ObserveOptions>"), "Slim API methods should expose observe-aware generics");
129+
assertTrue(apiSource.contains("options: TObserve = {} as TObserve): Promise<TObserve extends ResponseObserveOptions ? AxiosResponse<"), "Slim API methods should return payload types by default through observe-aware generics");
129130
assertFalse(apiSource.contains("AxiosPromise<"), "Slim API interface should not expose AxiosPromise return wrappers");
130-
assertFalse(apiSource.contains("Promise<AxiosResponse<"), "Slim API class should not return Promise<AxiosResponse<T>>");
131-
assertTrue(apiSource.contains("return localVarResponse.data;"), "Slim API class should resolve axios response data directly");
131+
assertTrue(apiSource.contains("options: ResponseObserveOptions): Promise<AxiosResponse<"), "Slim API methods should preserve explicit response overloads when observe=response");
132+
assertTrue(apiSource.contains("const localVarObserve = options.observe ?? 'body';"), "Slim API class should default observe mode to body");
133+
assertTrue(apiSource.contains("return localVarResponse as TObserve extends ResponseObserveOptions ? AxiosResponse<"), "Slim API class should return raw axios responses when observe=response");
134+
assertTrue(apiSource.contains("return localVarResponse.data as TObserve extends ResponseObserveOptions ? AxiosResponse<"), "Slim API class should resolve axios response data directly");
135+
}
136+
137+
@Test(description = "slim: observe options are exported for typed response access")
138+
public void shouldExportObserveOptionsForTypedResponseAccess() throws Exception {
139+
IdentitySurface slimSurface = generateIdentity("typescript-axios-slim", EDGE_CASE_SPEC, NO_CUSTOMIZER);
140+
String commonSource = slimSurface.supportingFiles.getOrDefault("common.ts", "");
141+
String apiSource = String.join(" ", slimSurface.apiFiles.values());
142+
143+
assertTrue(commonSource.contains("export type Observe = 'body' | 'response';"), "Slim common runtime should export observe mode types");
144+
assertTrue(commonSource.contains("export type ObserveOptions = BodyObserveOptions | ResponseObserveOptions;"), "Slim common runtime should export observe option union types");
145+
assertTrue(apiSource.contains("const { observe, ...axiosOptions } = options;"), "Slim API class should strip observe before forwarding axios options");
132146
}
133147

134148
@Test(description = "slim: useSingleRequestParameter remains enabled even if configured false")
@@ -181,6 +195,12 @@ private IdentitySurface extractIdentity(Path outputDir) throws IOException {
181195
surface.modelFiles.put(relativePath, normalize(Files.readString(modelFile)));
182196
}
183197

198+
List<Path> supportingFiles = collectSupportingFiles(outputDir);
199+
for (Path supportingFile : supportingFiles) {
200+
String relativePath = normalizePath(outputDir.relativize(supportingFile));
201+
surface.supportingFiles.put(relativePath, normalize(Files.readString(supportingFile)));
202+
}
203+
184204
assertTrue(!surface.requestInterfaces.isEmpty() || !surface.apiInterfaceMethods.isEmpty(),
185205
"No comparable request/response surface was extracted from generated sources under " + outputDir);
186206

@@ -226,6 +246,18 @@ private List<Path> collectModelFiles(Path outputDir) throws IOException {
226246
return sorted;
227247
}
228248

249+
private List<Path> collectSupportingFiles(Path outputDir) throws IOException {
250+
LinkedHashSet<Path> files = new LinkedHashSet<>();
251+
Path common = outputDir.resolve("common.ts");
252+
if (Files.exists(common)) {
253+
files.add(common);
254+
}
255+
256+
List<Path> sorted = new ArrayList<>(files);
257+
Collections.sort(sorted);
258+
return sorted;
259+
}
260+
229261
private void extractRequestInterfaces(Map<String, String> target, String apiSource) {
230262
Matcher matcher = REQUEST_INTERFACE_PATTERN.matcher(apiSource);
231263
while (matcher.find()) {
@@ -241,14 +273,24 @@ private void extractApiInterfaceMethods(Map<String, Set<String>> target, String
241273
Matcher methodMatcher = API_INTERFACE_METHOD_PATTERN.matcher(interfaceBody);
242274
while (methodMatcher.find()) {
243275
String methodName = methodMatcher.group(1);
244-
String params = normalize(methodMatcher.group(2));
276+
String rawParams = methodMatcher.group(2);
277+
if (rawParams.contains("ResponseObserveOptions")) {
278+
continue;
279+
}
280+
String params = normalizeComparableParams(rawParams);
245281
String returnType = normalizeComparableReturnType(methodMatcher.group(3));
246282
target.computeIfAbsent(interfaceName, ignored -> new TreeSet<>())
247283
.add(methodName + "(" + params + "):" + returnType);
248284
}
249285
}
250286
}
251287

288+
private String normalizeComparableParams(String params) {
289+
return normalize(params)
290+
.replace("<TObserve extends ObserveOptions = ObserveOptions>", "")
291+
.replace("ObserveOptions", "RawAxiosRequestConfig");
292+
}
293+
252294
private String normalizeComparableReturnType(String returnType) {
253295
String current = normalize(returnType);
254296
while (true) {
@@ -327,6 +369,7 @@ private static final class IdentitySurface {
327369
private final Map<String, String> operationEnums = new TreeMap<>();
328370
private final Map<String, String> modelFiles = new TreeMap<>();
329371
private final Map<String, String> apiFiles = new TreeMap<>();
372+
private final Map<String, String> supportingFiles = new TreeMap<>();
330373

331374
private Set<String> allMethodNames() {
332375
Set<String> names = new TreeSet<>();

0 commit comments

Comments
 (0)