@player-lang/functional-dsl-generator 0.0.2-next.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.
Files changed (42) hide show
  1. package/dist/cjs/index.cjs +2146 -0
  2. package/dist/cjs/index.cjs.map +1 -0
  3. package/dist/index.legacy-esm.js +2075 -0
  4. package/dist/index.mjs +2075 -0
  5. package/dist/index.mjs.map +1 -0
  6. package/package.json +38 -0
  7. package/src/__tests__/__snapshots__/generator.test.ts.snap +886 -0
  8. package/src/__tests__/builder-class-generator.test.ts +627 -0
  9. package/src/__tests__/cli.test.ts +685 -0
  10. package/src/__tests__/default-value-generator.test.ts +365 -0
  11. package/src/__tests__/generator.test.ts +2860 -0
  12. package/src/__tests__/import-generator.test.ts +444 -0
  13. package/src/__tests__/path-utils.test.ts +174 -0
  14. package/src/__tests__/type-collector.test.ts +674 -0
  15. package/src/__tests__/type-transformer.test.ts +934 -0
  16. package/src/__tests__/utils.test.ts +597 -0
  17. package/src/builder-class-generator.ts +254 -0
  18. package/src/cli.ts +285 -0
  19. package/src/default-value-generator.ts +307 -0
  20. package/src/generator.ts +257 -0
  21. package/src/import-generator.ts +331 -0
  22. package/src/index.ts +38 -0
  23. package/src/path-utils.ts +155 -0
  24. package/src/ts-morph-type-finder.ts +319 -0
  25. package/src/type-categorizer.ts +131 -0
  26. package/src/type-collector.ts +296 -0
  27. package/src/type-resolver.ts +266 -0
  28. package/src/type-transformer.ts +487 -0
  29. package/src/utils.ts +762 -0
  30. package/types/builder-class-generator.d.ts +56 -0
  31. package/types/cli.d.ts +6 -0
  32. package/types/default-value-generator.d.ts +74 -0
  33. package/types/generator.d.ts +102 -0
  34. package/types/import-generator.d.ts +77 -0
  35. package/types/index.d.ts +12 -0
  36. package/types/path-utils.d.ts +65 -0
  37. package/types/ts-morph-type-finder.d.ts +73 -0
  38. package/types/type-categorizer.d.ts +46 -0
  39. package/types/type-collector.d.ts +62 -0
  40. package/types/type-resolver.d.ts +49 -0
  41. package/types/type-transformer.d.ts +74 -0
  42. package/types/utils.d.ts +205 -0
