@vertz/openapi 0.1.0 → 0.1.2

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,245 @@
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
+ The generated SDK uses `@vertz/fetch` under the hood. Install it in the project that consumes the SDK:
108
+
109
+ ```bash
110
+ bun add @vertz/fetch
111
+ ```
112
+
113
+ ```ts
114
+ import { createClient } from './generated/client';
115
+ import { isOk } from '@vertz/fetch';
116
+
117
+ const api = createClient({ baseURL: 'https://api.example.com' });
118
+
119
+ // Fully typed — params, body, and response types are inferred
120
+ // Returns FetchResponse<T> (Result type) — use isOk/isErr to handle
121
+ const result = await api.tasks.list();
122
+ if (isOk(result)) {
123
+ console.log(result.data); // typed as Task[]
124
+ }
125
+
126
+ // All FetchClient features available: auth strategies, retries, hooks
127
+ const api = createClient({
128
+ baseURL: 'https://api.example.com',
129
+ authStrategies: [{ type: 'bearer', token: 'my-token' }],
130
+ retry: { retries: 3 },
131
+ });
132
+ ```
133
+
134
+ ## Custom Operation ID Normalization
135
+
136
+ The generator auto-cleans operationIds (strips controller prefixes, detects CRUD patterns). For more control:
137
+
138
+ ### Static overrides
139
+
140
+ ```ts
141
+ export default defineConfig({
142
+ source: './openapi.json',
143
+ operationIds: {
144
+ overrides: {
145
+ listTasks: 'fetchAll',
146
+ getTask: 'findById',
147
+ },
148
+ },
149
+ });
150
+ ```
151
+
152
+ ### Transform function
153
+
154
+ The transform receives the auto-cleaned name and a full `OperationContext`:
155
+
156
+ ```ts
157
+ export default defineConfig({
158
+ source: './openapi.json',
159
+ operationIds: {
160
+ transform: (cleaned, ctx) => {
161
+ // ctx.operationId — raw operationId from the spec
162
+ // ctx.method — GET, POST, PUT, DELETE, PATCH
163
+ // ctx.path — /v1/tasks/{id}
164
+ // ctx.tags — ['tasks']
165
+ // ctx.hasBody — whether the operation has a request body
166
+ return cleaned;
167
+ },
168
+ },
169
+ });
170
+ ```
171
+
172
+ ## Framework Adapters
173
+
174
+ Built-in adapters handle operationId quirks for common backend frameworks. Import from `@vertz/openapi/adapters`:
175
+
176
+ ### FastAPI
177
+
178
+ FastAPI generates operationIds like `list_tasks_tasks_get` (function name + route + verb). The adapter strips the route+verb suffix and handles API version prefixes.
179
+
180
+ ```ts
181
+ import { defineConfig } from '@vertz/openapi';
182
+ import { fastapi } from '@vertz/openapi/adapters';
183
+
184
+ export default defineConfig({
185
+ source: './openapi.json',
186
+ output: './src/generated',
187
+ operationIds: fastapi(),
188
+ });
189
+ ```
190
+
191
+ | FastAPI operationId | Path | Result |
192
+ | ---------------------------- | ---------------- | ---------------- |
193
+ | `list_tasks_tasks_get` | `/tasks` | `list_tasks` |
194
+ | `get_user_v1_users__id__get` | `/v1/users/{id}` | `get_user_v1` |
195
+ | `create_task_v2_tasks_post` | `/v2/tasks` | `create_task_v2` |
196
+
197
+ ### NestJS
198
+
199
+ NestJS (`@nestjs/swagger`) generates operationIds like `TasksController_findAll`. The adapter strips the Controller prefix.
200
+
201
+ ```ts
202
+ import { defineConfig } from '@vertz/openapi';
203
+ import { nestjs } from '@vertz/openapi/adapters';
204
+
205
+ export default defineConfig({
206
+ source: './openapi.json',
207
+ output: './src/generated',
208
+ operationIds: nestjs(),
209
+ });
210
+ ```
211
+
212
+ | NestJS operationId | Result |
213
+ | ------------------------- | --------- |
214
+ | `TasksController_findAll` | `findAll` |
215
+ | `UsersController.getById` | `getById` |
216
+
217
+ ### Writing a Custom Adapter
218
+
219
+ An adapter is just a function that returns `{ transform }`:
220
+
221
+ ```ts
222
+ function myFramework() {
223
+ return {
224
+ transform: (cleaned, ctx) => {
225
+ // Your logic here using ctx.operationId, ctx.method, ctx.path, etc.
226
+ return cleaned;
227
+ },
228
+ };
229
+ }
230
+
231
+ export default defineConfig({
232
+ operationIds: myFramework(),
233
+ });
234
+ ```
235
+
236
+ ## Incremental Writes
237
+
238
+ 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.
239
+
240
+ ## Supported Specs
241
+
242
+ - OpenAPI 3.0.x
243
+ - OpenAPI 3.1.x
244
+ - JSON and YAML formats
245
+ - 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-h8tb765a.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
@@ -7,6 +7,7 @@ interface ParsedSpec {
7
7
  };
