@let-aurn/swagger-client 0.3.3 → 0.5.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
@@ -24,6 +24,94 @@ npx swagger-client generate --config=swagger-client.json
24
24
 
25
25
  `client` can be `angular` or `react`.
26
26
 
27
+ `enum-style` is optional and accepts `"union"` (the default) or `"enum"`:
28
+
29
+ ```json
30
+ {
31
+ "enum-style": "enum"
32
+ }
33
+ ```
34
+
35
+ Union style emits literal types such as `"PENDING" | "IN_PROGRESS"`.
36
+ Enum style emits exported runtime TypeScript enums in `api-models.ts` and uses
37
+ them in model properties and endpoint request/response types:
38
+
39
+ ```ts
40
+ export enum TaskState {
41
+ PENDING = "PENDING",
42
+ IN_PROGRESS = "IN_PROGRESS",
43
+ COMPLETED = "COMPLETED",
44
+ }
45
+
46
+ export interface Task {
47
+ state?: TaskState;
48
+ }
49
+ ```
50
+
51
+ Standalone enum schemas keep their generated schema names. Inline enums use
52
+ the enclosing model name plus the property path; array items add `Item`, and
53
+ dictionary values add `Value`. Endpoint enums use the endpoint name followed
54
+ by `Path`, `Query`, `Header`, `Body`, or `Response` and the nested property path.
55
+ Declaration and member name collisions receive numeric suffixes. Identical
56
+ inline enums in different locations remain separate declarations; use a shared
57
+ schema with `$ref` to share a single enum.
58
+
59
+ String values retain valid member names such as `PENDING`; invalid identifier
60
+ characters are removed, leading digits receive a `Value` prefix, and reserved
61
+ names are adjusted. Numeric members use names such as `Value_0` and
62
+ `Value_Minus_1`. Every member has an explicit initializer that preserves its
63
+ original JSON value. Duplicate values are emitted once. String, numeric, and
64
+ mixed string/numeric enums are supported; enums containing boolean or structured
65
+ values remain literal unions because TypeScript enums cannot represent them.
66
+ Null values and `nullable: true` become `EnumName | null`. Nullable standalone
67
+ enum schemas emit an `EnumNameValue` enum plus an `EnumName` type alias.
68
+
69
+ When enabling enum style, use enum members instead of assigning raw strings:
70
+
71
+ ```ts
72
+ import { TaskState } from './api-models';
73
+ import type { Task } from './api-models';
74
+
75
+ const task: Task = {
76
+ state: TaskState.PENDING,
77
+ };
78
+ ```
79
+
80
+ Enums exist at runtime; serialized request values are still the original strings
81
+ or numbers. This option changes generated types only and does not validate or
82
+ convert server responses at runtime.
83
+
84
+ `swaggers-directory` accepts a directory (all JSON files recursively), a file
85
+ glob, or an array of include/exclude glob patterns. Paths are relative to the
86
+ command's working directory; absolute paths also work. Use forward slashes in
87
+ glob patterns. `*` matches within one directory and `**` matches across nested
88
+ directories.
89
+
90
+ To include only one subtree:
91
+
92
+ ```json
93
+ {
94
+ "swaggers-directory": "swaggers/should-not-be-ignored/**/*.json"
95
+ }
96
+ ```
97
+
98
+ To include JSON files while excluding a subtree:
99
+
100
+ ```json
101
+ {
102
+ "swaggers-directory": [
103
+ "swaggers/**/*.json",
104
+ "!swaggers/should-be-ignored/**"
105
+ ]
106
+ }
107
+ ```
108
+
109
+ Exclusions apply regardless of pattern order. Duplicate matches are processed
110
+ once, in sorted file order; only `.json` files are read. Glob selections with
111
+ no matching JSON files fail with an error. For glob or array selections,
112
+ generated `sourceFile` metadata is relative to the working directory; a plain
113
+ directory keeps metadata relative to that directory.
114
+
27
115
  `serverUrlIndex` is optional and selects a zero-based index from each document's
28
116
  top-level `servers` array to prefix generated endpoint paths:
29
117
 
