@let-aurn/swagger-client 0.1.0 → 0.3.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 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,6 +36,41 @@ 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
 
40
76
  ```ts
@@ -48,7 +84,34 @@ bootstrapApplication(AppComponent, {
48
84
  });
49
85
  ```
50
86
 
51
- Custom HTTP implementations are supported:
87
+ ### Custom HTTP adapters
88
+
89
+ Implement `SwaggerHttpClient` and pass the adapter class to `provideSwaggerClient`.
90
+ The adapter receives `{ method, url, headers, query, body }` and returns an
91
+ `Observable<T>` of the response body. For a transport returning `Promise<{ data: unknown }>`:
92
+
93
+ ```ts
94
+ import { Injectable } from '@angular/core';
95
+ import { defer, map, Observable } from 'rxjs';
96
+ import type {
97
+ SwaggerHttpClient,
98
+ SwaggerHttpRequest
99
+ } from './api/generated/swagger-client';
100
+ import { NativeTransport } from './native-transport';
101
+
102
+ @Injectable()
103
+ export class MySwaggerHttpClient implements SwaggerHttpClient {
104
+ constructor(private readonly transport: NativeTransport) {}
105
+
106
+ request<T>(request: SwaggerHttpRequest): Observable<T> {
107
+ return defer(() => this.transport.request(request)).pipe(
108
+ map(response => response.data as T)
109
+ );
110
+ }
111
+ }
112
+ ```
113
+
114
+ Register the adapter class through `provideSwaggerClient`:
52
115
 
53
116
  ```ts
54
117
  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;
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,10 +1,10 @@
1
1
  {
2
2
  "name": "@let-aurn/swagger-client",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Generate typed Angular and React clients from OpenAPI JSON files.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "swagger-client": "./dist/cli/index.js"
7
+ "swagger-client": "dist/cli/index.js"
8
8
  },
9
9
  "main": "./dist/index.js",
10
10
  "types": "./dist/index.d.ts",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^24.0.0",
34
- "typescript": "^5.8.0"
34
+ "typescript": "^7.0.2"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@angular/common": ">=16",