@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 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
- Custom HTTP implementations are supported:
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
+ }
@@ -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"];
@@ -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
- throw new Error(`${configPath}: "endpoint-name-strategy" must be "operationId" or "tagAndOperationId".`);
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
- endpoints.push(...collectEndpoints(document, sourceFile, schemaNameMap, usedEndpointNames, endpointNameStrategy));
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 !== "tagAndOperationId") {
100
+ if (strategy === "operationId") {
93
101
  return operationName;
94
102
  }
95
103
  const tag = operation.tags?.[0];
96
- if (!tag) {
97
- return operationName;
98
- }
99
- const tagName = ensureIdentifier(toPascalCase(tag), "endpoint");
100
- if (operationName === tagName || operationName.startsWith(`${tagName}_`)) {
101
- return operationName;
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 `${tagName}_${operationName}`;
111
+ return `${namespace}_${taggedName}`;
104
112
  }
105
113
  function variablesFor(parameters, location, schemaNameMap) {
106
114
  return parameters
@@ -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, Provider, Type } from '@angular/core';
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 Angular providers for the generated client.
136
+ * @returns Environment providers for application or route configuration.
137
137
  */
138
- export function provideSwaggerClient(config: SwaggerClientConfig): Provider[] {
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@let-aurn/swagger-client",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Generate typed Angular and React clients from OpenAPI JSON files.",
5
5
  "type": "module",
6
6
  "bin": {