@@ -0,0 +1,627 @@
1
+ import { describe, test, expect, beforeEach } from "vitest";
2
+ import { setupTestEnv } from "@player-lang/test-utils";
3
+ import { TsConverter } from "@xlr-lib/xlr-converters";
4
+ import type { NamedType, ObjectType, RefType } from "@xlr-lib/xlr";
5
+ import {
6
+ BuilderClassGenerator,
7
+ type BuilderInfo,
8
+ } from "../builder-class-generator";
9
+ import {
10
+ TypeTransformer,
11
+ type TypeTransformContext,
12
+ } from "../type-transformer";
13
+ import {
14
+ toBuilderClassName,
15
+ toFactoryName,
16
+ getAssetTypeFromExtends,
17
+ } from "../utils";
18
+
19
+ /** Custom primitives that should be treated as refs rather than resolved */
20
+ const CUSTOM_PRIMITIVES = ["Asset", "AssetWrapper", "Binding", "Expression"];
21
+
22
+ /**
23
+ * Converts TypeScript source code to XLR types
24
+ */
25
+ function convertTsToXLR(
26
+ sourceCode: string,
27
+ customPrimitives = CUSTOM_PRIMITIVES,
28
+ ) {
29
+ const { sf, tc } = setupTestEnv(sourceCode);
30
+ const converter = new TsConverter(tc, customPrimitives);
31
+ return converter.convertSourceFile(sf).data.types;
32
+ }
33
+
34
+ /**
35
+ * Mock TypeTransformContext for testing
36
+ */
37
+ class MockTypeTransformContext implements TypeTransformContext {
38
+ private needsAssetImport = false;
39
+ private readonly namespaceMemberMap = new Map<string, string>();
40
+ private readonly genericParamSymbols = new Set<string>();
41
+
42
+ setNeedsAssetImport(value: boolean): void {
43
+ this.needsAssetImport = value;
44
+ }
45
+
46
+ getNeedsAssetImport(): boolean {
47
+ return this.needsAssetImport;
48
+ }
49
+
50
+ trackReferencedType(_typeName: string): void {
51
+ // No-op for tests
52
+ }
53
+
54
+ trackNamespaceImport(_namespaceName: string): void {
55
+ // No-op for tests
56
+ }
57
+
58
+ getNamespaceMemberMap(): Map<string, string> {
59
+ return this.namespaceMemberMap;
60
+ }
61
+
62
+ getGenericParamSymbols(): Set<string> {
63
+ return this.genericParamSymbols;
64
+ }
65
+
66
+ getAssetWrapperExtendsRef(_typeName: string): RefType | undefined {
67
+ return undefined;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Creates a BuilderInfo from an XLR NamedType
73
+ */
74
+ function createBuilderInfo(namedType: NamedType<ObjectType>): BuilderInfo {
75
+ const assetType = getAssetTypeFromExtends(namedType);
76
+ return {
77
+ name: namedType.name,
78
+ className: toBuilderClassName(namedType.name),
79
+ factoryName: toFactoryName(namedType.name),
80
+ objectType: namedType,
81
+ assetType,
82
+ genericParams: undefined,
83
+ isAsset: !!assetType,
84
+ };
85
+ }
86
+
87
+ describe("BuilderClassGenerator", () => {
88
+ let context: MockTypeTransformContext;
89
+ let transformer: TypeTransformer;
90
+ let generator: BuilderClassGenerator;
91
+
92
+ beforeEach(() => {
93
+ context = new MockTypeTransformContext();
94
+ transformer = new TypeTransformer(context);
95
+ generator = new BuilderClassGenerator(transformer);
96
+ });
97
+
98
+ describe("Basic Builder Generation", () => {
99
+ test("generates builder class with correct name", () => {
100
+ const source = `
101
+ interface Asset<T extends string = string> {
102
+ id: string;
103
+ type: T;
104
+ }
105
+
106
+ export interface TextAsset extends Asset<"text"> {
107
+ value: string;
108
+ }
109
+ `;
110
+
111
+ const types = convertTsToXLR(source);
112
+ const asset = types.find(
113
+ (t) => t.name === "TextAsset",
114
+ ) as NamedType<ObjectType>;
115
+ const info = createBuilderInfo(asset);
116
+
117
+ const code = generator.generateBuilderClass(info);
118
+
119
+ expect(code).toContain("export class TextAssetBuilder");
120
+ expect(code).toContain("TextAssetBuilderMethods");
121
+ });
122
+
123
+ test("generates factory function", () => {
124
+ const source = `
125
+ interface Asset<T extends string = string> {
126
+ id: string;
127
+ type: T;
128
+ }
129
+
130
+ export interface TextAsset extends Asset<"text"> {
131
+ value: string;
132
+ }
133
+ `;
134
+
135
+ const types = convertTsToXLR(source);
136
+ const asset = types.find(
137
+ (t) => t.name === "TextAsset",
138
+ ) as NamedType<ObjectType>;
139
+ const info = createBuilderInfo(asset);
140
+
141
+ const code = generator.generateBuilderClass(info);
142
+
143
+ expect(code).toContain("export function text(");
144
+ expect(code).toContain("return new TextAssetBuilder(initial)");
145
+ });
146
+
147
+ test("generates methods for properties", () => {
148
+ const source = `
149
+ interface Asset<T extends string = string> {
150
+ id: string;
151
+ type: T;
152
+ }
153
+
154
+ export interface TextAsset extends Asset<"text"> {
155
+ value: string;
156
+ label?: string;
157
+ }
158
+ `;
159
+
160
+ const types = convertTsToXLR(source);
161
+ const asset = types.find(
162
+ (t) => t.name === "TextAsset",
163
+ ) as NamedType<ObjectType>;
164
+ const info = createBuilderInfo(asset);
165
+
166
+ const code = generator.generateBuilderClass(info);
167
+
168
+ expect(code).toContain("withValue(value:");
169
+ expect(code).toContain("withLabel(value:");
170
+ });
171
+ });
172
+
173
+ describe("Asset Type Defaults", () => {
174
+ test("includes type default for assets", () => {
175
+ const source = `
176
+ interface Asset<T extends string = string> {
177
+ id: string;
178
+ type: T;
179
+ }
180
+
181
+ export interface TextAsset extends Asset<"text"> {
182
+ value: string;
183
+ }
184
+ `;
185
+
186
+ const types = convertTsToXLR(source);
187
+ const asset = types.find(
188
+ (t) => t.name === "TextAsset",
189
+ ) as NamedType<ObjectType>;
190
+ const info = createBuilderInfo(asset);
191
+
192
+ const code = generator.generateBuilderClass(info);
193
+
194
+ expect(code).toContain('"type":"text"');
195
+ });
196
+
197
+ test("includes id default for assets", () => {
198
+ const source = `
199
+ interface Asset<T extends string = string> {
200
+ id: string;
201
+ type: T;
202
+ }
203
+
204
+ export interface TextAsset extends Asset<"text"> {
205
+ value: string;
206
+ }
207
+ `;
208
+
209
+ const types = convertTsToXLR(source);
210
+ const asset = types.find(
211
+ (t) => t.name === "TextAsset",
212
+ ) as NamedType<ObjectType>;
213
+ const info = createBuilderInfo(asset);
214
+
215
+ const code = generator.generateBuilderClass(info);
216
+
217
+ expect(code).toContain('"id":""');
218
+ });
219
+
220
+ test("includes const property defaults", () => {
221
+ const source = `
222
+ interface Asset<T extends string = string> {
223
+ id: string;
224
+ type: T;
225
+ }
226
+
227
+ export interface ButtonAsset extends Asset<"button"> {
228
+ variant: "primary";
229
+ label: string;
230
+ }
231
+ `;
232
+
233
+ const types = convertTsToXLR(source);
234
+ const asset = types.find(
235
+ (t) => t.name === "ButtonAsset",
236
+ ) as NamedType<ObjectType>;
237
+ const info = createBuilderInfo(asset);
238
+
239
+ const code = generator.generateBuilderClass(info);
240
+
241
+ expect(code).toContain('"variant":"primary"');
242
+ });
243
+ });
244
+
245
+ describe("Array Properties", () => {
246
+ test("generates __arrayProperties__ for array properties", () => {
247
+ const source = `
248
+ interface Asset<T extends string = string> {
249
+ id: string;
250
+ type: T;
251
+ }
252
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
253
+
254
+ export interface CollectionAsset extends Asset<"collection"> {
255
+ values: Array<AssetWrapper<Asset>>;
256
+ label?: AssetWrapper<Asset>;
257
+ }
258
+ `;
259
+
260
+ const types = convertTsToXLR(source);
261
+ const asset = types.find(
262
+ (t) => t.name === "CollectionAsset",
263
+ ) as NamedType<ObjectType>;
264
+ const info = createBuilderInfo(asset);
265
+
266
+ const code = generator.generateBuilderClass(info);
267
+
268
+ expect(code).toContain("__arrayProperties__");
269
+ expect(code).toContain('"values"');
270
+ // label is not an array, should not be in __arrayProperties__
271
+ expect(code).not.toMatch(/__arrayProperties__.*"label"/);
272
+ });
273
+
274
+ test("does not generate __arrayProperties__ when no arrays", () => {
275
+ const source = `
276
+ interface Asset<T extends string = string> {
277
+ id: string;
278
+ type: T;
279
+ }
280
+
281
+ export interface TextAsset extends Asset<"text"> {
282
+ value: string;
283
+ }
284
+ `;
285
+
286
+ const types = convertTsToXLR(source);
287
+ const asset = types.find(
288
+ (t) => t.name === "TextAsset",
289
+ ) as NamedType<ObjectType>;
290
+ const info = createBuilderInfo(asset);
291
+
292
+ const code = generator.generateBuilderClass(info);
293
+
294
+ expect(code).not.toContain("__arrayProperties__");
295
+ });
296
+
297
+ test("detects arrays in union types", () => {
298
+ const source = `
299
+ interface Asset<T extends string = string> {
300
+ id: string;
301
+ type: T;
302
+ }
303
+
304
+ export interface ActionAsset extends Asset<"action"> {
305
+ validate?: Array<string> | string;
306
+ }
307
+ `;
308
+
309
+ const types = convertTsToXLR(source);
310
+ const asset = types.find(
311
+ (t) => t.name === "ActionAsset",
312
+ ) as NamedType<ObjectType>;
313
+ const info = createBuilderInfo(asset);
314
+
315
+ const code = generator.generateBuilderClass(info);
316
+
317
+ expect(code).toContain("__arrayProperties__");
318
+ expect(code).toContain('"validate"');
319
+ });
320
+
321
+ test("generates __arrayProperties__ with all array property names", () => {
322
+ const source = `
323
+ interface Asset<T extends string = string> {
324
+ id: string;
325
+ type: T;
326
+ }
327
+ type AssetWrapper<T extends Asset = Asset> = { asset: T };
328
+
329
+ export interface CollectionAsset extends Asset<"collection"> {
330
+ values: Array<AssetWrapper<Asset>>;
331
+ actions: Array<AssetWrapper<Asset>>;
332
+ items: Array<string>;
333
+ }
334
+ `;
335
+
336
+ const types = convertTsToXLR(source);
337
+ const asset = types.find(
338
+ (t) => t.name === "CollectionAsset",
339
+ ) as NamedType<ObjectType>;
340
+ const info = createBuilderInfo(asset);
341
+
342
+ const code = generator.generateBuilderClass(info);
343
+
344
+ expect(code).toContain("__arrayProperties__");
345
+ expect(code).toContain('"values"');
346
+ expect(code).toContain('"actions"');
347
+ expect(code).toContain('"items"');
348
+ });
349
+
350
+ test("handles properties with union types containing arrays", () => {
351
+ const source = `
352
+ interface Asset<T extends string = string> {
353
+ id: string;
354
+ type: T;
355
+ }
356
+
357
+ export interface FlexibleAsset extends Asset<"flexible"> {
358
+ data?: string | number | Array<string>;
359
+ }
360
+ `;
361
+
362
+ const types = convertTsToXLR(source);
363
+ const asset = types.find(
364
+ (t) => t.name === "FlexibleAsset",
365
+ ) as NamedType<ObjectType>;
366
+ const info = createBuilderInfo(asset);
367
+
368
+ const code = generator.generateBuilderClass(info);
369
+
370
+ // Should detect array within the union
371
+ expect(code).toContain("__arrayProperties__");
372
+ expect(code).toContain('"data"');
373
+ });
374
+ });
375
+
376
+ describe("Generic Class Generation", () => {
377
+ test("generates class with constrained generic parameters", () => {
378
+ const source = `
379
+ interface Asset<T extends string = string> {
380
+ id: string;
381
+ type: T;
382
+ }
383
+
384
+ interface ConstraintType {
385
+ name: string;
386
+ }
387
+
388
+ export interface ConstrainedAsset<T extends ConstraintType = ConstraintType> extends Asset<"constrained"> {
389
+ data: T;
390
+ }
391
+ `;
392
+
393
+ const types = convertTsToXLR(source);
394
+ const asset = types.find(
395
+ (t) => t.name === "ConstrainedAsset",
396
+ ) as NamedType<ObjectType>;
397
+ const info: BuilderInfo = {
398
+ ...createBuilderInfo(asset),
399
+ genericParams: "T extends ConstraintType = ConstraintType",
400
+ };
401
+
402
+ const code = generator.generateBuilderClass(info);
403
+
404
+ expect(code).toContain(
405
+ "export class ConstrainedAssetBuilder<T extends ConstraintType = ConstraintType>",
406
+ );
407
+ });
408
+
409
+ test("generates interface with generic parameters", () => {
410
+ const source = `
411
+ interface Asset<T extends string = string> {
412
+ id: string;
413
+ type: T;
414
+ }
415
+
416
+ export interface ParameterizedAsset<T, U extends string = "default"> extends Asset<"param"> {
417
+ first?: T;
418
+ second?: U;
419
+ }
420
+ `;
421
+
422
+ const types = convertTsToXLR(source);
423
+ const asset = types.find(
424
+ (t) => t.name === "ParameterizedAsset",
425
+ ) as NamedType<ObjectType>;
426
+ const info: BuilderInfo = {
427
+ ...createBuilderInfo(asset),
428
+ genericParams: 'T, U extends string = "default"',
429
+ };
430
+
431
+ const code = generator.generateBuilderClass(info);
432
+
433
+ expect(code).toContain(
434
+ 'export interface ParameterizedAssetBuilderMethods<T, U extends string = "default">',
435
+ );
436
+ expect(code).toContain(
437
+ 'export function parameterized<T, U extends string = "default">(',
438
+ );
439
+ });
440
+ });
441
+
442
+ describe("Interface Methods", () => {
443
+ test("generates interface with method signatures", () => {
444
+ const source = `
445
+ interface Asset<T extends string = string> {
446
+ id: string;
447
+ type: T;
448
+ }
449
+
450
+ export interface TextAsset extends Asset<"text"> {
451
+ value: string;
452
+ count?: number;
453
+ }
454
+ `;
455
+
456
+ const types = convertTsToXLR(source);
457
+ const asset = types.find(
458
+ (t) => t.name === "TextAsset",
459
+ ) as NamedType<ObjectType>;
460
+ const info = createBuilderInfo(asset);
461
+
462
+ const code = generator.generateBuilderClass(info);
463
+
464
+ expect(code).toContain("export interface TextAssetBuilderMethods");
465
+ expect(code).toContain("withValue(value:");
466
+ expect(code).toContain("withCount(value:");
467
+ });
468
+ });
469
+
470
+ describe("Build Method", () => {
471
+ test("generates build method with correct return type", () => {
472
+ const source = `
473
+ interface Asset<T extends string = string> {
474
+ id: string;
475
+ type: T;
476
+ }
477
+
478
+ export interface TextAsset extends Asset<"text"> {
479
+ value: string;
480
+ }
481
+ `;
482
+
483
+ const types = convertTsToXLR(source);
484
+ const asset = types.find(
485
+ (t) => t.name === "TextAsset",
486
+ ) as NamedType<ObjectType>;
487
+ const info = createBuilderInfo(asset);
488
+
489
+ const code = generator.generateBuilderClass(info);
490
+
491
+ expect(code).toContain("build(context?: BaseBuildContext): TextAsset");
492
+ expect(code).toContain(
493
+ "return this.buildWithDefaults(TextAssetBuilder.defaults, context)",
494
+ );
495
+ });
496
+ });
497
+
498
+ describe("Inspect Method", () => {
499
+ test("generates inspect method for debugging", () => {
500
+ const source = `
501
+ interface Asset<T extends string = string> {
502
+ id: string;
503
+ type: T;
504
+ }
505
+
506
+ export interface TextAsset extends Asset<"text"> {
507
+ value: string;
508
+ }
509
+ `;
510
+
511
+ const types = convertTsToXLR(source);
512
+ const asset = types.find(
513
+ (t) => t.name === "TextAsset",
514
+ ) as NamedType<ObjectType>;
515
+ const info = createBuilderInfo(asset);
516
+
517
+ const code = generator.generateBuilderClass(info);
518
+
519
+ expect(code).toContain('[Symbol.for("nodejs.util.inspect.custom")]');
520
+ expect(code).toContain('createInspectMethod("TextAssetBuilder"');
521
+ });
522
+ });
523
+
524
+ describe("Generic Types", () => {
525
+ test("generates generic builder class", () => {
526
+ const source = `
527
+ interface Asset<T extends string = string> {
528
+ id: string;
529
+ type: T;
530
+ }
531
+
532
+ export interface GenericAsset<T extends string = string> extends Asset<"generic"> {
533
+ value?: T;
534
+ }
535
+ `;
536
+
537
+ const types = convertTsToXLR(source);
538
+ const asset = types.find(
539
+ (t) => t.name === "GenericAsset",
540
+ ) as NamedType<ObjectType>;
541
+ const info: BuilderInfo = {
542
+ ...createBuilderInfo(asset),
543
+ genericParams: "T extends string = string",
544
+ };
545
+
546
+ const code = generator.generateBuilderClass(info);
547
+
548
+ expect(code).toContain(
549
+ "export class GenericAssetBuilder<T extends string = string>",
550
+ );
551
+ expect(code).toContain(
552
+ "export interface GenericAssetBuilderMethods<T extends string = string>",
553
+ );
554
+ expect(code).toContain(
555
+ "export function generic<T extends string = string>(",
556
+ );
557
+ });
558
+ });
559
+
560
+ describe("Inherited Properties", () => {
561
+ test("adds id property for assets without explicit id", () => {
562
+ const source = `
563
+ interface Asset<T extends string = string> {
564
+ id: string;
565
+ type: T;
566
+ }
567
+
568
+ export interface TextAsset extends Asset<"text"> {
569
+ value: string;
570
+ }
571
+ `;
572
+
573
+ const types = convertTsToXLR(source);
574
+ const asset = types.find(
575
+ (t) => t.name === "TextAsset",
576
+ ) as NamedType<ObjectType>;
577
+ const info = createBuilderInfo(asset);
578
+
579
+ const code = generator.generateBuilderClass(info);
580
+
581
+ expect(code).toContain("withId(value:");
582
+ });
583
+ });
584
+
585
+ describe("Non-Asset Types", () => {
586
+ test("generates builder for non-Asset type", () => {
587
+ const source = `
588
+ export interface Metadata {
589
+ beacon?: string;
590
+ role?: string;
591
+ }
592
+ `;
593
+
594
+ const types = convertTsToXLR(source);
595
+ const metadata = types.find(
596
+ (t) => t.name === "Metadata",
597
+ ) as NamedType<ObjectType>;
598
+ const info = createBuilderInfo(metadata);
599
+
600
+ const code = generator.generateBuilderClass(info);
601
+
602
+ expect(code).toContain("export class MetadataBuilder");
603
+ expect(code).toContain("export function metadata(");
604
+ expect(code).toContain("withBeacon(value:");
605
+ expect(code).toContain("withRole(value:");
606
+ });
607
+
608
+ test("adds id default for non-Asset types with id property", () => {
609
+ const source = `
610
+ export interface Item {
611
+ id: string;
612
+ name: string;
613
+ }
614
+ `;
615
+
616
+ const types = convertTsToXLR(source);
617
+ const item = types.find(
618
+ (t) => t.name === "Item",
619
+ ) as NamedType<ObjectType>;
620
+ const info = createBuilderInfo(item);
621
+
622
+ const code = generator.generateBuilderClass(info);
623
+
624
+ expect(code).toContain('"id":""');
625
+ });
626
+ });
627
+ });