@let-aurn/swagger-client 0.3.3 → 0.4.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,37 @@ npx swagger-client generate --config=swagger-client.json
24
24
 
25
25
  `client` can be `angular` or `react`.
26
26
 
27
+ `swaggers-directory` accepts a directory (all JSON files recursively), a file
28
+ glob, or an array of include/exclude glob patterns. Paths are relative to the
29
+ command's working directory; absolute paths also work. Use forward slashes in
30
+ glob patterns. `*` matches within one directory and `**` matches across nested
31
+ directories.
32
+
33
+ To include only one subtree:
34
+
35
+ ```json
36
+ {
37
+ "swaggers-directory": "swaggers/should-not-be-ignored/**/*.json"
38
+ }
39
+ ```
40
+
41
+ To include JSON files while excluding a subtree:
42
+
43
+ ```json
44
+ {
45
+ "swaggers-directory": [
46
+ "swaggers/**/*.json",
47
+ "!swaggers/should-be-ignored/**"
48
+ ]
49
+ }
50
+ ```
51
+
52
+ Exclusions apply regardless of pattern order. Duplicate matches are processed
53
+ once, in sorted file order; only `.json` files are read. Glob selections with
54
+ no matching JSON files fail with an error. For glob or array selections,
55
+ generated `sourceFile` metadata is relative to the working directory; a plain
56
+ directory keeps metadata relative to that directory.
57
+
27
58
  `serverUrlIndex` is optional and selects a zero-based index from each document's
28
59
  top-level `servers` array to prefix generated endpoint paths:
29
60
 
@@ -2,6 +2,7 @@ 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";
5
6
  /**
6
7
  * Loads and validates a swagger-client configuration file.
7
8
  *
@@ -16,7 +17,7 @@ export async function loadConfig(configPath) {
16
17
  validateConfig(config, absolutePath);
17
18
  return {
18
19
  ...config,
19
- "swaggers-directory": resolve(config["swaggers-directory"]),
20
+ "swaggers-directory": resolveSwaggerInput(config["swaggers-directory"]),
20
21
  output: {
21
22
  "api-models.ts": resolve(config.output["api-models.ts"]),
22
23
  "api-endpoints.ts": resolve(config.output["api-endpoints.ts"]),
@@ -29,9 +30,7 @@ function validateConfig(config, configPath) {
29
30
  if (config.client !== "angular" && config.client !== "react") {
30
31
  throw new Error(`${configPath}: "client" must be "angular" or "react".`);
31
32
  }
32
- if (!config["swaggers-directory"]) {
33
- throw new Error(`${configPath}: "swaggers-directory" is required.`);
34
- }
33
+ validateSwaggerInput(config["swaggers-directory"], configPath);
35
34
  if (config["endpoint-name-strategy"] !== undefined &&
36
35
  config["endpoint-name-strategy"] !== "operationId" &&
37
36
  config["endpoint-name-strategy"] !== "tagAndOperationId" &&
@@ -1,12 +1,12 @@
1
1
  import type { EndpointNameStrategy, 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
9
  * @returns The normalized schemas and endpoints discovered in the OpenAPI files.
10
10
  * @throws Error when a JSON file cannot be read or parsed.
11
11
  */
12
- export declare function readOpenApiProject(swaggerDirectory: string, endpointNameStrategy?: EndpointNameStrategy, endpointNamespace?: EndpointNamespaceConfig, serverUrlIndex?: number): Promise<ProjectModel>;
12
+ export declare function readOpenApiProject(swaggerDirectory: string | string[], endpointNameStrategy?: EndpointNameStrategy, endpointNamespace?: EndpointNamespaceConfig, serverUrlIndex?: number): Promise<ProjectModel>;
@@ -1,14 +1,15 @@
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";
7
8
  const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
8
9
  /**
9
- * Reads OpenAPI JSON files from a directory and converts them to the generator project model.
10
+ * Reads selected OpenAPI JSON files and converts them to the generator project model.
10
11
  *
11
- * @param swaggerDirectory The directory containing one or more OpenAPI JSON files.
12
+ * @param swaggerDirectory A directory, glob, or array of include/exclude glob patterns.
12
13
  * @param endpointNameStrategy The strategy used to name generated endpoint constants.
13
14
  * @param endpointNamespace Configurable document field and optional namespace extraction regex.
14
15
  * @param serverUrlIndex Optional document server URL index to prefix endpoint paths with.
@@ -16,18 +17,18 @@ const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete", "head", "
16
17
  * @throws Error when a JSON file cannot be read or parsed.
17
18
  */
18
19
  export async function readOpenApiProject(swaggerDirectory, endpointNameStrategy = "operationId", endpointNamespace, serverUrlIndex) {
19
- validateServerUrlIndex(serverUrlIndex, swaggerDirectory);
20
+ validateServerUrlIndex(serverUrlIndex, String(swaggerDirectory));
20
21
  if (endpointNameStrategy === "namespaceTagAndOperationId") {
21
- validateEndpointNamespace(endpointNamespace, swaggerDirectory);
22
+ validateEndpointNamespace(endpointNamespace, String(swaggerDirectory));
22
23
  }
23
- const files = await findJsonFiles(swaggerDirectory);
24
+ const { files, sourceDirectory } = await discoverSwaggerFiles(swaggerDirectory);
24
25
  const schemas = [];
25
26
  const endpoints = [];
26
27
  const usedSchemaNames = new Set();
27
28
  const usedEndpointNames = new Set();
28
29
  for (const file of files) {
29
30
  const document = JSON.parse(await readFile(file, "utf8"));
30
- const sourceFile = relative(swaggerDirectory, file);
31
+ const sourceFile = relative(sourceDirectory, file);
31
32
  const serverUrl = selectServerUrl(document, serverUrlIndex, sourceFile);
32
33
  const schemaNameMap = collectSchemas(document, schemas, usedSchemaNames);
33
34
  const namespace = endpointNameStrategy === "namespaceTagAndOperationId"
@@ -37,20 +38,6 @@ export async function readOpenApiProject(swaggerDirectory, endpointNameStrategy
37
38
  }
38
39
  return { schemas, endpoints };
39
40
  }
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
41
  function collectSchemas(document, schemas, usedSchemaNames) {
55
42
  const schemaNameMap = new Map();
56
43
  const sourceSchemas = document.components?.schemas ?? document.definitions ?? {};
@@ -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
+ }
@@ -8,7 +8,8 @@ export interface EndpointNamespaceConfig {
8
8
  }
9
9
  export interface SwaggerClientConfig {
10
10
  client: ClientKind;
11
- "swaggers-directory": string;
11
+ /** Directory, file glob, or include/exclude glob patterns, relative to the working directory. */
12
+ "swaggers-directory": string | string[];
12
13
  "endpoint-name-strategy"?: EndpointNameStrategy;
13
14
  "endpoint-namespace"?: EndpointNamespaceConfig;
14
15
  /** Index of the document server URL to prefix endpoint paths with. */
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.4.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
  }