@@ -0,0 +1,12 @@
1
+ import type { EnumStyle, OpenApiSchema, SchemaModel } from "./types.js";
2
+ export declare function validateEnumStyle(style: EnumStyle | undefined, source: string): void;
3
+ /** Extracts inline enums before endpoint schemas are converted to type strings. */
4
+ export declare class EnumCollector {
5
+ private readonly schemas;
6
+ private readonly usedNames;
7
+ private readonly style;
8
+ constructor(schemas: SchemaModel[], usedNames: Set<string>, style: EnumStyle);
9
+ addModel(name: string, schema: OpenApiSchema): void;
10
+ normalize(schema: OpenApiSchema, context: string): OpenApiSchema;
11
+ }
12
+ export declare function emitEnum(name: string, values: (string | number)[]): string;
@@ -0,0 +1,77 @@
1
+ import { ensureIdentifier, toPascalCase, uniqueName } from "./names.js";
2
+ export function validateEnumStyle(style, source) {
3
+ if (style !== undefined && style !== "union" && style !== "enum") {
4
+ throw new Error(`${source}: "enum-style" must be "union" or "enum".`);
5
+ }
6
+ }
7
+ function enumValues(schema) {
8
+ if (schema.$ref || !schema.enum?.length)
9
+ return undefined;
10
+ const values = schema.enum.filter((value) => value !== null);
11
+ if (!values.length || !values.every((value) => typeof value === "string" || (typeof value === "number" && Number.isFinite(value)))) {
12
+ return undefined;
13
+ }
14
+ return [...new Set(values)];
15
+ }
16
+ /** Extracts inline enums before endpoint schemas are converted to type strings. */
17
+ export class EnumCollector {
18
+ schemas;
19
+ usedNames;
20
+ style;
21
+ constructor(schemas, usedNames, style) {
22
+ this.schemas = schemas;
23
+ this.usedNames = usedNames;
24
+ this.style = style;
25
+ }
26
+ addModel(name, schema) {
27
+ const values = this.style === "enum" ? enumValues(schema) : undefined;
28
+ if (values && !schema.nullable && !schema.enum.includes(null)) {
29
+ this.schemas.push({ name, schema, enumValues: values });
30
+ }
31
+ else {
32
+ this.schemas.push({ name, schema: this.normalize(schema, values ? `${name}Value` : name) });
33
+ }
34
+ }
35
+ normalize(schema, context) {
36
+ if (this.style === "union")
37
+ return schema;
38
+ const values = enumValues(schema);
39
+ if (values) {
40
+ const name = uniqueName(toPascalCase(context), this.usedNames);
41
+ this.schemas.push({ name, schema: { enum: values }, enumValues: values });
42
+ return {
43
+ $ref: `#/components/schemas/${name}`,
44
+ nullable: schema.nullable === true || schema.enum.includes(null)
45
+ };
46
+ }
47
+ const result = { ...schema };
48
+ if (schema.properties) {
49
+ result.properties = Object.fromEntries(Object.entries(schema.properties).map(([key, child]) => [key, this.normalize(child, `${context}${toPascalCase(key)}`)]));
50
+ }
51
+ if (schema.items)
52
+ result.items = this.normalize(schema.items, `${context}Item`);
53
+ if (typeof schema.additionalProperties === "object") {
54
+ result.additionalProperties = this.normalize(schema.additionalProperties, `${context}Value`);
55
+ }
56
+ for (const kind of ["allOf", "oneOf", "anyOf"]) {
57
+ if (schema[kind]) {
58
+ result[kind] = schema[kind].map((child, index) => this.normalize(child, `${context}${toPascalCase(kind)}${index + 1}`));
59
+ }
60
+ }
61
+ return result;
62
+ }
63
+ }
64
+ export function emitEnum(name, values) {
65
+ const usedNames = new Set();
66
+ const members = values.map((value) => {
67
+ const preferred = typeof value === "number" ? `Value_${String(value).replace(/-/g, "Minus_").replace(/\./g, "_")}` : value;
68
+ const identifier = ensureIdentifier(preferred || "Value", "Value");
69
+ // These names conflict with the enum object's prototype or its JS setter.
70
+ const safeIdentifier = identifier === "prototype" || identifier === "__proto__"
71
+ ? `${identifier}Value`
72
+ : identifier;
73
+ const member = uniqueName(safeIdentifier, usedNames);
74
+ return ` ${member} = ${JSON.stringify(value)},`;
75
+ });
76
+ return [`export enum ${name} {`, ...members, "}"].join("\n");
77
+ }
@@ -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"], config["endpoint-namespace"], config.serverUrlIndex);
16
+ const project = await readOpenApiProject(config["swaggers-directory"], config["endpoint-name-strategy"], config["endpoint-namespace"], config.serverUrlIndex, config["enum-style"]);
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"];
@@ -2,6 +2,8 @@ import { readFile } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
  import { validateEndpointNamespace } from "./endpoint-namespace.js";
