@vertz/openapi 0.1.0 → 0.1.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 ADDED
@@ -0,0 +1,229 @@
1
+ # @vertz/openapi
2
+
3
+ Generate typed TypeScript SDKs from OpenAPI 3.x specs. Produces a fully typed client with resource methods, TypeScript interfaces, and optional Zod schemas.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @vertz/openapi
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ npx @vertz/openapi generate --from ./openapi.json --output ./src/generated
15
+ ```
16
+
17
+ This generates:
18
+
19
+ ```
20
+ src/generated/
21
+ client.ts # createClient() factory + HttpClient interface
22
+ types/ # TypeScript interfaces per resource
23
+ resources/ # Typed resource methods per resource
24
+ schemas/ # Zod schemas (opt-in with --schemas)
25
+ README.md # Usage documentation
26
+ ```
27
+
28
+ ## CLI
29
+
30
+ ### `generate` — Generate SDK from a spec
31
+
32
+ ```bash
33
+ npx @vertz/openapi generate [options]
34
+ ```
35
+
36
+ | Flag | Description | Default |
37
+ | ---------------------- | ---------------------------------------- | ----------------------------- |
38
+ | `--from <path-or-url>` | Path to OpenAPI spec file or URL | Required (or use config file) |
39
+ | `--output <dir>` | Output directory | `./src/generated` |
40
+ | `--base-url <url>` | Default base URL for API calls | `''` |
41
+ | `--group-by <mode>` | Grouping strategy: `tag`, `path`, `none` | `tag` |
42
+ | `--schemas` | Generate Zod validation schemas | `false` |
43
+ | `--exclude-tags <t>` | Comma-separated tags to exclude | none |
44
+ | `--dry-run` | Preview without writing files | `false` |
45
+
46
+ ### `validate` — Validate a spec without generating
47
+
48
+ ```bash
49
+ npx @vertz/openapi validate --from ./openapi.json
50
+ ```
51
+
52
+ ## Config File
53
+
54
+ Create an `openapi.config.ts` in your project root:
55
+
56
+ ```ts
57
+ import { defineConfig } from '@vertz/openapi';
58
+
59
+ export default defineConfig({
60
+ source: './openapi.json',
61
+ output: './src/generated',
62
+ baseURL: 'https://api.example.com',
63
+ groupBy: 'tag',
64
+ schemas: true,
65
+ });
66
+ ```
67
+
68
+ CLI flags override config file values.
69
+
70
+ ## Excluding Tags
71
+
72
+ Skip internal, deprecated, or catch-all tags that don't map cleanly to SDK resources:
73
+
74
+ ```ts
75
+ export default defineConfig({
76
+ source: './openapi.json',
77
+ excludeTags: ['internal', 'deprecated'],
78
+ });
79
+ ```
80
+
81
+ Or via CLI:
82
+
83
+ ```bash
84
+ npx @vertz/openapi generate --from ./openapi.json --exclude-tags internal,deprecated
85
+ ```
86
+
87
+ Operations where **any** tag matches the exclude list are skipped entirely.
88
+
89
+ ## Programmatic API
90
+
91
+ ```ts
92
+ import { generateFromOpenAPI } from '@vertz/openapi';
93
+
94
+ const result = await generateFromOpenAPI({
95
+ source: './openapi.json',
96
+ output: './src/generated',
97
+ baseURL: 'https://api.example.com',
98
+ groupBy: 'tag',
99
+ schemas: false,
100
+ });
101
+
102
+ console.log(`${result.written} files written, ${result.skipped} unchanged`);
103
+ ```
104
+
105
+ ## Using the Generated SDK
106
+
107
+ ```ts
108
+ import { createClient } from './generated/client';
109
+
110
+ const api = createClient({ baseURL: 'https://api.example.com' });
111
+
112
+ // Fully typed — params, body, and response types are inferred
113
+ const tasks = await api.tasks.list();
114
+ const task = await api.tasks.get(taskId);
115
+ const created = await api.tasks.create({ title: 'New task' });
116
+ ```
117
+
118
+ ## Custom Operation ID Normalization
119
+
120
+ The generator auto-cleans operationIds (strips controller prefixes, detects CRUD patterns). For more control:
121
+
122
+ ### Static overrides
123
+
124
+ ```ts
125
+ export default defineConfig({
126
+ source: './openapi.json',
127
+ operationIds: {
128
+ overrides: {
129
+ listTasks: 'fetchAll',
130
+ getTask: 'findById',
131
+ },
132
+ },
133
+ });
134
+ ```
135
+
136
+ ### Transform function
137
+
138
+ The transform receives the auto-cleaned name and a full `OperationContext`:
139
+
140
+ ```ts
141
+ export default defineConfig({
142
+ source: './openapi.json',
143
+ operationIds: {
144
+ transform: (cleaned, ctx) => {
145
+ // ctx.operationId — raw operationId from the spec
146
+ // ctx.method — GET, POST, PUT, DELETE, PATCH
147
+ // ctx.path — /v1/tasks/{id}
148
+ // ctx.tags — ['tasks']
149
+ // ctx.hasBody — whether the operation has a request body
150
+ return cleaned;
151
+ },
152
+ },
153
+ });
154
+ ```
155
+
156
+ ## Framework Adapters
157
+
158
+ Built-in adapters handle operationId quirks for common backend frameworks. Import from `@vertz/openapi/adapters`:
159
+
160
+ ### FastAPI
161
+
162
+ FastAPI generates operationIds like `list_tasks_tasks_get` (function name + route + verb). The adapter strips the route+verb suffix and handles API version prefixes.
163
+
164
+ ```ts
165
+ import { defineConfig } from '@vertz/openapi';
166
+ import { fastapi } from '@vertz/openapi/adapters';
167
+
168
+ export default defineConfig({
169
+ source: './openapi.json',
170
+ output: './src/generated',
171
+ operationIds: fastapi(),
172
+ });
173
+ ```
174
+
175
+ | FastAPI operationId | Path | Result |
176
+ | ---------------------------- | ---------------- | ---------------- |
177
+ | `list_tasks_tasks_get` | `/tasks` | `list_tasks` |
178
+ | `get_user_v1_users__id__get` | `/v1/users/{id}` | `get_user_v1` |
179
+ | `create_task_v2_tasks_post` | `/v2/tasks` | `create_task_v2` |
180
+
181
+ ### NestJS
182
+
183
+ NestJS (`@nestjs/swagger`) generates operationIds like `TasksController_findAll`. The adapter strips the Controller prefix.
184
+
185
+ ```ts
186
+ import { defineConfig } from '@vertz/openapi';
187
+ import { nestjs } from '@vertz/openapi/adapters';
188
+
189
+ export default defineConfig({
190
+ source: './openapi.json',
191
+ output: './src/generated',
192
+ operationIds: nestjs(),
193
+ });
194
+ ```
195
+
196
+ | NestJS operationId | Result |
197
+ | ------------------------- | --------- |
198
+ | `TasksController_findAll` | `findAll` |
199
+ | `UsersController.getById` | `getById` |
200
+
201
+ ### Writing a Custom Adapter
202
+
203
+ An adapter is just a function that returns `{ transform }`:
204
+
205
+ ```ts
206
+ function myFramework() {
207
+ return {
208
+ transform: (cleaned, ctx) => {
209
+ // Your logic here using ctx.operationId, ctx.method, ctx.path, etc.
210
+ return cleaned;
211
+ },
212
+ };
213
+ }
214
+
215
+ export default defineConfig({
216
+ operationIds: myFramework(),
217
+ });
218
+ ```
219
+
220
+ ## Incremental Writes
221
+
222
+ The generator only writes files whose content has changed (SHA-256 comparison). Unchanged files are left untouched, so downstream tools (watchers, bundlers) aren't triggered unnecessarily. Stale files are automatically removed.
223
+
224
+ ## Supported Specs
225
+
226
+ - OpenAPI 3.0.x
227
+ - OpenAPI 3.1.x
228
+ - JSON and YAML formats
229
+ - File paths and URLs
package/bin/openapi.ts ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bun
2
+ import { runCLI } from '../src/cli';
3
+
4
+ const result = await runCLI(process.argv.slice(2));
5
+
6
+ if (result.message) {
7
+ if (result.exitCode === 0) {
8
+ console.log(result.message);
9
+ } else {
10
+ console.error(result.message);
11
+ }
12
+ }
13
+
14
+ process.exit(result.exitCode);
package/dist/cli.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ interface CLIResult {
2
+ exitCode: number;
3
+ message: string;
4
+ }
5
+ /**
6
+ * Run the CLI with the given arguments.
7
+ * Returns a result with exit code and message (for testability).
8
+ */
9
+ declare function runCLI(args: string[], cwd?: string): Promise<CLIResult>;
10
+ export { runCLI, CLIResult };
package/dist/cli.js ADDED
@@ -0,0 +1,133 @@
1
+ import {
2
+ generateFromOpenAPI,
3
+ loadConfigFile,
4
+ loadSpec,
5
+ parseOpenAPI,
6
+ resolveConfig
7
+ } from "./shared/chunk-er3k8t3a.js";
8
+
9
+ // src/cli.ts
10
+ function parseArgs(args) {
11
+ const command = args[0] ?? "";
12
+ const flags = {};
13
+ for (let i = 1;i < args.length; i++) {
14
+ const arg = args[i];
15
+ if (arg.startsWith("--")) {
16
+ const key = arg.slice(2);
17
+ const next = args[i + 1];
18
+ if (next && !next.startsWith("--")) {
19
+ flags[key] = next;
20
+ i++;
21
+ } else {
22
+ flags[key] = true;
23
+ }
24
+ }
25
+ }
26
+ return { command, flags };
27
+ }
28
+ function flagsToPartialConfig(flags) {
29
+ const result = {};
30
+ if (typeof flags.from === "string")
31
+ result.from = flags.from;
32
+ if (typeof flags.output === "string")
33
+ result.output = flags.output;
34
+ if (typeof flags["base-url"] === "string")
35
+ result.baseURL = flags["base-url"];
36
+ if (typeof flags["group-by"] === "string") {
37
+ const value = flags["group-by"];
38
+ if (value === "tag" || value === "path" || value === "none") {
39
+ result.groupBy = value;
40
+ }
41
+ }
42
+ if (flags.schemas === true)
43
+ result.schemas = true;
44
+ if (typeof flags["exclude-tags"] === "string") {
45
+ result.excludeTags = flags["exclude-tags"].split(",").map((t) => t.trim());
46
+ }
47
+ if (flags["dry-run"] === true)
48
+ result.dryRun = true;
49
+ return result;
50
+ }
51
+ async function handleGenerate(flags, cwd) {
52
+ const cliFlags = flagsToPartialConfig(flags);
53
+ const configFile = await loadConfigFile(cwd);
54
+ let config;
55
+ try {
56
+ const resolved = resolveConfig(cliFlags, configFile);
57
+ config = { ...resolved, dryRun: cliFlags.dryRun };
58
+ } catch (err) {
59
+ return {
60
+ exitCode: 1,
61
+ message: `Error: ${err instanceof Error ? err.message : String(err)}`
62
+ };
63
+ }
64
+ try {
65
+ const result = await generateFromOpenAPI(config);
66
+ if (config.dryRun) {
67
+ return {
68
+ exitCode: 0,
69
+ message: `Generated ${result.written + result.skipped} files (dry run) — ${result.written} would be written, ${result.skipped} unchanged`
70
+ };
71
+ }
72
+ const parts = [];
73
+ parts.push(`Generated ${result.written + result.skipped} files in ${config.output}`);
74
+ parts.push(`${result.written} written`);
75
+ if (result.skipped > 0)
76
+ parts.push(`${result.skipped} unchanged`);
77
+ if (result.removed > 0)
78
+ parts.push(`${result.removed} removed`);
79
+ return {
80
+ exitCode: 0,
81
+ message: parts.join(", ")
82
+ };
83
+ } catch (err) {
84
+ return {
85
+ exitCode: 1,
86
+ message: `Error: ${err instanceof Error ? err.message : String(err)}`
87
+ };
88
+ }
89
+ }
90
+ async function handleValidate(flags) {
91
+ const source = typeof flags.from === "string" ? flags.from : undefined;
92
+ if (!source) {
93
+ return {
94
+ exitCode: 1,
95
+ message: "Error: Missing --from flag for validate command"
96
+ };
97
+ }
98
+ try {
99
+ const raw = await loadSpec(source);
100
+ const parsed = parseOpenAPI(raw);
101
+ return {
102
+ exitCode: 0,
103
+ message: `Spec is valid — OpenAPI ${parsed.version}, ${parsed.operations.length} operations`
104
+ };
105
+ } catch (err) {
106
+ return {
107
+ exitCode: 1,
108
+ message: `Error: ${err instanceof Error ? err.message : String(err)}`
109
+ };
110
+ }
111
+ }
112
+ async function runCLI(args, cwd) {
113
+ const { command, flags } = parseArgs(args);
114
+ if (!command) {
115
+ return {
116
+ exitCode: 1,
117
+ message: "Usage: @vertz/openapi <generate|validate> [options]"
118
+ };
119
+ }
120
+ if (command === "generate") {
121
+ return handleGenerate(flags, cwd ?? process.cwd());
122
+ }
123
+ if (command === "validate") {
124
+ return handleValidate(flags);
125
+ }
126
+ return {
127
+ exitCode: 1,
128
+ message: `Unknown command: ${command}. Use "generate" or "validate".`
129
+ };
130
+ }
131
+ export {
132
+ runCLI
133
+ };
package/dist/index.d.ts CHANGED
@@ -34,14 +34,123 @@ interface ParsedSchema {
34
34
  name?: string;
35
35
  jsonSchema: Record<string, unknown>;
36
36
  }
37
- type GroupByStrategy = "tag" | "path" | "none";
38
- declare function groupOperations(operations: ParsedOperation[], strategy: GroupByStrategy): ParsedResource[];
39
- declare function sanitizeIdentifier(name: string): string;
37
+ interface OperationContext {
38
+ /** Raw operationId from the spec */
39
+ operationId: string;
40
+ /** HTTP method (GET, POST, etc.) */
41
+ method: HttpMethod;
42
+ /** Route path (e.g. /v1/tasks/{id}) */
43
+ path: string;
44
+ /** Tags from the operation */
45
+ tags: string[];
46
+ /** Whether the operation has a request body */
47
+ hasBody: boolean;
48
+ }
40
49
  interface NormalizerConfig {
41
50
  overrides?: Record<string, string>;
42
- transform?: (cleaned: string, original: string) => string;
51
+ transform?: (cleaned: string, context: OperationContext) => string;
52
+ }
53
+ declare function normalizeOperationId(operationId: string, method: HttpMethod, path: string, config?: NormalizerConfig, context?: OperationContext): string;
54
+ interface OpenAPIConfig {
55
+ source: string;
56
+ output: string;
57
+ baseURL: string;
58
+ groupBy: "tag" | "path" | "none";
59
+ schemas: boolean;
60
+ excludeTags?: string[];
61
+ operationIds?: {
62
+ overrides?: Record<string, string>;
63
+ transform?: (cleaned: string, context: OperationContext) => string;
64
+ };
65
+ }
66
+ /**
67
+ * Merge CLI flags with config file values. CLI flags take precedence.
68
+ */
69
+ declare function resolveConfig(cliFlags: Partial<OpenAPIConfig> & {
70
+ from?: string;
71
+ }, configFile?: Partial<OpenAPIConfig>): OpenAPIConfig;
72
+ /**
73
+ * Load config from openapi.config.ts if it exists.
74
+ */
75
+ declare function loadConfigFile(cwd: string): Promise<Partial<OpenAPIConfig> | undefined>;
76
+ /**
77
+ * Type helper for config files.
78
+ */
79
+ declare function defineConfig(config: Partial<OpenAPIConfig>): Partial<OpenAPIConfig>;
80
+ interface GeneratedFile {
81
+ path: string;
82
+ content: string;
83
+ }
84
+ interface GenerateOptions {
85
+ schemas?: boolean;
86
+ baseURL?: string;
43
87
  }
44
- declare function normalizeOperationId(operationId: string, method: HttpMethod, path: string, config?: NormalizerConfig): string;
88
+ interface WriteResult {
89
+ written: number;
90
+ skipped: number;
91
+ removed: number;
92
+ filesWritten: string[];
93
+ }
94
+ /**
95
+ * Write generated files to disk, only updating files whose content changed.
96
+ */
97
+ declare function writeIncremental(files: GeneratedFile[], outputDir: string, options?: {
98
+ clean?: boolean;
99
+ dryRun?: boolean;
100
+ }): Promise<WriteResult>;
101
+ /**
102
+ * Generate a typed SDK from an OpenAPI spec.
103
+ * This is the main programmatic API.
104
+ */
105
+ declare function generateFromOpenAPI(config: OpenAPIConfig & {
106
+ dryRun?: boolean;
107
+ }): Promise<WriteResult>;
108
+ type GroupByStrategy = "tag" | "path" | "none";
109
+ interface GroupOptions {
110
+ excludeTags?: string[];
111
+ }
112
+ declare function groupOperations(operations: ParsedOperation[], strategy: GroupByStrategy, options?: GroupOptions): ParsedResource[];
113
+ declare function sanitizeIdentifier(name: string): string;
114
+ /**
115
+ * Generate all SDK files from a parsed spec.
116
+ */
117
+ declare function generateAll(spec: ParsedSpec, options?: GenerateOptions): GeneratedFile[];
118
+ /**
119
+ * Generate the main client.ts file.
120
+ */
121
+ declare function generateClient(resources: ParsedResource[], config: {
122
+ baseURL?: string;
123
+ }): GeneratedFile;
124
+ /**
125
+ * Convert a JSON Schema object to a TypeScript type expression string.
126
+ */
127
+ declare function jsonSchemaToTS(schema: Record<string, unknown>, namedSchemas: Map<string, string>): string;
128
+ /**
129
+ * Sanitize a name to be a valid TypeScript identifier (PascalCase for types).
130
+ * Strips invalid chars, preserves casing of segments, prefixes with _ if starts with digit.
131
+ */
132
+ declare function sanitizeTypeName(name: string): string;
133
+ /**
134
+ * Generate a full TypeScript interface declaration from a named schema.
135
+ */
136
+ declare function generateInterface(name: string, schema: Record<string, unknown>, namedSchemas: Map<string, string>): string;
137
+ declare function isValidIdentifier(name: string): boolean;
138
+ /**
139
+ * Convert a JSON Schema object to a Zod expression string.
140
+ */
141
+ declare function jsonSchemaToZod(schema: Record<string, unknown>, namedSchemas: Map<string, string>): string;
142
+ /**
143
+ * Generate resource SDK files for all resources + a barrel index.
144
+ */
145
+ declare function generateResources(resources: ParsedResource[]): GeneratedFile[];
146
+ /**
147
+ * Generate Zod schema files for all resources + barrel index.
148
+ */
149
+ declare function generateSchemas(resources: ParsedResource[], schemas: ParsedSchema[]): GeneratedFile[];
150
+ /**
151
+ * Generate types files for all resources + a barrel index.
152
+ */
153
+ declare function generateTypes(resources: ParsedResource[], schemas: ParsedSchema[]): GeneratedFile[];
45
154
  declare function parseOpenAPI(spec: Record<string, unknown>): {
46
155
  operations: ParsedOperation[];
47
156
  schemas: ParsedSchema[];
@@ -52,4 +161,9 @@ interface ResolveOptions {
52
161
  }
53
162
  declare function resolveRef(ref: string, document: Record<string, unknown>, options: ResolveOptions): Record<string, unknown>;
54
163
  declare function resolveSchema(schema: Record<string, unknown>, document: Record<string, unknown>, options: ResolveOptions, resolving?: Set<string>): Record<string, unknown>;
55
- export { sanitizeIdentifier, resolveSchema, resolveRef, parseOpenAPI, normalizeOperationId, groupOperations, ResolveOptions, ParsedSpec, ParsedSchema, ParsedResource, ParsedParameter, ParsedOperation, NormalizerConfig, HttpMethod, GroupByStrategy };
164
+ /**
165
+ * Load an OpenAPI spec from a file path or URL.
166
+ * Auto-detects JSON vs YAML from file extension or content.
167
+ */
168
+ declare function loadSpec(source: string): Promise<Record<string, unknown>>;
169
+ export { writeIncremental, sanitizeTypeName, sanitizeIdentifier, resolveSchema, resolveRef, resolveConfig, parseOpenAPI, normalizeOperationId, loadSpec, loadConfigFile, jsonSchemaToZod, jsonSchemaToTS, isValidIdentifier, groupOperations, generateTypes, generateSchemas, generateResources, generateInterface, generateFromOpenAPI, generateClient, generateAll, defineConfig, WriteResult, ResolveOptions, ParsedSpec, ParsedSchema, ParsedResource, ParsedParameter, ParsedOperation, OperationContext, OpenAPIConfig, NormalizerConfig, HttpMethod, GroupOptions, GroupByStrategy, GeneratedFile, GenerateOptions };