klasik 1.0.12 โ†’ 1.0.14

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
@@ -6,6 +6,9 @@ Download OpenAPI specifications from remote URLs and generate TypeScript clients
6
6
 
7
7
  - ๐Ÿ“ฅ **Download OpenAPI specs** from remote URLs or local files
8
8
  - ๐Ÿ“ **Multiple input formats** - HTTP/HTTPS URLs, file:// URLs, absolute/relative paths
9
+ - ๐Ÿ“„ **JSON and YAML support** - Automatically parse and handle both formats
10
+ - ๐Ÿ” **Authentication support** - Custom headers including Bearer tokens and API keys
11
+ - ๐Ÿ”— **External reference resolution** - Automatically download referenced schema files (`$ref`)
9
12
  - ๐Ÿ”„ **Automatic transformation** - Converts API responses to class instances using class-transformer
10
13
  - ๐ŸŽฏ **Type-safe** - Full TypeScript support with decorators
11
14
  - ๐Ÿ› ๏ธ **Configurable** - Custom headers, timeouts, and templates
@@ -25,17 +28,23 @@ npm install klasik
25
28
  Generate a TypeScript client from a remote OpenAPI spec:
26
29
 
27
30
  ```bash
31
+ # JSON spec
28
32
  npx klasik generate \
29
33
  --url https://raw.githubusercontent.com/kubernetes/kubernetes/master/api/openapi-spec/swagger.json \
30
34
  --output ./k8s-client
35
+
36
+ # YAML spec
37
+ npx klasik generate \
38
+ --url https://api.example.com/openapi.yaml \
39
+ --output ./client
31
40
  ```
32
41
 
33
- Generate from a local OpenAPI spec file:
42
+ Generate from a local OpenAPI spec file (JSON or YAML):
34
43
 
35
44
  ```bash
36
45
  # Absolute path
37
46
  npx klasik generate \
38
- --url /path/to/openapi.json \
47
+ --url /path/to/openapi.yaml \
39
48
  --output ./client
40
49
 
41
50
  # Relative path
@@ -45,16 +54,22 @@ npx klasik generate \
45
54
 
46
55
  # file:// URL
47
56
  npx klasik generate \
48
- --url file:///path/to/openapi.json \
57
+ --url file:///path/to/openapi.yaml \
49
58
  --output ./client
50
59
  ```
51
60
 
52
- Download an OpenAPI spec without generating:
61
+ Download an OpenAPI spec without generating (supports JSON and YAML):
53
62
 
54
63
  ```bash
64
+ # Download JSON spec
55
65
  npx klasik download \
56
66
  --url https://api.example.com/openapi.json \
57
67
  --output ./specs/api-spec.json
68
+
69
+ # Download YAML spec
70
+ npx klasik download \
71
+ --url https://api.example.com/openapi.yaml \
72
+ --output ./specs/api-spec.yaml
58
73
  ```
59
74
 
60
75
  ### Programmatic Usage
@@ -81,6 +96,9 @@ Generate a TypeScript client from an OpenAPI spec (remote URL or local file).
81
96
  - Supports: `https://...`, `http://...`, `file://...`, `/absolute/path`, `./relative/path`
82
97
  - `-o, --output <dir>` - Output directory for generated client code (required)
83
98
  - `-H, --header <header...>` - Custom headers for HTTP requests (format: "Key: Value")
99
+ - Can be used multiple times for multiple headers
100
+ - Perfect for authorization: `--header "Authorization: Bearer token"`
101
+ - `-r, --resolve-refs` - Resolve and download external `$ref` references
84
102
  - `-t, --template <dir>` - Custom template directory
85
103
  - `-k, --keep-spec` - Keep the downloaded spec file after generation
86
104
  - `--timeout <ms>` - Request timeout in milliseconds for HTTP requests (default: 30000)
@@ -88,13 +106,27 @@ Generate a TypeScript client from an OpenAPI spec (remote URL or local file).
88
106
  **Examples:**
89
107
 
90
108
  ```bash
91
- # From remote URL
109
+ # From remote URL with authorization
92
110
  klasik generate \
93
111
  --url https://api.example.com/openapi.json \
94
112
  --output ./client \
95
113
  --header "Authorization: Bearer token123" \
96
114
  --timeout 60000
97
115
 
116
+ # With external reference resolution
117
+ klasik generate \
118
+ --url https://api.example.com/openapi.json \
119
+ --output ./client \
120
+ --header "Authorization: Bearer token123" \
121
+ --resolve-refs
122
+
123
+ # Multiple headers
124
+ klasik generate \
125
+ --url https://api.example.com/openapi.json \
126
+ --output ./client \
127
+ --header "Authorization: Bearer token" \
128
+ --header "X-API-Version: v1"
129
+
98
130
  # From local file
99
131
  klasik generate \
100
132
  --url ./my-spec.json \
@@ -114,15 +146,24 @@ Download an OpenAPI spec from a remote URL without generating code.
114
146
  - `-u, --url <url>` - Remote URL to download the OpenAPI spec from (required)
115
147
  - `-o, --output <file>` - Output file path for the downloaded spec (required)
116
148
  - `-H, --header <header...>` - Custom headers for the request
149
+ - `-r, --resolve-refs` - Resolve and download external `$ref` references
117
150
  - `--timeout <ms>` - Request timeout in milliseconds (default: 30000)
118
151
 
119
- **Example:**
152
+ **Examples:**
120
153
 
121
154
  ```bash
155
+ # Download spec only
122
156
  klasik download \
