@let-aurn/swagger-client 0.2.0 → 0.3.1
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 +74 -1
- package/dist/core/endpoint-namespace.d.ts +3 -0
- package/dist/core/endpoint-namespace.js +40 -0
- package/dist/core/generate.js +1 -1
- package/dist/core/load-config.js +7 -2
- package/dist/core/read-openapi.d.ts +3 -2
- package/dist/core/read-openapi.js +21 -13
- package/dist/core/types.d.ts +8 -1
- package/dist/generators/angular-client.js +6 -6
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ npx swagger-client generate --config=swagger-client.json
|
|
|
28
28
|
|
|
29
29
|
- `operationId` uses the OpenAPI `operationId` as-is. This is the default.
|
|
30
30
|
- `tagAndOperationId` prefixes endpoint exports with the first OpenAPI tag, unless the `operationId` already starts with that tag.
|
|
31
|
+
- `namespaceTagAndOperationId` adds a document namespace before the tag and operation ID. Requires `endpoint-namespace`.
|
|
31
32
|
|
|
32
33
|
With `tagAndOperationId`, an operation tagged `SomeController` with `operationId: "someEndpoint"` is generated as:
|
|
33
34
|
|
|
@@ -35,9 +36,49 @@ With `tagAndOperationId`, an operation tagged `SomeController` with `operationId
|
|
|
35
36
|
ApiEndpoints.SomeController_someEndpoint
|
|
36
37
|
```
|
|
37
38
|
|
|
39
|
+
To include a namespace, configure where it comes from in each Swagger document:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"endpoint-name-strategy": "namespaceTagAndOperationId",
|
|
44
|
+
"endpoint-namespace": {
|
|
45
|
+
"pointer": "/servers/0/url",
|
|
46
|
+
"pattern": "^/([^/]+)"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For `servers: [{ "url": "/some-namespace/v1" }]`, tag `SomeController`, and
|
|
52
|
+
operation ID `someMethod`, this generates:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
ApiEndpoints.SomeNamespace_SomeController_someMethod
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`pointer` is a JSON Pointer relative to the document root. It can select any
|
|
59
|
+
string field, including custom extensions. Array indexes are path segments;
|
|
60
|
+
escape `/` in property names as `~1` and `~` as `~0`.
|
|
61
|
+
For example, `{ "pointer": "/info/x-namespace" }` uses that field's entire value.
|
|
62
|
+
|
|
63
|
+
`pattern` is an optional JavaScript regular expression without delimiters or
|
|
64
|
+
flags. When present, its first capture group supplies the namespace; without
|
|
65
|
+
capture groups, the full match is used. Omit it to use the entire selected string.
|
|
66
|
+
The namespace is converted to PascalCase. Only the first tag is used; operations
|
|
67
|
+
without tags receive the namespace and operation ID. Existing tag or namespace
|
|
68
|
+
prefixes followed by `_` are not repeated. Name collisions receive numeric suffixes.
|
|
69
|
+
|
|
70
|
+
Extraction runs separately for each Swagger file. Missing or non-string fields,
|
|
71
|
+
unmatched patterns, and empty namespaces fail generation with a file-specific
|
|
72
|
+
error. This setting affects endpoint names only, not request URLs.
|
|
73
|
+
|
|
38
74
|
## Angular
|
|
39
75
|
|
|
76
|
+
Use `provideSwaggerClient` to register the client as
|
|
77
|
+
`EnvironmentProviders` without spreading a provider array:
|
|
78
|
+
|
|
40
79
|
```ts
|
|
80
|
+
import { provideSwaggerClient } from './api/generated/swagger-client';
|
|
81
|
+
|
|
41
82
|
bootstrapApplication(AppComponent, {
|
|
42
83
|
providers: [
|
|
43
84
|
provideHttpClient(),
|
|
@@ -48,7 +89,39 @@ bootstrapApplication(AppComponent, {
|
|
|
48
89
|
});
|
|
49
90
|
```
|
|
50
91
|
|
|
51
|
-
|
|
92
|
+
Starting with version `0.3.1`, `provideSwaggerClient(config)` returns
|
|
93
|
+
`EnvironmentProviders` for application or route configuration. Remove any spread
|
|
94
|
+
operator (`...`) when registering it. Version `0.3.0` retains the `Provider[]`
|
|
95
|
+
implementation.
|
|
96
|
+
|
|
97
|
+
### Custom HTTP adapters
|
|
98
|
+
|
|
99
|
+
Implement `SwaggerHttpClient` and pass the adapter class to `provideSwaggerClient`.
|
|
100
|
+
The adapter receives `{ method, url, headers, query, body }` and returns an
|
|
101
|
+
`Observable<T>` of the response body. For a transport returning `Promise<{ data: unknown }>`:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { Injectable } from '@angular/core';
|
|
105
|
+
import { defer, map, Observable } from 'rxjs';
|
|
106
|
+
import type {
|
|
107
|
+
SwaggerHttpClient,
|
|
108
|
+
SwaggerHttpRequest
|
|
109
|
+
} from './api/generated/swagger-client';
|
|
110
|
+
import { NativeTransport } from './native-transport';
|
|
111
|
+
|
|
112
|
+
@Injectable()
|
|
113
|
+
export class MySwaggerHttpClient implements SwaggerHttpClient {
|
|
114
|
+
constructor(private readonly transport: NativeTransport) {}
|
|
115
|
+
|
|
116
|
+
request<T>(request: SwaggerHttpRequest): Observable<T> {
|
|
117
|
+
return defer(() => this.transport.request(request)).pipe(
|
|
118
|
+
map(response => response.data as T)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Register the adapter class through `provideSwaggerClient`:
|
|
52
125
|
|
|
53
126
|
```ts
|
|
54
127
|
provideSwaggerClient({
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { EndpointNamespaceConfig } from "./types.js";
|
|
2
|
+
export declare function validateEndpointNamespace(config: EndpointNamespaceConfig | undefined, source: string): void;
|
|
3
|
+
export declare function extractEndpointNamespace(document: unknown, config: EndpointNamespaceConfig, source: string): string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { toPascalCase } from "./names.js";
|
|
2
|
+
export function validateEndpointNamespace(config, source) {
|
|
3
|
+
if (!config || typeof config.pointer !== "string" || !/^(?:\/(?:[^~]|~[01])*)*$/.test(config.pointer)) {
|
|
4
|
+
throw new Error(`${source}: "endpoint-namespace.pointer" must be a JSON Pointer (for example "/servers/0/url").`);
|
|
5
|
+
}
|
|
6
|
+
if (config.pattern !== undefined) {
|
|
7
|
+
if (typeof config.pattern !== "string") {
|
|
8
|
+
throw new Error(`${source}: "endpoint-namespace.pattern" must be a regex string.`);
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
new RegExp(config.pattern);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
throw new Error(`${source}: "endpoint-namespace.pattern" is not a valid regex.`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function extractEndpointNamespace(document, config, source) {
|
|
19
|
+
let value = document;
|
|
20
|
+
const tokens = config.pointer === "" ? [] : config.pointer.slice(1).split("/");
|
|
21
|
+
for (const token of tokens) {
|
|
22
|
+
const key = token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
23
|
+
if (value === null || typeof value !== "object" || !Object.hasOwn(value, key)) {
|
|
24
|
+
value = undefined;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
value = value[key];
|
|
28
|
+
}
|
|
29
|
+
if (typeof value !== "string") {
|
|
30
|
+
throw new Error(`${source}: namespace at "${config.pointer}" must be a string.`);
|
|
31
|
+
}
|
|
32
|
+
if (config.pattern !== undefined) {
|
|
33
|
+
const match = new RegExp(config.pattern).exec(value);
|
|
34
|
+
value = match ? (match.length > 1 ? match[1] : match[0]) : undefined;
|
|
35
|
+
}
|
|
36
|
+
if (typeof value !== "string" || !/[A-Za-z0-9]/.test(value)) {
|
|
37
|
+
throw new Error(`${source}: namespace extracted from "${config.pointer}" is empty or does not match the configured pattern.`);
|
|
38
|
+
}
|
|
39
|
+
return toPascalCase(value);
|
|
40
|
+
}
|
package/dist/core/generate.js
CHANGED
|
@@ -13,7 +13,7 @@ import { emitReactClient } from "../generators/react-client.js";
|
|
|
13
13
|
* @throws Error when OpenAPI files cannot be read or generated files cannot be written.
|
|
14
14
|
*/
|
|
15
15
|
export async function generate(config) {
|
|
16
|
-
const project = await readOpenApiProject(config["swaggers-directory"], config["endpoint-name-strategy"]);
|
|
16
|
+
const project = await readOpenApiProject(config["swaggers-directory"], config["endpoint-name-strategy"], config["endpoint-namespace"]);
|
|
17
17
|
const modelsPath = config.output["api-models.ts"];
|
|
18
18
|
const endpointsPath = config.output["api-endpoints.ts"];
|
|
19
19
|
const clientPath = config.output["client.ts"];
|
package/dist/core/load-config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
+
import { validateEndpointNamespace } from "./endpoint-namespace.js";
|
|
3
4
|
/**
|
|
4
5
|
* Loads and validates a swagger-client configuration file.
|
|
5
6
|
*
|
|
@@ -31,8 +32,12 @@ function validateConfig(config, configPath) {
|
|
|
31
32
|
}
|
|
32
33
|
if (config["endpoint-name-strategy"] !== undefined &&
|
|
33
34
|
config["endpoint-name-strategy"] !== "operationId" &&
|
|
34
|
-
config["endpoint-name-strategy"] !== "tagAndOperationId"
|
|
35
|
-
|
|
35
|
+
config["endpoint-name-strategy"] !== "tagAndOperationId" &&
|
|
36
|
+
config["endpoint-name-strategy"] !== "namespaceTagAndOperationId") {
|
|
37
|
+
throw new Error(`${configPath}: "endpoint-name-strategy" must be "operationId", "tagAndOperationId", or "namespaceTagAndOperationId".`);
|
|
38
|
+
}
|
|
39
|
+
if (config["endpoint-namespace"] !== undefined || config["endpoint-name-strategy"] === "namespaceTagAndOperationId") {
|
|
40
|
+
validateEndpointNamespace(config["endpoint-namespace"], configPath);
|
|
36
41
|
}
|
|
37
42
|
for (const key of ["api-models.ts", "api-endpoints.ts", "client.ts"]) {
|
|
38
43
|
if (!config.output?.[key]) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { EndpointNameStrategy, ProjectModel } from "./types.js";
|
|
1
|
+
import type { EndpointNameStrategy, EndpointNamespaceConfig, ProjectModel } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Reads OpenAPI JSON files from a directory and converts them to the generator project model.
|
|
4
4
|
*
|
|
5
5
|
* @param swaggerDirectory The directory containing one or more OpenAPI JSON files.
|
|
6
6
|
* @param endpointNameStrategy The strategy used to name generated endpoint constants.
|
|
7
|
+
* @param endpointNamespace Configurable document field and optional namespace extraction regex.
|
|
7
8
|
* @returns The normalized schemas and endpoints discovered in the OpenAPI files.
|
|
8
9
|
* @throws Error when a JSON file cannot be read or parsed.
|
|
9
10
|
*/
|
|
10
|
-
export declare function readOpenApiProject(swaggerDirectory: string, endpointNameStrategy?: EndpointNameStrategy): Promise<ProjectModel>;
|
|
11
|
+
export declare function readOpenApiProject(swaggerDirectory: string, endpointNameStrategy?: EndpointNameStrategy, endpointNamespace?: EndpointNamespaceConfig): Promise<ProjectModel>;
|
|
@@ -2,16 +2,21 @@ import { readdir, readFile } from "node:fs/promises";
|
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
3
|
import { schemaToType } from "./schema-to-typescript.js";
|
|
4
4
|
import { ensureIdentifier, toPascalCase, uniqueName } from "./names.js";
|
|
5
|
+
import { extractEndpointNamespace, validateEndpointNamespace } from "./endpoint-namespace.js";
|
|
5
6
|
const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
6
7
|
/**
|
|
7
8
|
* Reads OpenAPI JSON files from a directory and converts them to the generator project model.
|
|
8
9
|
*
|
|
9
10
|
* @param swaggerDirectory The directory containing one or more OpenAPI JSON files.
|
|
10
11
|
* @param endpointNameStrategy The strategy used to name generated endpoint constants.
|
|
12
|
+
* @param endpointNamespace Configurable document field and optional namespace extraction regex.
|
|
11
13
|
* @returns The normalized schemas and endpoints discovered in the OpenAPI files.
|
|
12
14
|
* @throws Error when a JSON file cannot be read or parsed.
|
|
13
15
|
*/
|
|
14
|
-
export async function readOpenApiProject(swaggerDirectory, endpointNameStrategy = "operationId") {
|
|
16
|
+
export async function readOpenApiProject(swaggerDirectory, endpointNameStrategy = "operationId", endpointNamespace) {
|
|
17
|
+
if (endpointNameStrategy === "namespaceTagAndOperationId") {
|
|
18
|
+
validateEndpointNamespace(endpointNamespace, swaggerDirectory);
|
|
19
|
+
}
|
|
15
20
|
const files = await findJsonFiles(swaggerDirectory);
|
|
16
21
|
const schemas = [];
|
|
17
22
|
const endpoints = [];
|
|
@@ -21,7 +26,10 @@ export async function readOpenApiProject(swaggerDirectory, endpointNameStrategy
|
|
|
21
26
|
const document = JSON.parse(await readFile(file, "utf8"));
|
|
22
27
|
const sourceFile = relative(swaggerDirectory, file);
|
|
23
28
|
const schemaNameMap = collectSchemas(document, schemas, usedSchemaNames);
|
|
24
|
-
|
|
29
|
+
const namespace = endpointNameStrategy === "namespaceTagAndOperationId"
|
|
30
|
+
? extractEndpointNamespace(document, endpointNamespace, sourceFile)
|
|
31
|
+
: undefined;
|
|
32
|
+
endpoints.push(...collectEndpoints(document, sourceFile, schemaNameMap, usedEndpointNames, endpointNameStrategy, namespace));
|
|
25
33
|
}
|
|
26
34
|
return { schemas, endpoints };
|
|
27
35
|
}
|
|
@@ -58,7 +66,7 @@ function collectSchemas(document, schemas, usedSchemaNames) {
|
|
|
58
66
|
}
|
|
59
67
|
return schemaNameMap;
|
|
60
68
|
}
|
|
61
|
-
function collectEndpoints(document, sourceFile, schemaNameMap, usedEndpointNames, endpointNameStrategy) {
|
|
69
|
+
function collectEndpoints(document, sourceFile, schemaNameMap, usedEndpointNames, endpointNameStrategy, namespace) {
|
|
62
70
|
const endpoints = [];
|
|
63
71
|
for (const [path, pathItem] of Object.entries(document.paths ?? {})) {
|
|
64
72
|
if (!isRecord(pathItem)) {
|
|
@@ -69,7 +77,7 @@ function collectEndpoints(document, sourceFile, schemaNameMap, usedEndpointNames
|
|
|
69
77
|
continue;
|
|
70
78
|
}
|
|
71
79
|
const operationId = operation.operationId ?? fallbackOperationId(method, path);
|
|
72
|
-
const endpointName = uniqueName(endpointNameForOperation(operation, operationId, endpointNameStrategy), usedEndpointNames);
|
|
80
|
+
const endpointName = uniqueName(endpointNameForOperation(operation, operationId, endpointNameStrategy, namespace), usedEndpointNames);
|
|
73
81
|
const parameters = operation.parameters ?? [];
|
|
74
82
|
endpoints.push({
|
|
75
83
|
name: endpointName,
|
|
@@ -87,20 +95,20 @@ function collectEndpoints(document, sourceFile, schemaNameMap, usedEndpointNames
|
|
|
87
95
|
}
|
|
88
96
|
return endpoints;
|
|
89
97
|
}
|
|
90
|
-
function endpointNameForOperation(operation, operationId, strategy) {
|
|
98
|
+
function endpointNameForOperation(operation, operationId, strategy, namespace) {
|
|
91
99
|
const operationName = ensureIdentifier(operationId, "endpoint");
|
|
92
|
-
if (strategy
|
|
100
|
+
if (strategy === "operationId") {
|
|
93
101
|
return operationName;
|
|
94
102
|
}
|
|
95
103
|
const tag = operation.tags?.[0];
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
return
|
|
104
|
+
const tagName = tag ? ensureIdentifier(toPascalCase(tag), "endpoint") : undefined;
|
|
105
|
+
const taggedName = !tagName || operationName === tagName || operationName.startsWith(`${tagName}_`)
|
|
106
|
+
? operationName
|
|
107
|
+
: `${tagName}_${operationName}`;
|
|
108
|
+
if (!namespace || taggedName === namespace || taggedName.startsWith(`${namespace}_`)) {
|
|
109
|
+
return taggedName;
|
|
102
110
|
}
|
|
103
|
-
return `${
|
|
111
|
+
return `${namespace}_${taggedName}`;
|
|
104
112
|
}
|
|
105
113
|
function variablesFor(parameters, location, schemaNameMap) {
|
|
106
114
|
return parameters
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
export type ClientKind = "angular" | "react";
|
|
2
|
-
export type EndpointNameStrategy = "operationId" | "tagAndOperationId";
|
|
2
|
+
export type EndpointNameStrategy = "operationId" | "tagAndOperationId" | "namespaceTagAndOperationId";
|
|
3
|
+
export interface EndpointNamespaceConfig {
|
|
4
|
+
/** JSON Pointer to a string in each OpenAPI document. */
|
|
5
|
+
pointer: string;
|
|
6
|
+
/** Optional regex: uses the first capture group, or the full match if none exists. */
|
|
7
|
+
pattern?: string;
|
|
8
|
+
}
|
|
3
9
|
export interface SwaggerClientConfig {
|
|
4
10
|
client: ClientKind;
|
|
5
11
|
"swaggers-directory": string;
|
|
6
12
|
"endpoint-name-strategy"?: EndpointNameStrategy;
|
|
13
|
+
"endpoint-namespace"?: EndpointNamespaceConfig;
|
|
7
14
|
output: {
|
|
8
15
|
"api-models.ts": string;
|
|
9
16
|
"api-endpoints.ts": string;
|
|
@@ -4,7 +4,7 @@ export function emitAngularClient(options = {}) {
|
|
|
4
4
|
/* eslint-disable */
|
|
5
5
|
// This file was generated by @let-aurn/swagger-client.
|
|
6
6
|
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
|
7
|
-
import { Inject, Injectable, InjectionToken,
|
|
7
|
+
import { EnvironmentProviders, Inject, Injectable, InjectionToken, makeEnvironmentProviders, Type } from '@angular/core';
|
|
8
8
|
import { Observable } from 'rxjs';
|
|
9
9
|
import type {
|
|
10
10
|
AnyApiEndpoint,
|
|
@@ -130,17 +130,17 @@ export class SwaggerClientService {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
|
-
* Provides the generated Angular swagger client and its HTTP adapter.
|
|
133
|
+
* Provides the generated Angular swagger client and its HTTP adapter in an environment injector.
|
|
134
134
|
*
|
|
135
135
|
* @param config The swagger client runtime configuration.
|
|
136
|
-
* @returns
|
|
136
|
+
* @returns Environment providers for application or route configuration.
|
|
137
137
|
*/
|
|
138
|
-
export function provideSwaggerClient(config: SwaggerClientConfig):
|
|
139
|
-
return [
|
|
138
|
+
export function provideSwaggerClient(config: SwaggerClientConfig): EnvironmentProviders {
|
|
139
|
+
return makeEnvironmentProviders([
|
|
140
140
|
{ provide: SWAGGER_CLIENT_CONFIG, useValue: config },
|
|
141
141
|
{ provide: SWAGGER_HTTP_CLIENT, useClass: config.httpClient ?? AngularSwaggerHttpClient },
|
|
142
142
|
SwaggerClientService
|
|
143
|
-
];
|
|
143
|
+
]);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
function buildRequest<TEndpoint extends AnyApiEndpoint>(
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { ClientKind, EndpointModel, ProjectModel, SchemaModel, SwaggerClientConfig, VariableModel } from "./core/types.js";
|
|
1
|
+
export type { ClientKind, EndpointNameStrategy, EndpointNamespaceConfig, EndpointModel, ProjectModel, SchemaModel, SwaggerClientConfig, VariableModel } from "./core/types.js";
|
|
2
2
|
export { generate } from "./core/generate.js";
|
|
3
3
|
export { loadConfig } from "./core/load-config.js";
|
|
4
4
|
export { readOpenApiProject } from "./core/read-openapi.js";
|