@let-aurn/swagger-client 0.1.0
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/README.md +70 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +33 -0
- package/dist/core/generate.d.ts +9 -0
- package/dist/core/generate.js +36 -0
- package/dist/core/load-config.d.ts +9 -0
- package/dist/core/load-config.js +42 -0
- package/dist/core/names.d.ts +30 -0
- package/dist/core/names.js +87 -0
- package/dist/core/read-openapi.d.ts +10 -0
- package/dist/core/read-openapi.js +169 -0
- package/dist/core/schema-to-typescript.d.ts +17 -0
- package/dist/core/schema-to-typescript.js +92 -0
- package/dist/core/types.d.ts +89 -0
- package/dist/core/types.js +1 -0
- package/dist/generators/angular-client.d.ts +9 -0
- package/dist/generators/angular-client.js +210 -0
- package/dist/generators/endpoints.d.ts +11 -0
- package/dist/generators/endpoints.js +142 -0
- package/dist/generators/models.d.ts +8 -0
- package/dist/generators/models.js +18 -0
- package/dist/generators/react-client.d.ts +9 -0
- package/dist/generators/react-client.js +187 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/package.json +52 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export type ClientKind = "angular" | "react";
|
|
2
|
+
export type EndpointNameStrategy = "operationId" | "tagAndOperationId";
|
|
3
|
+
export interface SwaggerClientConfig {
|
|
4
|
+
client: ClientKind;
|
|
5
|
+
"swaggers-directory": string;
|
|
6
|
+
"endpoint-name-strategy"?: EndpointNameStrategy;
|
|
7
|
+
output: {
|
|
8
|
+
"api-models.ts": string;
|
|
9
|
+
"api-endpoints.ts": string;
|
|
10
|
+
"client.ts": string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface ProjectModel {
|
|
14
|
+
schemas: SchemaModel[];
|
|
15
|
+
endpoints: EndpointModel[];
|
|
16
|
+
}
|
|
17
|
+
export interface SchemaModel {
|
|
18
|
+
name: string;
|
|
19
|
+
schema: OpenApiSchema;
|
|
20
|
+
}
|
|
21
|
+
export interface EndpointModel {
|
|
22
|
+
name: string;
|
|
23
|
+
method: HttpMethod;
|
|
24
|
+
path: string;
|
|
25
|
+
operationId: string;
|
|
26
|
+
sourceFile: string;
|
|
27
|
+
pathVariables: VariableModel[];
|
|
28
|
+
queryVariables: VariableModel[];
|
|
29
|
+
headerVariables: VariableModel[];
|
|
30
|
+
bodyType: string | null;
|
|
31
|
+
responseType: string;
|
|
32
|
+
}
|
|
33
|
+
export interface VariableModel {
|
|
34
|
+
name: string;
|
|
35
|
+
type: string;
|
|
36
|
+
required: boolean;
|
|
37
|
+
}
|
|
38
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
39
|
+
export interface OpenApiDocument {
|
|
40
|
+
openapi?: string;
|
|
41
|
+
swagger?: string;
|
|
42
|
+
info?: {
|
|
43
|
+
title?: string;
|
|
44
|
+
version?: string;
|
|
45
|
+
};
|
|
46
|
+
paths?: Record<string, Record<string, OpenApiOperation | unknown>>;
|
|
47
|
+
components?: {
|
|
48
|
+
schemas?: Record<string, OpenApiSchema>;
|
|
49
|
+
};
|
|
50
|
+
definitions?: Record<string, OpenApiSchema>;
|
|
51
|
+
}
|
|
52
|
+
export interface OpenApiOperation {
|
|
53
|
+
operationId?: string;
|
|
54
|
+
tags?: string[];
|
|
55
|
+
parameters?: OpenApiParameter[];
|
|
56
|
+
requestBody?: {
|
|
57
|
+
required?: boolean;
|
|
58
|
+
content?: Record<string, {
|
|
59
|
+
schema?: OpenApiSchema;
|
|
60
|
+
}>;
|
|
61
|
+
};
|
|
62
|
+
responses?: Record<string, OpenApiResponse>;
|
|
63
|
+
}
|
|
64
|
+
export interface OpenApiParameter {
|
|
65
|
+
name: string;
|
|
66
|
+
in: "path" | "query" | "header" | "cookie";
|
|
67
|
+
required?: boolean;
|
|
68
|
+
schema?: OpenApiSchema;
|
|
69
|
+
}
|
|
70
|
+
export interface OpenApiResponse {
|
|
71
|
+
description?: string;
|
|
72
|
+
content?: Record<string, {
|
|
73
|
+
schema?: OpenApiSchema;
|
|
74
|
+
}>;
|
|
75
|
+
}
|
|
76
|
+
export interface OpenApiSchema {
|
|
77
|
+
$ref?: string;
|
|
78
|
+
type?: string;
|
|
79
|
+
format?: string;
|
|
80
|
+
enum?: unknown[];
|
|
81
|
+
nullable?: boolean;
|
|
82
|
+
required?: string[];
|
|
83
|
+
properties?: Record<string, OpenApiSchema>;
|
|
84
|
+
items?: OpenApiSchema;
|
|
85
|
+
additionalProperties?: boolean | OpenApiSchema;
|
|
86
|
+
allOf?: OpenApiSchema[];
|
|
87
|
+
oneOf?: OpenApiSchema[];
|
|
88
|
+
anyOf?: OpenApiSchema[];
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emits the generated Angular swagger client implementation.
|
|
3
|
+
*
|
|
4
|
+
* @returns The TypeScript source for the Angular client file.
|
|
5
|
+
*/
|
|
6
|
+
export interface EmitAngularClientOptions {
|
|
7
|
+
endpointsImportPath?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function emitAngularClient(options?: EmitAngularClientOptions): string;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
export function emitAngularClient(options = {}) {
|
|
2
|
+
const endpointsImportPath = options.endpointsImportPath ?? "./api-endpoints";
|
|
3
|
+
return String.raw `
|
|
4
|
+
/* eslint-disable */
|
|
5
|
+
// This file was generated by @let-aurn/swagger-client.
|
|
6
|
+
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
|
7
|
+
import { Inject, Injectable, InjectionToken, Provider, Type } from '@angular/core';
|
|
8
|
+
import { Observable } from 'rxjs';
|
|
9
|
+
import type {
|
|
10
|
+
AnyApiEndpoint,
|
|
11
|
+
BodyVariablesOf,
|
|
12
|
+
HeaderVariablesOf,
|
|
13
|
+
PathVariablesOf,
|
|
14
|
+
QueryVariablesOf,
|
|
15
|
+
ResponseOf
|
|
16
|
+
} from '${endpointsImportPath}';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Configures the generated Angular swagger client.
|
|
20
|
+
*/
|
|
21
|
+
export interface SwaggerClientConfig {
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
httpClient?: Type<SwaggerHttpClient>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Describes an HTTP request produced by the generated swagger client.
|
|
28
|
+
*/
|
|
29
|
+
export interface SwaggerHttpRequest {
|
|
30
|
+
method: string;
|
|
31
|
+
url: string;
|
|
32
|
+
headers?: Record<string, string>;
|
|
33
|
+
query?: Record<string, unknown>;
|
|
34
|
+
body?: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Defines the HTTP adapter contract used by the generated Angular swagger client.
|
|
39
|
+
*/
|
|
40
|
+
export interface SwaggerHttpClient {
|
|
41
|
+
/**
|
|
42
|
+
* Executes an HTTP request and returns the typed response stream.
|
|
43
|
+
*
|
|
44
|
+
* @param request The normalized swagger HTTP request.
|
|
45
|
+
* @returns An observable of the typed response body.
|
|
46
|
+
*/
|
|
47
|
+
request<T>(request: SwaggerHttpRequest): Observable<T>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolves the request variable object accepted by a generated endpoint.
|
|
52
|
+
*/
|
|
53
|
+
export type SwaggerRequestVariables<TEndpoint extends AnyApiEndpoint> =
|
|
54
|
+
OptionalProperty<'pathVariables', PathVariablesOf<TEndpoint>> &
|
|
55
|
+
OptionalProperty<'queryVariables', QueryVariablesOf<TEndpoint>> &
|
|
56
|
+
OptionalProperty<'headerVariables', HeaderVariablesOf<TEndpoint>> &
|
|
57
|
+
OptionalProperty<'bodyVariables', BodyVariablesOf<TEndpoint>>;
|
|
58
|
+
|
|
59
|
+
type OptionalProperty<TKey extends string, TValue> = IsEmptyVariables<TValue> extends true
|
|
60
|
+
? { [K in TKey]?: never }
|
|
61
|
+
: undefined extends TValue
|
|
62
|
+
? { [K in TKey]?: TValue }
|
|
63
|
+
: { [K in TKey]: TValue };
|
|
64
|
+
|
|
65
|
+
type IsEmptyVariables<TValue> = keyof TValue extends never
|
|
66
|
+
? true
|
|
67
|
+
: TValue extends Record<string, never>
|
|
68
|
+
? true
|
|
69
|
+
: false;
|
|
70
|
+
|
|
71
|
+
export const SWAGGER_CLIENT_CONFIG = new InjectionToken<SwaggerClientConfig>('SWAGGER_CLIENT_CONFIG');
|
|
72
|
+
export const SWAGGER_HTTP_CLIENT = new InjectionToken<SwaggerHttpClient>('SWAGGER_HTTP_CLIENT');
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Adapts Angular HttpClient to the generated swagger HTTP adapter contract.
|
|
76
|
+
*/
|
|
77
|
+
@Injectable()
|
|
78
|
+
export class AngularSwaggerHttpClient implements SwaggerHttpClient {
|
|
79
|
+
/**
|
|
80
|
+
* Creates the default Angular HTTP adapter.
|
|
81
|
+
*
|
|
82
|
+
* @param http The Angular HttpClient used to execute requests.
|
|
83
|
+
*/
|
|
84
|
+
constructor(private readonly http: HttpClient) {}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Executes a normalized swagger request with Angular HttpClient.
|
|
88
|
+
*
|
|
89
|
+
* @param request The normalized swagger HTTP request.
|
|
90
|
+
* @returns An observable of the typed response body.
|
|
91
|
+
*/
|
|
92
|
+
request<T>(request: SwaggerHttpRequest): Observable<T> {
|
|
93
|
+
return this.http.request<T>(request.method, request.url, {
|
|
94
|
+
body: request.body,
|
|
95
|
+
headers: new HttpHeaders(request.headers ?? {}),
|
|
96
|
+
params: toHttpParams(request.query)
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Sends typed requests to generated API endpoint definitions.
|
|
103
|
+
*/
|
|
104
|
+
@Injectable()
|
|
105
|
+
export class SwaggerClientService {
|
|
106
|
+
/**
|
|
107
|
+
* Creates the generated swagger client service.
|
|
108
|
+
*
|
|
109
|
+
* @param config The swagger client runtime configuration.
|
|
110
|
+
* @param http The HTTP adapter used to execute requests.
|
|
111
|
+
*/
|
|
112
|
+
constructor(
|
|
113
|
+
@Inject(SWAGGER_CLIENT_CONFIG) private readonly config: SwaggerClientConfig,
|
|
114
|
+
@Inject(SWAGGER_HTTP_CLIENT) private readonly http: SwaggerHttpClient
|
|
115
|
+
) {}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Sends a typed request for a generated API endpoint.
|
|
119
|
+
*
|
|
120
|
+
* @param endpoint The generated endpoint definition to call.
|
|
121
|
+
* @param variables The path, query, header, and body variables for the endpoint.
|
|
122
|
+
* @returns An observable of the endpoint response type.
|
|
123
|
+
*/
|
|
124
|
+
request<TEndpoint extends AnyApiEndpoint>(
|
|
125
|
+
endpoint: TEndpoint,
|
|
126
|
+
variables?: SwaggerRequestVariables<TEndpoint>
|
|
127
|
+
): Observable<ResponseOf<TEndpoint>> {
|
|
128
|
+
return this.http.request<ResponseOf<TEndpoint>>(buildRequest(this.config.baseUrl, endpoint, variables));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Provides the generated Angular swagger client and its HTTP adapter.
|
|
134
|
+
*
|
|
135
|
+
* @param config The swagger client runtime configuration.
|
|
136
|
+
* @returns Angular providers for the generated client.
|
|
137
|
+
*/
|
|
138
|
+
export function provideSwaggerClient(config: SwaggerClientConfig): Provider[] {
|
|
139
|
+
return [
|
|
140
|
+
{ provide: SWAGGER_CLIENT_CONFIG, useValue: config },
|
|
141
|
+
{ provide: SWAGGER_HTTP_CLIENT, useClass: config.httpClient ?? AngularSwaggerHttpClient },
|
|
142
|
+
SwaggerClientService
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function buildRequest<TEndpoint extends AnyApiEndpoint>(
|
|
147
|
+
baseUrl: string,
|
|
148
|
+
endpoint: TEndpoint,
|
|
149
|
+
variables?: SwaggerRequestVariables<TEndpoint>
|
|
150
|
+
): SwaggerHttpRequest {
|
|
151
|
+
const pathVariables = variables?.pathVariables as Record<string, unknown> | undefined;
|
|
152
|
+
const queryVariables = variables?.queryVariables as Record<string, unknown> | undefined;
|
|
153
|
+
const headerVariables = variables?.headerVariables as Record<string, unknown> | undefined;
|
|
154
|
+
const bodyVariables = variables?.bodyVariables;
|
|
155
|
+
const path = endpoint.path.replace(/\{([^}]+)\}/g, (_, key: string) => {
|
|
156
|
+
const value = pathVariables?.[key];
|
|
157
|
+
|
|
158
|
+
if (value === undefined || value === null) {
|
|
159
|
+
throw new Error('Missing path variable "' + key + '" for endpoint "' + endpoint.name + '".');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return encodeURIComponent(String(value));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
method: endpoint.method,
|
|
167
|
+
url: joinUrl(baseUrl, path),
|
|
168
|
+
headers: stringifyRecord(headerVariables),
|
|
169
|
+
query: queryVariables,
|
|
170
|
+
body: bodyVariables
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function toHttpParams(query: Record<string, unknown> | undefined): HttpParams {
|
|
175
|
+
let params = new HttpParams();
|
|
176
|
+
|
|
177
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
178
|
+
if (value === undefined || value === null) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (Array.isArray(value)) {
|
|
183
|
+
for (const item of value) {
|
|
184
|
+
params = params.append(key, String(item));
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
params = params.set(key, String(value));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return params;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function stringifyRecord(record: Record<string, unknown> | undefined): Record<string, string> | undefined {
|
|
195
|
+
if (!record) {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return Object.fromEntries(
|
|
200
|
+
Object.entries(record)
|
|
201
|
+
.filter(([, value]) => value !== undefined && value !== null)
|
|
202
|
+
.map(([key, value]) => [key, String(value)])
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function joinUrl(baseUrl: string, path: string): string {
|
|
207
|
+
return baseUrl.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '');
|
|
208
|
+
}
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ProjectModel } from "../core/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Emits the generated typed API endpoint definition file.
|
|
4
|
+
*
|
|
5
|
+
* @param project The normalized OpenAPI project model.
|
|
6
|
+
* @returns The TypeScript source for api-endpoints.ts.
|
|
7
|
+
*/
|
|
8
|
+
export interface EmitEndpointsOptions {
|
|
9
|
+
modelsImportPath?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function emitEndpoints(project: ProjectModel, options?: EmitEndpointsOptions): string;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { quote } from "../core/names.js";
|
|
2
|
+
export function emitEndpoints(project, options = {}) {
|
|
3
|
+
const modelsImportPath = options.modelsImportPath ?? "./api-models";
|
|
4
|
+
const modelNames = new Set(project.schemas.map((schema) => schema.name));
|
|
5
|
+
const lines = [
|
|
6
|
+
"/* eslint-disable */",
|
|
7
|
+
"// This file was generated by @let-aurn/swagger-client.",
|
|
8
|
+
`import type * as ApiModels from '${modelsImportPath}';`,
|
|
9
|
+
"",
|
|
10
|
+
"/**",
|
|
11
|
+
" * Represents the HTTP methods supported by generated endpoints.",
|
|
12
|
+
" */",
|
|
13
|
+
"export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';",
|
|
14
|
+
"",
|
|
15
|
+
"/**",
|
|
16
|
+
" * Describes a generated API endpoint and carries its request and response types.",
|
|
17
|
+
" */",
|
|
18
|
+
"export interface ApiEndpointDefinition<TPathVariables, TQueryVariables, THeaderVariables, TBody, TResponse> {",
|
|
19
|
+
" readonly name: string;",
|
|
20
|
+
" readonly method: HttpMethod;",
|
|
21
|
+
" readonly path: string;",
|
|
22
|
+
" readonly operationId: string;",
|
|
23
|
+
" readonly sourceFile: string;",
|
|
24
|
+
" readonly pathVariables: readonly ApiVariableDefinition[];",
|
|
25
|
+
" readonly queryVariables: readonly ApiVariableDefinition[];",
|
|
26
|
+
" readonly headerVariables: readonly ApiVariableDefinition[];",
|
|
27
|
+
" readonly __types?: {",
|
|
28
|
+
" readonly pathVariables: TPathVariables;",
|
|
29
|
+
" readonly queryVariables: TQueryVariables;",
|
|
30
|
+
" readonly headerVariables: THeaderVariables;",
|
|
31
|
+
" readonly bodyVariables: TBody;",
|
|
32
|
+
" readonly response: TResponse;",
|
|
33
|
+
" };",
|
|
34
|
+
"}",
|
|
35
|
+
"",
|
|
36
|
+
"/**",
|
|
37
|
+
" * Describes a request variable using its exact OpenAPI parameter name.",
|
|
38
|
+
" */",
|
|
39
|
+
"export interface ApiVariableDefinition {",
|
|
40
|
+
" readonly name: string;",
|
|
41
|
+
" readonly required: boolean;",
|
|
42
|
+
"}",
|
|
43
|
+
"",
|
|
44
|
+
"/**",
|
|
45
|
+
" * Creates a typed endpoint definition while preserving phantom request and response types.",
|
|
46
|
+
" *",
|
|
47
|
+
" * @param endpoint The serializable endpoint metadata.",
|
|
48
|
+
" * @returns The endpoint metadata with request and response type information.",
|
|
49
|
+
" */",
|
|
50
|
+
"function defineEndpoint<TPathVariables, TQueryVariables, THeaderVariables, TBody, TResponse>(",
|
|
51
|
+
" endpoint: Omit<ApiEndpointDefinition<TPathVariables, TQueryVariables, THeaderVariables, TBody, TResponse>, '__types'>",
|
|
52
|
+
"): ApiEndpointDefinition<TPathVariables, TQueryVariables, THeaderVariables, TBody, TResponse> {",
|
|
53
|
+
" return endpoint as ApiEndpointDefinition<TPathVariables, TQueryVariables, THeaderVariables, TBody, TResponse>;",
|
|
54
|
+
"}",
|
|
55
|
+
"",
|
|
56
|
+
"/**",
|
|
57
|
+
" * Represents any generated API endpoint.",
|
|
58
|
+
" */",
|
|
59
|
+
"export type AnyApiEndpoint = ApiEndpointDefinition<any, any, any, any, any>;",
|
|
60
|
+
"/**",
|
|
61
|
+
" * Extracts the path variable type from a generated endpoint.",
|
|
62
|
+
" */",
|
|
63
|
+
"export type PathVariablesOf<TEndpoint extends AnyApiEndpoint> = NonNullable<TEndpoint['__types']>['pathVariables'];",
|
|
64
|
+
"/**",
|
|
65
|
+
" * Extracts the query variable type from a generated endpoint.",
|
|
66
|
+
" */",
|
|
67
|
+
"export type QueryVariablesOf<TEndpoint extends AnyApiEndpoint> = NonNullable<TEndpoint['__types']>['queryVariables'];",
|
|
68
|
+
"/**",
|
|
69
|
+
" * Extracts the header variable type from a generated endpoint.",
|
|
70
|
+
" */",
|
|
71
|
+
"export type HeaderVariablesOf<TEndpoint extends AnyApiEndpoint> = NonNullable<TEndpoint['__types']>['headerVariables'];",
|
|
72
|
+
"/**",
|
|
73
|
+
" * Extracts the body variable type from a generated endpoint.",
|
|
74
|
+
" */",
|
|
75
|
+
"export type BodyVariablesOf<TEndpoint extends AnyApiEndpoint> = NonNullable<TEndpoint['__types']>['bodyVariables'];",
|
|
76
|
+
"/**",
|
|
77
|
+
" * Extracts the response type from a generated endpoint.",
|
|
78
|
+
" */",
|
|
79
|
+
"export type ResponseOf<TEndpoint extends AnyApiEndpoint> = NonNullable<TEndpoint['__types']>['response'];",
|
|
80
|
+
"",
|
|
81
|
+
"type EmptyObject = Record<string, never>;",
|
|
82
|
+
"",
|
|
83
|
+
"/**",
|
|
84
|
+
" * Contains all generated API endpoint definitions.",
|
|
85
|
+
" */",
|
|
86
|
+
"export namespace ApiEndpoints {"
|
|
87
|
+
];
|
|
88
|
+
for (const endpoint of project.endpoints) {
|
|
89
|
+
lines.push(emitEndpoint(endpoint, modelNames));
|
|
90
|
+
}
|
|
91
|
+
lines.push("}", "");
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
}
|
|
94
|
+
function emitEndpoint(endpoint, modelNames) {
|
|
95
|
+
const pathType = variablesObjectType(endpoint.pathVariables, modelNames);
|
|
96
|
+
const queryType = variablesObjectType(endpoint.queryVariables, modelNames);
|
|
97
|
+
const headerType = variablesObjectType(endpoint.headerVariables, modelNames);
|
|
98
|
+
const bodyType = endpoint.bodyType ? qualifyModelTypes(endpoint.bodyType, modelNames) : "void";
|
|
99
|
+
const responseType = qualifyModelTypes(endpoint.responseType, modelNames);
|
|
100
|
+
return [
|
|
101
|
+
" /**",
|
|
102
|
+
` * Defines the ${endpoint.operationId} API endpoint.`,
|
|
103
|
+
" */",
|
|
104
|
+
` export const ${endpoint.name} = defineEndpoint<${pathType}, ${queryType}, ${headerType}, ${bodyType}, ${responseType}>({`,
|
|
105
|
+
` name: ${quote(endpoint.name)},`,
|
|
106
|
+
` method: ${quote(endpoint.method)},`,
|
|
107
|
+
` path: ${quote(endpoint.path)},`,
|
|
108
|
+
` operationId: ${quote(endpoint.operationId)},`,
|
|
109
|
+
` sourceFile: ${quote(endpoint.sourceFile)},`,
|
|
110
|
+
` pathVariables: ${variablesMetadata(endpoint.pathVariables)},`,
|
|
111
|
+
` queryVariables: ${variablesMetadata(endpoint.queryVariables)},`,
|
|
112
|
+
` headerVariables: ${variablesMetadata(endpoint.headerVariables)}`,
|
|
113
|
+
" });"
|
|
114
|
+
].join("\n");
|
|
115
|
+
}
|
|
116
|
+
function variablesMetadata(variables) {
|
|
117
|
+
if (variables.length === 0) {
|
|
118
|
+
return "[]";
|
|
119
|
+
}
|
|
120
|
+
const entries = variables.map((variable) => {
|
|
121
|
+
return `{ name: ${quote(variable.name)}, required: ${String(variable.required)} }`;
|
|
122
|
+
});
|
|
123
|
+
return `[${entries.join(", ")}]`;
|
|
124
|
+
}
|
|
125
|
+
function variablesObjectType(variables, modelNames = new Set()) {
|
|
126
|
+
if (variables.length === 0) {
|
|
127
|
+
return "EmptyObject";
|
|
128
|
+
}
|
|
129
|
+
const props = variables.map((variable) => {
|
|
130
|
+
const optional = variable.required ? "" : "?";
|
|
131
|
+
return `${propertyKey(variable.name)}${optional}: ${qualifyModelTypes(variable.type, modelNames)}`;
|
|
132
|
+
});
|
|
133
|
+
return `{ ${props.join("; ")} }`;
|
|
134
|
+
}
|
|
135
|
+
function propertyKey(name) {
|
|
136
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : quote(name);
|
|
137
|
+
}
|
|
138
|
+
function qualifyModelTypes(type, modelNames) {
|
|
139
|
+
return type.replace(/\b[A-Z][A-Za-z0-9_]*\b/g, (name) => {
|
|
140
|
+
return modelNames.has(name) ? `ApiModels.${name}` : name;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ProjectModel } from "../core/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Emits the generated TypeScript API model file.
|
|
4
|
+
*
|
|
5
|
+
* @param project The normalized OpenAPI project model.
|
|
6
|
+
* @returns The TypeScript source for api-models.ts.
|
|
7
|
+
*/
|
|
8
|
+
export declare function emitModels(project: ProjectModel): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { emitSchemaInterface } from "../core/schema-to-typescript.js";
|
|
2
|
+
/**
|
|
3
|
+
* Emits the generated TypeScript API model file.
|
|
4
|
+
*
|
|
5
|
+
* @param project The normalized OpenAPI project model.
|
|
6
|
+
* @returns The TypeScript source for api-models.ts.
|
|
7
|
+
*/
|
|
8
|
+
export function emitModels(project) {
|
|
9
|
+
const lines = [
|
|
10
|
+
"/* eslint-disable */",
|
|
11
|
+
"// This file was generated by @let-aurn/swagger-client.",
|
|
12
|
+
""
|
|
13
|
+
];
|
|
14
|
+
for (const schema of project.schemas) {
|
|
15
|
+
lines.push(emitSchemaInterface(schema.name, schema.schema), "");
|
|
16
|
+
}
|
|
17
|
+
return lines.join("\n");
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emits the generated React swagger client implementation.
|
|
3
|
+
*
|
|
4
|
+
* @returns The TypeScript source for the React client file.
|
|
5
|
+
*/
|
|
6
|
+
export interface EmitReactClientOptions {
|
|
7
|
+
endpointsImportPath?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function emitReactClient(options?: EmitReactClientOptions): string;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
export function emitReactClient(options = {}) {
|
|
2
|
+
const endpointsImportPath = options.endpointsImportPath ?? "./api-endpoints";
|
|
3
|
+
return String.raw `
|
|
4
|
+
/* eslint-disable */
|
|
5
|
+
// This file was generated by @let-aurn/swagger-client.
|
|
6
|
+
import { useMemo } from 'react';
|
|
7
|
+
import type {
|
|
8
|
+
AnyApiEndpoint,
|
|
9
|
+
BodyVariablesOf,
|
|
10
|
+
HeaderVariablesOf,
|
|
11
|
+
PathVariablesOf,
|
|
12
|
+
QueryVariablesOf,
|
|
13
|
+
ResponseOf
|
|
14
|
+
} from '${endpointsImportPath}';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Configures the generated React swagger client.
|
|
18
|
+
*/
|
|
19
|
+
export interface SwaggerClientConfig {
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
fetch?: typeof globalThis.fetch;
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolves the request variable object accepted by a generated endpoint.
|
|
27
|
+
*/
|
|
28
|
+
export type SwaggerRequestVariables<TEndpoint extends AnyApiEndpoint> =
|
|
29
|
+
OptionalProperty<'pathVariables', PathVariablesOf<TEndpoint>> &
|
|
30
|
+
OptionalProperty<'queryVariables', QueryVariablesOf<TEndpoint>> &
|
|
31
|
+
OptionalProperty<'headerVariables', HeaderVariablesOf<TEndpoint>> &
|
|
32
|
+
OptionalProperty<'bodyVariables', BodyVariablesOf<TEndpoint>>;
|
|
33
|
+
|
|
34
|
+
type OptionalProperty<TKey extends string, TValue> = IsEmptyVariables<TValue> extends true
|
|
35
|
+
? { [K in TKey]?: never }
|
|
36
|
+
: undefined extends TValue
|
|
37
|
+
? { [K in TKey]?: TValue }
|
|
38
|
+
: { [K in TKey]: TValue };
|
|
39
|
+
|
|
40
|
+
type IsEmptyVariables<TValue> = keyof TValue extends never
|
|
41
|
+
? true
|
|
42
|
+
: TValue extends Record<string, never>
|
|
43
|
+
? true
|
|
44
|
+
: false;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Sends typed requests to one generated API endpoint.
|
|
48
|
+
*/
|
|
49
|
+
export interface SwaggerEndpointClient<TEndpoint extends AnyApiEndpoint> {
|
|
50
|
+
/**
|
|
51
|
+
* Sends a typed request for the configured endpoint.
|
|
52
|
+
*
|
|
53
|
+
* @param variables The path, query, header, and body variables for the endpoint.
|
|
54
|
+
* @returns A promise of the endpoint response type.
|
|
55
|
+
* @throws SwaggerHttpError when the HTTP response status is not successful.
|
|
56
|
+
*/
|
|
57
|
+
request(variables?: SwaggerRequestVariables<TEndpoint>): Promise<ResponseOf<TEndpoint>>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Creates a memoized typed client for a generated API endpoint.
|
|
62
|
+
*
|
|
63
|
+
* @param endpoint The generated endpoint definition to call.
|
|
64
|
+
* @param config The swagger client runtime configuration.
|
|
65
|
+
* @returns A typed endpoint client.
|
|
66
|
+
*/
|
|
67
|
+
export function useSwaggerClient<TEndpoint extends AnyApiEndpoint>(
|
|
68
|
+
endpoint: TEndpoint,
|
|
69
|
+
config: SwaggerClientConfig
|
|
70
|
+
): SwaggerEndpointClient<TEndpoint> {
|
|
71
|
+
return useMemo(() => createSwaggerClient(endpoint, config), [endpoint, config]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Creates a typed client for a generated API endpoint.
|
|
76
|
+
*
|
|
77
|
+
* @param endpoint The generated endpoint definition to call.
|
|
78
|
+
* @param config The swagger client runtime configuration.
|
|
79
|
+
* @returns A typed endpoint client.
|
|
80
|
+
*/
|
|
81
|
+
export function createSwaggerClient<TEndpoint extends AnyApiEndpoint>(
|
|
82
|
+
endpoint: TEndpoint,
|
|
83
|
+
config: SwaggerClientConfig
|
|
84
|
+
): SwaggerEndpointClient<TEndpoint> {
|
|
85
|
+
return {
|
|
86
|
+
async request(variables?: SwaggerRequestVariables<TEndpoint>): Promise<ResponseOf<TEndpoint>> {
|
|
87
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
88
|
+
const request = buildRequest(config, endpoint, variables);
|
|
89
|
+
const response = await fetchImpl(request.url, request.init);
|
|
90
|
+
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new SwaggerHttpError(response);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (response.status === 204) {
|
|
96
|
+
return undefined as ResponseOf<TEndpoint>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return await response.json() as ResponseOf<TEndpoint>;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Represents a failed HTTP response from the generated React swagger client.
|
|
106
|
+
*/
|
|
107
|
+
export class SwaggerHttpError extends Error {
|
|
108
|
+
/**
|
|
109
|
+
* Creates an error for a failed HTTP response.
|
|
110
|
+
*
|
|
111
|
+
* @param response The failed fetch response.
|
|
112
|
+
*/
|
|
113
|
+
constructor(readonly response: Response) {
|
|
114
|
+
super('Swagger request failed with status ' + response.status + '.');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildRequest<TEndpoint extends AnyApiEndpoint>(
|
|
119
|
+
config: SwaggerClientConfig,
|
|
120
|
+
endpoint: TEndpoint,
|
|
121
|
+
variables?: SwaggerRequestVariables<TEndpoint>
|
|
122
|
+
): { url: string; init: RequestInit } {
|
|
123
|
+
const pathVariables = variables?.pathVariables as Record<string, unknown> | undefined;
|
|
124
|
+
const queryVariables = variables?.queryVariables as Record<string, unknown> | undefined;
|
|
125
|
+
const headerVariables = variables?.headerVariables as Record<string, unknown> | undefined;
|
|
126
|
+
const bodyVariables = variables?.bodyVariables;
|
|
127
|
+
const path = endpoint.path.replace(/\{([^}]+)\}/g, (_, key: string) => {
|
|
128
|
+
const value = pathVariables?.[key];
|
|
129
|
+
|
|
130
|
+
if (value === undefined || value === null) {
|
|
131
|
+
throw new Error('Missing path variable "' + key + '" for endpoint "' + endpoint.name + '".');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return encodeURIComponent(String(value));
|
|
135
|
+
});
|
|
136
|
+
const url = new URL(joinUrl(config.baseUrl, path));
|
|
137
|
+
|
|
138
|
+
for (const [key, value] of Object.entries(queryVariables ?? {})) {
|
|
139
|
+
if (value === undefined || value === null) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (Array.isArray(value)) {
|
|
144
|
+
for (const item of value) {
|
|
145
|
+
url.searchParams.append(key, String(item));
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
url.searchParams.set(key, String(value));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const headers = {
|
|
153
|
+
...config.headers,
|
|
154
|
+
...stringifyRecord(headerVariables)
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (bodyVariables !== undefined) {
|
|
158
|
+
headers['content-type'] ??= 'application/json';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
url: url.toString(),
|
|
163
|
+
init: {
|
|
164
|
+
method: endpoint.method,
|
|
165
|
+
headers,
|
|
166
|
+
body: bodyVariables === undefined ? undefined : JSON.stringify(bodyVariables)
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function stringifyRecord(record: Record<string, unknown> | undefined): Record<string, string> | undefined {
|
|
172
|
+
if (!record) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return Object.fromEntries(
|
|
177
|
+
Object.entries(record)
|
|
178
|
+
.filter(([, value]) => value !== undefined && value !== null)
|
|
179
|
+
.map(([key, value]) => [key, String(value)])
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function joinUrl(baseUrl: string, path: string): string {
|
|
184
|
+
return baseUrl.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '');
|
|
185
|
+
}
|
|
186
|
+
`;
|
|
187
|
+
}
|