123
157
  --url https://api.example.com/openapi.json \
124
158
  --output ./specs/api-spec.json \
125
159
  --header "Authorization: Bearer token123"
160
+
161
+ # Download spec with all external references
162
+ klasik download \
163
+ --url https://api.example.com/openapi.json \
164
+ --output ./specs/api-spec.json \
165
+ --header "Authorization: Bearer token123" \
166
+ --resolve-refs
126
167
  ```
127
168
 
128
169
  ## Programmatic API
@@ -141,6 +182,7 @@ await generator.generate({
141
182
  'Authorization': 'Bearer token123',
142
183
  'X-Custom-Header': 'value'
143
184
  },
185
+ resolveReferences: true, // Download external $ref files
144
186
  templateDir: './custom-templates', // Optional
145
187
  keepSpec: true, // Keep downloaded spec file
146
188
  timeout: 60000 // Request timeout in ms
@@ -160,6 +202,7 @@ const specPath = await downloader.download({
160
202
  headers: {
161
203
  'Authorization': 'Bearer token123'
162
204
  },
205
+ resolveReferences: true, // Download external $ref files
163
206
  timeout: 30000
164
207
  });
165
208
 
@@ -235,6 +278,90 @@ const user = response.data; // user is instanceof User โœ…
235
278
  console.log(user.name); // Fully typed!
236
279
  ```
237
280
 
281
+ ## External Reference Resolution
282
+
283
+ Klasik can automatically resolve and download external `$ref` references in your OpenAPI specs. This is useful when your spec splits schemas across multiple files.
284
+
285
+ ### How It Works
286
+
287
+ When you enable `--resolve-refs` (CLI) or `resolveReferences: true` (programmatic), klasik will:
288
+
289
+ 1. **Parse the main spec** for external `$ref` references
290
+ 2. **Download all referenced files** preserving directory structure
291
+ 3. **Recursively resolve nested references** in downloaded files
292
+ 4. **Use the same authentication** for all downloads
293
+
294
+ ### Example OpenAPI Spec with External References
295
+
296
+ ```json
297
+ {
298
+ "openapi": "3.0.0",
299
+ "paths": {
300
+ "/users": {
301
+ "get": {
302
+ "responses": {
303
+ "200": {
304
+ "content": {
305
+ "application/json": {
306
+ "schema": {
307
+ "$ref": "./schemas/users-orgs.yaml#/components/schemas/Org"
308
+ }
309
+ }
310
+ }
311
+ }
312
+ }
313
+ }
314
+ }
315
+ }
316
+ }
317
+ ```
318
+
319
+ With `--resolve-refs`, klasik will automatically download `./schemas/users-orgs.yaml` and any files it references.
320
+
321
+ ### Output Structure
322
+
323
+ Referenced files are downloaded preserving the directory structure:
324
+
325
+ ```
326
+ output/
327
+ โ”œโ”€โ”€ openapi-spec.json # Main spec
328
+ โ””โ”€โ”€ schemas/
329
+ โ”œโ”€โ”€ users-orgs.yaml # Referenced schema
330
+ โ””โ”€โ”€ common/
331
+ โ””โ”€โ”€ types.yaml # Nested reference
332
+ ```
333
+
334
+ ### Supported Reference Types
335
+
336
+ - **Relative paths**: `./schemas/user.yaml`, `../common/types.yaml`
337
+ - **Remote URLs**: `https://api.example.com/schemas/user.yaml`
338
+ - **Mixed**: Main spec from HTTPS, references can be relative or absolute
339
+
340
+ ### CLI Example
341
+
342
+ ```bash
343
+ # Download spec with all external references
344
+ npx klasik generate \
345
+ --url https://api.example.com/openapi.json \
346
+ --output ./client \
347
+ --header "Authorization: Bearer token" \
348
+ --resolve-refs
349
+ ```
350
+
351
+ ### Programmatic Example
352
+
353
+ ```typescript
354
+ import { K8sClientGenerator } from 'klasik';
355
+
356
+ await new K8sClientGenerator().generate({
357
+ specUrl: 'https://api.example.com/openapi.json',
358
+ outputDir: './client',
359
+ headers: { 'Authorization': 'Bearer token' },
360
+ resolveReferences: true,
361
+ keepSpec: true // Keep all downloaded files
362
+ });
363
+ ```
364
+
238
365
  ## Advanced Configuration
239
366
 
240
367
  ### Custom Error Handling