4
4
  import { validateServerUrlIndex } from "./server-url.js";
5
+ import { resolveSwaggerInput, validateSwaggerInput } from "./swagger-files.js";
6
+ import { validateEnumStyle } from "./enums.js";
5
7
  /**
6
8
  * Loads and validates a swagger-client configuration file.
7
9
  *
@@ -16,7 +18,7 @@ export async function loadConfig(configPath) {
16
18
  validateConfig(config, absolutePath);
17
19
  return {
18
20
  ...config,
19
- "swaggers-directory": resolve(config["swaggers-directory"]),
21
+ "swaggers-directory": resolveSwaggerInput(config["swaggers-directory"]),
20
22
  output: {
21
23
  "api-models.ts": resolve(config.output["api-models.ts"]),
22
24
  "api-endpoints.ts": resolve(config.output["api-endpoints.ts"]),
@@ -25,13 +27,12 @@ export async function loadConfig(configPath) {
25
27
  };
26
28
  }
27
29
  function validateConfig(config, configPath) {
30
+ validateEnumStyle(config["enum-style"], configPath);
28
31
  validateServerUrlIndex(config.serverUrlIndex, configPath);
29
32
  if (config.client !== "angular" && config.client !== "react") {
30
33
  throw new Error(`${configPath}: "client" must be "angular" or "react".`);
31
34
  }
32
- if (!config["swaggers-directory"]) {
33
- throw new Error(`${configPath}: "swaggers-directory" is required.`);
34
- }
35
+ validateSwaggerInput(config["swaggers-directory"], configPath);
35
36
  if (config["endpoint-name-strategy"] !== undefined &&
36
37
  config["endpoint-name-strategy"] !== "operationId" &&
37
38
  config["endpoint-name-strategy"] !== "tagAndOperationId" &&
@@ -1,12 +1,13 @@
1
- import type { EndpointNameStrategy, EndpointNamespaceConfig, ProjectModel } from "./types.js";
1
+ import type { EndpointNameStrategy, EnumStyle, EndpointNamespaceConfig, ProjectModel } from "./types.js";
2
2
  /**
3
- * Reads OpenAPI JSON files from a directory and converts them to the generator project model.
3
+ * Reads selected OpenAPI JSON files and converts them to the generator project model.
4
4
  *
5
- * @param swaggerDirectory The directory containing one or more OpenAPI JSON files.
5
+ * @param swaggerDirectory A directory, glob, or array of include/exclude glob patterns.
6
6
  * @param endpointNameStrategy The strategy used to name generated endpoint constants.
7
7
  * @param endpointNamespace Configurable document field and optional namespace extraction regex.
8
8
  * @param serverUrlIndex Optional document server URL index to prefix endpoint paths with.
9
+ * @param enumStyle Emit literal unions (default) or named runtime enums.
9
10
  * @returns The normalized schemas and endpoints discovered in the OpenAPI files.
10
11
  * @throws Error when a JSON file cannot be read or parsed.
11
12
  */
12
- export declare function readOpenApiProject(swaggerDirectory: string, endpointNameStrategy?: EndpointNameStrategy, endpointNamespace?: EndpointNamespaceConfig, serverUrlIndex?: number): Promise<ProjectModel>;
13
+ export declare function readOpenApiProject(swaggerDirectory: string | string[], endpointNameStrategy?: EndpointNameStrategy, endpointNamespace?: EndpointNamespaceConfig, serverUrlIndex?: number, enumStyle?: EnumStyle): Promise<ProjectModel>;
@@ -1,57 +1,48 @@
1
- import { readdir, readFile } from "node:fs/promises";
2
- import { join, relative } from "node:path";
1
+ import { readFile } from "node:fs/promises";
2
+ import { relative } from "node:path";
3
3
  import { schemaToType } from "./schema-to-typescript.js";
