klasik 1.0.13 → 1.0.16
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 +19 -4
- package/USAGE_EXAMPLES.md +61 -0
- package/dist/spec-downloader.d.ts +2 -2
- package/dist/spec-downloader.js +61 -41
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ 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
|
|
9
10
|
- 🔐 **Authentication support** - Custom headers including Bearer tokens and API keys
|
|
10
11
|
- 🔗 **External reference resolution** - Automatically download referenced schema files (`$ref`)
|
|
11
12
|
- 🔄 **Automatic transformation** - Converts API responses to class instances using class-transformer
|
|
@@ -27,17 +28,23 @@ npm install klasik
|
|
|
27
28
|
Generate a TypeScript client from a remote OpenAPI spec:
|
|
28
29
|
|
|
29
30
|
```bash
|
|
31
|
+
# JSON spec
|
|
30
32
|
npx klasik generate \
|
|
31
33
|
--url https://raw.githubusercontent.com/kubernetes/kubernetes/master/api/openapi-spec/swagger.json \
|
|
32
34
|
--output ./k8s-client
|
|
35
|
+
|
|
36
|
+
# YAML spec
|
|
37
|
+
npx klasik generate \
|
|
38
|
+
--url https://api.example.com/openapi.yaml \
|
|
39
|
+
--output ./client
|
|
33
40
|
```
|
|
34
41
|
|
|
35
|
-
Generate from a local OpenAPI spec file:
|
|
42
|
+
Generate from a local OpenAPI spec file (JSON or YAML):
|
|
36
43
|
|
|
37
44
|
```bash
|
|
38
45
|
# Absolute path
|
|
39
46
|
npx klasik generate \
|
|
40
|
-
--url /path/to/openapi.
|
|
47
|
+
--url /path/to/openapi.yaml \
|
|
41
48
|
--output ./client
|
|
42
49
|
|
|
43
50
|
# Relative path
|
|
@@ -47,16 +54,22 @@ npx klasik generate \
|
|
|
47
54
|
|
|
48
55
|
# file:// URL
|
|
49
56
|
npx klasik generate \
|
|
50
|
-
--url file:///path/to/openapi.
|
|
57
|
+
--url file:///path/to/openapi.yaml \
|
|
51
58
|
--output ./client
|
|
52
59
|
```
|
|
53
60
|
|
|
54
|
-
Download an OpenAPI spec without generating:
|
|
61
|
+
Download an OpenAPI spec without generating (supports JSON and YAML):
|
|
55
62
|
|
|
56
63
|
```bash
|
|
64
|
+
# Download JSON spec
|
|
57
65
|
npx klasik download \
|
|
58
66
|
--url https://api.example.com/openapi.json \
|
|
59
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
|
|
60
73
|
```
|
|
61
74
|
|
|
62
75
|
### Programmatic Usage
|
|
@@ -321,6 +334,8 @@ output/
|
|
|
321
334
|
### Supported Reference Types
|
|
322
335
|
|
|
323
336
|
- **Relative paths**: `./schemas/user.yaml`, `../common/types.yaml`
|
|
337
|
+
- When the main spec is from a remote URL, relative paths are resolved against the spec's URL
|
|
338
|
+
- Example: spec at `https://raw.githubusercontent.com/org/repo/main/api/openapi.yaml` with ref `./schemas/user.yaml` resolves to `https://raw.githubusercontent.com/org/repo/main/api/schemas/user.yaml`
|
|
324
339
|
- **Remote URLs**: `https://api.example.com/schemas/user.yaml`
|
|
325
340
|
- **Mixed**: Main spec from HTTPS, references can be relative or absolute
|
|
326
341
|
|
package/USAGE_EXAMPLES.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# Klasik Usage Examples
|
|
2
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
|
+
|
|
3
64
|
## Using Custom Headers (Authorization)
|
|
4
65
|
|
|
5
66
|
### CLI Example - Authorization Token
|
|
@@ -54,7 +54,7 @@ export declare class SpecDownloader {
|
|
|
54
54
|
*/
|
|
55
55
|
private generateTempPath;
|
|
56
56
|
/**
|
|
57
|
-
* Validate that the
|
|
57
|
+
* Validate that the parsed content is an OpenAPI spec
|
|
58
58
|
*/
|
|
59
59
|
private validateSpec;
|
|
60
60
|
/**
|
|
@@ -62,7 +62,7 @@ export declare class SpecDownloader {
|
|
|
62
62
|
*/
|
|
63
63
|
cleanupTemp(): void;
|
|
64
64
|
/**
|
|
65
|
-
* Get base URL from a full URL (removes the filename)
|
|
65
|
+
* Get base URL from a full URL (removes the filename and ensures trailing slash)
|
|
66
66
|
*/
|
|
67
67
|
private getBaseUrl;
|
|
68
68
|
/**
|
package/dist/spec-downloader.js
CHANGED
|
@@ -40,6 +40,7 @@ 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 {
|
|
44
45
|
constructor() {
|
|
45
46
|
this.downloadedRefs = new Set();
|
|
@@ -78,22 +79,31 @@ class SpecDownloader {
|
|
|
78
79
|
}
|
|
79
80
|
// Parse and validate the spec
|
|
80
81
|
let specContent;
|
|
82
|
+
let parsedSpec;
|
|
81
83
|
if (typeof response.data === 'string') {
|
|
82
|
-
// Try to parse as JSON
|
|
84
|
+
// Try to parse as JSON first
|
|
83
85
|
try {
|
|
84
|
-
|
|
85
|
-
specContent = JSON.stringify(
|
|
86
|
+
parsedSpec = JSON.parse(response.data);
|
|
87
|
+
specContent = JSON.stringify(parsedSpec, null, 2);
|
|
86
88
|
}
|
|
87
89
|
catch {
|
|
88
|
-
// If not JSON,
|
|
89
|
-
|
|
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
|
+
}
|
|
90
99
|
}
|
|
91
100
|
}
|
|
92
101
|
else {
|
|
102
|
+
parsedSpec = response.data;
|
|
93
103
|
specContent = JSON.stringify(response.data, null, 2);
|
|
94
104
|
}
|
|
95
105
|
// Validate it's an OpenAPI spec
|
|
96
|
-
this.validateSpec(
|
|
106
|
+
this.validateSpec(parsedSpec);
|
|
97
107
|
// Write to file
|
|
98
108
|
fs.writeFileSync(specPath, specContent, 'utf-8');
|
|
99
109
|
console.log(`OpenAPI spec downloaded successfully to ${specPath}`);
|
|
@@ -154,24 +164,29 @@ class SpecDownloader {
|
|
|
154
164
|
}
|
|
155
165
|
// Read the file
|
|
156
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
|
+
}
|
|
157
180
|
// Validate it's an OpenAPI spec
|
|
158
|
-
this.validateSpec(
|
|
181
|
+
this.validateSpec(parsedSpec);
|
|
159
182
|
// If outputPath is provided, copy to that location
|
|
160
183
|
if (outputPath) {
|
|
161
184
|
const dir = path.dirname(outputPath);
|
|
162
185
|
if (!fs.existsSync(dir)) {
|
|
163
186
|
fs.mkdirSync(dir, { recursive: true });
|
|
164
187
|
}
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
const parsed = JSON.parse(content);
|
|
169
|
-
formattedContent = JSON.stringify(parsed, null, 2);
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
// Not JSON, keep original
|
|
173
|
-
}
|
|
174
|
-
fs.writeFileSync(outputPath, formattedContent, 'utf-8');
|
|
188
|
+
// Keep the original format (JSON or YAML)
|
|
189
|
+
fs.writeFileSync(outputPath, content, 'utf-8');
|
|
175
190
|
console.log(`OpenAPI spec copied to ${outputPath}`);
|
|
176
191
|
return outputPath;
|
|
177
192
|
}
|
|
@@ -189,28 +204,22 @@ class SpecDownloader {
|
|
|
189
204
|
return path.join(process.cwd(), '.tmp', filename);
|
|
190
205
|
}
|
|
191
206
|
/**
|
|
192
|
-
* Validate that the
|
|
207
|
+
* Validate that the parsed content is an OpenAPI spec
|
|
193
208
|
*/
|
|
194
|
-
validateSpec(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// Check for OpenAPI version
|
|
198
|
-
if (!spec.openapi && !spec.swagger) {
|
|
199
|
-
throw new Error('Invalid OpenAPI spec: missing "openapi" or "swagger" field');
|
|
200
|
-
}
|
|
201
|
-
// Check for required fields
|
|
202
|
-
if (!spec.info) {
|
|
203
|
-
throw new Error('Invalid OpenAPI spec: missing "info" field');
|
|
204
|
-
}
|
|
205
|
-
if (!spec.paths && !spec.components) {
|
|
206
|
-
throw new Error('Invalid OpenAPI spec: must have either "paths" or "components"');
|
|
207
|
-
}
|
|
209
|
+
validateSpec(spec) {
|
|
210
|
+
if (!spec || typeof spec !== 'object') {
|
|
211
|
+
throw new Error('Invalid OpenAPI spec: not a valid object');
|
|
208
212
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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"');
|
|
214
223
|
}
|
|
215
224
|
}
|
|
216
225
|
/**
|
|
@@ -224,14 +233,16 @@ class SpecDownloader {
|
|
|
224
233
|
}
|
|
225
234
|
}
|
|
226
235
|
/**
|
|
227
|
-
* Get base URL from a full URL (removes the filename)
|
|
236
|
+
* Get base URL from a full URL (removes the filename and ensures trailing slash)
|
|
228
237
|
*/
|
|
229
238
|
getBaseUrl(url) {
|
|
230
239
|
try {
|
|
231
240
|
const urlObj = new URL(url);
|
|
232
241
|
const pathParts = urlObj.pathname.split('/');
|
|
233
242
|
pathParts.pop(); // Remove filename
|
|
234
|
-
|
|
243
|
+
// Ensure the path ends with a slash for proper relative URL resolution
|
|
244
|
+
const basePath = pathParts.join('/');
|
|
245
|
+
urlObj.pathname = basePath.endsWith('/') ? basePath : basePath + '/';
|
|
235
246
|
return urlObj.toString();
|
|
236
247
|
}
|
|
237
248
|
catch {
|
|
@@ -250,8 +261,13 @@ class SpecDownloader {
|
|
|
250
261
|
spec = JSON.parse(content);
|
|
251
262
|
}
|
|
252
263
|
catch {
|
|
253
|
-
|
|
254
|
-
|
|
264
|
+
try {
|
|
265
|
+
spec = yaml.load(content);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
console.log('Spec is not valid JSON or YAML, skipping reference resolution');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
255
271
|
}
|
|
256
272
|
const refs = this.findExternalRefs(spec);
|
|
257
273
|
if (refs.length === 0) {
|
|
@@ -331,6 +347,10 @@ class SpecDownloader {
|
|
|
331
347
|
outputPath = path.resolve(specDir, ref.split('#')[0]);
|
|
332
348
|
}
|
|
333
349
|
console.log(` - Downloading reference: ${ref}`);
|
|
350
|
+
if (isRemoteSpec && baseUrl) {
|
|
351
|
+
console.log(` Base URL: ${baseUrl}`);
|
|
352
|
+
console.log(` Resolved to: ${refUrl}`);
|
|
353
|
+
}
|
|
334
354
|
try {
|
|
335
355
|
// Ensure output directory exists
|
|
336
356
|
const outputDir = path.dirname(outputPath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klasik",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
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",
|