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.
@@ -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
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/test'],
5
+ testMatch: ['**/*.test.ts'],
6
+ collectCoverageFrom: [
7
+ 'src/**/*.ts',
8
+ '!src/**/*.d.ts',
9
+ ],
10
+ };
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,2 @@
1
+ export { K8sClientGenerator, K8sClientGeneratorOptions, GenerationMode } from './k8s-client-generator';
2
+ export { SpecDownloader, DownloadOptions } from './spec-downloader';
@@ -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
+ }