4
4
  import { ensureIdentifier, toPascalCase, uniqueName } from "./names.js";
5
5
  import { extractEndpointNamespace, validateEndpointNamespace } from "./endpoint-namespace.js";
6
6
  import { selectServerUrl, validateServerUrlIndex } from "./server-url.js";
7
+ import { discoverSwaggerFiles } from "./swagger-files.js";
8
+ import { EnumCollector, validateEnumStyle } from "./enums.js";
7
9
  const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
8
10
  /**
9
- * Reads OpenAPI JSON files from a directory and converts them to the generator project model.
11
+ * Reads selected OpenAPI JSON files and converts them to the generator project model.
10
12
  *
11
- * @param swaggerDirectory The directory containing one or more OpenAPI JSON files.
13
+ * @param swaggerDirectory A directory, glob, or array of include/exclude glob patterns.
12
14
  * @param endpointNameStrategy The strategy used to name generated endpoint constants.
13
15
  * @param endpointNamespace Configurable document field and optional namespace extraction regex.
14
16
  * @param serverUrlIndex Optional document server URL index to prefix endpoint paths with.
17
+ * @param enumStyle Emit literal unions (default) or named runtime enums.
15
18
  * @returns The normalized schemas and endpoints discovered in the OpenAPI files.
16
19
  * @throws Error when a JSON file cannot be read or parsed.
17
20
  */
