naider 1.14.0 → 1.16.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,1819 @@
1
+ export class KotlinGenerator {
2
+ constructor(options = {}) {
3
+ this.indent = 0;
4
+ this.output = [];
5
+ this.models = new Map();
6
+ this.schemas = new Map();
7
+ this.usesKtor = false;
8
+ this.imports = new Set();
9
+ this.hasTests = false;
10
+ this.hasAsserts = false;
11
+ this.sourceMap = [];
12
+ this.currentSourceLine = 0;
13
+ this.sourceFile = options.sourceFile || null;
14
+ this.authSecret = null;
15
+ this.dbVar = null;
16
+ }
17
+
18
+ generate(ast) {
19
+ this.visitProgram(ast);
20
+
21
+ const preamble = [];
22
+
23
+ for (const imp of this.imports) {
24
+ preamble.push(`import ${imp}`);
25
+ }
26
+
27
+ if (this.hasTests) {
28
+ this.addImport('kotlin.test.Test');
29
+ this.addImport('kotlin.test.assertEquals');
30
+ }
31
+
32
+ if (preamble.length > 0) {
33
+ preamble.push('');
34
+ this.output.unshift(...preamble);
35
+ }
36
+
37
+ return this.output.join('\n');
38
+ }
39
+
40
+ emit(line) {
41
+ this.output.push(' '.repeat(this.indent) + line);
42
+ this.sourceMap.push(this.currentSourceLine);
43
+ }
44
+
45
+ emitRaw(line) {
46
+ this.output.push(line);
47
+ this.sourceMap.push(this.currentSourceLine);
48
+ }
49
+
50
+ addImport(path) {
51
+ this.imports.add(path);
52
+ }
53
+
54
+ visitProgram(node) {
55
+ for (const stmt of node.body) {
56
+ this.visitStatement(stmt);
57
+ }
58
+ }
59
+
60
+ // ===== Statement dispatcher =====
61
+
62
+ visitStatement(node) {
63
+ if (node._line) this.currentSourceLine = node._line;
64
+ switch (node.type) {
65
+ case 'Use': return this.visitUse(node);
66
+ case 'UseDestructured': return this.visitUseDestructured(node);
67
+ case 'Function': return this.visitFunction(node);
68
+ case 'Return': return this.visitReturn(node);
69
+ case 'ReturnStatus': return this.visitReturnStatus(node);
70
+ case 'ReturnMethod': return this.visitReturnMethod(node);
71
+ case 'TypedVar': return this.visitTypedVar(node);
72
+ case 'If': return this.visitIf(node);
73
+ case 'Each': return this.visitEach(node);
74
+ case 'For': return this.visitFor(node);
75
+ case 'While': return this.visitWhile(node);
76
+ case 'Match': return this.visitMatch(node);
77
+ case 'Try': return this.visitTry(node);
78
+ case 'Server': return this.visitServer(node);
79
+ case 'Bot': return this.visitBot(node);
80
+ case 'Model': return this.visitModel(node);
81
+ case 'On': return this.visitOn(node);
82
+ case 'Log': return this.visitLog(node);
83
+ case 'Throw': return this.visitThrow(node);
84
+ case 'Break': this.emit('break'); return;
85
+ case 'Continue': this.emit('continue'); return;
86
+ case 'Assignment': return this.visitAssignment(node);
87
+ case 'CompoundAssign': return this.visitCompoundAssign(node);
88
+ case 'ExprStatement': this.emit(this.expr(node.expression)); return;
89
+ case 'DbConnect': return this.visitDbConnect(node);
90
+ case 'DbDir': return this.visitDbDir(node);
91
+ case 'DbSql': return this.visitDbSql(node);
92
+ case 'AwaitAll': return this.visitAwaitAllStatement(node);
93
+ case 'SchemaDecl': return this.visitSchema(node);
94
+ case 'CrudDecl': return this.visitCrud('app', node);
95
+ case 'AuthDecl': return this.visitAuth('app', node);
96
+ case 'CorsDecl': return this.visitCors('app', node);
97
+ case 'LimitDecl': return this.visitLimit('app', node);
98
+ case 'EnvDecl': return this.visitEnv(node);
99
+ case 'EveryDecl': return this.visitEvery(node);
100
+ case 'WatchDecl': return this.visitWatch(node);
101
+ case 'StaticDecl': return this.visitStatic('app', node);
102
+ case 'WsDecl': return this.visitWs(node);
103
+ case 'GroupDecl': return this.visitGroup('app', node);
104
+ case 'ErrorHandler': return this.visitErrorHandler('app', node);
105
+ case 'CookieDecl': return this.visitCookie('app', node);
106
+ case 'UploadDecl': return this.visitUpload('app', node);
107
+ case 'SessionDecl': return this.visitSession('app', node);
108
+ case 'ViewDecl': return this.visitView('app', node);
109
+ case 'SseDecl': return this.visitSse('app', node);
110
+ case 'CacheDecl': return this.visitCache('app', node);
111
+ case 'MiddlewareRef': return this.visitMiddlewareRef('app', node);
112
+ case 'ReturnRender': return this.visitReturnRender(node);
113
+ case 'ValidateDecl': return this.visitValidate('app', node);
114
+ case 'TestDecl': return this.visitTest(node);
115
+ case 'AssertStmt': return this.visitAssert(node);
116
+ case 'QueueDecl': return this.visitQueue(node);
117
+ case 'OpenapiDecl': return this.visitOpenapi('app', node);
118
+ case 'ReturnRedirect': return this.visitReturnRedirect(node);
119
+ case 'ReturnDownload': return this.visitReturnDownload(node);
120
+ case 'PromptDecl': return this.visitPrompt(node);
121
+ case 'Page': return this.visitPage(node);
122
+ case 'CliApp': return this.visitCli(node);
123
+ case 'MailConfig': return this.visitMail(node);
124
+ case 'DesktopApp': return this.visitDesktop(node);
125
+ case 'Screen': return this.visitScreen(node);
126
+ case 'OauthDecl': return this.visitOauth(node);
127
+ case 'PayDecl': return this.visitPay(node);
128
+ case 'StorageDecl': return this.visitStorage(node);
129
+ case 'PdfDecl': return this.visitPdf(node);
130
+ case 'I18nDecl': return this.visitI18n(node);
131
+ case 'PushDecl': return this.visitPush(node);
132
+ case 'SearchDecl': return this.visitSearch(node);
133
+ case 'ImageDecl': return this.visitImage(node);
134
+ case 'CsvDecl': return this.visitCsv(node);
135
+ case 'LoggingDecl': return this.visitLogging(node);
136
+ case 'MigrateDecl': return this.visitMigrate(node);
137
+ case 'GrpcDecl': return this.visitGrpc(node);
138
+ case 'WebrtcDecl': return this.visitWebrtc(node);
139
+ case 'BlockchainDecl': return this.visitBlockchain(node);
140
+ default:
141
+ this.emit(`// unknown: ${node.type}`);
142
+ }
143
+ }
144
+
145
+ // ===== Type mapping =====
146
+
147
+ mapType(naideType) {
148
+ const map = {
149
+ 'str': 'String',
150
+ 'string': 'String',
151
+ 'int': 'Int',
152
+ 'integer': 'Int',
153
+ 'num': 'Double',
154
+ 'number': 'Double',
155
+ 'bool': 'Boolean',
156
+ 'boolean': 'Boolean',
157
+ 'list': 'MutableList<Any>',
158
+ 'map': 'MutableMap<String, Any>',
159
+ 'any': 'Any',
160
+ 'void': 'Unit',
161
+ 'auto': 'Int',
162
+ 'timestamp': 'String',
163
+ };
164
+ return map[naideType] || 'Any';
165
+ }
166
+
167
+ // ===== Imports =====
168
+
169
+ visitUse(node) {
170
+ const raw = node.source ? this.rawString(node.source) : node.name;
171
+ const alias = node.alias || node.name;
172
+ const mapped = this.mapModuleName(raw);
173
+ if (alias !== mapped) {
174
+ this.emit(`import ${mapped} as ${alias}`);
175
+ } else {
176
+ this.emit(`import ${mapped}`);
177
+ }
178
+ }
179
+
180
+ visitUseDestructured(node) {
181
+ const raw = this.rawString(node.source);
182
+ const mapped = this.mapModuleName(raw);
183
+ for (const n of node.names) {
184
+ if (n.alias) {
185
+ this.emit(`import ${mapped}.${n.name} as ${n.alias}`);
186
+ } else {
187
+ this.emit(`import ${mapped}.${n.name}`);
188
+ }
189
+ }
190
+ }
191
+
192
+ mapModuleName(name) {
193
+ const clean = name.replace(/^['"]|['"]$/g, '');
194
+ const map = {
195
+ 'express': 'io.ktor.server.netty',
196
+ 'axios': 'io.ktor.client',
197
+ 'fs': 'java.io.File',
198
+ 'path': 'java.nio.file.Paths',
199
+ 'crypto': 'java.security.MessageDigest',
200
+ 'uuid': 'java.util.UUID',
201
+ };
202
+ return map[clean] || clean;
203
+ }
204
+
205
+ // ===== Functions =====
206
+
207
+ visitFunction(node) {
208
+ const async = node.isAsync ? 'suspend ' : '';
209
+ const params = node.params.map(p => {
210
+ let s = '';
211
+ if (p.spread) s += 'vararg ';
212
+ s += `${p.name}: Any`;
213
+ if (p.defaultValue) s += ` = ${this.expr(p.defaultValue)}`;
214
+ return s;
215
+ }).join(', ');
216
+
217
+ this.emit(`${async}fun ${node.name}(${params}) {`);
218
+ this.indent++;
219
+ if (node.body.length === 0) {
220
+ this.emit('// empty');
221
+ } else {
222
+ for (const stmt of node.body) {
223
+ this.visitStatement(stmt);
224
+ }
225
+ }
226
+ this.indent--;
227
+ this.emit('}');
228
+ this.emitRaw('');
229
+ }
230
+
231
+ visitReturn(node) {
232
+ if (node.value === null) {
233
+ this.emit('return');
234
+ } else {
235
+ this.emit(`return ${this.expr(node.value)}`);
236
+ }
237
+ }
238
+
239
+ visitReturnStatus(node) {
240
+ this.emit(`call.respond(HttpStatusCode.fromValue(${this.expr(node.statusCode)}), ${this.expr(node.body)})`);
241
+ this.emit('return');
242
+ }
243
+
244
+ visitReturnMethod(node) {
245
+ const val = this.expr(node.value);
246
+ switch (node.method) {
247
+ case 'redirect':
248
+ this.emit(`call.respondRedirect(${val})`);
249
+ break;
250
+ case 'html':
251
+ this.emit(`call.respondText(${val}, ContentType.Text.Html)`);
252
+ break;
253
+ case 'text':
254
+ this.emit(`call.respondText(${val})`);
255
+ break;
256
+ case 'file':
257
+ this.emit(`call.respondFile(File(${val}))`);
258
+ break;
259
+ default:
260
+ this.emit(`call.respond(${val})`);
261
+ }
262
+ this.emit('return');
263
+ }
264
+
265
+ // ===== Variables =====
266
+
267
+ visitTypedVar(node) {
268
+ const keyword = node.isMut ? 'var' : 'val';
269
+ this.emit(`${keyword} ${node.name} = ${this.expr(node.value)}`);
270
+ }
271
+
272
+ visitAssignment(node) {
273
+ const value = this.expr(node.value);
274
+
275
+ if (node.target.type === 'Array') {
276
+ const names = node.target.elements.map(e => this.expr(e));
277
+ this.emit(`val (__tmp) = ${value}`);
278
+ names.forEach((n, i) => {
279
+ this.emit(`val ${n} = __tmp[${i}]`);
280
+ });
281
+ } else if (node.target.type === 'Object') {
282
+ const tempVar = '__d';
283
+ this.emit(`val ${tempVar} = ${value}`);
284
+ for (const p of node.target.properties) {
285
+ const key = p.type === 'shorthand' ? p.key : p.key;
286
+ const varName = p.type === 'shorthand' ? p.key : this.expr(p.value);
287
+ this.emit(`val ${varName} = ${tempVar}["${key}"]`);
288
+ }
289
+ } else {
290
+ this.emit(`${this.expr(node.target)} = ${value}`);
291
+ }
292
+ }
293
+
294
+ visitCompoundAssign(node) {
295
+ this.emit(`${this.expr(node.target)} ${node.op} ${this.expr(node.value)}`);
296
+ }
297
+
298
+ // ===== Control flow =====
299
+
300
+ visitIf(node) {
301
+ this.emit(`if (${this.expr(node.condition)}) {`);
302
+ this.indent++;
303
+ if (node.body.length === 0) {
304
+ this.emit('// empty');
305
+ } else {
306
+ for (const stmt of node.body) this.visitStatement(stmt);
307
+ }
308
+ this.indent--;
309
+
310
+ for (const elif of node.elifs) {
311
+ this.emit(`} else if (${this.expr(elif.condition)}) {`);
312
+ this.indent++;
313
+ if (elif.body.length === 0) {
314
+ this.emit('// empty');
315
+ } else {
316
+ for (const stmt of elif.body) this.visitStatement(stmt);
317
+ }
318
+ this.indent--;
319
+ }
320
+
321
+ if (node.elseBody) {
322
+ this.emit('} else {');
323
+ this.indent++;
324
+ if (node.elseBody.length === 0) {
325
+ this.emit('// empty');
326
+ } else {
327
+ for (const stmt of node.elseBody) this.visitStatement(stmt);
328
+ }
329
+ this.indent--;
330
+ }
331
+ this.emit('}');
332
+ }
333
+
334
+ visitEach(node) {
335
+ const collection = this.expr(node.collection);
336
+ if (node.key) {
337
+ this.emit(`for ((${node.key}, ${node.value}) in ${collection}) {`);
338
+ } else {
339
+ this.emit(`for (${node.value} in ${collection}) {`);
340
+ }
341
+ this.indent++;
342
+ if (node.body.length === 0) {
343
+ this.emit('// empty');
344
+ } else {
345
+ for (const stmt of node.body) this.visitStatement(stmt);
346
+ }
347
+ this.indent--;
348
+ this.emit('}');
349
+ }
350
+
351
+ visitFor(node) {
352
+ const varName = node.varName;
353
+ const start = this.expr(node.start);
354
+ const end = this.expr(node.end);
355
+ this.emit(`for (${varName} in ${start} until ${end}) {`);
356
+ this.indent++;
357
+ if (node.body.length === 0) {
358
+ this.emit('// empty');
359
+ } else {
360
+ for (const stmt of node.body) this.visitStatement(stmt);
361
+ }
362
+ this.indent--;
363
+ this.emit('}');
364
+ }
365
+
366
+ visitWhile(node) {
367
+ this.emit(`while (${this.expr(node.condition)}) {`);
368
+ this.indent++;
369
+ if (node.body.length === 0) {
370
+ this.emit('// empty');
371
+ } else {
372
+ for (const stmt of node.body) this.visitStatement(stmt);
373
+ }
374
+ this.indent--;
375
+ this.emit('}');
376
+ }
377
+
378
+ visitMatch(node) {
379
+ this.emit(`when (${this.expr(node.value)}) {`);
380
+ this.indent++;
381
+ for (const c of node.cases) {
382
+ if (c.pattern.type === 'DefaultPattern') {
383
+ this.emit('else -> {');
384
+ } else {
385
+ this.emit(`${this.expr(c.pattern)} -> {`);
386
+ }
387
+ this.indent++;
388
+ for (const stmt of c.body) this.visitStatement(stmt);
389
+ this.indent--;
390
+ this.emit('}');
391
+ }
392
+ this.indent--;
393
+ this.emit('}');
394
+ }
395
+
396
+ visitTry(node) {
397
+ this.emit('try {');
398
+ this.indent++;
399
+ if (node.body.length === 0) {
400
+ this.emit('// empty');
401
+ } else {
402
+ for (const stmt of node.body) this.visitStatement(stmt);
403
+ }
404
+ this.indent--;
405
+
406
+ if (node.catchBody) {
407
+ const catchParam = node.catchVar || 'e';
408
+ this.emit(`} catch (${catchParam}: Exception) {`);
409
+ this.indent++;
410
+ if (node.catchBody.length === 0) {
411
+ this.emit('// empty');
412
+ } else {
413
+ for (const stmt of node.catchBody) this.visitStatement(stmt);
414
+ }
415
+ this.indent--;
416
+ }
417
+
418
+ if (node.ensureBody) {
419
+ this.emit('} finally {');
420
+ this.indent++;
421
+ if (node.ensureBody.length === 0) {
422
+ this.emit('// empty');
423
+ } else {
424
+ for (const stmt of node.ensureBody) this.visitStatement(stmt);
425
+ }
426
+ this.indent--;
427
+ }
428
+ this.emit('}');
429
+ }
430
+
431
+ // ===== Server (Ktor) =====
432
+
433
+ visitServer(node) {
434
+ this.usesKtor = true;
435
+ this.addImport('io.ktor.server.engine.*');
436
+ this.addImport('io.ktor.server.netty.*');
437
+ this.addImport('io.ktor.server.routing.*');
438
+ this.addImport('io.ktor.server.response.*');
439
+ this.addImport('io.ktor.server.request.*');
440
+ this.addImport('io.ktor.http.*');
441
+ this.addImport('io.ktor.serialization.kotlinx.json.*');
442
+ this.addImport('io.ktor.server.plugins.contentnegotiation.*');
443
+
444
+ const port = node.port ? this.expr(node.port) : '3000';
445
+
446
+ this.emitRaw('');
447
+ this.emit(`fun main() {`);
448
+ this.indent++;
449
+ this.emit(`embeddedServer(Netty, port = ${port}) {`);
450
+ this.indent++;
451
+ this.emit('install(ContentNegotiation) {');
452
+ this.indent++;
453
+ this.emit('json()');
454
+ this.indent--;
455
+ this.emit('}');
456
+
457
+ for (const mid of node.middleware) {
458
+ this.emit(`// middleware: custom`);
459
+ }
460
+
461
+ this.emit('routing {');
462
+ this.indent++;
463
+
464
+ const errorHandlers = [];
465
+ for (const child of node.routes) {
466
+ if (child.type === 'Route') {
467
+ this.visitRoute(node.name, child);
468
+ } else if (child.type === 'ErrorHandler') {
469
+ errorHandlers.push(child);
470
+ } else if (child.type === 'CorsDecl') {
471
+ this.visitCors(node.name, child);
472
+ } else if (child.type === 'AuthDecl') {
473
+ this.visitAuth(node.name, child);
474
+ } else if (child.type === 'StaticDecl') {
475
+ this.visitStatic(node.name, child);
476
+ } else if (child.type === 'GroupDecl') {
477
+ this.visitGroup(node.name, child);
478
+ } else if (child.type === 'CrudDecl') {
479
+ this.visitCrud(node.name, child);
480
+ } else if (child.type === 'WsDecl') {
481
+ this.visitWs(child);
482
+ } else if (child.type === 'SchemaDecl') {
483
+ this.visitSchema(child);
484
+ } else if (child.type === 'GraphqlDecl') {
485
+ this.emit(`// GraphQL: ${this.rawString(child.path)}`);
486
+ } else {
487
+ this.visitStatement(child);
488
+ }
489
+ }
490
+
491
+ this.indent--;
492
+ this.emit('}');
493
+
494
+ for (const eh of errorHandlers) {
495
+ this.visitErrorHandler(node.name, eh);
496
+ }
497
+
498
+ this.indent--;
499
+ this.emit('}.start(wait = true)');
500
+ this.indent--;
501
+ this.emit('}');
502
+ this.emitRaw('');
503
+ }
504
+
505
+ visitRoute(appName, route) {
506
+ const method = route.method === 'del' ? 'delete' : route.method;
507
+ const path = this.rawString(route.path);
508
+
509
+ this.emit(`${method}("${path}") {`);
510
+ this.indent++;
511
+
512
+ for (let i = 0; i < route.body.length; i++) {
513
+ const stmt = route.body[i];
514
+ if (stmt.type === 'Return' && stmt.value !== null) {
515
+ this.emit(`call.respond(${this.expr(stmt.value)})`);
516
+ } else {
517
+ this.visitStatement(stmt);
518
+ }
519
+ }
520
+
521
+ this.indent--;
522
+ this.emit('}');
523
+ }
524
+
525
+ visitGroup(appName, node) {
526
+ const prefix = this.rawString(node.prefix);
527
+ this.emit(`route("${prefix}") {`);
528
+ this.indent++;
529
+
530
+ for (const child of node.routes) {
531
+ if (child.type === 'Route') {
532
+ this.visitRoute(appName, child);
533
+ } else if (child.type === 'CrudDecl') {
534
+ this.visitCrud(appName, child);
535
+ } else if (child.type === 'GroupDecl') {
536
+ this.visitGroup(appName, child);
537
+ } else {
538
+ this.visitStatement(child);
539
+ }
540
+ }
541
+
542
+ this.indent--;
543
+ this.emit('}');
544
+ this.emitRaw('');
545
+ }
546
+
547
+ visitErrorHandler(appName, node) {
548
+ this.emit('// error handler');
549
+ this.emit('install(StatusPages) {');
550
+ this.indent++;
551
+ this.emit('exception<Throwable> { call, cause ->');
552
+ this.indent++;
553
+ for (const stmt of node.body) this.visitStatement(stmt);
554
+ if (node.body.length === 0) {
555
+ this.emit('call.respondText("Internal Server Error", status = HttpStatusCode.InternalServerError)');
556
+ }
557
+ this.indent--;
558
+ this.emit('}');
559
+ this.indent--;
560
+ this.emit('}');
561
+ this.emitRaw('');
562
+ }
563
+
564
+ // ===== Model =====
565
+
566
+ visitModel(node) {
567
+ this.models.set(node.name, node);
568
+
569
+ const ext = node.parent ? ` : ${node.parent}()` : '';
570
+ const parentModel = node.parent ? this.models.get(node.parent) : null;
571
+ const parentFields = parentModel ? parentModel.fields : [];
572
+ const allFields = [...parentFields, ...node.fields];
573
+
574
+ const isSimple = node.methods.length === 0;
575
+
576
+ if (isSimple && allFields.length > 0) {
577
+ const params = allFields.map(f => {
578
+ const type = this.mapType(f.type || 'any');
579
+ if (f.defaultValue) return `val ${f.name}: ${type} = ${this.expr(f.defaultValue)}`;
580
+ return `val ${f.name}: ${type}`;
581
+ }).join(', ');
582
+ this.emit(`data class ${node.name}(${params})`);
583
+ } else {
584
+ this.emit(`class ${node.name}${ext} {`);
585
+ this.indent++;
586
+
587
+ for (const f of node.fields) {
588
+ const type = this.mapType(f.type || 'any');
589
+ if (f.defaultValue) {
590
+ this.emit(`var ${f.name}: ${type} = ${this.expr(f.defaultValue)}`);
591
+ } else {
592
+ this.emit(`var ${f.name}: ${type} = ${this.defaultForType(f.type || 'any')}`);
593
+ }
594
+ }
595
+
596
+ if (allFields.length > 0 && node.methods.length > 0) {
597
+ this.emitRaw('');
598
+ const consParams = allFields.map(f => {
599
+ const type = this.mapType(f.type || 'any');
600
+ return `${f.name}: ${type}`;
601
+ }).join(', ');
602
+ this.emit(`constructor(${consParams}) {`);
603
+ this.indent++;
604
+ for (const f of node.fields) {
605
+ this.emit(`this.${f.name} = ${f.name}`);
606
+ }
607
+ this.indent--;
608
+ this.emit('}');
609
+ }
610
+
611
+ for (const method of node.methods) {
612
+ this.emitRaw('');
613
+ const async = method.isAsync ? 'suspend ' : '';
614
+ const params = method.params.map(p => {
615
+ let s = '';
616
+ if (p.spread) s += 'vararg ';
617
+ s += `${p.name}: Any`;
618
+ if (p.defaultValue) s += ` = ${this.expr(p.defaultValue)}`;
619
+ return s;
620
+ }).join(', ');
621
+
622
+ this.emit(`${async}fun ${method.name}(${params}) {`);
623
+ this.indent++;
624
+ if (method.body.length === 0) {
625
+ this.emit('// empty');
626
+ } else {
627
+ for (const stmt of method.body) {
628
+ this.visitStatement(stmt);
629
+ }
630
+ }
631
+ this.indent--;
632
+ this.emit('}');
633
+ }
634
+
635
+ this.indent--;
636
+ this.emit('}');
637
+ }
638
+ this.emitRaw('');
639
+ }
640
+
641
+ defaultForType(type) {
642
+ const map = {
643
+ 'str': '""',
644
+ 'string': '""',
645
+ 'int': '0',
646
+ 'integer': '0',
647
+ 'num': '0.0',
648
+ 'number': '0.0',
649
+ 'bool': 'false',
650
+ 'boolean': 'false',
651
+ 'list': 'mutableListOf()',
652
+ 'map': 'mutableMapOf()',
653
+ };
654
+ return map[type] || 'null';
655
+ }
656
+
657
+ // ===== Events =====
658
+
659
+ visitOn(node) {
660
+ const event = this.expr(node.event);
661
+ this.emit(`// on ${event}`);
662
+ this.emit(`fun __on${event.replace(/\./g, '_')}() {`);
663
+ this.indent++;
664
+ if (node.body.length === 0) {
665
+ this.emit('// empty');
666
+ } else {
667
+ for (const stmt of node.body) this.visitStatement(stmt);
668
+ }
669
+ this.indent--;
670
+ this.emit('}');
671
+ this.emitRaw('');
672
+ }
673
+
674
+ // ===== Logging and errors =====
675
+
676
+ visitLog(node) {
677
+ const args = node.args.map(a => this.expr(a)).join(', ');
678
+ if (node.level === 'warn' || node.level === 'error') {
679
+ this.emit(`System.err.println(${args})`);
680
+ } else {
681
+ this.emit(`println(${args})`);
682
+ }
683
+ }
684
+
685
+ visitThrow(node) {
686
+ const value = this.expr(node.value);
687
+ if (node.value.type === 'String') {
688
+ this.emit(`throw Exception(${value})`);
689
+ } else {
690
+ this.emit(`throw ${value}`);
691
+ }
692
+ }
693
+
694
+ // ===== Database =====
695
+
696
+ visitDbConnect(node) {
697
+ this.emit(`// db connect: ${this.expr(node.connectionString)}`);
698
+ }
699
+
700
+ visitDbDir(node) {
701
+ const raw = node.path.raw || node.path.parts?.map(p => p.value).join('') || 'data/';
702
+ this.emit(`// db dir: ${raw}`);
703
+ }
704
+
705
+ visitDbSql(node) {
706
+ const driverRaw = node.driver.raw || node.driver.parts?.map(p => p.value).join('') || 'sqlite';
707
+ const conn = node.connection ? this.ktStringValue(node.connection) : '"data.db"';
708
+
709
+ if (driverRaw === 'sqlite') {
710
+ this.addImport('java.sql.DriverManager');
711
+ this.emit(`val __db = DriverManager.getConnection("jdbc:sqlite:" + ${conn})`);
712
+ } else if (driverRaw === 'postgres' || driverRaw === 'pg') {
713
+ this.addImport('java.sql.DriverManager');
714
+ this.emit(`val __db = DriverManager.getConnection(${conn})`);
715
+ }
716
+ this.emitRaw('');
717
+ }
718
+
719
+ visitAwaitAllStatement(node) {
720
+ this.addImport('kotlinx.coroutines.*');
721
+ const exprs = node.expressions.map(e => `async { ${this.expr(e)} }`).join(', ');
722
+ this.emit(`awaitAll(${exprs})`);
723
+ }
724
+
725
+ // ===== Schema =====
726
+
727
+ visitSchema(node) {
728
+ this.emit(`data class ${node.name}(`);
729
+ this.indent++;
730
+ for (let i = 0; i < node.fields.length; i++) {
731
+ const f = node.fields[i];
732
+ const type = this.mapType(f.type);
733
+ const nullable = f.modifiers.some(m => m.name === 'optional') || f.type === 'auto' || f.type === 'timestamp';
734
+ const suffix = nullable ? '?' : '';
735
+ const defaultVal = this.schemaDefault(f);
736
+ const comma = i < node.fields.length - 1 ? ',' : '';
737
+ if (defaultVal !== null) {
738
+ this.emit(`val ${f.name}: ${type}${suffix} = ${defaultVal}${comma}`);
739
+ } else {
740
+ this.emit(`val ${f.name}: ${type}${suffix}${comma}`);
741
+ }
742
+ }
743
+ this.indent--;
744
+ this.emit(')');
745
+ this.emitRaw('');
746
+
747
+ this.schemas.set(node.name, node);
748
+ }
749
+
750
+ schemaDefault(fieldNode) {
751
+ for (const mod of fieldNode.modifiers) {
752
+ if (mod.name === 'default') {
753
+ return this.expr(mod.args[0]);
754
+ }
755
+ }
756
+ if (fieldNode.modifiers.some(m => m.name === 'optional')) return 'null';
757
+ if (fieldNode.type === 'auto') return 'null';
758
+ if (fieldNode.type === 'timestamp') return 'null';
759
+ return null;
760
+ }
761
+
762
+ // ===== CORS =====
763
+
764
+ visitCors(appName, node) {
765
+ this.addImport('io.ktor.server.plugins.cors.routing.*');
766
+ const origins = this.expr(node.origins);
767
+ this.emit('install(CORS) {');
768
+ this.indent++;
769
+ this.emit(`allowHost(${origins})`);
770
+ this.emit('allowHeader(HttpHeaders.ContentType)');
771
+ this.indent--;
772
+ this.emit('}');
773
+ this.emitRaw('');
774
+ }
775
+
776
+ // ===== Auth =====
777
+
778
+ visitAuth(appName, node) {
779
+ const secret = this.expr(node.secret);
780
+ this.authSecret = secret;
781
+ this.emit(`val __authSecret = ${secret}`);
782
+ this.emit('// JWT auth middleware');
783
+ this.emit(`// Protected paths use __authSecret for token verification`);
784
+ this.emitRaw('');
785
+ }
786
+
787
+ // ===== Static =====
788
+
789
+ visitStatic(appName, node) {
790
+ let raw = node.path.raw || node.path.parts?.map(p => p.value).join('') || 'public';
791
+ if (raw.startsWith('/')) raw = raw.slice(1);
792
+ this.addImport('io.ktor.server.http.content.*');
793
+ this.emit(`staticResources("/", "${raw}")`);
794
+ this.emitRaw('');
795
+ }
796
+
797
+ // ===== CRUD =====
798
+
799
+ visitCrud(appName, node) {
800
+ const path = this.rawString(node.path);
801
+ const schema = node.schemaName;
802
+ const store = `__${schema.toLowerCase()}Store`;
803
+
804
+ this.emitRaw('');
805
+ this.emit(`val ${store} = mutableListOf<${schema}>()`);
806
+ this.emit(`var __${schema.toLowerCase()}Id = 1`);
807
+ this.emitRaw('');
808
+
809
+ this.emit(`route("${path}") {`);
810
+ this.indent++;
811
+ this.emit('get {');
812
+ this.indent++;
813
+ this.emit(`call.respond(${store})`);
814
+ this.indent--;
815
+ this.emit('}');
816
+ this.emit('post {');
817
+ this.indent++;
818
+ this.emit(`val item = call.receive<${schema}>()`);
819
+ this.emit(`${store}.add(item)`);
820
+ this.emit('call.respond(HttpStatusCode.Created, item)');
821
+ this.indent--;
822
+ this.emit('}');
823
+ this.emit('get("{id}") {');
824
+ this.indent++;
825
+ this.emit('val id = call.parameters["id"]');
826
+ this.emit(`val item = ${store}.find { it.toString().contains(id ?: "") }`);
827
+ this.emit('if (item != null) call.respond(item) else call.respond(HttpStatusCode.NotFound)');
828
+ this.indent--;
829
+ this.emit('}');
830
+ this.emit('delete("{id}") {');
831
+ this.indent++;
832
+ this.emit('val id = call.parameters["id"]');
833
+ this.emit(`${store}.removeIf { it.toString().contains(id ?: "") }`);
834
+ this.emit('call.respond(mapOf("deleted" to true))');
835
+ this.indent--;
836
+ this.emit('}');
837
+ this.indent--;
838
+ this.emit('}');
839
+ this.emitRaw('');
840
+ }
841
+
842
+ // ===== Rate Limit =====
843
+
844
+ visitLimit(appName, node) {
845
+ const path = this.rawString(node.path);
846
+ const max = this.expr(node.max);
847
+ const window = this.expr(node.window);
848
+ this.emit(`// rate limit: ${max} requests per ${window} on ${path}`);
849
+ }
850
+
851
+ // ===== WebSocket =====
852
+
853
+ visitWs(node) {
854
+ this.addImport('io.ktor.server.websocket.*');
855
+ this.addImport('io.ktor.websocket.*');
856
+ const path = this.rawString(node.path);
857
+
858
+ this.emit('install(WebSockets)');
859
+ this.emit(`webSocket("${path}") {`);
860
+ this.indent++;
861
+
862
+ for (const evt of node.events || []) {
863
+ const evtName = evt.name.raw || evt.name.parts?.map(p => p.value).join('');
864
+ if (evtName === 'connect' || evtName === 'open') {
865
+ this.emit('// on connect');
866
+ for (const stmt of evt.body) this.visitStatement(stmt);
867
+ } else if (evtName === 'message') {
868
+ const param = evt.params[0] || 'data';
869
+ this.emit(`for (frame in incoming) {`);
870
+ this.indent++;
871
+ this.emit(`if (frame is Frame.Text) {`);
872
+ this.indent++;
873
+ this.emit(`val ${param} = frame.readText()`);
874
+ for (const stmt of evt.body) this.visitStatement(stmt);
875
+ this.indent--;
876
+ this.emit('}');
877
+ this.indent--;
878
+ this.emit('}');
879
+ } else if (evtName === 'close') {
880
+ this.emit('// on close');
881
+ for (const stmt of evt.body) this.visitStatement(stmt);
882
+ }
883
+ }
884
+
885
+ this.indent--;
886
+ this.emit('}');
887
+ this.emitRaw('');
888
+ }
889
+
890
+ // ===== Env =====
891
+
892
+ visitEnv(node) {
893
+ this.emitRaw('');
894
+ for (const v of node.vars) {
895
+ let defaultVal = null;
896
+ for (const mod of v.modifiers) {
897
+ if (mod.name === 'default') {
898
+ defaultVal = this.expr(mod.args[0]);
899
+ }
900
+ }
901
+
902
+ if (defaultVal !== null) {
903
+ this.emit(`val ${v.name} = System.getenv("${v.name}") ?: ${defaultVal}`);
904
+ } else {
905
+ this.emit(`val ${v.name} = System.getenv("${v.name}")`);
906
+ }
907
+
908
+ if (v.type === 'int') {
909
+ this.emit(`val ${v.name}Int = ${v.name}?.toIntOrNull()`);
910
+ } else if (v.type === 'num') {
911
+ this.emit(`val ${v.name}Num = ${v.name}?.toDoubleOrNull()`);
912
+ } else if (v.type === 'bool') {
913
+ this.emit(`val ${v.name}Bool = ${v.name}?.lowercase() in listOf("true", "1", "yes")`);
914
+ }
915
+ }
916
+ this.emitRaw('');
917
+ }
918
+
919
+ // ===== Tests =====
920
+
921
+ visitTest(node) {
922
+ this.hasTests = true;
923
+ this.addImport('kotlin.test.Test');
924
+ const name = this.rawString(node.name);
925
+ const funcName = `test_${name.replace(/[^a-zA-Z0-9]/g, '_').replace(/_+/g, '_')}`;
926
+
927
+ this.emit(`@Test`);
928
+ this.emit(`fun ${funcName}() {`);
929
+ this.indent++;
930
+ if (node.body.length === 0) {
931
+ this.emit('// empty test');
932
+ } else {
933
+ for (const stmt of node.body) this.visitStatement(stmt);
934
+ }
935
+ this.indent--;
936
+ this.emit('}');
937
+ this.emitRaw('');
938
+ }
939
+
940
+ visitAssert(node) {
941
+ this.hasAsserts = true;
942
+ this.addImport('kotlin.test.assertEquals');
943
+ this.addImport('kotlin.test.assertTrue');
944
+ const exprNode = node.expr;
945
+ if (exprNode.type === 'Binary' && (exprNode.op === '===' || exprNode.op === '==')) {
946
+ this.emit(`assertEquals(${this.expr(exprNode.right)}, ${this.expr(exprNode.left)})`);
947
+ } else if (exprNode.type === 'Binary' && (exprNode.op === '!==' || exprNode.op === '!=')) {
948
+ this.emit(`assertTrue(${this.expr(exprNode.left)} != ${this.expr(exprNode.right)})`);
949
+ } else {
950
+ this.emit(`assertTrue(${this.expr(exprNode)})`);
951
+ }
952
+ }
953
+
954
+ // ===== Bot =====
955
+
956
+ visitBot(node) {
957
+ const tokenExpr = node.token ? this.expr(node.token) : 'System.getenv("BOT_TOKEN")';
958
+ const botType = this.rawBotType(node.botType);
959
+
960
+ this.emit(`// ${botType} bot: ${node.name}`);
961
+ this.emit(`val __botToken = ${tokenExpr}`);
962
+
963
+ if (botType === 'discord') {
964
+ this.addImport('net.dv8tion.jda.api.*');
965
+ this.emit(`// Discord bot using JDA`);
966
+ this.emit(`val ${node.name} = JDABuilder.createDefault(__botToken).build()`);
967
+ } else if (botType === 'telegram') {
968
+ this.emit(`// Telegram bot`);
969
+ } else if (botType === 'slack') {
970
+ this.emit(`// Slack bot`);
971
+ } else {
972
+ this.emit(`// ${botType} bot`);
973
+ }
974
+
975
+ for (const handler of node.handlers) {
976
+ if (handler.type === 'BotEvent') {
977
+ const evtName = handler.event.raw || handler.event.parts?.map(p => p.value).join('');
978
+ this.emit(`// on ${evtName}`);
979
+ this.emit(`fun handle_${evtName}() {`);
980
+ this.indent++;
981
+ for (const stmt of handler.body) this.visitStatement(stmt);
982
+ this.indent--;
983
+ this.emit('}');
984
+ } else if (handler.type === 'BotSlashCmd') {
985
+ const cmdName = handler.name.raw || handler.name.parts?.map(p => p.value).join('');
986
+ this.emit(`// slash command: /${cmdName}`);
987
+ this.emit(`fun cmd_${cmdName}() {`);
988
+ this.indent++;
989
+ for (const stmt of handler.body) this.visitStatement(stmt);
990
+ this.indent--;
991
+ this.emit('}');
992
+ }
993
+ }
994
+ this.emitRaw('');
995
+ }
996
+
997
+ rawBotType(bt) {
998
+ if (!bt) return 'discord';
999
+ return bt.raw || bt.parts?.map(p => p.value).join('') || 'discord';
1000
+ }
1001
+
1002
+ // ===== Cookie =====
1003
+
1004
+ visitCookie(appName, node) {
1005
+ this.emit('// cookie support enabled');
1006
+ }
1007
+
1008
+ // ===== Upload =====
1009
+
1010
+ visitUpload(appName, node) {
1011
+ const path = this.rawString(node.path);
1012
+ this.emit(`post("${path}") {`);
1013
+ this.indent++;
1014
+ this.emit('val multipart = call.receiveMultipart()');
1015
+ this.emit('// handle file upload');
1016
+ for (const stmt of node.body) this.visitStatement(stmt);
1017
+ this.indent--;
1018
+ this.emit('}');
1019
+ this.emitRaw('');
1020
+ }
1021
+
1022
+ // ===== Session =====
1023
+
1024
+ visitSession(appName, node) {
1025
+ this.addImport('io.ktor.server.sessions.*');
1026
+ this.emit('install(Sessions) {');
1027
+ this.indent++;
1028
+ this.emit('// configure sessions');
1029
+ this.indent--;
1030
+ this.emit('}');
1031
+ this.emitRaw('');
1032
+ }
1033
+
1034
+ // ===== View =====
1035
+
1036
+ visitView(appName, node) {
1037
+ const dir = this.rawString(node.dir);
1038
+ this.emit(`// view engine: templates from "${dir}"`);
1039
+ }
1040
+
1041
+ // ===== SSE =====
1042
+
1043
+ visitSse(appName, node) {
1044
+ const path = this.rawString(node.path);
1045
+ this.emit(`// SSE endpoint: ${path}`);
1046
+ }
1047
+
1048
+ // ===== Cache =====
1049
+
1050
+ visitCache(appName, node) {
1051
+ const path = this.rawString(node.path);
1052
+ this.emit(`// cache: ${path}`);
1053
+ }
1054
+
1055
+ // ===== MiddlewareRef =====
1056
+
1057
+ visitMiddlewareRef(appName, node) {
1058
+ if (node.path) {
1059
+ this.emit(`// middleware "${node.name}" on ${this.rawString(node.path)}`);
1060
+ } else {
1061
+ this.emit(`// middleware "${node.name}"`);
1062
+ }
1063
+ }
1064
+
1065
+ // ===== Return variants =====
1066
+
1067
+ visitReturnRender(node) {
1068
+ const template = this.expr(node.template);
1069
+ this.emit(`call.respondText(renderTemplate(${template}), ContentType.Text.Html)`);
1070
+ this.emit('return');
1071
+ }
1072
+
1073
+ visitReturnRedirect(node) {
1074
+ this.emit(`call.respondRedirect(${this.expr(node.url)})`);
1075
+ this.emit('return');
1076
+ }
1077
+
1078
+ visitReturnDownload(node) {
1079
+ this.emit(`call.respondFile(File(${this.expr(node.filePath)}))`);
1080
+ this.emit('return');
1081
+ }
1082
+
1083
+ // ===== Validate =====
1084
+
1085
+ visitValidate(appName, node) {
1086
+ const path = this.rawString(node.path);
1087
+ this.emit(`// validate: ${path} against ${node.schemaName}`);
1088
+ }
1089
+
1090
+ // ===== Queue =====
1091
+
1092
+ visitQueue(node) {
1093
+ this.emit(`// queue: ${node.name}`);
1094
+ this.emit(`object ${node.name} {`);
1095
+ this.indent++;
1096
+ for (const job of node.jobs) {
1097
+ const params = job.params.length > 0 ? job.params.join(', ') : 'data';
1098
+ this.emit(`fun ${this.rawString(job.name)}(${params}: Any) {`);
1099
+ this.indent++;
1100
+ for (const stmt of job.body) this.visitStatement(stmt);
1101
+ this.indent--;
1102
+ this.emit('}');
1103
+ }
1104
+ this.indent--;
1105
+ this.emit('}');
1106
+ this.emitRaw('');
1107
+ }
1108
+
1109
+ // ===== OpenAPI =====
1110
+
1111
+ visitOpenapi(appName, node) {
1112
+ const path = this.rawString(node.path);
1113
+ this.emit(`// OpenAPI spec at ${path}`);
1114
+ }
1115
+
1116
+ // ===== Prompt =====
1117
+
1118
+ visitPrompt(node) {
1119
+ const name = node.name;
1120
+ this.emit(`fun ${name}(vars: Map<String, Any> = emptyMap()): String {`);
1121
+ this.indent++;
1122
+ const lines = node.lines.map(l => this.generateString(l));
1123
+ const template = lines.length === 1 ? lines[0] : lines.join(' + "\\n" + ');
1124
+ this.emit(`var template = ${template}`);
1125
+ this.emit(`for ((k, v) in vars) {`);
1126
+ this.indent++;
1127
+ this.emit('template = template.replace("{$k}", v.toString())');
1128
+ this.indent--;
1129
+ this.emit('}');
1130
+ this.emit('return template');
1131
+ this.indent--;
1132
+ this.emit('}');
1133
+ this.emitRaw('');
1134
+ }
1135
+
1136
+ // ===== Page =====
1137
+
1138
+ visitPage(node) {
1139
+ const filename = this.rawPageString(node.filename);
1140
+ this.addImport('java.io.File');
1141
+
1142
+ const headParts = [];
1143
+ const bodyParts = [];
1144
+ let pageTitle = 'NAIDE Page';
1145
+
1146
+ for (const el of node.elements) {
1147
+ this.classifyPageElement(el, headParts, bodyParts, (t) => { pageTitle = t; });
1148
+ }
1149
+
1150
+ this.emit(`File("${filename}").writeText("""`);
1151
+ this.emit(`<!DOCTYPE html>`);
1152
+ this.emit(`<html lang="en">`);
1153
+ this.emit(`<head>`);
1154
+ this.emit(`<meta charset="UTF-8">`);
1155
+ this.emit(`<meta name="viewport" content="width=device-width, initial-scale=1.0">`);
1156
+ this.emit(`<title>${pageTitle}</title>`);
1157
+ for (const h of headParts) this.emit(h);
1158
+ this.emit(`</head>`);
1159
+ this.emit(`<body>`);
1160
+ for (const b of bodyParts) this.emit(b);
1161
+ this.emit(`</body>`);
1162
+ this.emit(`</html>`);
1163
+ this.emit(`""".trimIndent())`);
1164
+ this.emitRaw('');
1165
+ }
1166
+
1167
+ classifyPageElement(el, head, body, setTitle) {
1168
+ const tag = el.tag;
1169
+ const arg0 = el.args[0] ? this.rawPageString(el.args[0]) : '';
1170
+ const arg1 = el.args[1] ? this.rawPageString(el.args[1]) : '';
1171
+ if (tag === 'title') { setTitle(arg0); return; }
1172
+ if (tag === 'style' || tag === 'link') { head.push(`<link rel="stylesheet" href="${arg0}">`); return; }
1173
+ if (tag === 'meta') { head.push(`<meta name="${arg0}" content="${arg1}">`); return; }
1174
+ if (tag === 'script') { body.push(`<script src="${arg0}"></script>`); return; }
1175
+ if (tag === 'img') { body.push(`<img src="${arg0}" alt="${arg1}">`); return; }
1176
+ if (tag === 'a') { body.push(`<a href="${arg0}">${arg1}</a>`); return; }
1177
+ if (tag === 'input') { body.push(`<input type="${arg0}" name="${arg1}">`); return; }
1178
+ if (el.children && el.children.length > 0) {
1179
+ const cls = arg0 ? ` class="${arg0}"` : '';
1180
+ body.push(`<${tag}${cls}>`);
1181
+ for (const child of el.children) this.classifyPageElement(child, head, body, setTitle);
1182
+ body.push(`</${tag}>`);
1183
+ } else {
1184
+ body.push(`<${tag}>${arg0}</${tag}>`);
1185
+ }
1186
+ }
1187
+
1188
+ rawPageString(strData) {
1189
+ if (!strData) return '';
1190
+ if (strData.raw !== null && strData.raw !== undefined) return strData.raw;
1191
+ if (strData.parts) return strData.parts.map(p => p.value).join('');
1192
+ return '';
1193
+ }
1194
+
1195
+ // ===== CLI App =====
1196
+
1197
+ visitCli(node) {
1198
+ const desc = node.description ? this.rawPageString(node.description) : node.name;
1199
+
1200
+ this.emit('fun main(args: Array<String>) {');
1201
+ this.indent++;
1202
+ this.emit(`val parsedArgs = mutableMapOf<String, Any>()`);
1203
+ this.emit('var i = 0');
1204
+ this.emit('while (i < args.size) {');
1205
+ this.indent++;
1206
+ this.emit('when (args[i]) {');
1207
+ this.indent++;
1208
+ this.emit('"--help", "-h" -> {');
1209
+ this.indent++;
1210
+ this.emit(`println("${desc}")`);
1211
+ this.emit('return');
1212
+ this.indent--;
1213
+ this.emit('}');
1214
+ for (const arg of node.args) {
1215
+ const argName = this.rawPageString(arg.name);
1216
+ const coerce = arg.type === 'int' ? 'args[++i].toInt()' :
1217
+ arg.type === 'num' ? 'args[++i].toDouble()' :
1218
+ arg.type === 'bool' ? 'true' : 'args[++i]';
1219
+ this.emit(`"--${argName}" -> parsedArgs["${argName}"] = ${coerce}`);
1220
+ }
1221
+ for (const flag of node.flags) {
1222
+ const s = this.rawPageString(flag.short);
1223
+ const l = this.rawPageString(flag.long);
1224
+ this.emit(`"-${s}", "--${l}" -> parsedArgs["${l}"] = true`);
1225
+ }
1226
+ this.indent--;
1227
+ this.emit('}');
1228
+ this.emit('i++');
1229
+ this.indent--;
1230
+ this.emit('}');
1231
+ this.emitRaw('');
1232
+
1233
+ if (node.run) {
1234
+ const runParam = node.run.params.length > 0 ? node.run.params[0] : 'parsedArgs';
1235
+ if (runParam !== 'parsedArgs') {
1236
+ this.emit(`val ${runParam} = parsedArgs`);
1237
+ }
1238
+ for (const stmt of node.run.body) this.visitStatement(stmt);
1239
+ }
1240
+
1241
+ this.indent--;
1242
+ this.emit('}');
1243
+ this.emitRaw('');
1244
+ }
1245
+
1246
+ // ===== Mail =====
1247
+
1248
+ visitMail(node) {
1249
+ const host = this.generateString(node.host);
1250
+ const port = this.expr(node.port);
1251
+ const user = node.user ? this.expr(node.user) : 'System.getenv("MAIL_USER")';
1252
+ const pass = node.pass ? this.expr(node.pass) : 'System.getenv("MAIL_PASS")';
1253
+
1254
+ this.addImport('javax.mail.*');
1255
+ this.addImport('javax.mail.internet.*');
1256
+ this.emit(`val mailProps = Properties().apply {`);
1257
+ this.indent++;
1258
+ this.emit(`put("mail.smtp.host", ${host})`);
1259
+ this.emit(`put("mail.smtp.port", ${port})`);
1260
+ this.emit(`put("mail.smtp.auth", "true")`);
1261
+ this.emit(`put("mail.smtp.starttls.enable", "true")`);
1262
+ this.indent--;
1263
+ this.emit('}');
1264
+ this.emit(`val mailSession = Session.getInstance(mailProps, object : Authenticator() {`);
1265
+ this.indent++;
1266
+ this.emit(`override fun getPasswordAuthentication() = PasswordAuthentication(${user}, ${pass})`);
1267
+ this.indent--;
1268
+ this.emit('})');
1269
+ this.emitRaw('');
1270
+ }
1271
+
1272
+ // ===== Desktop =====
1273
+
1274
+ visitDesktop(node) {
1275
+ const title = node.title ? this.rawPageString(node.title) : node.name;
1276
+ const width = node.width ? this.expr(node.width) : '800';
1277
+ const height = node.height ? this.expr(node.height) : '600';
1278
+
1279
+ this.addImport('javax.swing.*');
1280
+ this.emit(`fun main() {`);
1281
+ this.indent++;
1282
+ this.emit(`val frame = JFrame("${title}")`);
1283
+ this.emit(`frame.setSize(${width}, ${height})`);
1284
+ this.emit('frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE');
1285
+ this.emit('frame.isVisible = true');
1286
+ this.indent--;
1287
+ this.emit('}');
1288
+ this.emitRaw('');
1289
+ }
1290
+
1291
+ // ===== Screen =====
1292
+
1293
+ visitScreen(node) {
1294
+ this.emit(`// Mobile screen: ${node.name}`);
1295
+ this.emit(`// Use Jetpack Compose or similar for mobile UI`);
1296
+ this.emit(`@Composable`);
1297
+ this.emit(`fun ${node.name}() {`);
1298
+ this.indent++;
1299
+ this.emit('Column(modifier = Modifier.padding(16.dp)) {');
1300
+ this.indent++;
1301
+ for (const el of node.elements) {
1302
+ const tag = el.tag;
1303
+ const arg0 = el.args[0] ? this.rawPageString(el.args[0]) : '';
1304
+ if (tag === 'text') {
1305
+ this.emit(`Text("${arg0}")`);
1306
+ } else if (tag === 'button') {
1307
+ this.emit(`Button(onClick = { /* action */ }) { Text("${arg0}") }`);
1308
+ } else if (tag === 'input') {
1309
+ this.emit(`TextField(value = "", onValueChange = {}, label = { Text("${arg0}") })`);
1310
+ } else {
1311
+ this.emit(`Text("${arg0}")`);
1312
+ }
1313
+ }
1314
+ this.indent--;
1315
+ this.emit('}');
1316
+ this.indent--;
1317
+ this.emit('}');
1318
+ this.emitRaw('');
1319
+ }
1320
+
1321
+ // ===== Every (scheduler) =====
1322
+
1323
+ visitEvery(node) {
1324
+ const interval = this.expr(node.interval);
1325
+ this.addImport('java.util.Timer');
1326
+ this.addImport('kotlin.concurrent.timerTask');
1327
+
1328
+ this.emit(`Timer().scheduleAtFixedRate(timerTask {`);
1329
+ this.indent++;
1330
+ for (const stmt of node.body) this.visitStatement(stmt);
1331
+ this.indent--;
1332
+ this.emit(`}, 0, ${interval})`);
1333
+ this.emitRaw('');
1334
+ }
1335
+
1336
+ // ===== Watch =====
1337
+
1338
+ visitWatch(node) {
1339
+ const params = node.params.length > 0 ? node.params.join(', ') : 'event';
1340
+ this.emit(`// watch: ${node.eventName}`);
1341
+ this.emit(`fun __onWatch_${node.eventName}(${params}: Any) {`);
1342
+ this.indent++;
1343
+ for (const stmt of node.body) this.visitStatement(stmt);
1344
+ this.indent--;
1345
+ this.emit('}');
1346
+ this.emitRaw('');
1347
+ }
1348
+
1349
+ // ===== OAuth =====
1350
+
1351
+ visitOauth(node) {
1352
+ const provider = this.rawPageString(node.provider);
1353
+ this.emit(`// OAuth: ${provider}`);
1354
+ this.emit(`// Configure OAuth via Ktor or Spring Security`);
1355
+ this.emitRaw('');
1356
+ }
1357
+
1358
+ // ===== Pay =====
1359
+
1360
+ visitPay(node) {
1361
+ const provider = this.rawPageString(node.provider);
1362
+ this.emit(`// Payment: ${provider}`);
1363
+ this.emit(`// Use Stripe Java SDK or equivalent`);
1364
+ this.emitRaw('');
1365
+ }
1366
+
1367
+ // ===== Storage =====
1368
+
1369
+ visitStorage(node) {
1370
+ const provider = this.rawPageString(node.provider);
1371
+ this.emit(`// Storage: ${provider}`);
1372
+ this.emit(`// Use AWS SDK for Kotlin or equivalent`);
1373
+ this.emitRaw('');
1374
+ }
1375
+
1376
+ // ===== PDF =====
1377
+
1378
+ visitPdf(node) {
1379
+ const filename = this.rawPageString(node.filename);
1380
+ this.emit(`// PDF generation: ${filename}`);
1381
+ this.emit(`// Use iText or Apache PDFBox`);
1382
+ this.emitRaw('');
1383
+ }
1384
+
1385
+ // ===== i18n =====
1386
+
1387
+ visitI18n(node) {
1388
+ const defaultLang = node.defaultLang ? this.rawPageString(node.defaultLang) : 'en';
1389
+ this.emit(`// i18n: default language = ${defaultLang}`);
1390
+ this.emit(`// Use ResourceBundle for internationalization`);
1391
+ this.emitRaw('');
1392
+ }
1393
+
1394
+ // ===== Push =====
1395
+
1396
+ visitPush(node) {
1397
+ this.emit('// Push notifications');
1398
+ this.emit('// Use Firebase Admin SDK for Kotlin/JVM');
1399
+ this.emitRaw('');
1400
+ }
1401
+
1402
+ // ===== Search =====
1403
+
1404
+ visitSearch(node) {
1405
+ const engine = this.rawPageString(node.engine);
1406
+ this.emit(`// Search: ${engine}`);
1407
+ this.emit(`// Use Meilisearch or Elasticsearch Java client`);
1408
+ this.emitRaw('');
1409
+ }
1410
+
1411
+ // ===== Image =====
1412
+
1413
+ visitImage(node) {
1414
+ const input = this.expr(node.input);
1415
+ this.emit(`// Image processing: ${input}`);
1416
+ this.emit(`// Use javax.imageio or thumbnailator`);
1417
+ this.emitRaw('');
1418
+ }
1419
+
1420
+ // ===== CSV =====
1421
+
1422
+ visitCsv(node) {
1423
+ const name = this.rawPageString(node.name);
1424
+ this.emit(`// CSV/Excel export: ${name}`);
1425
+ this.emit(`// Use Apache POI or opencsv`);
1426
+ this.emitRaw('');
1427
+ }
1428
+
1429
+ // ===== Logging =====
1430
+
1431
+ visitLogging(node) {
1432
+ const name = this.rawPageString(node.name);
1433
+ const level = node.level ? this.rawPageString(node.level) : 'INFO';
1434
+ this.addImport('org.slf4j.LoggerFactory');
1435
+ this.emit(`val logger = LoggerFactory.getLogger("${name}")`);
1436
+ this.emit(`// log level: ${level}`);
1437
+ this.emitRaw('');
1438
+ }
1439
+
1440
+ // ===== Migrate =====
1441
+
1442
+ visitMigrate(node) {
1443
+ const name = this.rawPageString(node.name);
1444
+ this.emit(`// Migration: ${name}`);
1445
+ this.emit(`// Use Flyway or Liquibase`);
1446
+ this.emitRaw('');
1447
+ }
1448
+
1449
+ // ===== gRPC =====
1450
+
1451
+ visitGrpc(node) {
1452
+ const name = this.rawPageString(node.name);
1453
+ const port = node.port ? this.expr(node.port) : '50051';
1454
+ this.emit(`// gRPC service: ${name} on port ${port}`);
1455
+ this.emit(`// Use grpc-kotlin`);
1456
+ this.emitRaw('');
1457
+ }
1458
+
1459
+ // ===== WebRTC =====
1460
+
1461
+ visitWebrtc(node) {
1462
+ const name = this.rawPageString(node.name);
1463
+ this.emit(`// WebRTC: ${name}`);
1464
+ this.emit(`// Use Ktor WebSocket for signaling`);
1465
+ this.emitRaw('');
1466
+ }
1467
+
1468
+ // ===== Blockchain =====
1469
+
1470
+ visitBlockchain(node) {
1471
+ const name = this.rawPageString(node.name);
1472
+ this.emit(`// Blockchain: ${name}`);
1473
+ this.emit(`// Use web3j for Ethereum integration`);
1474
+ this.emitRaw('');
1475
+ }
1476
+
1477
+ // ===== Expression generation =====
1478
+
1479
+ expr(node) {
1480
+ if (!node) return 'null';
1481
+
1482
+ switch (node.type) {
1483
+ case 'Number': return node.value;
1484
+ case 'String': return this.generateString(node.value);
1485
+ case 'Bool': return node.value ? 'true' : 'false';
1486
+ case 'Null': return 'null';
1487
+ case 'Self': return 'this';
1488
+ case 'Identifier': return this.mapIdentifier(node.name);
1489
+
1490
+ case 'Binary': {
1491
+ const op = this.mapBinaryOp(node.op);
1492
+ const l = this.expr(node.left);
1493
+ const r = this.expr(node.right);
1494
+ const simple = node.left.type !== 'Binary' && node.right.type !== 'Binary';
1495
+ return simple ? `${l} ${op} ${r}` : `(${l} ${op} ${r})`;
1496
+ }
1497
+
1498
+ case 'Unary': {
1499
+ const op = node.op === '!' ? '!' : node.op;
1500
+ return `${op}${this.expr(node.expr)}`;
1501
+ }
1502
+
1503
+ case 'Await':
1504
+ return this.expr(node.expr);
1505
+
1506
+ case 'AwaitAllExpr': {
1507
+ this.addImport('kotlinx.coroutines.*');
1508
+ return `awaitAll(${this.expr(node.expr)})`;
1509
+ }
1510
+
1511
+ case 'Spread':
1512
+ return `*${this.expr(node.expr)}`;
1513
+
1514
+ case 'New':
1515
+ return this.expr(node.expr);
1516
+
1517
+ case 'TypeOf':
1518
+ return `${this.expr(node.expr)}::class.simpleName`;
1519
+
1520
+ case 'MemberAccess':
1521
+ return this.mapMemberAccess(node);
1522
+
1523
+ case 'OptionalAccess':
1524
+ return `${this.expr(node.object)}?.${node.property}`;
1525
+
1526
+ case 'Call':
1527
+ return this.generateCall(node);
1528
+
1529
+ case 'IndexAccess':
1530
+ return `${this.expr(node.object)}[${this.expr(node.index)}]`;
1531
+
1532
+ case 'Array':
1533
+ return `mutableListOf(${node.elements.map(e => this.expr(e)).join(', ')})`;
1534
+
1535
+ case 'Object': {
1536
+ const props = node.properties.map(p => {
1537
+ if (p.type === 'spread') return `// spread: ${this.expr(p.value)}`;
1538
+ const key = p.type === 'shorthand' ? p.key :
1539
+ (typeof p.key === 'string' ? p.key :
1540
+ (p.key.type === 'String' ? this.rawString(p.key.value) :
1541
+ (p.key.type === 'Computed' ? this.expr(p.key.expr) : this.expr(p.key))));
1542
+ const val = p.type === 'shorthand' ? p.key : this.expr(p.value);
1543
+ return `"${key}" to ${val}`;
1544
+ }).join(', ');
1545
+ return `mapOf(${props})`;
1546
+ }
1547
+
1548
+ case 'ArrowFn': {
1549
+ const params = node.params.map(p => p.name).join(', ');
1550
+ const body = this.expr(node.body);
1551
+ return `{ ${params} -> ${body} }`;
1552
+ }
1553
+
1554
+ case 'Lambda': {
1555
+ const params = node.params.map(p => p.name).join(', ');
1556
+ const bodyLines = [];
1557
+ const savedOutput = this.output;
1558
+ const savedIndent = this.indent;
1559
+ this.output = bodyLines;
1560
+ this.indent = 0;
1561
+ for (const stmt of node.body) this.visitStatement(stmt);
1562
+ this.output = savedOutput;
1563
+ this.indent = savedIndent;
1564
+ return `{ ${params} -> ${bodyLines.join('; ')} }`;
1565
+ }
1566
+
1567
+ case 'Ternary':
1568
+ return `(if (${this.expr(node.condition)}) ${this.expr(node.consequent)} else ${this.expr(node.alternate)})`;
1569
+
1570
+ case 'Pipe':
1571
+ return this.generatePipe(node);
1572
+
1573
+ default:
1574
+ return `null /* expr:${node.type} */`;
1575
+ }
1576
+ }
1577
+
1578
+ mapIdentifier(name) {
1579
+ const map = {
1580
+ 'null': 'null',
1581
+ 'undefined': 'null',
1582
+ 'true': 'true',
1583
+ 'false': 'false',
1584
+ 'this': 'this',
1585
+ };
1586
+ return map[name] || name;
1587
+ }
1588
+
1589
+ mapBinaryOp(op) {
1590
+ const map = {
1591
+ '===': '==',
1592
+ '!==': '!=',
1593
+ '==': '==',
1594
+ '!=': '!=',
1595
+ '&&': '&&',
1596
+ '||': '||',
1597
+ 'and': '&&',
1598
+ 'or': '||',
1599
+ '>': '>',
1600
+ '<': '<',
1601
+ '>=': '>=',
1602
+ '<=': '<=',
1603
+ '+': '+',
1604
+ '-': '-',
1605
+ '*': '*',
1606
+ '/': '/',
1607
+ '%': '%',
1608
+ '**': '.pow(',
1609
+ 'instanceof': 'is',
1610
+ 'in': 'in',
1611
+ };
1612
+ return map[op] || op;
1613
+ }
1614
+
1615
+ mapMemberAccess(node) {
1616
+ const obj = this.expr(node.object);
1617
+ const prop = node.property;
1618
+
1619
+ if (prop === 'length') return `${obj}.size`;
1620
+ if (prop === 'toString') return `${obj}.toString()`;
1621
+
1622
+ if (obj === 'console' && prop === 'log') return 'println';
1623
+ if (obj === 'console' && prop === 'error') return 'System.err.println';
1624
+ if (obj === 'console' && prop === 'warn') return 'System.err.println';
1625
+
1626
+ if (obj === 'JSON' && prop === 'parse') return 'Json.decodeFromString';
1627
+ if (obj === 'JSON' && prop === 'stringify') return 'Json.encodeToString';
1628
+
1629
+ if (obj === 'Math' && prop === 'floor') return 'kotlin.math.floor';
1630
+ if (obj === 'Math' && prop === 'ceil') return 'kotlin.math.ceil';
1631
+ if (obj === 'Math' && prop === 'round') return 'kotlin.math.round';
1632
+ if (obj === 'Math' && prop === 'abs') return 'kotlin.math.abs';
1633
+ if (obj === 'Math' && prop === 'min') return 'minOf';
1634
+ if (obj === 'Math' && prop === 'max') return 'maxOf';
1635
+ if (obj === 'Math' && prop === 'random') return 'Math.random';
1636
+ if (obj === 'Math' && prop === 'PI') return 'kotlin.math.PI';
1637
+
1638
+ return `${obj}.${prop}`;
1639
+ }
1640
+
1641
+ generateCall(node) {
1642
+ const args = node.args.map(a => this.expr(a)).join(', ');
1643
+
1644
+ if (node.callee.type === 'Identifier') {
1645
+ const name = node.callee.name;
1646
+ if (name === 'parseInt') return `(${args}).toInt()`;
1647
+ if (name === 'parseFloat') return `(${args}).toDouble()`;
1648
+ if (name === 'String') return `(${args}).toString()`;
1649
+ if (name === 'Number') return `(${args}).toDouble()`;
1650
+ if (name === 'Boolean') return `(${args}).toBoolean()`;
1651
+ if (name === 'isNaN') return `(${args}).isNaN()`;
1652
+ }
1653
+
1654
+ if (node.callee.type === 'MemberAccess') {
1655
+ const obj = this.expr(node.callee.object);
1656
+ const prop = node.callee.property;
1657
+
1658
+ if (prop === 'push') return `${obj}.add(${args})`;
1659
+ if (prop === 'pop') return `${obj}.removeAt(${obj}.size - 1)`;
1660
+ if (prop === 'shift') return `${obj}.removeAt(0)`;
1661
+ if (prop === 'includes') return `${obj}.contains(${args})`;
1662
+ if (prop === 'indexOf') return `${obj}.indexOf(${args})`;
1663
+ if (prop === 'join') return `${obj}.joinToString(${args})`;
1664
+ if (prop === 'slice') return `${obj}.subList(${args})`;
1665
+ if (prop === 'forEach') return `${obj}.forEach { ${args} }`;
1666
+ if (prop === 'map') return `${obj}.map { ${args} }`;
1667
+ if (prop === 'filter') return `${obj}.filter { ${args} }`;
1668
+ if (prop === 'find') return `${obj}.find { ${args} }`;
1669
+ if (prop === 'some') return `${obj}.any { ${args} }`;
1670
+ if (prop === 'every') return `${obj}.all { ${args} }`;
1671
+ if (prop === 'reduce') return `${obj}.reduce { acc, it -> ${args} }`;
1672
+ if (prop === 'reverse') return `${obj}.reversed()`;
1673
+ if (prop === 'sort') return `${obj}.sorted()`;
1674
+ if (prop === 'flat') return `${obj}.flatten()`;
1675
+ if (prop === 'concat') return `${obj} + ${args}`;
1676
+
1677
+ if (prop === 'split') return `${obj}.split(${args})`;
1678
+ if (prop === 'trim') return `${obj}.trim()`;
1679
+ if (prop === 'trimStart') return `${obj}.trimStart()`;
1680
+ if (prop === 'trimEnd') return `${obj}.trimEnd()`;
1681
+ if (prop === 'toUpperCase') return `${obj}.uppercase()`;
1682
+ if (prop === 'toLowerCase') return `${obj}.lowercase()`;
1683
+ if (prop === 'startsWith') return `${obj}.startsWith(${args})`;
1684
+ if (prop === 'endsWith') return `${obj}.endsWith(${args})`;
1685
+ if (prop === 'replace') return `${obj}.replace(${args})`;
1686
+ if (prop === 'replaceAll') return `${obj}.replace(${args})`;
1687
+ if (prop === 'padStart') return `${obj}.padStart(${args})`;
1688
+ if (prop === 'padEnd') return `${obj}.padEnd(${args})`;
1689
+ if (prop === 'charAt') return `${obj}[${args}]`;
1690
+ if (prop === 'substring') return `${obj}.substring(${args})`;
1691
+ if (prop === 'repeat') return `${obj}.repeat(${args})`;
1692
+
1693
+ if (obj === 'Object' && prop === 'keys') return `${args}.keys.toList()`;
1694
+ if (obj === 'Object' && prop === 'values') return `${args}.values.toList()`;
1695
+ if (obj === 'Object' && prop === 'entries') return `${args}.entries.toList()`;
1696
+
1697
+ if (obj === 'Array' && prop === 'isArray') return `${args} is List<*>`;
1698
+ if (obj === 'Array' && prop === 'from') return `${args}.toList()`;
1699
+
1700
+ if (obj === 'Promise' && prop === 'all') return `awaitAll(${args})`;
1701
+
1702
+ if (obj === 'Date' && prop === 'now') return 'System.currentTimeMillis()';
1703
+ }
1704
+
1705
+ const callee = this.expr(node.callee);
1706
+ return `${callee}(${args})`;
1707
+ }
1708
+
1709
+ generateString(strData) {
1710
+ if (!strData || !strData.parts) return '""';
1711
+
1712
+ const hasInterpolation = strData.parts.some(p => p.type === 'expr');
1713
+
1714
+ if (!hasInterpolation) {
1715
+ const raw = strData.parts.map(p => p.value).join('');
1716
+ return JSON.stringify(raw);
1717
+ }
1718
+
1719
+ let result = '"';
1720
+ for (const part of strData.parts) {
1721
+ if (part.type === 'text') {
1722
+ result += part.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$');
1723
+ } else {
1724
+ const exprCode = part.value.replace(/\bself\b/g, 'this');
1725
+ result += '${' + exprCode + '}';
1726
+ }
1727
+ }
1728
+ result += '"';
1729
+ return result;
1730
+ }
1731
+
1732
+ rawString(strData) {
1733
+ if (!strData) return '';
1734
+ if (strData.raw !== null && strData.raw !== undefined) return strData.raw;
1735
+ if (strData.parts) return strData.parts.map(p => p.value).join('');
1736
+ return '';
1737
+ }
1738
+
1739
+ ktStringValue(strData) {
1740
+ if (!strData || !strData.parts) return '""';
1741
+ if (strData.raw !== null && strData.raw !== undefined) {
1742
+ return JSON.stringify(strData.raw);
1743
+ }
1744
+ return this.generateString(strData);
1745
+ }
1746
+
1747
+ generatePipe(node) {
1748
+ const steps = [];
1749
+ let current = node;
1750
+ while (current.type === 'Pipe') {
1751
+ steps.unshift(current.right);
1752
+ current = current.left;
1753
+ }
1754
+ steps.unshift(current);
1755
+
1756
+ const COLLECTION_METHODS = new Set(['filter', 'map', 'reduce', 'find', 'some', 'every', 'sort', 'reverse', 'join', 'flat']);
1757
+
1758
+ let result = this.expr(steps[0]);
1759
+ for (let i = 1; i < steps.length; i++) {
1760
+ const step = steps[i];
1761
+ if (step.type === 'Call' && step.callee.type === 'Identifier') {
1762
+ const name = step.callee.name;
1763
+ const args = step.args.map(a => this.expr(a)).join(', ');
1764
+
1765
+ if (name === 'filter') {
1766
+ result = `${result}.filter { ${args} }`;
1767
+ } else if (name === 'map') {
1768
+ result = `${result}.map { ${args} }`;
1769
+ } else if (name === 'reduce') {
1770
+ result = `${result}.reduce { acc, it -> ${args} }`;
1771
+ } else if (name === 'sort') {
1772
+ result = `${result}.sorted()`;
1773
+ } else if (name === 'reverse') {
1774
+ result = `${result}.reversed()`;
1775
+ } else if (name === 'join') {
1776
+ result = `${result}.joinToString(${args})`;
1777
+ } else if (name === 'flat') {
1778
+ result = `${result}.flatten()`;
1779
+ } else {
1780
+ result = `${name}(${result}${args ? ', ' + args : ''})`;
1781
+ }
1782
+ } else if (step.type === 'Identifier') {
1783
+ result = `${step.name}(${result})`;
1784
+ } else if (step.type === 'Call') {
1785
+ const callee = this.expr(step.callee);
1786
+ const args = step.args.map(a => this.expr(a)).join(', ');
1787
+ result = `${callee}(${result}${args ? ', ' + args : ''})`;
1788
+ } else {
1789
+ result = `(${this.expr(step)})(${result})`;
1790
+ }
1791
+ }
1792
+ return result;
1793
+ }
1794
+
1795
+ bodyUsesAwait(body) {
1796
+ for (const stmt of body) {
1797
+ if (this.stmtUsesAwait(stmt)) return true;
1798
+ }
1799
+ return false;
1800
+ }
1801
+
1802
+ stmtUsesAwait(node) {
1803
+ if (!node) return false;
1804
+ const json = JSON.stringify(node);
1805
+ return json.includes('"Await"') || json.includes('"AwaitAll"');
1806
+ }
1807
+
1808
+ statementToString(stmt) {
1809
+ const saved = this.output;
1810
+ const savedIndent = this.indent;
1811
+ this.output = [];
1812
+ this.indent = 0;
1813
+ this.visitStatement(stmt);
1814
+ const result = this.output.join('\n');
1815
+ this.output = saved;
1816
+ this.indent = savedIndent;
1817
+ return result;
1818
+ }
1819
+ }