@@ -0,0 +1,221 @@
1
+ # Klasik Usage Examples
2
+
3
+ ## JSON and YAML Support
4
+
5
+ Klasik automatically detects and parses both JSON and YAML OpenAPI specifications. The original format is preserved when downloading.
6
+
7
+ ### CLI Examples
8
+
9
+ ```bash
10
+ # JSON spec
11
+ npx klasik generate \
12
+ --url https://api.example.com/openapi.json \
13
+ --output ./client
14
+
15
+ # YAML spec
16
+ npx klasik generate \
17
+ --url https://api.example.com/openapi.yaml \
18
+ --output ./client
19
+
20
+ # Local YAML file
21
+ npx klasik generate \
22
+ --url ./specs/openapi.yaml \
23
+ --output ./client
24
+
25
+ # Download and keep YAML format
26
+ npx klasik download \
27
+ --url https://api.example.com/openapi.yaml \
28
+ --output ./specs/api-spec.yaml \
29
+ --resolve-refs
30
+ ```
31
+
32
+ ### Programmatic Examples
33
+
34
+ ```typescript
35
+ import { K8sClientGenerator } from 'klasik';
36
+
37
+ // Works with both JSON and YAML
38
+ await new K8sClientGenerator().generate({
39
+ specUrl: 'https://api.example.com/openapi.yaml',
40
+ outputDir: './client'
41
+ });
42
+ ```
43
+
44
+ ### Mixed Formats with References
45
+
46
+ Klasik handles specs where the main file is in one format and referenced files are in another:
47
+
48
+ ```yaml
49
+ # openapi.yaml (main spec in YAML)
50
+ openapi: 3.0.0
51
+ paths:
52
+ /users:
53
+ get:
54
+ responses:
55
+ '200':
56
+ content:
57
+ application/json:
58
+ schema:
59
+ $ref: './schemas/users.json#/components/schemas/User' # JSON reference
60
+ ```
61
+
62
+ Both formats work seamlessly together!
63
+
64
+ ## Using Custom Headers (Authorization)
65
+
66
+ ### CLI Example - Authorization Token
67
+
68
+ ```bash
69
+ # Using Bearer token
70
+ npx klasik generate \
71
+ --url https://api.example.com/openapi.json \
72
+ --output ./client \
73
+ --header "Authorization: Bearer your-token-here"
74
+
75
+ # Using API key
76
+ npx klasik generate \
77
+ --url https://api.example.com/openapi.json \
78
+ --output ./client \
79
+ --header "X-API-Key: your-api-key"
80
+
81
+ # Multiple headers
82
+ npx klasik generate \
83
+ --url https://api.example.com/openapi.json \
84
+ --output ./client \
85
+ --header "Authorization: Bearer your-token" \
86
+ --header "X-Custom-Header: custom-value"
87
+ ```
88
+
89
+ ### Programmatic Example - Authorization Token
90
+
91
+ ```typescript
92
+ import { K8sClientGenerator } from 'klasik';
93
+
94
+ const generator = new K8sClientGenerator();
95
+
96
+ await generator.generate({
97
+ specUrl: 'https://api.example.com/openapi.json',
98
+ outputDir: './client',
99
+ headers: {
100
+ 'Authorization': 'Bearer your-token-here',
101
+ 'X-API-Key': 'your-api-key'
102
+ }
103
+ });
104
+ ```
105
+
106
+ ## Resolving External References
107
+
108
+ ### CLI Example - With Reference Resolution
109
+
110
+ ```bash
111
+ # Download spec and all external $ref files
112
+ npx klasik generate \
113
+ --url https://api.example.com/openapi.json \
114
+ --output ./client \
115
+ --resolve-refs \
116
+ --header "Authorization: Bearer your-token"
117
+
118
+ # Download only (no code generation)
119
+ npx klasik download \
120
+ --url https://api.example.com/openapi.json \
121
+ --output ./specs/api-spec.json \
122
+ --resolve-refs \
123
+ --header "Authorization: Bearer your-token"
124
+ ```
125
+
126
+ ### Programmatic Example - With Reference Resolution
127
+
128
+ ```typescript
129
+ import { K8sClientGenerator } from 'klasik';
130
+
131
+ const generator = new K8sClientGenerator();
132
+
133
+ await generator.generate({
134
+ specUrl: 'https://api.example.com/openapi.json',
135
+ outputDir: './client',
136
+ resolveReferences: true,
137
+ headers: {
138
+ 'Authorization': 'Bearer your-token-here'
139
+ }
140
+ });
141
+ ```
142
+
143
+ ## How Reference Resolution Works
144
+
145
+ When you enable `--resolve-refs` (or `resolveReferences: true`), klasik will:
146
+
147
+ 1. **Parse the main OpenAPI spec** for external `$ref` references like:
148
+ ```yaml
149
+ schema:
150
+ $ref: './schemas/users-orgs.yaml#/components/schemas/Org'
151
+ ```
152
+
153
+ 2. **Download all referenced files** preserving the directory structure:
154
+ ```
155
+ output/
156
+ โ”œโ”€โ”€ openapi-spec.json (main spec)
157
+ โ””โ”€โ”€ schemas/
158
+ โ””โ”€โ”€ users-orgs.yaml (referenced schema)
159
+ ```
160
+
161
+ 3. **Recursively resolve nested references** - if `users-orgs.yaml` has its own `$ref` to other files, those will be downloaded too
162
+
163
+ 4. **Use the same headers** for all downloads (useful when all files require authentication)
164
+
165
+ ### Supported Reference Types
166
+
167
+ - **Relative paths**: `./schemas/user.yaml`, `../common/types.yaml`
168
+ - **Remote URLs**: `https://api.example.com/schemas/user.yaml`
169
+ - **Mixed**: Main spec from HTTPS, references can be relative or absolute
170
+
171
+ ### Example OpenAPI Spec with External References
172
+
173
+ ```json
174
+ {
175
+ "openapi": "3.0.0",
176
+ "info": {
177
+ "title": "My API",
178
+ "version": "1.0.0"
179
+ },
180
+ "paths": {
181
+ "/users": {
182
+ "get": {
183
+ "responses": {
184
+ "200": {
185
+ "content": {
186
+ "application/json": {
187
+ "schema": {
188
+ "$ref": "./schemas/users.yaml#/components/schemas/UserList"
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
194
+ }
195
+ }
196
+ }
197
+ }
198
+ ```
199
+
200
+ When you run with `--resolve-refs`, klasik will automatically download `./schemas/users.yaml`.
201
+
202
+ ## Complete Example
203
+
204
+ ```bash
205
+ # Full workflow with private API
206
+ npx klasik generate \
207
+ --url https://private-api.example.com/v1/openapi.json \
208
+ --output ./generated-client \
209
+ --header "Authorization: Bearer eyJhbGc..." \
210
+ --header "X-API-Version: v1" \
211
+ --resolve-refs \
212
+ --keep-spec \
213
+ --timeout 60000
214
+ ```
215
+
216
+ This will:
217
+ - Download the OpenAPI spec with authorization headers
218
+ - Resolve and download all external schema references
219
+ - Generate the TypeScript client
220
+ - Keep all downloaded specs in the output directory
221
+ - Use a 60-second timeout for all HTTP requests
package/dist/cli.js CHANGED
@@ -51,6 +51,7 @@ program
51
51
  .option('-H, --header <header...>', 'Custom headers for the request (format: "Key: Value")')