18
- export async function readOpenApiProject(swaggerDirectory, endpointNameStrategy = "operationId", endpointNamespace, serverUrlIndex) {
19
- validateServerUrlIndex(serverUrlIndex, swaggerDirectory);
21
+ export async function readOpenApiProject(swaggerDirectory, endpointNameStrategy = "operationId", endpointNamespace, serverUrlIndex, enumStyle = "union") {
22
+ validateEnumStyle(enumStyle, String(swaggerDirectory));
23
+ validateServerUrlIndex(serverUrlIndex, String(swaggerDirectory));
20
24
  if (endpointNameStrategy === "namespaceTagAndOperationId") {
21
- validateEndpointNamespace(endpointNamespace, swaggerDirectory);
25
+ validateEndpointNamespace(endpointNamespace, String(swaggerDirectory));
22
26
  }
23
- const files = await findJsonFiles(swaggerDirectory);
27
+ const { files, sourceDirectory } = await discoverSwaggerFiles(swaggerDirectory);
24
28
  const schemas = [];
25
29
  const endpoints = [];
26
30
  const usedSchemaNames = new Set();
27
31
  const usedEndpointNames = new Set();
32
+ const enums = new EnumCollector(schemas, usedSchemaNames, enumStyle);
28
33
  for (const file of files) {
29
34
  const document = JSON.parse(await readFile(file, "utf8"));
30
- const sourceFile = relative(swaggerDirectory, file);
35
+ const sourceFile = relative(sourceDirectory, file);
31
36
  const serverUrl = selectServerUrl(document, serverUrlIndex, sourceFile);
32
- const schemaNameMap = collectSchemas(document, schemas, usedSchemaNames);
37
+ const schemaNameMap = collectSchemas(document, enums, usedSchemaNames);
33
38
  const namespace = endpointNameStrategy === "namespaceTagAndOperationId"
34
39
  ? extractEndpointNamespace(document, endpointNamespace, sourceFile)
35
40
  : undefined;
36
- endpoints.push(...collectEndpoints(document, sourceFile, schemaNameMap, usedEndpointNames, endpointNameStrategy, namespace, serverUrl));
41
+ endpoints.push(...collectEndpoints(document, sourceFile, schemaNameMap, usedEndpointNames, endpointNameStrategy, namespace, serverUrl, enums));
37
42
  }
38
43
  return { schemas, endpoints };
39
44
  }
40
- async function findJsonFiles(directory) {
41
- const entries = await readdir(directory, { withFileTypes: true });
42
- const files = [];
43
- for (const entry of entries) {
44
- const path = join(directory, entry.name);
45
- if (entry.isDirectory()) {
46
- files.push(...await findJsonFiles(path));
47
- }
48
- else if (entry.isFile() && entry.name.endsWith(".json")) {
49
- files.push(path);
50
- }
51
- }
52
- return files.sort();
53
- }
54
- function collectSchemas(document, schemas, usedSchemaNames) {
45
+ function collectSchemas(document, enums, usedSchemaNames) {
55
46
  const schemaNameMap = new Map();
56
47
  const sourceSchemas = document.components?.schemas ?? document.definitions ?? {};
57
48
  for (const rawName of Object.keys(sourceSchemas)) {
@@ -63,14 +54,11 @@ function collectSchemas(document, schemas, usedSchemaNames) {
63
54
  if (!generatedName) {
64
55
  continue;
65
56
  }
66
- schemas.push({
67
- name: generatedName,
68
- schema: rewriteRefs(schema, schemaNameMap)
69
- });
57
+ enums.addModel(generatedName, rewriteRefs(schema, schemaNameMap));
70
58
  }
71
59
  return schemaNameMap;
72
60
  }
73
- function collectEndpoints(document, sourceFile, schemaNameMap, usedEndpointNames, endpointNameStrategy, namespace, serverUrl = "") {
61
+ function collectEndpoints(document, sourceFile, schemaNameMap, usedEndpointNames, endpointNameStrategy, namespace, serverUrl, enums) {
74
62
  const endpoints = [];
75
63
  for (const [path, pathItem] of Object.entries(document.paths ?? {})) {
76
64
  if (!isRecord(pathItem)) {
@@ -89,11 +77,11 @@ function collectEndpoints(document, sourceFile, schemaNameMap, usedEndpointNames
89
77
  path: serverUrl && path ? `${serverUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}` : serverUrl || path,
90
78
  operationId,
91
79
  sourceFile,
92
- pathVariables: variablesFor(parameters, "path", schemaNameMap),
93
- queryVariables: variablesFor(parameters, "query", schemaNameMap),
94
- headerVariables: variablesFor(parameters, "header", schemaNameMap),
95
- bodyType: requestBodyType(operation, schemaNameMap),
96
- responseType: responseType(operation.responses, schemaNameMap)
80
+ pathVariables: variablesFor(parameters, "path", schemaNameMap, enums, endpointName),
81
+ queryVariables: variablesFor(parameters, "query", schemaNameMap, enums, endpointName),
82
+ headerVariables: variablesFor(parameters, "header", schemaNameMap, enums, endpointName),
83
+ bodyType: requestBodyType(operation, schemaNameMap, enums, endpointName),
84
+ responseType: responseType(operation.responses, schemaNameMap, enums, endpointName)
97
85
  });
98
86
  }
99
87
  }
@@ -114,26 +102,26 @@ function endpointNameForOperation(operation, operationId, strategy, namespace) {
114
102
  }
115
103
  return `${namespace}_${taggedName}`;
116
104
  }
117
- function variablesFor(parameters, location, schemaNameMap) {
105
+ function variablesFor(parameters, location, schemaNameMap, enums, endpointName) {
118
106
  return parameters
119
107
  .filter((parameter) => parameter.in === location)
120
108
  .map((parameter) => ({
121
109
  name: parameter.name,
122
- type: schemaToType(rewriteRefs(parameter.schema ?? { type: "string" }, schemaNameMap)),
110
+ type: schemaToType(enums.normalize(rewriteRefs(parameter.schema ?? { type: "string" }, schemaNameMap), `${toPascalCase(endpointName)}${toPascalCase(location)}${toPascalCase(parameter.name)}`)),
123
111
  required: parameter.required === true || location === "path"
124
112
  }));
125
113
  }
126
- function requestBodyType(operation, schemaNameMap) {
114
+ function requestBodyType(operation, schemaNameMap, enums, endpointName) {
127
115
  const schema = preferredContentSchema(operation.requestBody?.content);
128
- return schema ? schemaToType(rewriteRefs(schema, schemaNameMap)) : null;
116
+ return schema ? schemaToType(enums.normalize(rewriteRefs(schema, schemaNameMap), `${toPascalCase(endpointName)}Body`)) : null;
129
117
  }
130
- function responseType(responses, schemaNameMap) {
118
+ function responseType(responses, schemaNameMap, enums, endpointName) {
131
119
  const response = responses?.["200"] ?? responses?.["201"] ?? responses?.["204"] ?? firstSuccessResponse(responses);
132
120
  const schema = preferredContentSchema(response?.content);
133
121
  if (!schema || response === responses?.["204"]) {
134
122
  return "void";
135
123
  }
136
- return schemaToType(rewriteRefs(schema, schemaNameMap));
124
+ return schemaToType(enums.normalize(rewriteRefs(schema, schemaNameMap), `${toPascalCase(endpointName)}Response`));
137
125
  }
138
126
  function firstSuccessResponse(responses) {
139
127
  return Object.entries(responses ?? {}).find(([status]) => status.startsWith("2"))?.[1];
@@ -59,7 +59,7 @@ function schemaToNonNullableType(schema, fallback) {
59
59
  return (schema.oneOf ?? schema.anyOf ?? []).map((item) => schemaToType(item, fallback)).join(" | ") || fallback;
60
60
  }
61
61
  if (schema.type === "array") {
62
- return `${schemaToType(schema.items, "unknown")}[]`;
62
+ return `Array<${schemaToType(schema.items, "unknown")}>`;
63
63
  }
64
64
  if (schema.type === "object" || schema.properties) {
65
65
  const entries = Object.entries(schema.properties ?? {});
@@ -0,0 +1,6 @@
1
+ export declare function validateSwaggerInput(input: string | string[], source: string): void;
2
+ export declare function resolveSwaggerInput(input: string | string[]): string | string[];
3
+ export declare function discoverSwaggerFiles(input: string | string[]): Promise<{
4
+ files: string[];
5
+ sourceDirectory: string;
6
+ }>;
@@ -0,0 +1,57 @@
1
+ import { readdir, stat } from "node:fs/promises";
2
+ import { join, posix, resolve } from "node:path";
3
+ import { glob, isDynamicPattern } from "tinyglobby";
4
+ export function validateSwaggerInput(input, source) {
5
+ const patterns = Array.isArray(input) ? input : [input];
6
+ if (patterns.length === 0 || patterns.some(pattern => typeof pattern !== "string" || pattern.trim() === "" || pattern === "!")) {
7
+ throw new Error(`${source}: "swaggers-directory" must be a non-empty directory, glob, or array of patterns.`);
8
+ }
9
+ if (patterns.every(isExclusion)) {
10
+ throw new Error(`${source}: "swaggers-directory" must include at least one positive pattern.`);
11
+ }
12
+ }
13
+ export function resolveSwaggerInput(input) {
14
+ const resolvePattern = (pattern) => {
15
+ const excluded = isExclusion(pattern);
16
+ const path = excluded ? pattern.slice(1) : pattern;
17
+ return (excluded ? "!" : "") + posix.resolve(process.cwd().split("\\").join("/"), path);
18
+ };
19
+ return Array.isArray(input) ? input.map(resolvePattern) : resolvePattern(input);
20
+ }
21
+ export async function discoverSwaggerFiles(input) {
22
+ validateSwaggerInput(input, "OpenAPI input");
23
+ if (typeof input === "string" && !isDynamicPattern(input)) {
24
+ const path = resolve(input);
25
+ if ((await stat(path)).isDirectory()) {
26
+ return { files: await findJsonFiles(path), sourceDirectory: path };
27
+ }
28
+ }
29
+ const matches = await glob(input, {
30
+ absolute: true,
31
+ onlyFiles: true,
32
+ dot: true,
33
+ followSymbolicLinks: false
34
+ });
35
+ const files = [...new Set(matches.filter(file => file.endsWith(".json")))].sort();
36
+ if (files.length === 0) {
37
+ throw new Error(`No OpenAPI JSON files match "swaggers-directory": ${JSON.stringify(input)}.`);
38
+ }
39
+ return { files, sourceDirectory: process.cwd() };
40
+ }
41
+ function isExclusion(pattern) {
42
+ return pattern.startsWith("!") && !pattern.startsWith("!(");
43
+ }
44
+ async function findJsonFiles(directory) {
45
+ const entries = await readdir(directory, { withFileTypes: true });
46
+ const files = [];
47
+ for (const entry of entries) {
48
+ const path = join(directory, entry.name);
49
+ if (entry.isDirectory()) {
50
+ files.push(...await findJsonFiles(path));
51
+ }
52
+ else if (entry.isFile() && entry.name.endsWith(".json")) {
53
+ files.push(path);
54
+ }
55
+ }
56
+ return files.sort();
57
+ }
@@ -1,4 +1,5 @@
1
1
  export type ClientKind = "angular" | "react";
2
+ export type EnumStyle = "union" | "enum";
2
3
  export type EndpointNameStrategy = "operationId" | "tagAndOperationId" | "namespaceTagAndOperationId";
3
4
  export interface EndpointNamespaceConfig {
4
5
  /** JSON Pointer to a string in each OpenAPI document. */
@@ -8,7 +9,10 @@ export interface EndpointNamespaceConfig {
8
9
  }
9
10
  export interface SwaggerClientConfig {
10
11
  client: ClientKind;
11
- "swaggers-directory": string;
12
+ /** Emit literal unions by default, or named runtime enums. */
13
+ "enum-style"?: EnumStyle;
14
+ /** Directory, file glob, or include/exclude glob patterns, relative to the working directory. */
15
+ "swaggers-directory": string | string[];
12
16
  "endpoint-name-strategy"?: EndpointNameStrategy;
13
17
  "endpoint-namespace"?: EndpointNamespaceConfig;
14
18
  /** Index of the document server URL to prefix endpoint paths with. */
@@ -26,6 +30,7 @@ export interface ProjectModel {
26
30
  export interface SchemaModel {
27
31
  name: string;
28
32
  schema: OpenApiSchema;
33
+ enumValues?: (string | number)[];
29
34
  }
30
35
  export interface EndpointModel {
31
36
  name: string;
@@ -136,7 +136,11 @@ function propertyKey(name) {
136
136
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : quote(name);
137
137
  }
138
138
  function qualifyModelTypes(type, modelNames) {
139
- return type.replace(/\b[A-Z][A-Za-z0-9_]*\b/g, (name) => {
139
+ return type.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\b[A-Z][A-Za-z0-9_]*\b/g, (name, offset) => {
140
+ // Preserve string literals and property keys that happen to match model names.
141
+ if (name.startsWith('"') || name.startsWith("'") || /^\s*\??\s*:/.test(type.slice(offset + name.length))) {
142
+ return name;
143
+ }
140
144
  return modelNames.has(name) ? `ApiModels.${name}` : name;
141
145
  });
142
146
  }
@@ -1,4 +1,5 @@
1
1
  import { emitSchemaInterface } from "../core/schema-to-typescript.js";
2
+ import { emitEnum } from "../core/enums.js";
2
3
  /**
3
4
  * Emits the generated TypeScript API model file.
4
5
  *
@@ -12,7 +13,9 @@ export function emitModels(project) {
12
13
  ""
13
14
  ];
14
15
  for (const schema of project.schemas) {
15
- lines.push(emitSchemaInterface(schema.name, schema.schema), "");
16
+ lines.push(schema.enumValues
17
+ ? emitEnum(schema.name, schema.enumValues)
18
+ : emitSchemaInterface(schema.name, schema.schema), "");
16
19
  }
17
20
  return lines.join("\n");
18
21
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { ClientKind, EndpointNameStrategy, EndpointNamespaceConfig, EndpointModel, ProjectModel, SchemaModel, SwaggerClientConfig, VariableModel } from "./core/types.js";
1
+ export type { ClientKind, EnumStyle, 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.3.3",
3
+ "version": "0.5.0",
4
4
  "description": "Generate typed Angular and React clients from OpenAPI JSON files.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -48,5 +48,8 @@
48
48
  "react": {
49
49
  "optional": true
50
50
  }
51
+ },
52
+ "dependencies": {
53
+ "tinyglobby": "^0.2.17"
51
54
  }
52
55
  }