klasik 2.3.0 → 2.4.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/dist/bin/go-schema-gen +0 -0
- package/package.json +7 -4
- package/demo-output/models/index.ts +0 -2
- package/demo-output/models/owner.ts +0 -74
- package/demo-output/models/pet.ts +0 -107
- package/demo-output/package.json +0 -14
- package/demo-output/tsconfig.json +0 -26
- package/dist/__tests__/test-helpers/cleanup-utils.d.ts +0 -64
- package/dist/__tests__/test-helpers/cleanup-utils.d.ts.map +0 -1
- package/dist/__tests__/test-helpers/cleanup-utils.js +0 -236
- package/dist/__tests__/test-helpers/cleanup-utils.js.map +0 -1
- package/docs/ARCHITECTURE.md +0 -845
- package/docs/JSDOC_ESCAPING.md +0 -280
- package/docs/json-schema-support.md +0 -353
- package/docs/validation.md +0 -350
- package/tools/go-schema-gen/go-schema-gen +0 -0
package/docs/ARCHITECTURE.md
DELETED
|
@@ -1,845 +0,0 @@
|
|
|
1
|
-
# Klasik Architecture
|
|
2
|
-
|
|
3
|
-
This document provides technical implementation details for developers who want to understand, modify, or contribute to Klasik.
|
|
4
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
|
|
7
|
-
- [Overview](#overview)
|
|
8
|
-
- [Why ts-morph?](#why-ts-morph)
|
|
9
|
-
- [Why Plugin Architecture?](#why-plugin-architecture)
|
|
10
|
-
- [Why Intermediate Representation (IR)?](#why-intermediate-representation-ir)
|
|
11
|
-
- [Architecture Components](#architecture-components)
|
|
12
|
-
- [Code Generation Pipeline](#code-generation-pipeline)
|
|
13
|
-
- [Plugin System](#plugin-system)
|
|
14
|
-
- [Testing Strategy](#testing-strategy)
|
|
15
|
-
- [Development Guide](#development-guide)
|
|
16
|
-
|
|
17
|
-
## Overview
|
|
18
|
-
|
|
19
|
-
- **Type Safety**: Generated code is parsed and validated as TypeScript AST
|
|
20
|
-
- **Maintainability**: AST manipulation is more robust than string concatenation
|
|
21
|
-
- **Extensibility**: Plugin architecture allows easy feature additions
|
|
22
|
-
- **Format Agnostic**: Unified IR handles OpenAPI, CRDs, and JSON Schema
|
|
23
|
-
|
|
24
|
-
## Why ts-morph?
|
|
25
|
-
|
|
26
|
-
### Problem with String-Based Generation
|
|
27
|
-
|
|
28
|
-
The original Klasik used template strings and manual code construction:
|
|
29
|
-
|
|
30
|
-
```typescript
|
|
31
|
-
// Original Klasik v1 approach
|
|
32
|
-
let code = `export class ${className} {\n`;
|
|
33
|
-
code += ` ${propertyName}: ${propertyType};\n`;
|
|
34
|
-
code += `}\n`;
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
**Issues:**
|
|
38
|
-
- ❌ No syntax validation until runtime
|
|
39
|
-
- ❌ Hard to maintain complex structures (decorators, imports, etc.)
|
|
40
|
-
- ❌ String escaping complexity
|
|
41
|
-
- ❌ Difficult to refactor or modify generated code
|
|
42
|
-
- ❌ No IDE support for template content
|
|
43
|
-
|
|
44
|
-
### Solution: AST-Based Generation
|
|
45
|
-
|
|
46
|
-
Klasik uses ts-morph to build proper TypeScript Abstract Syntax Trees:
|
|
47
|
-
|
|
48
|
-
```typescript
|
|
49
|
-
// Klasik approach
|
|
50
|
-
const classDeclaration = sourceFile.addClass({
|
|
51
|
-
name: className,
|
|
52
|
-
isExported: true
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
classDeclaration.addProperty({
|
|
56
|
-
name: propertyName,
|
|
57
|
-
type: propertyType,
|
|
58
|
-
decorators: [
|
|
59
|
-
{ name: 'Expose', arguments: [] },
|
|
60
|
-
{ name: 'ApiProperty', arguments: ['{ type: String }'] }
|
|
61
|
-
]
|
|
62
|
-
});
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**Benefits:**
|
|
66
|
-
- ✅ TypeScript-validated AST construction
|
|
67
|
-
- ✅ Automatic formatting and indentation
|
|
68
|
-
- ✅ Type-safe API for code manipulation
|
|
69
|
-
- ✅ Built-in import management
|
|
70
|
-
- ✅ Easier to test and maintain
|
|
71
|
-
- ✅ IDE autocomplete and type checking
|
|
72
|
-
|
|
73
|
-
### Performance Considerations
|
|
74
|
-
|
|
75
|
-
While AST manipulation is slightly slower than string concatenation, the benefits far outweigh the cost:
|
|
76
|
-
|
|
77
|
-
- **Correctness**: Generated code is always syntactically valid
|
|
78
|
-
- **Maintainability**: Changes to generation logic are easier and safer
|
|
79
|
-
- **Debugging**: AST nodes can be inspected and validated
|
|
80
|
-
- **Testing**: Unit tests can verify AST structure without running TypeScript compiler
|
|
81
|
-
|
|
82
|
-
## Why Plugin Architecture?
|
|
83
|
-
|
|
84
|
-
### Problem with Monolithic Code Generation
|
|
85
|
-
|
|
86
|
-
Original Klasik had all generation logic in a single large module:
|
|
87
|
-
- Hard to add new features without breaking existing code
|
|
88
|
-
- Difficult to test individual features in isolation
|
|
89
|
-
- No way to selectively enable/disable features
|
|
90
|
-
- Code duplication across different generators
|
|
91
|
-
|
|
92
|
-
### Solution: Hook-Based Plugin System
|
|
93
|
-
|
|
94
|
-
Klasik uses a priority-ordered hook system:
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
interface Plugin {
|
|
98
|
-
name: string;
|
|
99
|
-
priority: number;
|
|
100
|
-
|
|
101
|
-
hooks: {
|
|
102
|
-
beforeGeneration?(context: GenerationContext): void;
|
|
103
|
-
onSchemaLoad?(schema: SchemaIR): void;
|
|
104
|
-
onClassGeneration?(classNode: ClassDeclaration, schema: ObjectSchema): void;
|
|
105
|
-
onPropertyGeneration?(property: PropertyDeclaration, field: Field): void;
|
|
106
|
-
afterGeneration?(context: GenerationContext): void;
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
**Benefits:**
|
|
112
|
-
- ✅ **Modularity**: Each feature is a self-contained plugin
|
|
113
|
-
- ✅ **Testability**: Plugins can be tested independently
|
|
114
|
-
- ✅ **Extensibility**: Add new features without modifying core
|
|
115
|
-
- ✅ **Flexibility**: Users can enable/disable features via CLI flags
|
|
116
|
-
- ✅ **Priority Control**: Plugins execute in defined order
|
|
117
|
-
|
|
118
|
-
### Plugin Examples
|
|
119
|
-
|
|
120
|
-
**1. NestJS Swagger Plugin**
|
|
121
|
-
```typescript
|
|
122
|
-
class NestJSSwaggerPlugin implements Plugin {
|
|
123
|
-
name = 'nestjs-swagger';
|
|
124
|
-
priority = 100;
|
|
125
|
-
|
|
126
|
-
hooks = {
|
|
127
|
-
onPropertyGeneration(property, field) {
|
|
128
|
-
property.addDecorator({
|
|
129
|
-
name: 'ApiProperty',
|
|
130
|
-
arguments: [`{ type: ${field.type}, required: ${field.required} }`]
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
**2. Class Validator Plugin**
|
|
138
|
-
```typescript
|
|
139
|
-
class ClassValidatorPlugin implements Plugin {
|
|
140
|
-
name = 'class-validator';
|
|
141
|
-
priority = 90;
|
|
142
|
-
|
|
143
|
-
hooks = {
|
|
144
|
-
onPropertyGeneration(property, field) {
|
|
145
|
-
if (field.required) {
|
|
146
|
-
property.addDecorator({ name: 'IsNotEmpty' });
|
|
147
|
-
}
|
|
148
|
-
if (field.type === 'string') {
|
|
149
|
-
property.addDecorator({ name: 'IsString' });
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
**3. ESM Plugin**
|
|
157
|
-
```typescript
|
|
158
|
-
class ESMPlugin implements Plugin {
|
|
159
|
-
name = 'esm';
|
|
160
|
-
priority = 200; // Runs after others
|
|
161
|
-
|
|
162
|
-
hooks = {
|
|
163
|
-
afterGeneration(context) {
|
|
164
|
-
// Add .js extensions to all imports
|
|
165
|
-
for (const sourceFile of context.project.getSourceFiles()) {
|
|
166
|
-
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
167
|
-
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
168
|
-
if (moduleSpecifier.startsWith('./') && !moduleSpecifier.endsWith('.js')) {
|
|
169
|
-
importDecl.setModuleSpecifier(moduleSpecifier + '.js');
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### Plugin Priority System
|
|
179
|
-
|
|
180
|
-
Plugins execute in priority order (lower numbers first):
|
|
181
|
-
|
|
182
|
-
1. **Core plugins** (priority 0-50): Base class-transformer decorators
|
|
183
|
-
2. **Feature plugins** (priority 50-150): NestJS, class-validator
|
|
184
|
-
3. **Transform plugins** (priority 150-200): ESM, custom templates
|
|
185
|
-
4. **Finalization plugins** (priority 200+): Formatting, validation
|
|
186
|
-
|
|
187
|
-
## Why Intermediate Representation (IR)?
|
|
188
|
-
|
|
189
|
-
### Problem with Format-Specific Generators
|
|
190
|
-
|
|
191
|
-
Without IR, each input format needs its own generator:
|
|
192
|
-
|
|
193
|
-
```
|
|
194
|
-
OpenAPI → OpenAPI Generator → TypeScript
|
|
195
|
-
CRD → CRD Generator → TypeScript
|
|
196
|
-
JSON Schema → JSON Schema Generator → TypeScript
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
This leads to:
|
|
200
|
-
- ❌ Code duplication across generators
|
|
201
|
-
- ❌ Inconsistent output for similar schemas
|
|
202
|
-
- ❌ Hard to add new input formats
|
|
203
|
-
- ❌ Difficult to maintain shared features
|
|
204
|
-
|
|
205
|
-
### Solution: Unified IR Layer
|
|
206
|
-
|
|
207
|
-
Klasik uses a unified Intermediate Representation:
|
|
208
|
-
|
|
209
|
-
```
|
|
210
|
-
OpenAPI ──┐
|
|
211
|
-
├──→ SchemaIR ──→ TypeScript Generator ──→ TypeScript
|
|
212
|
-
CRD ──────┤
|
|
213
|
-
│
|
|
214
|
-
JSON Schema ─┘
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
**IR Structure:**
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
interface SchemaIR {
|
|
221
|
-
schemas: Map<string, ObjectSchema>;
|
|
222
|
-
metadata: {
|
|
223
|
-
sourceFormat: 'openapi' | 'crd' | 'jsonschema';
|
|
224
|
-
version: string;
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
interface ObjectSchema {
|
|
229
|
-
name: string;
|
|
230
|
-
description?: string;
|
|
231
|
-
fields: Field[];
|
|
232
|
-
required: string[];
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
interface Field {
|
|
236
|
-
name: string;
|
|
237
|
-
type: string;
|
|
238
|
-
description?: string;
|
|
239
|
-
required: boolean;
|
|
240
|
-
array?: boolean;
|
|
241
|
-
enum?: string[];
|
|
242
|
-
format?: string;
|
|
243
|
-
}
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
**Benefits:**
|
|
247
|
-
- ✅ **Single Source of Truth**: One generator for all formats
|
|
248
|
-
- ✅ **Consistency**: Same TypeScript output for equivalent schemas
|
|
249
|
-
- ✅ **Extensibility**: Add new formats by implementing IR converter
|
|
250
|
-
- ✅ **Testability**: Test converters and generator separately
|
|
251
|
-
- ✅ **Optimization**: Transform IR before generation (deduplication, normalization)
|
|
252
|
-
|
|
253
|
-
### IR Conversion Flow
|
|
254
|
-
|
|
255
|
-
**OpenAPI to IR:**
|
|
256
|
-
```typescript
|
|
257
|
-
OpenAPILoader.load(spec)
|
|
258
|
-
→ OpenAPIConverter.toIR(spec)
|
|
259
|
-
→ SchemaIR
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
**CRD to IR:**
|
|
263
|
-
```typescript
|
|
264
|
-
CRDLoader.load(yaml)
|
|
265
|
-
→ CRDConverter.toIR(crd)
|
|
266
|
-
→ SchemaIR
|
|
267
|
-
→ mergeIRs() // Deduplication for multiple CRDs
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
**JSON Schema to IR:**
|
|
271
|
-
```typescript
|
|
272
|
-
JSONSchemaLoader.load(schema)
|
|
273
|
-
→ JSONSchemaConverter.toIR(schema)
|
|
274
|
-
→ SchemaIR
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
## Architecture Components
|
|
278
|
-
|
|
279
|
-
### 1. Loaders (`src/loaders/`)
|
|
280
|
-
|
|
281
|
-
Responsible for fetching and parsing input specifications:
|
|
282
|
-
|
|
283
|
-
- **SpecLoader**: Fetches from URL or file, detects format (JSON/YAML)
|
|
284
|
-
- **CRDLoader**: Loads Kubernetes CRDs, handles multi-document YAML
|
|
285
|
-
- **JSONSchemaLoader**: Loads JSON Schema files
|
|
286
|
-
|
|
287
|
-
**Key Features:**
|
|
288
|
-
- HTTP/HTTPS support with custom headers
|
|
289
|
-
- File system support
|
|
290
|
-
- Automatic format detection
|
|
291
|
-
- External reference resolution (`--resolve-refs`)
|
|
292
|
-
|
|
293
|
-
### 2. Converters (`src/converters/`)
|
|
294
|
-
|
|
295
|
-
Transform input specifications to IR:
|
|
296
|
-
|
|
297
|
-
- **OpenAPIConverter**: OpenAPI 3.0 → SchemaIR
|
|
298
|
-
- **CRDConverter**: Kubernetes CRD → SchemaIR
|
|
299
|
-
- **JSONSchemaConverter**: JSON Schema → SchemaIR
|
|
300
|
-
|
|
301
|
-
**Responsibilities:**
|
|
302
|
-
- Schema traversal and extraction
|
|
303
|
-
- Type mapping (OpenAPI types → TypeScript types)
|
|
304
|
-
- Reference resolution
|
|
305
|
-
- Metadata extraction
|
|
306
|
-
|
|
307
|
-
### 3. Intermediate Representation (`src/ir/`)
|
|
308
|
-
|
|
309
|
-
Core data structures:
|
|
310
|
-
|
|
311
|
-
- **SchemaIR**: Top-level container
|
|
312
|
-
- **ObjectSchema**: Class/interface representation
|
|
313
|
-
- **Field**: Property representation
|
|
314
|
-
- **IRHelpers**: Utility functions for IR manipulation
|
|
315
|
-
|
|
316
|
-
### 4. Generator (`src/generator/`)
|
|
317
|
-
|
|
318
|
-
Produces TypeScript code from IR:
|
|
319
|
-
|
|
320
|
-
- **ClassGenerator**: Creates class declarations
|
|
321
|
-
- **PropertyGenerator**: Creates property declarations with decorators
|
|
322
|
-
- **ImportGenerator**: Manages import statements
|
|
323
|
-
- **ExportGenerator**: Creates barrel exports (index.ts)
|
|
324
|
-
|
|
325
|
-
**Uses ts-morph for:**
|
|
326
|
-
- AST construction
|
|
327
|
-
- Import management
|
|
328
|
-
- Formatting and pretty-printing
|
|
329
|
-
- Type-safe code generation
|
|
330
|
-
|
|
331
|
-
### 5. Plugins (`src/plugins/`)
|
|
332
|
-
|
|
333
|
-
Feature extensions:
|
|
334
|
-
|
|
335
|
-
- **CorePlugin**: Base class-transformer decorators (@Expose, @Type)
|
|
336
|
-
- **NestJSSwaggerPlugin**: @ApiProperty decorators (--nestjs-swagger)
|
|
337
|
-
- **ClassValidatorPlugin**: Validation decorators (--class-validator)
|
|
338
|
-
- **ESMPlugin**: .js extension injection (--esm)
|
|
339
|
-
|
|
340
|
-
### 6. CLI (`src/cli/`)
|
|
341
|
-
|
|
342
|
-
Command-line interface:
|
|
343
|
-
|
|
344
|
-
- **generate**: OpenAPI → TypeScript
|
|
345
|
-
- **download**: Download spec without generation
|
|
346
|
-
- **generate-crd**: Kubernetes CRD → TypeScript
|
|
347
|
-
- **generate-jsonschema**: JSON Schema → TypeScript
|
|
348
|
-
|
|
349
|
-
## Code Generation Pipeline
|
|
350
|
-
|
|
351
|
-
### Full Pipeline Flow
|
|
352
|
-
|
|
353
|
-
```
|
|
354
|
-
1. INPUT
|
|
355
|
-
├─ User runs CLI command
|
|
356
|
-
├─ Parse CLI arguments
|
|
357
|
-
└─ Initialize configuration
|
|
358
|
-
|
|
359
|
-
2. LOADING
|
|
360
|
-
├─ SpecLoader fetches specification
|
|
361
|
-
├─ Detect format (JSON/YAML)
|
|
362
|
-
├─ Parse with js-yaml or JSON.parse()
|
|
363
|
-
└─ Resolve external $refs if --resolve-refs
|
|
364
|
-
|
|
365
|
-
3. CONVERSION
|
|
366
|
-
├─ Select converter based on input format
|
|
367
|
-
├─ Convert to SchemaIR
|
|
368
|
-
├─ Merge IRs if multiple inputs (CRDs)
|
|
369
|
-
└─ Normalize and deduplicate
|
|
370
|
-
|
|
371
|
-
4. PLUGIN INITIALIZATION
|
|
372
|
-
├─ Load enabled plugins based on CLI flags
|
|
373
|
-
├─ Sort by priority
|
|
374
|
-
└─ Call beforeGeneration hooks
|
|
375
|
-
|
|
376
|
-
5. GENERATION
|
|
377
|
-
├─ Create ts-morph Project
|
|
378
|
-
├─ For each schema in IR:
|
|
379
|
-
│ ├─ Create SourceFile
|
|
380
|
-
│ ├─ Call onSchemaLoad hooks
|
|
381
|
-
│ ├─ Generate class declaration
|
|
382
|
-
│ ├─ Call onClassGeneration hooks
|
|
383
|
-
│ ├─ For each field:
|
|
384
|
-
│ │ ├─ Generate property
|
|
385
|
-
│ │ └─ Call onPropertyGeneration hooks
|
|
386
|
-
│ └─ Add imports
|
|
387
|
-
├─ Generate index.ts (barrel exports)
|
|
388
|
-
└─ Call afterGeneration hooks
|
|
389
|
-
|
|
390
|
-
6. OUTPUT
|
|
391
|
-
├─ Format all files (prettier via ts-morph)
|
|
392
|
-
├─ Write to disk
|
|
393
|
-
└─ Report statistics
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
### Example: Generating a Simple Class
|
|
397
|
-
|
|
398
|
-
**Input OpenAPI Schema:**
|
|
399
|
-
```yaml
|
|
400
|
-
components:
|
|
401
|
-
schemas:
|
|
402
|
-
User:
|
|
403
|
-
type: object
|
|
404
|
-
required:
|
|
405
|
-
- id
|
|
406
|
-
- email
|
|
407
|
-
properties:
|
|
408
|
-
id:
|
|
409
|
-
type: string
|
|
410
|
-
format: uuid
|
|
411
|
-
email:
|
|
412
|
-
type: string
|
|
413
|
-
format: email
|
|
414
|
-
name:
|
|
415
|
-
type: string
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
**Step 1: Convert to IR**
|
|
419
|
-
```typescript
|
|
420
|
-
{
|
|
421
|
-
name: 'User',
|
|
422
|
-
fields: [
|
|
423
|
-
{ name: 'id', type: 'string', required: true, format: 'uuid' },
|
|
424
|
-
{ name: 'email', type: 'string', required: true, format: 'email' },
|
|
425
|
-
{ name: 'name', type: 'string', required: false }
|
|
426
|
-
],
|
|
427
|
-
required: ['id', 'email']
|
|
428
|
-
}
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
**Step 2: Generate AST**
|
|
432
|
-
```typescript
|
|
433
|
-
const classDecl = sourceFile.addClass({
|
|
434
|
-
name: 'User',
|
|
435
|
-
isExported: true
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
// Core plugin adds @Expose
|
|
439
|
-
classDecl.addProperty({
|
|
440
|
-
name: 'id',
|
|
441
|
-
type: 'string',
|
|
442
|
-
decorators: [{ name: 'Expose' }, { name: 'IsUUID' }]
|
|
443
|
-
});
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
**Step 3: Output TypeScript**
|
|
447
|
-
```typescript
|
|
448
|
-
import { Expose } from 'class-transformer';
|
|
449
|
-
import { IsUUID, IsEmail, IsString, IsOptional } from 'class-validator';
|
|
450
|
-
import { ApiProperty } from '@nestjs/swagger';
|
|
451
|
-
|
|
452
|
-
export class User {
|
|
453
|
-
@Expose()
|
|
454
|
-
@ApiProperty({ type: String, format: 'uuid', required: true })
|
|
455
|
-
@IsUUID()
|
|
456
|
-
id: string;
|
|
457
|
-
|
|
458
|
-
@Expose()
|
|
459
|
-
@ApiProperty({ type: String, format: 'email', required: true })
|
|
460
|
-
@IsEmail()
|
|
461
|
-
email: string;
|
|
462
|
-
|
|
463
|
-
@Expose()
|
|
464
|
-
@ApiProperty({ type: String, required: false })
|
|
465
|
-
@IsOptional()
|
|
466
|
-
@IsString()
|
|
467
|
-
name?: string;
|
|
468
|
-
}
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
## Plugin System
|
|
472
|
-
|
|
473
|
-
### Creating a Custom Plugin
|
|
474
|
-
|
|
475
|
-
```typescript
|
|
476
|
-
import { Plugin, GenerationContext, ObjectSchema, Field } from 'klasik';
|
|
477
|
-
import { ClassDeclaration, PropertyDeclaration } from 'ts-morph';
|
|
478
|
-
|
|
479
|
-
export class CustomPlugin implements Plugin {
|
|
480
|
-
name = 'my-custom-plugin';
|
|
481
|
-
priority = 100;
|
|
482
|
-
|
|
483
|
-
hooks = {
|
|
484
|
-
// Called once before generation starts
|
|
485
|
-
beforeGeneration(context: GenerationContext): void {
|
|
486
|
-
console.log(`Generating ${context.schemas.size} schemas`);
|
|
487
|
-
},
|
|
488
|
-
|
|
489
|
-
// Called for each schema
|
|
490
|
-
onSchemaLoad(schema: ObjectSchema): void {
|
|
491
|
-
// Modify schema before generation
|
|
492
|
-
schema.fields.forEach(field => {
|
|
493
|
-
if (field.name.startsWith('_')) {
|
|
494
|
-
field.name = field.name.slice(1); // Remove leading underscore
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
},
|
|
498
|
-
|
|
499
|
-
// Called when class is created
|
|
500
|
-
onClassGeneration(classNode: ClassDeclaration, schema: ObjectSchema): void {
|
|
501
|
-
// Add class-level decorators or JSDoc
|
|
502
|
-
classNode.addJsDoc({
|
|
503
|
-
description: schema.description || `Generated class for ${schema.name}`
|
|
504
|
-
});
|
|
505
|
-
},
|
|
506
|
-
|
|
507
|
-
// Called for each property
|
|
508
|
-
onPropertyGeneration(property: PropertyDeclaration, field: Field): void {
|
|
509
|
-
// Add custom decorators based on field metadata
|
|
510
|
-
if (field.format === 'date-time') {
|
|
511
|
-
property.addDecorator({
|
|
512
|
-
name: 'Transform',
|
|
513
|
-
arguments: ['({ value }) => new Date(value)', { isDecoratorFactory: true }]
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
},
|
|
517
|
-
|
|
518
|
-
// Called after all generation is complete
|
|
519
|
-
afterGeneration(context: GenerationContext): void {
|
|
520
|
-
// Post-process all files
|
|
521
|
-
for (const sourceFile of context.project.getSourceFiles()) {
|
|
522
|
-
// Add file-level comments, organize imports, etc.
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
### Plugin Registration
|
|
530
|
-
|
|
531
|
-
Plugins are registered in the generator configuration:
|
|
532
|
-
|
|
533
|
-
```typescript
|
|
534
|
-
const generator = new TypeScriptGenerator({
|
|
535
|
-
plugins: [
|
|
536
|
-
new CorePlugin(),
|
|
537
|
-
new ClassValidatorPlugin(),
|
|
538
|
-
new NestJSSwaggerPlugin(),
|
|
539
|
-
new CustomPlugin()
|
|
540
|
-
]
|
|
541
|
-
});
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
### Built-in Plugins
|
|
545
|
-
|
|
546
|
-
| Plugin | Priority | Flag | Purpose |
|
|
547
|
-
|--------|----------|------|---------|
|
|
548
|
-
| CorePlugin | 10 | (always) | @Expose, @Type decorators |
|
|
549
|
-
| ClassValidatorPlugin | 50 | --class-validator | @IsString, @IsNumber, etc. |
|
|
550
|
-
| NestJSSwaggerPlugin | 60 | --nestjs-swagger | @ApiProperty decorators |
|
|
551
|
-
| ESMPlugin | 200 | --esm | Add .js extensions to imports |
|
|
552
|
-
|
|
553
|
-
## Testing Strategy
|
|
554
|
-
|
|
555
|
-
### Test Coverage
|
|
556
|
-
|
|
557
|
-
Klasik has comprehensive test coverage across all components:
|
|
558
|
-
|
|
559
|
-
- **Total Tests**: 748 (as of latest build)
|
|
560
|
-
- **Test Framework**: Jest
|
|
561
|
-
- **Coverage Areas**:
|
|
562
|
-
- Loaders (URL fetching, format detection)
|
|
563
|
-
- Converters (OpenAPI, CRD, JSON Schema → IR)
|
|
564
|
-
- Generator (IR → TypeScript AST)
|
|
565
|
-
- Plugins (decorator generation)
|
|
566
|
-
- CLI (command parsing, execution)
|
|
567
|
-
- Integration (end-to-end generation)
|
|
568
|
-
|
|
569
|
-
### Test Organization
|
|
570
|
-
|
|
571
|
-
```
|
|
572
|
-
tests/
|
|
573
|
-
├── unit/
|
|
574
|
-
│ ├── loaders/
|
|
575
|
-
│ │ ├── spec-loader.test.ts
|
|
576
|
-
│ │ ├── crd-loader.test.ts
|
|
577
|
-
│ │ └── jsonschema-loader.test.ts
|
|
578
|
-
│ ├── converters/
|
|
579
|
-
│ │ ├── openapi-converter.test.ts
|
|
580
|
-
│ │ ├── crd-converter.test.ts
|
|
581
|
-
│ │ └── jsonschema-converter.test.ts
|
|
582
|
-
│ ├── generator/
|
|
583
|
-
│ │ ├── class-generator.test.ts
|
|
584
|
-
│ │ └── property-generator.test.ts
|
|
585
|
-
│ └── plugins/
|
|
586
|
-
│ ├── class-validator.test.ts
|
|
587
|
-
│ └── nestjs-swagger.test.ts
|
|
588
|
-
├── integration/
|
|
589
|
-
│ ├── openapi-generation.test.ts
|
|
590
|
-
│ ├── crd-generation.test.ts
|
|
591
|
-
│ └── jsonschema-generation.test.ts
|
|
592
|
-
└── fixtures/
|
|
593
|
-
├── openapi/
|
|
594
|
-
├── crds/
|
|
595
|
-
└── jsonschema/
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
### Test Examples
|
|
599
|
-
|
|
600
|
-
**Unit Test: Type Conversion**
|
|
601
|
-
```typescript
|
|
602
|
-
describe('OpenAPIConverter', () => {
|
|
603
|
-
it('should convert string type to TypeScript string', () => {
|
|
604
|
-
const field = converter.convertField({
|
|
605
|
-
type: 'string',
|
|
606
|
-
description: 'User name'
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
expect(field.type).toBe('string');
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
it('should convert array type with items', () => {
|
|
613
|
-
const field = converter.convertField({
|
|
614
|
-
type: 'array',
|
|
615
|
-
items: { type: 'string' }
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
expect(field.type).toBe('string');
|
|
619
|
-
expect(field.array).toBe(true);
|
|
620
|
-
});
|
|
621
|
-
});
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
**Integration Test: Full Generation**
|
|
625
|
-
```typescript
|
|
626
|
-
describe('CRD Generation', () => {
|
|
627
|
-
it('should generate TypeScript from ArgoCD Application CRD', async () => {
|
|
628
|
-
const result = await generateFromCRD({
|
|
629
|
-
url: 'fixtures/application-crd.yaml',
|
|
630
|
-
output: 'tmp/output',
|
|
631
|
-
nestjsSwagger: true,
|
|
632
|
-
classValidator: true
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
expect(result.filesGenerated).toBeGreaterThan(0);
|
|
636
|
-
|
|
637
|
-
const applicationFile = fs.readFileSync('tmp/output/models/application.ts', 'utf-8');
|
|
638
|
-
expect(applicationFile).toContain('export class Application');
|
|
639
|
-
expect(applicationFile).toContain('@ApiProperty');
|
|
640
|
-
expect(applicationFile).toContain('@IsOptional');
|
|
641
|
-
});
|
|
642
|
-
});
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
### Running Tests
|
|
646
|
-
|
|
647
|
-
```bash
|
|
648
|
-
# Run all tests
|
|
649
|
-
npm test
|
|
650
|
-
|
|
651
|
-
# Run with coverage
|
|
652
|
-
npm run test:coverage
|
|
653
|
-
|
|
654
|
-
# Run specific test file
|
|
655
|
-
npm test -- spec-loader.test.ts
|
|
656
|
-
|
|
657
|
-
# Run in watch mode
|
|
658
|
-
npm test -- --watch
|
|
659
|
-
```
|
|
660
|
-
|
|
661
|
-
## Development Guide
|
|
662
|
-
|
|
663
|
-
### Project Structure
|
|
664
|
-
|
|
665
|
-
```
|
|
666
|
-
klasik-2/
|
|
667
|
-
├── src/
|
|
668
|
-
│ ├── cli/ # Command-line interface
|
|
669
|
-
│ │ ├── commands/ # CLI command implementations
|
|
670
|
-
│ │ └── index.ts # Main CLI entry point
|
|
671
|
-
│ ├── loaders/ # Specification loaders
|
|
672
|
-
│ ├── converters/ # Format converters to IR
|
|
673
|
-
│ ├── ir/ # Intermediate Representation
|
|
674
|
-
│ ├── generator/ # TypeScript code generator
|
|
675
|
-
│ ├── plugins/ # Plugin implementations
|
|
676
|
-
│ └── utils/ # Shared utilities
|
|
677
|
-
├── tests/ # Test suites
|
|
678
|
-
├── docs/ # Additional documentation
|
|
679
|
-
└── examples/ # Usage examples
|
|
680
|
-
|
|
681
|
-
```
|
|
682
|
-
|
|
683
|
-
### Building from Source
|
|
684
|
-
|
|
685
|
-
```bash
|
|
686
|
-
# Clone repository
|
|
687
|
-
git clone https://github.com/your-org/klasik-2.git
|
|
688
|
-
cd klasik-2
|
|
689
|
-
|
|
690
|
-
# Install dependencies
|
|
691
|
-
npm install
|
|
692
|
-
|
|
693
|
-
# Build TypeScript
|
|
694
|
-
npm run build
|
|
695
|
-
|
|
696
|
-
# Run CLI locally
|
|
697
|
-
node dist/cli/index.js generate --help
|
|
698
|
-
|
|
699
|
-
# Run tests
|
|
700
|
-
npm test
|
|
701
|
-
```
|
|
702
|
-
|
|
703
|
-
### Making Changes
|
|
704
|
-
|
|
705
|
-
1. **Add New Feature**:
|
|
706
|
-
- Create plugin in `src/plugins/`
|
|
707
|
-
- Register plugin in generator configuration
|
|
708
|
-
- Add CLI flag if needed
|
|
709
|
-
- Write unit tests
|
|
710
|
-
- Update documentation
|
|
711
|
-
|
|
712
|
-
2. **Add New Input Format**:
|
|
713
|
-
- Create loader in `src/loaders/`
|
|
714
|
-
- Create converter in `src/converters/`
|
|
715
|
-
- Add CLI command in `src/cli/commands/`
|
|
716
|
-
- Write integration tests
|
|
717
|
-
- Update README with examples
|
|
718
|
-
|
|
719
|
-
3. **Fix Bug**:
|
|
720
|
-
- Write failing test that reproduces bug
|
|
721
|
-
- Fix the issue
|
|
722
|
-
- Verify test passes
|
|
723
|
-
- Check for regression with full test suite
|
|
724
|
-
|
|
725
|
-
### Debugging
|
|
726
|
-
|
|
727
|
-
**Debug Generated Code:**
|
|
728
|
-
```typescript
|
|
729
|
-
// Enable verbose logging
|
|
730
|
-
const generator = new TypeScriptGenerator({ verbose: true });
|
|
731
|
-
|
|
732
|
-
// Inspect IR before generation
|
|
733
|
-
console.log(JSON.stringify(schemaIR, null, 2));
|
|
734
|
-
|
|
735
|
-
// Inspect AST after generation
|
|
736
|
-
const sourceFile = project.getSourceFile('user.ts');
|
|
737
|
-
console.log(sourceFile.getFullText());
|
|
738
|
-
```
|
|
739
|
-
|
|
740
|
-
**Debug Plugin Execution:**
|
|
741
|
-
```typescript
|
|
742
|
-
class DebugPlugin implements Plugin {
|
|
743
|
-
name = 'debug';
|
|
744
|
-
priority = 1; // Run first
|
|
745
|
-
|
|
746
|
-
hooks = {
|
|
747
|
-
onPropertyGeneration(property, field) {
|
|
748
|
-
console.log(`Generating property: ${field.name} (${field.type})`);
|
|
749
|
-
}
|
|
750
|
-
};
|
|
751
|
-
}
|
|
752
|
-
```
|
|
753
|
-
|
|
754
|
-
### Performance Optimization
|
|
755
|
-
|
|
756
|
-
**IR Caching:**
|
|
757
|
-
```typescript
|
|
758
|
-
// Cache converted IR for repeated generation
|
|
759
|
-
const irCache = new Map<string, SchemaIR>();
|
|
760
|
-
|
|
761
|
-
if (irCache.has(specUrl)) {
|
|
762
|
-
schemaIR = irCache.get(specUrl);
|
|
763
|
-
} else {
|
|
764
|
-
schemaIR = await converter.toIR(spec);
|
|
765
|
-
irCache.set(specUrl, schemaIR);
|
|
766
|
-
}
|
|
767
|
-
```
|
|
768
|
-
|
|
769
|
-
**Parallel Generation:**
|
|
770
|
-
```typescript
|
|
771
|
-
// Generate multiple files in parallel
|
|
772
|
-
const promises = Array.from(schemaIR.schemas.entries()).map(([name, schema]) => {
|
|
773
|
-
return generateClass(schema);
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
await Promise.all(promises);
|
|
777
|
-
```
|
|
778
|
-
|
|
779
|
-
## Design Decisions
|
|
780
|
-
|
|
781
|
-
### Why Not Use openapi-generator?
|
|
782
|
-
|
|
783
|
-
The official openapi-generator has limitations:
|
|
784
|
-
- Java-based (requires JVM)
|
|
785
|
-
- Heavy dependencies
|
|
786
|
-
- Difficult to customize
|
|
787
|
-
- Inconsistent TypeScript output
|
|
788
|
-
- No CRD or JSON Schema support
|
|
789
|
-
|
|
790
|
-
Klasik provides:
|
|
791
|
-
- Pure TypeScript/Node.js (no JVM)
|
|
792
|
-
- Lightweight and fast
|
|
793
|
-
- Easy plugin system for customization
|
|
794
|
-
- Consistent, high-quality output
|
|
795
|
-
- Multi-format support
|
|
796
|
-
|
|
797
|
-
### Why class-transformer?
|
|
798
|
-
|
|
799
|
-
class-transformer provides:
|
|
800
|
-
- Serialization/deserialization with @Type decorators
|
|
801
|
-
- Nested object support
|
|
802
|
-
- Transform hooks
|
|
803
|
-
- Integration with NestJS and other frameworks
|
|
804
|
-
|
|
805
|
-
Alternative approaches (plain interfaces) lack runtime type information.
|
|
806
|
-
|
|
807
|
-
### Why Mustache Templates?
|
|
808
|
-
|
|
809
|
-
Custom templates use Mustache because:
|
|
810
|
-
- Simple, logic-less syntax
|
|
811
|
-
- Easy to learn
|
|
812
|
-
- No arbitrary code execution (security)
|
|
813
|
-
- Works with any text format
|
|
814
|
-
|
|
815
|
-
Users can override default templates for custom output formats.
|
|
816
|
-
|
|
817
|
-
## Future Enhancements
|
|
818
|
-
|
|
819
|
-
Potential improvements for future versions:
|
|
820
|
-
|
|
821
|
-
1. **GraphQL Support**: Add GraphQL schema → TypeScript converter
|
|
822
|
-
2. **Zod Integration**: Generate Zod schemas alongside classes
|
|
823
|
-
3. **Async API Support**: Support AsyncAPI specifications
|
|
824
|
-
4. **Watch Mode**: Regenerate on spec file changes
|
|
825
|
-
5. **Incremental Generation**: Only regenerate changed schemas
|
|
826
|
-
6. **Source Maps**: Map generated code back to spec locations
|
|
827
|
-
7. **Custom Validators**: Plugin API for custom validation decorators
|
|
828
|
-
8. **gRPC Support**: Generate from Protocol Buffers
|
|
829
|
-
|
|
830
|
-
## Contributing
|
|
831
|
-
|
|
832
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for:
|
|
833
|
-
- Code style guidelines
|
|
834
|
-
- Pull request process
|
|
835
|
-
- Development workflow
|
|
836
|
-
- Release process
|
|
837
|
-
|
|
838
|
-
## References
|
|
839
|
-
|
|
840
|
-
- [ts-morph Documentation](https://ts-morph.com/)
|
|
841
|
-
- [OpenAPI Specification](https://swagger.io/specification/)
|
|
842
|
-
- [Kubernetes CRD Documentation](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/)
|
|
843
|
-
- [JSON Schema Specification](https://json-schema.org/)
|
|
844
|
-
- [class-transformer](https://github.com/typestack/class-transformer)
|
|
845
|
-
- [class-validator](https://github.com/typestack/class-validator)
|