52
52
  .option('-t, --template <dir>', 'Custom template directory')
53
53
  .option('-k, --keep-spec', 'Keep the downloaded spec file after generation', false)
54
+ .option('-r, --resolve-refs', 'Resolve and download external $ref references', false)
54
55
  .option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
55
56
  .action(async (options) => {
56
57
  try {
@@ -80,6 +81,7 @@ program
80
81
  headers: Object.keys(headers).length > 0 ? headers : undefined,
81
82
  templateDir: options.template,
82
83
  keepSpec: options.keepSpec,
84
+ resolveReferences: options.resolveRefs,
83
85
  timeout: parseInt(options.timeout, 10),
84
86
  });
85
87
  process.exit(0);
@@ -95,6 +97,7 @@ program
95
97
  .requiredOption('-u, --url <url>', 'Remote URL to download the OpenAPI spec from')
96
98
  .requiredOption('-o, --output <file>', 'Output file path for the downloaded spec')
97
99
  .option('-H, --header <header...>', 'Custom headers for the request (format: "Key: Value")')
100
+ .option('-r, --resolve-refs', 'Resolve and download external $ref references', false)
98
101
  .option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
99
102
  .action(async (options) => {
100
103
  try {
@@ -115,6 +118,7 @@ program
115
118
  url: options.url,
116
119
  outputPath: path.resolve(options.output),
117
120
  headers: Object.keys(headers).length > 0 ? headers : undefined,
121
+ resolveReferences: options.resolveRefs,
118
122
  timeout: parseInt(options.timeout, 10),
119
123
  });
120
124
  process.exit(0);
@@ -28,6 +28,11 @@ export interface K8sClientGeneratorOptions {
28
28
  * @default false
29
29
  */
30
30
  keepSpec?: boolean;
31
+ /**
32
+ * Whether to resolve and download external $ref references
33
+ * @default false
34
+ */
35
+ resolveReferences?: boolean;
31
36
  /**
32
37
  * Request timeout for downloading spec in milliseconds
33
38
  * @default 30000
@@ -46,7 +46,7 @@ class K8sClientGenerator {
46
46
  * Generate TypeScript client from a remote OpenAPI spec URL
47
47
  */
48
48
  async generate(options) {
49
- const { specUrl, outputDir, mode = 'full', headers, templateDir, keepSpec = false, timeout, } = options;
49
+ const { specUrl, outputDir, mode = 'full', headers, templateDir, keepSpec = false, resolveReferences = false, timeout, } = options;
50
50
  let specPath;
51
51
  try {
52
52
  // Step 1: Download the OpenAPI spec
@@ -55,6 +55,7 @@ class K8sClientGenerator {
55
55
  url: specUrl,
56
56
  headers,
57
57
  timeout,
58
+ resolveReferences,
58
59
  // If keepSpec is true, save the spec in the output directory
59
60
  outputPath: keepSpec ? path.join(outputDir, 'openapi-spec.json') : undefined,
60
61
  };
@@ -22,8 +22,19 @@ export interface DownloadOptions {
22
22
  * @default 30000
23
23
  */
24
24
  timeout?: number;
25
+ /**
26
+ * Whether to resolve and download external $ref references
27
+ * @default false
28
+ */
29
+ resolveReferences?: boolean;
30
+ /**
31
+ * Base URL for resolving relative references (used when main spec is from HTTP)
32
+ * If not provided, will be derived from the main spec URL
33
+ */
34
+ baseUrl?: string;
25
35
  }
26
36
  export declare class SpecDownloader {
37
+ private downloadedRefs;
27
38
  /**
28
39
  * Download an OpenAPI specification from a URL or load from a local file
29
40
  * @param options Download options
@@ -43,11 +54,31 @@ export declare class SpecDownloader {
43
54
  */
44
55
  private generateTempPath;
45
56
  /**
46
- * Validate that the downloaded content is an OpenAPI spec
57
+ * Validate that the parsed content is an OpenAPI spec
47
58
  */
48
59
  private validateSpec;
49
60
  /**
50
61
  * Clean up temporary files
51
62
  */
52
63
  cleanupTemp(): void;
64
+ /**
65
+ * Get base URL from a full URL (removes the filename)
66
+ */
67
+ private getBaseUrl;
68
+ /**
69
+ * Resolve and download all external $ref references in the spec
70
+ */
71
+ private resolveExternalRefs;
72
+ /**
73
+ * Find all external $ref values in the spec
74
+ */
75
+ private findExternalRefs;
76
+ /**
77
+ * Check if a $ref is external (references another file)
78
+ */
79
+ private isExternalRef;
80
+ /**
81
+ * Download a single reference file
82
+ */
83
+ private downloadReference;
53
84
  }
@@ -40,17 +40,27 @@ exports.SpecDownloader = void 0;
40
40
  const axios_1 = __importDefault(require("axios"));
41
41
  const fs = __importStar(require("fs"));
42
42
  const path = __importStar(require("path"));
43
+ const yaml = __importStar(require("js-yaml"));
43
44
  class SpecDownloader {
45
+ constructor() {
46
+ this.downloadedRefs = new Set();
47
+ }
44
48
  /**
45
49
  * Download an OpenAPI specification from a URL or load from a local file
46
50
  * @param options Download options
47
51
  * @returns Path to the spec file
48
52
  */
49
53
  async download(options) {
50
- const { url, outputPath, headers, timeout = 30000 } = options;
54
+ const { url, outputPath, headers, timeout = 30000, resolveReferences = false, baseUrl } = options;
55
+ // Reset downloaded refs tracking for each new download
56
+ this.downloadedRefs.clear();
51
57
  // Check if it's a local file path
52
58
  if (this.isLocalFile(url)) {
53
- return this.loadLocalFile(url, outputPath);
59
+ const specPath = await this.loadLocalFile(url, outputPath);
60
+ if (resolveReferences) {
61
+ await this.resolveExternalRefs(specPath, headers, timeout, baseUrl || path.dirname(specPath));
62
+ }
63
+ return specPath;
54
64
  }
55
65
  // Otherwise, download from HTTP/HTTPS
56
66
  console.log(`Downloading OpenAPI spec from ${url}...`);
@@ -69,25 +79,39 @@ class SpecDownloader {
69
79
  }
70
80
  // Parse and validate the spec
71
81
  let specContent;
82
+ let parsedSpec;
72
83
  if (typeof response.data === 'string') {
73
- // Try to parse as JSON to validate
84
+ // Try to parse as JSON first
74
85
  try {
75
- const parsed = JSON.parse(response.data);
76
- specContent = JSON.stringify(parsed, null, 2);
86
+ parsedSpec = JSON.parse(response.data);
87
+ specContent = JSON.stringify(parsedSpec, null, 2);
77
88
  }
78
89
  catch {
79
- // If not JSON, might be YAML - save as-is
80
- specContent = response.data;
90
+ // If not JSON, try YAML
91
+ try {
92
+ parsedSpec = yaml.load(response.data);
93
+ // Keep YAML format if input was YAML
94
+ specContent = response.data;
95
+ }
96
+ catch {
97
+ throw new Error('Failed to parse spec: not valid JSON or YAML');
98
+ }
81
99
  }
82
100
  }
83
101
  else {
102
+ parsedSpec = response.data;
84
103
  specContent = JSON.stringify(response.data, null, 2);
85
104
  }
86
105
  // Validate it's an OpenAPI spec
87
- this.validateSpec(specContent);
106
+ this.validateSpec(parsedSpec);
88
107
  // Write to file
89
108
  fs.writeFileSync(specPath, specContent, 'utf-8');
90
109
  console.log(`OpenAPI spec downloaded successfully to ${specPath}`);
110
+ // Resolve references if requested
111
+ if (resolveReferences) {
112
+ const resolvedBaseUrl = baseUrl || this.getBaseUrl(url);
113
+ await this.resolveExternalRefs(specPath, headers, timeout, resolvedBaseUrl);
114
+ }
91
115
  return specPath;
92
116
  }
93
117
  catch (error) {
@@ -140,24 +164,29 @@ class SpecDownloader {
140
164
  }
141
165
  // Read the file
142
166
  const content = fs.readFileSync(resolvedPath, 'utf-8');
167
+ // Parse the content (JSON or YAML)
168
+ let parsedSpec;
169
+ try {
170
+ parsedSpec = JSON.parse(content);
171
+ }
172
+ catch {
173
+ try {
174
+ parsedSpec = yaml.load(content);
175
+ }
176
+ catch {
177
+ throw new Error(`Failed to parse spec at ${resolvedPath}: not valid JSON or YAML`);
178
+ }
179
+ }
143
180
  // Validate it's an OpenAPI spec
144
- this.validateSpec(content);
181
+ this.validateSpec(parsedSpec);
145
182
  // If outputPath is provided, copy to that location
146
183
  if (outputPath) {
147
184
  const dir = path.dirname(outputPath);
148
185
  if (!fs.existsSync(dir)) {
149
186
  fs.mkdirSync(dir, { recursive: true });
150
187
  }
151
- // Parse and format if it's JSON
152
- let formattedContent = content;
153
- try {
154
- const parsed = JSON.parse(content);
155
- formattedContent = JSON.stringify(parsed, null, 2);
156
- }
157
- catch {
158
- // Not JSON, keep original
159
- }
160
- fs.writeFileSync(outputPath, formattedContent, 'utf-8');
188
+ // Keep the original format (JSON or YAML)
189
+ fs.writeFileSync(outputPath, content, 'utf-8');
161
190
  console.log(`OpenAPI spec copied to ${outputPath}`);
162
191
  return outputPath;
163
192
  }
@@ -175,28 +204,22 @@ class SpecDownloader {
175
204
  return path.join(process.cwd(), '.tmp', filename);
176
205
  }
177
206
  /**
178
- * Validate that the downloaded content is an OpenAPI spec
207
+ * Validate that the parsed content is an OpenAPI spec
179
208
  */
180
- validateSpec(content) {
181
- try {
182
- const spec = JSON.parse(content);
183
- // Check for OpenAPI version
184
- if (!spec.openapi && !spec.swagger) {
185
- throw new Error('Invalid OpenAPI spec: missing "openapi" or "swagger" field');
186
- }
187
- // Check for required fields
188
- if (!spec.info) {
189
- throw new Error('Invalid OpenAPI spec: missing "info" field');
190
- }
191
- if (!spec.paths && !spec.components) {
192
- throw new Error('Invalid OpenAPI spec: must have either "paths" or "components"');
193
- }
209
+ validateSpec(spec) {
210
+ if (!spec || typeof spec !== 'object') {
211
+ throw new Error('Invalid OpenAPI spec: not a valid object');
194
212
  }
195
- catch (error) {
196
- if (error instanceof SyntaxError) {
197
- throw new Error('Invalid OpenAPI spec: not valid JSON');
198
- }
199
- throw error;
213
+ // Check for OpenAPI version
214
+ if (!spec.openapi && !spec.swagger) {
215
+ throw new Error('Invalid OpenAPI spec: missing "openapi" or "swagger" field');
216
+ }
217
+ // Check for required fields
218
+ if (!spec.info) {
219
+ throw new Error('Invalid OpenAPI spec: missing "info" field');
220
+ }
221
+ if (!spec.paths && !spec.components) {
222
+ throw new Error('Invalid OpenAPI spec: must have either "paths" or "components"');
200
223
  }
201
224
  }
202
225
  /**
@@ -209,5 +232,166 @@ class SpecDownloader {
209
232
  console.log('Cleaned up temporary files');
210
233
  }
211
234
  }
235
+ /**
236
+ * Get base URL from a full URL (removes the filename)
237
+ */
238
+ getBaseUrl(url) {
239
+ try {
240
+ const urlObj = new URL(url);
241
+ const pathParts = urlObj.pathname.split('/');
242
+ pathParts.pop(); // Remove filename
243
+ urlObj.pathname = pathParts.join('/');
244
+ return urlObj.toString();
245
+ }
246
+ catch {
247
+ // If URL parsing fails, return the original URL
248
+ return url;
249
+ }
250
+ }
251
+ /**
252
+ * Resolve and download all external $ref references in the spec
253
+ */
254
+ async resolveExternalRefs(specPath, headers, timeout, baseUrl) {
255
+ console.log('Resolving external references...');
256
+ const content = fs.readFileSync(specPath, 'utf-8');
257
+ let spec;
258
+ try {
259
+ spec = JSON.parse(content);
260
+ }
261
+ catch {
262
+ try {
263
+ spec = yaml.load(content);
264
+ }
265
+ catch {
266
+ console.log('Spec is not valid JSON or YAML, skipping reference resolution');
267
+ return;
268
+ }
269
+ }
270
+ const refs = this.findExternalRefs(spec);
271
+ if (refs.length === 0) {
272
+ console.log('No external references found');
273
+ return;
274
+ }
275
+ console.log(`Found ${refs.length} external reference(s)`);
276
+ const specDir = path.dirname(specPath);
277
+ const isRemoteSpec = !this.isLocalFile(baseUrl || '');
278
+ for (const ref of refs) {
279
+ await this.downloadReference(ref, specDir, baseUrl, isRemoteSpec, headers, timeout);
280
+ }
281
+ console.log('All external references resolved successfully');
282
+ }
283
+ /**
284
+ * Find all external $ref values in the spec
285
+ */
286
+ findExternalRefs(obj, refs = []) {
287
+ if (typeof obj !== 'object' || obj === null) {
288
+ return refs;
289
+ }
290
+ if (Array.isArray(obj)) {
291
+ for (const item of obj) {
292
+ this.findExternalRefs(item, refs);
293
+ }
294
+ return refs;
295
+ }
296
+ for (const [key, value] of Object.entries(obj)) {
297
+ if (key === '$ref' && typeof value === 'string') {
298
+ // Check if it's an external reference (contains a file path)
299
+ if (this.isExternalRef(value)) {
300
+ // Remove the fragment (#/...) part if present
301
+ const refPath = value.split('#')[0];
302
+ if (refPath && !refs.includes(refPath)) {
303
+ refs.push(refPath);
304
+ }
305
+ }
306
+ }
307
+ else {
308
+ this.findExternalRefs(value, refs);
309
+ }
310
+ }
311
+ return refs;
312
+ }
313
+ /**
314
+ * Check if a $ref is external (references another file)
315
+ */
316
+ isExternalRef(ref) {
317
+ // External refs start with ./ or ../ or / or http:// or https://
318
+ return (ref.startsWith('./') ||
319
+ ref.startsWith('../') ||
320
+ ref.startsWith('http://') ||
321
+ ref.startsWith('https://') ||
322
+ (ref.startsWith('/') && !ref.startsWith('/#')));
323
+ }
324
+ /**
325
+ * Download a single reference file
326
+ */
327
+ async downloadReference(ref, specDir, baseUrl, isRemoteSpec, headers, timeout) {
328
+ // Skip if already downloaded
329
+ if (this.downloadedRefs.has(ref)) {
330
+ return;
331
+ }
332
+ this.downloadedRefs.add(ref);
333
+ let refUrl;
334
+ let outputPath;
335
+ if (isRemoteSpec && baseUrl) {
336
+ // For remote specs, construct the full URL
337
+ refUrl = new URL(ref, baseUrl).toString();
338
+ // Preserve the directory structure in the output
339
+ const refPath = ref.split('#')[0];
340
+ outputPath = path.join(specDir, refPath);
341
+ }
342
+ else {
343
+ // For local specs, resolve relative to the spec directory
344
+ refUrl = ref;
345
+ outputPath = path.resolve(specDir, ref.split('#')[0]);
346
+ }
347
+ console.log(` - Downloading reference: ${ref}`);
348
+ try {
349
+ // Ensure output directory exists
350
+ const outputDir = path.dirname(outputPath);
351
+ if (!fs.existsSync(outputDir)) {
352
+ fs.mkdirSync(outputDir, { recursive: true });
353
+ }
354
+ if (this.isLocalFile(refUrl) && !isRemoteSpec) {
355
+ // Copy local file
356
+ const sourcePath = path.resolve(specDir, refUrl.split('#')[0]);
357
+ if (fs.existsSync(sourcePath)) {
358
+ const content = fs.readFileSync(sourcePath, 'utf-8');
359
+ fs.writeFileSync(outputPath, content, 'utf-8');
360
+ console.log(` โœ“ Copied: ${sourcePath} -> ${outputPath}`);
361
+ // Recursively resolve references in the downloaded file
362
+ await this.resolveExternalRefs(outputPath, headers, timeout, path.dirname(sourcePath));
363
+ }
364
+ else {
365
+ console.warn(` โš  Reference file not found: ${sourcePath}`);
366
+ }
367
+ }
368
+ else {
369
+ // Download from HTTP/HTTPS
370
+ const response = await axios_1.default.get(refUrl, {
371
+ headers,
372
+ timeout: timeout || 30000,
373
+ responseType: 'text',
374
+ });
375
+ let content = response.data;
376
+ if (typeof content !== 'string') {
377
+ content = JSON.stringify(content, null, 2);
378
+ }
379
+ fs.writeFileSync(outputPath, content, 'utf-8');
380
+ console.log(` โœ“ Downloaded: ${refUrl}`);
381
+ // Recursively resolve references in the downloaded file
382
+ const refBaseUrl = this.getBaseUrl(refUrl);
383
+ await this.resolveExternalRefs(outputPath, headers, timeout, refBaseUrl);
384
+ }
385
+ }
386
+ catch (error) {
387
+ if (axios_1.default.isAxiosError(error)) {
388
+ console.error(` โœ— Failed to download ${ref}: ${error.message}`);
389
+ }
390
+ else {
391
+ console.error(` โœ— Failed to process ${ref}:`, error);
392
+ }
393
+ // Continue with other references even if one fails
394
+ }
395
+ }
212
396
  }
213
397
  exports.SpecDownloader = SpecDownloader;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klasik",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "Download OpenAPI specs from remote URLs and generate TypeScript clients with class-transformer support",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -28,11 +28,13 @@
28
28
  "axios": "^1.6.0",
29
29
  "class-transformer": "^0.5.1",
30
30
  "commander": "^11.0.0",
31
+ "js-yaml": "^4.1.0",
31
32
  "openapi-class-transformer": "^1.0.11",
32
33
  "reflect-metadata": "^0.2.2"
33
34
  },
34
35
  "devDependencies": {
35
36
  "@types/jest": "^29.5.0",
37
+ "@types/js-yaml": "^4.0.9",
36
38
  "@types/node": "^20.0.0",
37
39
  "jest": "^29.5.0",
38
40
  "ts-jest": "^29.1.0",
package/test-runtime.js DELETED
@@ -1,72 +0,0 @@
1
- var _a, _b, _c;
2
- import 'reflect-metadata';
3
- import { plainToInstance } from 'class-transformer';
4
- import { Capability } from './test-discriminated-output/models/capability.js';
5
- import { HelmComponent } from './test-discriminated-output/models/helm-component.js';
6
- import { KustomizeComponent } from './test-discriminated-output/models/kustomize-component.js';
7
- import { ManifestComponent } from './test-discriminated-output/models/manifest-component.js';
8
- // Test data with discriminated union
9
- var plainData = {
10
- name: 'my-app',
11
- description: 'Application deployment',
12
- components: [
13
- {
14
- type: 'helm',
15
- chartName: 'nginx',
16
- version: '1.0.0',
17
- values: { replicas: 3 }
18
- },
19
- {
20
- type: 'kustomize',
21
- path: './overlays/production',
22
- namespace: 'prod'
23
- },
24
- {
25
- type: 'manifest',
26
- manifests: [
27
- 'apiVersion: v1\nkind: ConfigMap',
28
- 'apiVersion: v1\nkind: Service'
29
- ]
30
- }
31
- ]
32
- };
33
- console.log('๐Ÿงช Testing Discriminated Union Runtime Transformation\n');
34
- // Transform using plainToInstance
35
- var capability = plainToInstance(Capability, plainData);
36
- // Verify instance types
37
- console.log('โœ… Verification Results:');
38
- console.log(' capability instanceof Capability:', capability instanceof Capability);
39
- console.log(' capability.name:', capability.name);
40
- console.log(' capability.components.length:', capability.components.length);
41
- console.log('');
42
- console.log('โœ… Component Type Verification:');
43
- console.log(' components[0] instanceof HelmComponent:', capability.components[0] instanceof HelmComponent);
44
- console.log(' components[1] instanceof KustomizeComponent:', capability.components[1] instanceof KustomizeComponent);
45
- console.log(' components[2] instanceof ManifestComponent:', capability.components[2] instanceof ManifestComponent);
46
- console.log('');
47
- console.log('โœ… Property Access:');
48
- console.log(' HelmComponent.chartName:', capability.components[0].chartName);
49
- console.log(' KustomizeComponent.path:', capability.components[1].path);
50
- console.log(' ManifestComponent.manifests.length:', (_a = capability.components[2].manifests) === null || _a === void 0 ? void 0 : _a.length);
51
- console.log('');
52
- console.log('โœ… attributeTypeMap Check:');
53
- var componentsMetadata = Capability.attributeTypeMap.find(function (m) { return m.name === 'components'; });
54
- console.log(' type:', componentsMetadata === null || componentsMetadata === void 0 ? void 0 : componentsMetadata.type);
55
- console.log(' modelClasses:', (_b = componentsMetadata === null || componentsMetadata === void 0 ? void 0 : componentsMetadata.modelClasses) === null || _b === void 0 ? void 0 : _b.map(function (c) { return c.name; }));
56
- console.log(' discriminatorProperty:', componentsMetadata === null || componentsMetadata === void 0 ? void 0 : componentsMetadata.discriminatorProperty);
57
- console.log('');
58
- // Final verification
59
- var allCorrect = capability instanceof Capability &&
60
- capability.components[0] instanceof HelmComponent &&
61
- capability.components[1] instanceof KustomizeComponent &&
62
- capability.components[2] instanceof ManifestComponent &&
63
- capability.components[0].chartName === 'nginx' &&
64
- capability.components[1].path === './overlays/production' &&
65
- ((_c = capability.components[2].manifests) === null || _c === void 0 ? void 0 : _c.length) === 2;
66
- if (allCorrect) {
67
- console.log('๐ŸŽ‰ SUCCESS! All discriminated union transformations working correctly!');
68
- }
69
- else {
70
- console.log('โŒ FAILED! Some transformations did not work as expected.');
71
- process.exit(1);
72
- }
package/test-runtime.ts DELETED
@@ -1,80 +0,0 @@
1
- import 'reflect-metadata';
2
- import { plainToInstance } from 'class-transformer';
3
- import { Capability } from './test-discriminated-output/models/capability.js';
4
- import { HelmComponent } from './test-discriminated-output/models/helm-component.js';
5
- import { KustomizeComponent } from './test-discriminated-output/models/kustomize-component.js';
6
- import { ManifestComponent } from './test-discriminated-output/models/manifest-component.js';
7
-
8
- // Test data with discriminated union
9
- const plainData = {
10
- name: 'my-app',
11
- description: 'Application deployment',
12
- components: [
13
- {
14
- type: 'helm',
15
- chartName: 'nginx',
16
- version: '1.0.0',
17
- values: { replicas: 3 }
18
- },
19
- {
20
- type: 'kustomize',
21
- path: './overlays/production',
22
- namespace: 'prod'
23
- },
24
- {
25
- type: 'manifest',
26
- manifests: [
27
- 'apiVersion: v1\nkind: ConfigMap',
28
- 'apiVersion: v1\nkind: Service'
29
- ]
30
- }
31
- ]
32
- };
33
-
34
- console.log('๐Ÿงช Testing Discriminated Union Runtime Transformation\n');
35
-
36
- // Transform using plainToInstance
37
- const capability = plainToInstance(Capability, plainData);
38
-
39
- // Verify instance types
40
- console.log('โœ… Verification Results:');
41
- console.log(' capability instanceof Capability:', capability instanceof Capability);
42
- console.log(' capability.name:', capability.name);
43
- console.log(' capability.components.length:', capability.components.length);
44
- console.log('');
45
-
46
- console.log('โœ… Component Type Verification:');
47
- console.log(' components[0] instanceof HelmComponent:', capability.components[0] instanceof HelmComponent);
48
- console.log(' components[1] instanceof KustomizeComponent:', capability.components[1] instanceof KustomizeComponent);
49
- console.log(' components[2] instanceof ManifestComponent:', capability.components[2] instanceof ManifestComponent);
50
- console.log('');
51
-
52
- console.log('โœ… Property Access:');
53
- console.log(' HelmComponent.chartName:', (capability.components[0] as HelmComponent).chartName);
54
- console.log(' KustomizeComponent.path:', (capability.components[1] as KustomizeComponent).path);
55
- console.log(' ManifestComponent.manifests.length:', (capability.components[2] as ManifestComponent).manifests?.length);
56
- console.log('');
57
-
58
- console.log('โœ… attributeTypeMap Check:');
59
- const componentsMetadata = Capability.attributeTypeMap.find(m => m.name === 'components');
60
- console.log(' type:', componentsMetadata?.type);
61
- console.log(' modelClasses:', (componentsMetadata as any)?.modelClasses?.map((c: any) => c.name));
62
- console.log(' discriminatorProperty:', (componentsMetadata as any)?.discriminatorProperty);
63
- console.log('');
64
-
65
- // Final verification
66
- const allCorrect =
67
- capability instanceof Capability &&
68
- capability.components[0] instanceof HelmComponent &&
69
- capability.components[1] instanceof KustomizeComponent &&
70
- capability.components[2] instanceof ManifestComponent &&
71
- (capability.components[0] as HelmComponent).chartName === 'nginx' &&
72
- (capability.components[1] as KustomizeComponent).path === './overlays/production' &&
73
- (capability.components[2] as ManifestComponent).manifests?.length === 2;
74
-
75
- if (allCorrect) {
76
- console.log('๐ŸŽ‰ SUCCESS! All discriminated union transformations working correctly!');
77
- } else {
78
- console.log('โŒ FAILED! Some transformations did not work as expected.');
79
- process.exit(1);
80
- }