8
8
  resources: ParsedResource[];
9
9
  schemas: ParsedSchema[];
10
+ securitySchemes: ParsedSecurityScheme[];
10
11
  }
11
12
  interface ParsedResource {
12
13
  name: string;
@@ -24,6 +25,11 @@ interface ParsedOperation {
24
25
  response?: ParsedSchema;
25
26
  responseStatus: number;
26
27
  tags: string[];
28
+ security?: OperationSecurity;
29
+ }
30
+ interface OperationSecurity {
31
+ required: boolean;
32
+ schemes: string[];
27
33
  }
28
34
  interface ParsedParameter {
29
35
  name: string;
@@ -34,17 +40,160 @@ interface ParsedSchema {
34
40
  name?: string;
35
41
  jsonSchema: Record<string, unknown>;
36
42
  }
37
- type GroupByStrategy = "tag" | "path" | "none";
38
- declare function groupOperations(operations: ParsedOperation[], strategy: GroupByStrategy): ParsedResource[];
39
- declare function sanitizeIdentifier(name: string): string;
43
+ type ParsedSecurityScheme = {
44
+ type: "bearer";
45
+ name: string;
46
+ description?: string;
47
+ } | {
48
+ type: "basic";
49
+ name: string;
50
+ description?: string;
51
+ } | {
52
+ type: "apiKey";
53
+ name: string;
54
+ in: "header" | "query" | "cookie";
55
+ paramName: string;
56
+ description?: string;
57
+ } | {
58
+ type: "oauth2";
59
+ name: string;
60
+ flows: ParsedOAuthFlows;
61
+ description?: string;
62
+ };
63
+ interface ParsedOAuthFlows {
64
+ authorizationCode?: {
65
+ authorizationUrl: string;
66
+ tokenUrl: string;
67
+ scopes: Record<string, string>;
68
+ };
69
+ clientCredentials?: {
70
+ tokenUrl: string;
71
+ scopes: Record<string, string>;
72
+ };
73
+ }
74
+ interface OperationContext {
75
+ /** Raw operationId from the spec */
76
+ operationId: string;
77
+ /** HTTP method (GET, POST, etc.) */
78
+ method: HttpMethod;
79
+ /** Route path (e.g. /v1/tasks/{id}) */
80
+ path: string;
81
+ /** Tags from the operation */
82
+ tags: string[];
83
+ /** Whether the operation has a request body */
84
+ hasBody: boolean;
85
+ }
40
86
  interface NormalizerConfig {
41
87
  overrides?: Record<string, string>;
42
- transform?: (cleaned: string, original: string) => string;
88
+ transform?: (cleaned: string, context: OperationContext) => string;
43
89
  }
44
- declare function normalizeOperationId(operationId: string, method: HttpMethod, path: string, config?: NormalizerConfig): string;
90
+ declare function normalizeOperationId(operationId: string, method: HttpMethod, path: string, config?: NormalizerConfig, context?: OperationContext): string;
91
+ interface OpenAPIConfig {
92
+ source: string;
93
+ output: string;
94
+ baseURL: string;
95
+ groupBy: "tag" | "path" | "none";
96
+ schemas: boolean;
97
+ excludeTags?: string[];
98
+ operationIds?: {
99
+ overrides?: Record<string, string>;
100
+ transform?: (cleaned: string, context: OperationContext) => string;
101
+ };
102
+ }
103
+ /**
104
+ * Merge CLI flags with config file values. CLI flags take precedence.
105
+ */
106
+ declare function resolveConfig(cliFlags: Partial<OpenAPIConfig> & {
107
+ from?: string;
108
+ }, configFile?: Partial<OpenAPIConfig>): OpenAPIConfig;
109
+ /**
110
+ * Load config from openapi.config.ts if it exists.
111
+ */
112
+ declare function loadConfigFile(cwd: string): Promise<Partial<OpenAPIConfig> | undefined>;
113
+ /**
114
+ * Type helper for config files.
115
+ */
116
+ declare function defineConfig(config: Partial<OpenAPIConfig>): Partial<OpenAPIConfig>;
117
+ interface GeneratedFile {
118
+ path: string;
119
+ content: string;
120
+ }
121
+ interface GenerateOptions {
122
+ schemas?: boolean;
123
+ baseURL?: string;
124
+ }
125
+ interface WriteResult {
126
+ written: number;
127
+ skipped: number;
128
+ removed: number;
129
+ filesWritten: string[];
130
+ }
131
+ /**
132
+ * Write generated files to disk, only updating files whose content changed.
133
+ */
134
+ declare function writeIncremental(files: GeneratedFile[], outputDir: string, options?: {
135
+ clean?: boolean;
136
+ dryRun?: boolean;
137
+ }): Promise<WriteResult>;
138
+ /**
139
+ * Generate a typed SDK from an OpenAPI spec.
140
+ * This is the main programmatic API.
141
+ */
142
+ declare function generateFromOpenAPI(config: OpenAPIConfig & {
143
+ dryRun?: boolean;
144
+ }): Promise<WriteResult>;
145
+ type GroupByStrategy = "tag" | "path" | "none";
146
+ interface GroupOptions {
147
+ excludeTags?: string[];
148
+ }
149
+ declare function groupOperations(operations: ParsedOperation[], strategy: GroupByStrategy, options?: GroupOptions): ParsedResource[];
150
+ declare function sanitizeIdentifier(name: string): string;
151
+ /**
152
+ * Generate all SDK files from a parsed spec.
153
+ */
154
+ declare function generateAll(spec: ParsedSpec, options?: GenerateOptions): GeneratedFile[];
155
+ /**
156
+ * Generate the main client.ts file.
157
+ * Uses @vertz/fetch FetchClient instead of hand-rolling HTTP methods.
158
+ */
159
+ declare function generateClient(resources: ParsedResource[], config: {
160
+ baseURL?: string;
161
+ securitySchemes?: ParsedSecurityScheme[];
162
+ }): GeneratedFile;
163
+ /**
164
+ * Convert a JSON Schema object to a TypeScript type expression string.
165
+ */
166
+ declare function jsonSchemaToTS(schema: Record<string, unknown>, namedSchemas: Map<string, string>): string;
167
+ /**
168
+ * Sanitize a name to be a valid TypeScript identifier (PascalCase for types).
169
+ * Strips invalid chars, preserves casing of segments, prefixes with _ if starts with digit.
170
+ */
171
+ declare function sanitizeTypeName(name: string): string;
172
+ /**
173
+ * Generate a full TypeScript interface declaration from a named schema.
174
+ */
175
+ declare function generateInterface(name: string, schema: Record<string, unknown>, namedSchemas: Map<string, string>): string;
176
+ declare function isValidIdentifier(name: string): boolean;
177
+ /**
178
+ * Convert a JSON Schema object to a Zod expression string.
179
+ */
180
+ declare function jsonSchemaToZod(schema: Record<string, unknown>, namedSchemas: Map<string, string>): string;
181
+ /**
182
+ * Generate resource SDK files for all resources + a barrel index.
183
+ */
184
+ declare function generateResources(resources: ParsedResource[]): GeneratedFile[];
185
+ /**
186
+ * Generate Zod schema files for all resources + barrel index.
187
+ */
188
+ declare function generateSchemas(resources: ParsedResource[], schemas: ParsedSchema[]): GeneratedFile[];
189
+ /**
190
+ * Generate types files for all resources + a barrel index.
191
+ */
192
+ declare function generateTypes(resources: ParsedResource[], schemas: ParsedSchema[]): GeneratedFile[];
45
193
  declare function parseOpenAPI(spec: Record<string, unknown>): {
46
194
  operations: ParsedOperation[];
47
195
  schemas: ParsedSchema[];
196
+ securitySchemes: ParsedSecurityScheme[];
48
197
  version: "3.0" | "3.1";
49
198
  };
50
199
  interface ResolveOptions {
@@ -52,4 +201,9 @@ interface ResolveOptions {
52
201
  }
53
202
  declare function resolveRef(ref: string, document: Record<string, unknown>, options: ResolveOptions): Record<string, unknown>;
54
203
  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 };
204
+ /**
205
+ * Load an OpenAPI spec from a file path or URL.
206
+ * Auto-detects JSON vs YAML from file extension or content.
207
+ */
208
+ declare function loadSpec(source: string): Promise<Record<string, unknown>>;
209
+ 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 };