klasik 1.0.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 +314 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +127 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/k8s-client-generator.d.ts +52 -0
- package/dist/k8s-client-generator.js +119 -0
- package/dist/spec-downloader.d.ts +53 -0
- package/dist/spec-downloader.js +213 -0
- package/example.md +139 -0
- package/jest.config.js +10 -0
- package/package.json +39 -0
- package/src/cli.ts +103 -0
- package/src/index.ts +2 -0
- package/src/k8s-client-generator.ts +145 -0
- package/src/spec-downloader.ts +233 -0
- package/test/k8s-client-generator.test.ts +257 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.SpecDownloader = void 0;
|
|
40
|
+
const axios_1 = __importDefault(require("axios"));
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
class SpecDownloader {
|
|
44
|
+
/**
|
|
45
|
+
* Download an OpenAPI specification from a URL or load from a local file
|
|
46
|
+
* @param options Download options
|
|
47
|
+
* @returns Path to the spec file
|
|
48
|
+
*/
|
|
49
|
+
async download(options) {
|
|
50
|
+
const { url, outputPath, headers, timeout = 30000 } = options;
|
|
51
|
+
// Check if it's a local file path
|
|
52
|
+
if (this.isLocalFile(url)) {
|
|
53
|
+
return this.loadLocalFile(url, outputPath);
|
|
54
|
+
}
|
|
55
|
+
// Otherwise, download from HTTP/HTTPS
|
|
56
|
+
console.log(`Downloading OpenAPI spec from ${url}...`);
|
|
57
|
+
try {
|
|
58
|
+
const response = await axios_1.default.get(url, {
|
|
59
|
+
headers,
|
|
60
|
+
timeout,
|
|
61
|
+
responseType: 'text',
|
|
62
|
+
});
|
|
63
|
+
// Determine output path
|
|
64
|
+
const specPath = outputPath || this.generateTempPath(url);
|
|
65
|
+
// Ensure directory exists
|
|
66
|
+
const dir = path.dirname(specPath);
|
|
67
|
+
if (!fs.existsSync(dir)) {
|
|
68
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
// Parse and validate the spec
|
|
71
|
+
let specContent;
|
|
72
|
+
if (typeof response.data === 'string') {
|
|
73
|
+
// Try to parse as JSON to validate
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(response.data);
|
|
76
|
+
specContent = JSON.stringify(parsed, null, 2);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// If not JSON, might be YAML - save as-is
|
|
80
|
+
specContent = response.data;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
specContent = JSON.stringify(response.data, null, 2);
|
|
85
|
+
}
|
|
86
|
+
// Validate it's an OpenAPI spec
|
|
87
|
+
this.validateSpec(specContent);
|
|
88
|
+
// Write to file
|
|
89
|
+
fs.writeFileSync(specPath, specContent, 'utf-8');
|
|
90
|
+
console.log(`OpenAPI spec downloaded successfully to ${specPath}`);
|
|
91
|
+
return specPath;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
95
|
+
throw new Error(`Failed to download OpenAPI spec from ${url}: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Check if the URL is a local file path
|
|
102
|
+
*/
|
|
103
|
+
isLocalFile(url) {
|
|
104
|
+
// Check for file:// protocol
|
|
105
|
+
if (url.startsWith('file://')) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
// Check for HTTP/HTTPS protocols
|
|
109
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
// Check if it's an absolute or relative path
|
|
113
|
+
// Absolute paths start with / (Unix) or C:\ (Windows)
|
|
114
|
+
// Relative paths start with ./ or ../
|
|
115
|
+
return (url.startsWith('/') ||
|
|
116
|
+
url.startsWith('./') ||
|
|
117
|
+
url.startsWith('../') ||
|
|
118
|
+
/^[a-zA-Z]:[\\\/]/.test(url) // Windows paths like C:\
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Load and validate an OpenAPI spec from a local file
|
|
123
|
+
*/
|
|
124
|
+
loadLocalFile(filePath, outputPath) {
|
|
125
|
+
// Handle file:// URLs
|
|
126
|
+
let resolvedPath = filePath;
|
|
127
|
+
if (filePath.startsWith('file://')) {
|
|
128
|
+
resolvedPath = filePath.replace('file://', '');
|
|
129
|
+
// On Windows, file URLs might be file:///C:/path
|
|
130
|
+
if (process.platform === 'win32' && resolvedPath.startsWith('/')) {
|
|
131
|
+
resolvedPath = resolvedPath.substring(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Resolve to absolute path
|
|
135
|
+
resolvedPath = path.resolve(resolvedPath);
|
|
136
|
+
console.log(`Loading OpenAPI spec from local file: ${resolvedPath}...`);
|
|
137
|
+
// Check if file exists
|
|
138
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
139
|
+
throw new Error(`File not found: ${resolvedPath}`);
|
|
140
|
+
}
|
|
141
|
+
// Read the file
|
|
142
|
+
const content = fs.readFileSync(resolvedPath, 'utf-8');
|
|
143
|
+
// Validate it's an OpenAPI spec
|
|
144
|
+
this.validateSpec(content);
|
|
145
|
+
// If outputPath is provided, copy to that location
|
|
146
|
+
if (outputPath) {
|
|
147
|
+
const dir = path.dirname(outputPath);
|
|
148
|
+
if (!fs.existsSync(dir)) {
|
|
149
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
150
|
+
}
|
|
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');
|
|
161
|
+
console.log(`OpenAPI spec copied to ${outputPath}`);
|
|
162
|
+
return outputPath;
|
|
163
|
+
}
|
|
164
|
+
console.log(`OpenAPI spec loaded successfully from ${resolvedPath}`);
|
|
165
|
+
return resolvedPath;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Generate a temporary file path based on the URL
|
|
169
|
+
*/
|
|
170
|
+
generateTempPath(url) {
|
|
171
|
+
const urlObj = new URL(url);
|
|
172
|
+
const hostname = urlObj.hostname.replace(/\./g, '-');
|
|
173
|
+
const timestamp = Date.now();
|
|
174
|
+
const filename = `openapi-${hostname}-${timestamp}.json`;
|
|
175
|
+
return path.join(process.cwd(), '.tmp', filename);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Validate that the downloaded content is an OpenAPI spec
|
|
179
|
+
*/
|
|
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
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (error instanceof SyntaxError) {
|
|
197
|
+
throw new Error('Invalid OpenAPI spec: not valid JSON');
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Clean up temporary files
|
|
204
|
+
*/
|
|
205
|
+
cleanupTemp() {
|
|
206
|
+
const tempDir = path.join(process.cwd(), '.tmp');
|
|
207
|
+
if (fs.existsSync(tempDir)) {
|
|
208
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
209
|
+
console.log('Cleaned up temporary files');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
exports.SpecDownloader = SpecDownloader;
|
package/example.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Example Usage
|
|
2
|
+
|
|
3
|
+
## Using klasik with a Real API
|
|
4
|
+
|
|
5
|
+
### Example 1: Generate client from Kubernetes OpenAPI spec
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Download and generate TypeScript client
|
|
9
|
+
npx klasik generate \
|
|
10
|
+
--url https://raw.githubusercontent.com/kubernetes/kubernetes/master/api/openapi-spec/swagger.json \
|
|
11
|
+
--output ./kubernetes-client
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Example 2: Generate client with authentication
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Generate client with custom headers
|
|
18
|
+
npx klasik generate \
|
|
19
|
+
--url https://api.example.com/openapi.json \
|
|
20
|
+
--output ./api-client \
|
|
21
|
+
--header "Authorization: Bearer YOUR_TOKEN" \
|
|
22
|
+
--header "X-API-Key: YOUR_KEY"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Example 3: Programmatic usage
|
|
26
|
+
|
|
27
|
+
Create a file `generate-client.ts`:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { K8sClientGenerator } from 'klasik';
|
|
31
|
+
|
|
32
|
+
async function generateClient() {
|
|
33
|
+
const generator = new K8sClientGenerator();
|
|
34
|
+
|
|
35
|
+
await generator.generate({
|
|
36
|
+
specUrl: 'https://petstore3.swagger.io/api/v3/openapi.json',
|
|
37
|
+
outputDir: './petstore-client',
|
|
38
|
+
keepSpec: true, // Keep the downloaded spec for reference
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
console.log('✅ Client generated successfully!');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
generateClient().catch(console.error);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Run it:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx ts-node generate-client.ts
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Example 4: Using the generated client
|
|
54
|
+
|
|
55
|
+
After generating the client, you can use it like this:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { DefaultApi, Configuration } from './petstore-client';
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
// Create configuration
|
|
62
|
+
const config = new Configuration({
|
|
63
|
+
basePath: 'https://petstore3.swagger.io/api/v3',
|
|
64
|
+
enableResponseTransformation: true,
|
|
65
|
+
onTransformationError: (error, modelClass, data) => {
|
|
66
|
+
console.error('Transformation error:', error.message);
|
|
67
|
+
console.error('Model:', modelClass.name);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Create API client
|
|
72
|
+
const api = new DefaultApi(config);
|
|
73
|
+
|
|
74
|
+
// Make API calls - responses are automatically transformed to class instances!
|
|
75
|
+
const response = await api.findPetsByStatus({ status: ['available'] });
|
|
76
|
+
const pets = response.data; // Array of Pet class instances
|
|
77
|
+
|
|
78
|
+
pets.forEach(pet => {
|
|
79
|
+
console.log(`Pet: ${pet.name}, ID: ${pet.id}`);
|
|
80
|
+
// pet is instanceof Pet, with all decorators applied!
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
main().catch(console.error);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Testing Locally
|
|
88
|
+
|
|
89
|
+
To test the package locally with the linked `openapi-class-transformer`:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# In openapi-class-transformer directory
|
|
93
|
+
cd /Users/eyald/koalaops/openapi-class-transformer
|
|
94
|
+
npm link
|
|
95
|
+
|
|
96
|
+
# In klasik directory
|
|
97
|
+
cd /Users/eyald/koalaops/klasik
|
|
98
|
+
npm link openapi-class-transformer
|
|
99
|
+
|
|
100
|
+
# Build and test
|
|
101
|
+
npm run build
|
|
102
|
+
npm test
|
|
103
|
+
|
|
104
|
+
# Try generating a client
|
|
105
|
+
node dist/cli.js generate \
|
|
106
|
+
--url https://petstore3.swagger.io/api/v3/openapi.json \
|
|
107
|
+
--output ./test-client
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## What Gets Generated?
|
|
111
|
+
|
|
112
|
+
When you run the generator, you'll get:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
./output-directory/
|
|
116
|
+
├── api/
|
|
117
|
+
│ └── default-api.ts # API client with methods
|
|
118
|
+
├── models/
|
|
119
|
+
│ ├── index.ts # Model exports
|
|
120
|
+
│ ├── pet.ts # Pet model with decorators
|
|
121
|
+
│ ├── category.ts # Category model
|
|
122
|
+
│ └── ... # Other models
|
|
123
|
+
├── base.ts # Base API classes
|
|
124
|
+
├── common.ts # Common utilities
|
|
125
|
+
├── configuration.ts # Configuration class (with transformation options!)
|
|
126
|
+
└── index.ts # Main exports
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Each model will have:
|
|
130
|
+
- ✅ `@Expose()` decorators on all properties
|
|
131
|
+
- ✅ `@Type()` decorators for nested objects
|
|
132
|
+
- ✅ Static `attributeTypeMap` for runtime metadata
|
|
133
|
+
- ✅ Vendor extensions in JSDoc comments
|
|
134
|
+
- ✅ Union types for `anyOf` schemas
|
|
135
|
+
|
|
136
|
+
Each API method will:
|
|
137
|
+
- ✅ Automatically transform responses to class instances
|
|
138
|
+
- ✅ Handle transformation errors gracefully
|
|
139
|
+
- ✅ Support configuration options for enabling/disabling transformation
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "klasik",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Download OpenAPI specs from remote URLs and generate TypeScript clients with class-transformer support",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"klasik": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"dev": "tsc --watch",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"openapi",
|
|
18
|
+
"kubernetes",
|
|
19
|
+
"k8s",
|
|
20
|
+
"typescript",
|
|
21
|
+
"class-transformer",
|
|
22
|
+
"api-client",
|
|
23
|
+
"generator"
|
|
24
|
+
],
|
|
25
|
+
"author": "",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"axios": "^1.6.0",
|
|
29
|
+
"commander": "^11.0.0",
|
|
30
|
+
"openapi-class-transformer": "file:../openapi-class-transformer"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/jest": "^29.5.0",
|
|
34
|
+
"@types/node": "^20.0.0",
|
|
35
|
+
"jest": "^29.5.0",
|
|
36
|
+
"ts-jest": "^29.1.0",
|
|
37
|
+
"typescript": "^5.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { K8sClientGenerator } from './k8s-client-generator';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name('klasik')
|
|
11
|
+
.description('Download OpenAPI specs from remote URLs and generate TypeScript clients with class-transformer support')
|
|
12
|
+
.version('1.0.0');
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.command('generate')
|
|
16
|
+
.description('Generate TypeScript client from a remote OpenAPI spec')
|
|
17
|
+
.requiredOption('-u, --url <url>', 'Remote URL to download the OpenAPI spec from')
|
|
18
|
+
.requiredOption('-o, --output <dir>', 'Output directory for generated client code')
|
|
19
|
+
.option('-m, --mode <mode>', 'Generation mode: "full" (models + APIs + config) or "models-only"', 'full')
|
|
20
|
+
.option('-H, --header <header...>', 'Custom headers for the request (format: "Key: Value")')
|
|
21
|
+
.option('-t, --template <dir>', 'Custom template directory')
|
|
22
|
+
.option('-k, --keep-spec', 'Keep the downloaded spec file after generation', false)
|
|
23
|
+
.option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
|
|
24
|
+
.action(async (options) => {
|
|
25
|
+
try {
|
|
26
|
+
// Validate mode
|
|
27
|
+
if (options.mode !== 'full' && options.mode !== 'models-only') {
|
|
28
|
+
console.error('Error: --mode must be either "full" or "models-only"');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Parse headers if provided
|
|
33
|
+
const headers: Record<string, string> = {};
|
|
34
|
+
if (options.header) {
|
|
35
|
+
for (const header of options.header) {
|
|
36
|
+
const [key, ...valueParts] = header.split(':');
|
|
37
|
+
const value = valueParts.join(':').trim();
|
|
38
|
+
if (key && value) {
|
|
39
|
+
headers[key.trim()] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Resolve output directory to absolute path
|
|
45
|
+
const outputDir = path.resolve(options.output);
|
|
46
|
+
|
|
47
|
+
const generator = new K8sClientGenerator();
|
|
48
|
+
await generator.generate({
|
|
49
|
+
specUrl: options.url,
|
|
50
|
+
outputDir,
|
|
51
|
+
mode: options.mode,
|
|
52
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
53
|
+
templateDir: options.template,
|
|
54
|
+
keepSpec: options.keepSpec,
|
|
55
|
+
timeout: parseInt(options.timeout, 10),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
process.exit(0);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
program
|
|
66
|
+
.command('download')
|
|
67
|
+
.description('Download an OpenAPI spec from a remote URL (without generating)')
|
|
68
|
+
.requiredOption('-u, --url <url>', 'Remote URL to download the OpenAPI spec from')
|
|
69
|
+
.requiredOption('-o, --output <file>', 'Output file path for the downloaded spec')
|
|
70
|
+
.option('-H, --header <header...>', 'Custom headers for the request (format: "Key: Value")')
|
|
71
|
+
.option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
|
|
72
|
+
.action(async (options) => {
|
|
73
|
+
try {
|
|
74
|
+
const { SpecDownloader } = await import('./spec-downloader');
|
|
75
|
+
|
|
76
|
+
// Parse headers if provided
|
|
77
|
+
const headers: Record<string, string> = {};
|
|
78
|
+
if (options.header) {
|
|
79
|
+
for (const header of options.header) {
|
|
80
|
+
const [key, ...valueParts] = header.split(':');
|
|
81
|
+
const value = valueParts.join(':').trim();
|
|
82
|
+
if (key && value) {
|
|
83
|
+
headers[key.trim()] = value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const downloader = new SpecDownloader();
|
|
89
|
+
await downloader.download({
|
|
90
|
+
url: options.url,
|
|
91
|
+
outputPath: path.resolve(options.output),
|
|
92
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
93
|
+
timeout: parseInt(options.timeout, 10),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
process.exit(0);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
program.parse();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Generator, GeneratorOptions } from 'openapi-class-transformer';
|
|
2
|
+
import { SpecDownloader, DownloadOptions } from './spec-downloader';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
|
|
5
|
+
export type GenerationMode = 'full' | 'models-only';
|
|
6
|
+
|
|
7
|
+
export interface K8sClientGeneratorOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Remote URL to download the OpenAPI spec from
|
|
10
|
+
*/
|
|
11
|
+
specUrl: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Output directory for generated client code
|
|
15
|
+
*/
|
|
16
|
+
outputDir: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generation mode
|
|
20
|
+
* - 'full': Generate models, APIs, and configuration (default)
|
|
21
|
+
* - 'models-only': Generate only model classes
|
|
22
|
+
* @default 'full'
|
|
23
|
+
*/
|
|
24
|
+
mode?: GenerationMode;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Optional headers to include in the spec download request
|
|
28
|
+
*/
|
|
29
|
+
headers?: Record<string, string>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Optional custom template directory
|
|
33
|
+
*/
|
|
34
|
+
templateDir?: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether to keep the downloaded spec file after generation
|
|
38
|
+
* @default false
|
|
39
|
+
*/
|
|
40
|
+
keepSpec?: boolean;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Request timeout for downloading spec in milliseconds
|
|
44
|
+
* @default 30000
|
|
45
|
+
*/
|
|
46
|
+
timeout?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class K8sClientGenerator {
|
|
50
|
+
private downloader: SpecDownloader;
|
|
51
|
+
|
|
52
|
+
constructor() {
|
|
53
|
+
this.downloader = new SpecDownloader();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generate TypeScript client from a remote OpenAPI spec URL
|
|
58
|
+
*/
|
|
59
|
+
async generate(options: K8sClientGeneratorOptions): Promise<void> {
|
|
60
|
+
const {
|
|
61
|
+
specUrl,
|
|
62
|
+
outputDir,
|
|
63
|
+
mode = 'full',
|
|
64
|
+
headers,
|
|
65
|
+
templateDir,
|
|
66
|
+
keepSpec = false,
|
|
67
|
+
timeout,
|
|
68
|
+
} = options;
|
|
69
|
+
|
|
70
|
+
let specPath: string | undefined;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Step 1: Download the OpenAPI spec
|
|
74
|
+
console.log('Step 1: Downloading OpenAPI specification...');
|
|
75
|
+
const downloadOptions: DownloadOptions = {
|
|
76
|
+
url: specUrl,
|
|
77
|
+
headers,
|
|
78
|
+
timeout,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
specPath = await this.downloader.download(downloadOptions);
|
|
82
|
+
|
|
83
|
+
// Step 2: Validate output directory
|
|
84
|
+
console.log('Step 2: Preparing output directory...');
|
|
85
|
+
this.prepareOutputDirectory(outputDir);
|
|
86
|
+
|
|
87
|
+
// Step 3: Generate client using openapi-class-transformer
|
|
88
|
+
const modeLabel = mode === 'models-only' ? 'models only' : 'full client';
|
|
89
|
+
console.log(`Step 3: Generating TypeScript ${modeLabel}...`);
|
|
90
|
+
const generatorOptions: GeneratorOptions = {
|
|
91
|
+
inputSpec: specPath,
|
|
92
|
+
outputDir,
|
|
93
|
+
modelsOnly: mode === 'models-only',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Only add templateDir if it's provided
|
|
97
|
+
if (templateDir) {
|
|
98
|
+
generatorOptions.templateDir = templateDir;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const generator = new Generator(generatorOptions);
|
|
102
|
+
await generator.generate();
|
|
103
|
+
|
|
104
|
+
console.log('✅ Client generation completed successfully!');
|
|
105
|
+
console.log(`📁 Generated files location: ${outputDir}`);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('❌ Client generation failed:', error);
|
|
108
|
+
throw error;
|
|
109
|
+
} finally {
|
|
110
|
+
// Cleanup
|
|
111
|
+
if (!keepSpec && specPath) {
|
|
112
|
+
this.cleanupSpecFile(specPath);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Prepare the output directory
|
|
119
|
+
*/
|
|
120
|
+
private prepareOutputDirectory(outputDir: string): void {
|
|
121
|
+
if (!fs.existsSync(outputDir)) {
|
|
122
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
123
|
+
console.log(`Created output directory: ${outputDir}`);
|
|
124
|
+
} else {
|
|
125
|
+
console.log(`Using existing output directory: ${outputDir}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Clean up the downloaded spec file
|
|
131
|
+
*/
|
|
132
|
+
private cleanupSpecFile(specPath: string): void {
|
|
133
|
+
try {
|
|
134
|
+
if (fs.existsSync(specPath)) {
|
|
135
|
+
fs.unlinkSync(specPath);
|
|
136
|
+
console.log(`Cleaned up downloaded spec file: ${specPath}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Also cleanup temp directory if empty
|
|
140
|
+
this.downloader.cleanupTemp();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.warn('Warning: Failed to cleanup spec file:', error);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|