@player-tools/fluent-generator 0.13.0--canary.221.5819
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/cjs/index.cjs +510 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/index.legacy-esm.js +472 -0
- package/dist/index.mjs +472 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +35 -0
- package/src/__tests__/__snapshots__/generator.test.ts.snap +875 -0
- package/src/__tests__/generator.test.ts +1142 -0
- package/src/cli.ts +192 -0
- package/src/generator.ts +596 -0
- package/src/index.ts +12 -0
- package/src/utils.ts +209 -0
- package/types/cli.d.ts +6 -0
- package/types/generator.d.ts +77 -0
- package/types/index.d.ts +8 -0
- package/types/utils.d.ts +73 -0
|
@@ -0,0 +1,1142 @@
|
|
|
1
|
+
import { describe, test, expect, vi } from "vitest";
|
|
2
|
+
import { setupTestEnv } from "@player-tools/test-utils";
|
|
3
|
+
import { TsConverter } from "@player-tools/xlr-converters";
|
|
4
|
+
import type { NamedType, ObjectType } from "@player-tools/xlr";
|
|
5
|
+
import { generateFluentBuilder, type GeneratorConfig } from "../generator";
|
|
6
|
+
|
|
7
|
+
/** Custom primitives that should be treated as refs rather than resolved */
|
|
8
|
+
const CUSTOM_PRIMITIVES = ["Asset", "AssetWrapper", "Binding", "Expression"];
|
|
9
|
+
|
|
10
|
+
vi.setConfig({
|
|
11
|
+
testTimeout: 2 * 60 * 1000,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Converts TypeScript source code to XLR types
|
|
16
|
+
*/
|
|
17
|
+
function convertTsToXLR(
|
|
18
|
+
sourceCode: string,
|
|
19
|
+
customPrimitives = CUSTOM_PRIMITIVES,
|
|
20
|
+
) {
|
|
21
|
+
const { sf, tc } = setupTestEnv(sourceCode);
|
|
22
|
+
const converter = new TsConverter(tc, customPrimitives);
|
|
23
|
+
return converter.convertSourceFile(sf).data.types;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("FluentBuilderGenerator", () => {
|
|
27
|
+
describe("Basic Types", () => {
|
|
28
|
+
test("generates builder for simple asset with string property", () => {
|
|
29
|
+
const source = `
|
|
30
|
+
interface Asset<T extends string> {
|
|
31
|
+
id: string;
|
|
32
|
+
type: T;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TextAsset extends Asset<"text"> {
|
|
36
|
+
value: string;
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const types = convertTsToXLR(source);
|
|
41
|
+
const textAsset = types.find(
|
|
42
|
+
(t) => t.name === "TextAsset",
|
|
43
|
+
) as NamedType<ObjectType>;
|
|
44
|
+
expect(textAsset).toBeDefined();
|
|
45
|
+
|
|
46
|
+
const code = generateFluentBuilder(textAsset);
|
|
47
|
+
expect(code).toMatchSnapshot();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("generates builder for asset with optional properties", () => {
|
|
51
|
+
const source = `
|
|
52
|
+
interface Asset<T extends string> {
|
|
53
|
+
id: string;
|
|
54
|
+
type: T;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface InputAsset extends Asset<"input"> {
|
|
58
|
+
binding: string;
|
|
59
|
+
label?: string;
|
|
60
|
+
placeholder?: string;
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
const types = convertTsToXLR(source);
|
|
65
|
+
const inputAsset = types.find(
|
|
66
|
+
(t) => t.name === "InputAsset",
|
|
67
|
+
) as NamedType<ObjectType>;
|
|
68
|
+
expect(inputAsset).toBeDefined();
|
|
69
|
+
|
|
70
|
+
const code = generateFluentBuilder(inputAsset);
|
|
71
|
+
expect(code).toMatchSnapshot();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("generates builder for asset with number and boolean properties", () => {
|
|
75
|
+
const source = `
|
|
76
|
+
interface Asset<T extends string> {
|
|
77
|
+
id: string;
|
|
78
|
+
type: T;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CounterAsset extends Asset<"counter"> {
|
|
82
|
+
value: number;
|
|
83
|
+
min?: number;
|
|
84
|
+
max?: number;
|
|
85
|
+
enabled?: boolean;
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
const types = convertTsToXLR(source);
|
|
90
|
+
const counterAsset = types.find(
|
|
91
|
+
(t) => t.name === "CounterAsset",
|
|
92
|
+
) as NamedType<ObjectType>;
|
|
93
|
+
expect(counterAsset).toBeDefined();
|
|
94
|
+
|
|
95
|
+
const code = generateFluentBuilder(counterAsset);
|
|
96
|
+
expect(code).toMatchSnapshot();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("Asset Wrapper (Slot) Types", () => {
|
|
101
|
+
test("generates builder for asset with AssetWrapper slots", () => {
|
|
102
|
+
const source = `
|
|
103
|
+
interface Asset<T extends string = string> {
|
|
104
|
+
id: string;
|
|
105
|
+
type: T;
|
|
106
|
+
}
|
|
107
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
108
|
+
|
|
109
|
+
export interface InfoAsset extends Asset<"info"> {
|
|
110
|
+
title?: AssetWrapper<Asset>;
|
|
111
|
+
subtitle?: AssetWrapper<Asset>;
|
|
112
|
+
primaryInfo: Array<AssetWrapper<Asset>>;
|
|
113
|
+
}
|
|
114
|
+
`;
|
|
115
|
+
|
|
116
|
+
const types = convertTsToXLR(source);
|
|
117
|
+
const infoAsset = types.find(
|
|
118
|
+
(t) => t.name === "InfoAsset",
|
|
119
|
+
) as NamedType<ObjectType>;
|
|
120
|
+
expect(infoAsset).toBeDefined();
|
|
121
|
+
|
|
122
|
+
const code = generateFluentBuilder(infoAsset);
|
|
123
|
+
expect(code).toMatchSnapshot();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("generates builder for asset with array of AssetWrapper slots", () => {
|
|
127
|
+
const source = `
|
|
128
|
+
interface Asset<T extends string = string> {
|
|
129
|
+
id: string;
|
|
130
|
+
type: T;
|
|
131
|
+
}
|
|
132
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
133
|
+
|
|
134
|
+
export interface CollectionAsset extends Asset<"collection"> {
|
|
135
|
+
label?: AssetWrapper<Asset>;
|
|
136
|
+
values?: Array<AssetWrapper<Asset>>;
|
|
137
|
+
}
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
const types = convertTsToXLR(source);
|
|
141
|
+
const collectionAsset = types.find(
|
|
142
|
+
(t) => t.name === "CollectionAsset",
|
|
143
|
+
) as NamedType<ObjectType>;
|
|
144
|
+
expect(collectionAsset).toBeDefined();
|
|
145
|
+
|
|
146
|
+
const code = generateFluentBuilder(collectionAsset);
|
|
147
|
+
expect(code).toMatchSnapshot();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("Binding and Expression Types", () => {
|
|
152
|
+
test("generates builder for asset with Binding property", () => {
|
|
153
|
+
const source = `
|
|
154
|
+
interface Asset<T extends string = string> {
|
|
155
|
+
id: string;
|
|
156
|
+
type: T;
|
|
157
|
+
}
|
|
158
|
+
type Binding = string;
|
|
159
|
+
|
|
160
|
+
export interface InputAsset extends Asset<"input"> {
|
|
161
|
+
binding: Binding;
|
|
162
|
+
label?: string;
|
|
163
|
+
}
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
const types = convertTsToXLR(source);
|
|
167
|
+
const inputAsset = types.find(
|
|
168
|
+
(t) => t.name === "InputAsset",
|
|
169
|
+
) as NamedType<ObjectType>;
|
|
170
|
+
expect(inputAsset).toBeDefined();
|
|
171
|
+
|
|
172
|
+
const code = generateFluentBuilder(inputAsset);
|
|
173
|
+
expect(code).toMatchSnapshot();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("generates builder for asset with Expression property", () => {
|
|
177
|
+
const source = `
|
|
178
|
+
interface Asset<T extends string = string> {
|
|
179
|
+
id: string;
|
|
180
|
+
type: T;
|
|
181
|
+
}
|
|
182
|
+
type Expression = string;
|
|
183
|
+
|
|
184
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
185
|
+
value?: string;
|
|
186
|
+
exp?: Expression;
|
|
187
|
+
}
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
const types = convertTsToXLR(source);
|
|
191
|
+
const actionAsset = types.find(
|
|
192
|
+
(t) => t.name === "ActionAsset",
|
|
193
|
+
) as NamedType<ObjectType>;
|
|
194
|
+
expect(actionAsset).toBeDefined();
|
|
195
|
+
|
|
196
|
+
const code = generateFluentBuilder(actionAsset);
|
|
197
|
+
expect(code).toMatchSnapshot();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("Nested Object Types", () => {
|
|
202
|
+
test("generates builder for asset with nested object property", () => {
|
|
203
|
+
const source = `
|
|
204
|
+
interface Asset<T extends string = string> {
|
|
205
|
+
id: string;
|
|
206
|
+
type: T;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
210
|
+
value?: string;
|
|
211
|
+
confirmation?: {
|
|
212
|
+
message: string;
|
|
213
|
+
affirmativeLabel: string;
|
|
214
|
+
negativeLabel?: string;
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
`;
|
|
218
|
+
|
|
219
|
+
const types = convertTsToXLR(source);
|
|
220
|
+
const actionAsset = types.find(
|
|
221
|
+
(t) => t.name === "ActionAsset",
|
|
222
|
+
) as NamedType<ObjectType>;
|
|
223
|
+
expect(actionAsset).toBeDefined();
|
|
224
|
+
|
|
225
|
+
const code = generateFluentBuilder(actionAsset);
|
|
226
|
+
expect(code).toMatchSnapshot();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("generates builder for asset with named nested type", () => {
|
|
230
|
+
const source = `
|
|
231
|
+
interface Asset<T extends string = string> {
|
|
232
|
+
id: string;
|
|
233
|
+
type: T;
|
|
234
|
+
}
|
|
235
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
236
|
+
|
|
237
|
+
export interface ActionMetaData {
|
|
238
|
+
beacon?: string | Record<string, unknown>;
|
|
239
|
+
role?: "primary" | "secondary" | "link";
|
|
240
|
+
skipValidation?: boolean;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
244
|
+
value?: string;
|
|
245
|
+
label?: AssetWrapper<Asset>;
|
|
246
|
+
metaData?: ActionMetaData;
|
|
247
|
+
}
|
|
248
|
+
`;
|
|
249
|
+
|
|
250
|
+
const types = convertTsToXLR(source);
|
|
251
|
+
const actionAsset = types.find(
|
|
252
|
+
(t) => t.name === "ActionAsset",
|
|
253
|
+
) as NamedType<ObjectType>;
|
|
254
|
+
expect(actionAsset).toBeDefined();
|
|
255
|
+
|
|
256
|
+
const code = generateFluentBuilder(actionAsset);
|
|
257
|
+
expect(code).toMatchSnapshot();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("Union Types", () => {
|
|
262
|
+
test("generates builder for asset with union property", () => {
|
|
263
|
+
const source = `
|
|
264
|
+
interface Asset<T extends string = string> {
|
|
265
|
+
id: string;
|
|
266
|
+
type: T;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
270
|
+
size?: "small" | "medium" | "large";
|
|
271
|
+
}
|
|
272
|
+
`;
|
|
273
|
+
|
|
274
|
+
const types = convertTsToXLR(source);
|
|
275
|
+
const actionAsset = types.find(
|
|
276
|
+
(t) => t.name === "ActionAsset",
|
|
277
|
+
) as NamedType<ObjectType>;
|
|
278
|
+
expect(actionAsset).toBeDefined();
|
|
279
|
+
|
|
280
|
+
const code = generateFluentBuilder(actionAsset);
|
|
281
|
+
expect(code).toMatchSnapshot();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("generates builder for asset with discriminated union modifiers", () => {
|
|
285
|
+
const source = `
|
|
286
|
+
interface Asset<T extends string = string> {
|
|
287
|
+
id: string;
|
|
288
|
+
type: T;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
interface CalloutModifier {
|
|
292
|
+
type: "callout";
|
|
293
|
+
value: "support" | "legal";
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
interface TagModifier {
|
|
297
|
+
type: "tag";
|
|
298
|
+
value: "block";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export interface CollectionAsset extends Asset<"collection"> {
|
|
302
|
+
modifiers?: Array<CalloutModifier | TagModifier>;
|
|
303
|
+
}
|
|
304
|
+
`;
|
|
305
|
+
|
|
306
|
+
const types = convertTsToXLR(source);
|
|
307
|
+
const collectionAsset = types.find(
|
|
308
|
+
(t) => t.name === "CollectionAsset",
|
|
309
|
+
) as NamedType<ObjectType>;
|
|
310
|
+
expect(collectionAsset).toBeDefined();
|
|
311
|
+
|
|
312
|
+
const code = generateFluentBuilder(collectionAsset);
|
|
313
|
+
expect(code).toMatchSnapshot();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("Array Types", () => {
|
|
318
|
+
test("generates builder for asset with array of primitives", () => {
|
|
319
|
+
const source = `
|
|
320
|
+
interface Asset<T extends string = string> {
|
|
321
|
+
id: string;
|
|
322
|
+
type: T;
|
|
323
|
+
}
|
|
324
|
+
type Binding = string;
|
|
325
|
+
|
|
326
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
327
|
+
validate?: Array<Binding> | Binding;
|
|
328
|
+
}
|
|
329
|
+
`;
|
|
330
|
+
|
|
331
|
+
const types = convertTsToXLR(source);
|
|
332
|
+
const actionAsset = types.find(
|
|
333
|
+
(t) => t.name === "ActionAsset",
|
|
334
|
+
) as NamedType<ObjectType>;
|
|
335
|
+
expect(actionAsset).toBeDefined();
|
|
336
|
+
|
|
337
|
+
const code = generateFluentBuilder(actionAsset);
|
|
338
|
+
expect(code).toMatchSnapshot();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("generates builder for asset with array of complex objects", () => {
|
|
342
|
+
const source = `
|
|
343
|
+
interface Asset<T extends string = string> {
|
|
344
|
+
id: string;
|
|
345
|
+
type: T;
|
|
346
|
+
}
|
|
347
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
348
|
+
|
|
349
|
+
export interface ChoiceItem {
|
|
350
|
+
id: string;
|
|
351
|
+
label?: AssetWrapper<Asset>;
|
|
352
|
+
value?: string | number | boolean | null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export interface ChoiceAsset extends Asset<"choice"> {
|
|
356
|
+
binding: string;
|
|
357
|
+
choices?: Array<ChoiceItem>;
|
|
358
|
+
}
|
|
359
|
+
`;
|
|
360
|
+
|
|
361
|
+
const types = convertTsToXLR(source);
|
|
362
|
+
const choiceAsset = types.find(
|
|
363
|
+
(t) => t.name === "ChoiceAsset",
|
|
364
|
+
) as NamedType<ObjectType>;
|
|
365
|
+
expect(choiceAsset).toBeDefined();
|
|
366
|
+
|
|
367
|
+
const code = generateFluentBuilder(choiceAsset);
|
|
368
|
+
expect(code).toMatchSnapshot();
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe("Generic Types", () => {
|
|
373
|
+
test("generates builder for asset with generic parameter", () => {
|
|
374
|
+
const source = `
|
|
375
|
+
interface Asset<T extends string = string> {
|
|
376
|
+
id: string;
|
|
377
|
+
type: T;
|
|
378
|
+
}
|
|
379
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
380
|
+
|
|
381
|
+
export interface InputAsset<AnyTextAsset extends Asset = Asset> extends Asset<"input"> {
|
|
382
|
+
binding: string;
|
|
383
|
+
label?: AssetWrapper<AnyTextAsset>;
|
|
384
|
+
note?: AssetWrapper<AnyTextAsset>;
|
|
385
|
+
}
|
|
386
|
+
`;
|
|
387
|
+
|
|
388
|
+
const types = convertTsToXLR(source);
|
|
389
|
+
const inputAsset = types.find(
|
|
390
|
+
(t) => t.name === "InputAsset",
|
|
391
|
+
) as NamedType<ObjectType>;
|
|
392
|
+
expect(inputAsset).toBeDefined();
|
|
393
|
+
|
|
394
|
+
const code = generateFluentBuilder(inputAsset);
|
|
395
|
+
expect(code).toMatchSnapshot();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe("Improvements over old fluent-gen-ts", () => {
|
|
401
|
+
test("string properties accept TaggedTemplateValue for binding support", () => {
|
|
402
|
+
const source = `
|
|
403
|
+
interface Asset<T extends string = string> {
|
|
404
|
+
id: string;
|
|
405
|
+
type: T;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export interface TextAsset extends Asset<"text"> {
|
|
409
|
+
value: string;
|
|
410
|
+
}
|
|
411
|
+
`;
|
|
412
|
+
|
|
413
|
+
const types = convertTsToXLR(source);
|
|
414
|
+
const textAsset = types.find(
|
|
415
|
+
(t) => t.name === "TextAsset",
|
|
416
|
+
) as NamedType<ObjectType>;
|
|
417
|
+
const code = generateFluentBuilder(textAsset);
|
|
418
|
+
|
|
419
|
+
// New generator adds TaggedTemplateValue support - OLD generator did NOT
|
|
420
|
+
expect(code).toContain(
|
|
421
|
+
"withValue(value: string | TaggedTemplateValue<string>)",
|
|
422
|
+
);
|
|
423
|
+
// Verify we don't just have plain string
|
|
424
|
+
expect(code).not.toMatch(/withValue\(value: string\):/);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("AssetWrapper slots accept Asset | FluentBuilder instead of AssetWrapper", () => {
|
|
428
|
+
const source = `
|
|
429
|
+
interface Asset<T extends string = string> {
|
|
430
|
+
id: string;
|
|
431
|
+
type: T;
|
|
432
|
+
}
|
|
433
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
434
|
+
|
|
435
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
436
|
+
label?: AssetWrapper<Asset>;
|
|
437
|
+
}
|
|
438
|
+
`;
|
|
439
|
+
|
|
440
|
+
const types = convertTsToXLR(source);
|
|
441
|
+
const actionAsset = types.find(
|
|
442
|
+
(t) => t.name === "ActionAsset",
|
|
443
|
+
) as NamedType<ObjectType>;
|
|
444
|
+
const code = generateFluentBuilder(actionAsset);
|
|
445
|
+
|
|
446
|
+
// New generator uses Asset | FluentBuilder<Asset> - OLD generator incorrectly used AssetWrapper
|
|
447
|
+
expect(code).toContain("Asset | FluentBuilder<Asset, BaseBuildContext>");
|
|
448
|
+
// Verify we don't use AssetWrapper (which was the bug)
|
|
449
|
+
expect(code).not.toContain("FluentBuilder<AssetWrapper");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("Binding properties accept TaggedTemplateValue", () => {
|
|
453
|
+
const source = `
|
|
454
|
+
interface Asset<T extends string = string> {
|
|
455
|
+
id: string;
|
|
456
|
+
type: T;
|
|
457
|
+
}
|
|
458
|
+
type Binding = string;
|
|
459
|
+
|
|
460
|
+
export interface InputAsset extends Asset<"input"> {
|
|
461
|
+
binding: Binding;
|
|
462
|
+
}
|
|
463
|
+
`;
|
|
464
|
+
|
|
465
|
+
const types = convertTsToXLR(source);
|
|
466
|
+
const inputAsset = types.find(
|
|
467
|
+
(t) => t.name === "InputAsset",
|
|
468
|
+
) as NamedType<ObjectType>;
|
|
469
|
+
const code = generateFluentBuilder(inputAsset);
|
|
470
|
+
|
|
471
|
+
// Bindings should accept TaggedTemplateValue
|
|
472
|
+
expect(code).toContain(
|
|
473
|
+
"withBinding(value: string | TaggedTemplateValue<string>)",
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("Expression properties accept TaggedTemplateValue", () => {
|
|
478
|
+
const source = `
|
|
479
|
+
interface Asset<T extends string = string> {
|
|
480
|
+
id: string;
|
|
481
|
+
type: T;
|
|
482
|
+
}
|
|
483
|
+
type Expression = string;
|
|
484
|
+
|
|
485
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
486
|
+
exp?: Expression;
|
|
487
|
+
}
|
|
488
|
+
`;
|
|
489
|
+
|
|
490
|
+
const types = convertTsToXLR(source);
|
|
491
|
+
const actionAsset = types.find(
|
|
492
|
+
(t) => t.name === "ActionAsset",
|
|
493
|
+
) as NamedType<ObjectType>;
|
|
494
|
+
const code = generateFluentBuilder(actionAsset);
|
|
495
|
+
|
|
496
|
+
// Expressions should accept TaggedTemplateValue
|
|
497
|
+
expect(code).toContain(
|
|
498
|
+
"withExp(value: string | TaggedTemplateValue<string>)",
|
|
499
|
+
);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
describe("Integration with Real Mock Types", () => {
|
|
504
|
+
test("generates builder for TextAsset", () => {
|
|
505
|
+
const source = `
|
|
506
|
+
interface Asset<T extends string = string> {
|
|
507
|
+
id: string;
|
|
508
|
+
type: T;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export interface TextAsset extends Asset<"text"> {
|
|
512
|
+
value: string;
|
|
513
|
+
}
|
|
514
|
+
`;
|
|
515
|
+
|
|
516
|
+
const types = convertTsToXLR(source);
|
|
517
|
+
const textAsset = types.find(
|
|
518
|
+
(t) => t.name === "TextAsset",
|
|
519
|
+
) as NamedType<ObjectType>;
|
|
520
|
+
|
|
521
|
+
const code = generateFluentBuilder(textAsset);
|
|
522
|
+
|
|
523
|
+
// Verify it has the expected parts
|
|
524
|
+
expect(code).toContain("TextAssetBuilder");
|
|
525
|
+
expect(code).toContain("withValue");
|
|
526
|
+
expect(code).toContain("string | TaggedTemplateValue<string>");
|
|
527
|
+
expect(code).toContain(
|
|
528
|
+
'defaults: Record<string, unknown> = {"type":"text","id":""}',
|
|
529
|
+
);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test("generates builder for InfoAsset", () => {
|
|
533
|
+
const source = `
|
|
534
|
+
interface Asset<T extends string = string> {
|
|
535
|
+
id: string;
|
|
536
|
+
type: T;
|
|
537
|
+
}
|
|
538
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
539
|
+
|
|
540
|
+
export interface InfoAsset extends Asset<"info"> {
|
|
541
|
+
primaryInfo: Array<AssetWrapper<Asset>>;
|
|
542
|
+
title?: AssetWrapper<Asset>;
|
|
543
|
+
subtitle?: AssetWrapper<Asset>;
|
|
544
|
+
}
|
|
545
|
+
`;
|
|
546
|
+
|
|
547
|
+
const types = convertTsToXLR(source);
|
|
548
|
+
const infoAsset = types.find(
|
|
549
|
+
(t) => t.name === "InfoAsset",
|
|
550
|
+
) as NamedType<ObjectType>;
|
|
551
|
+
|
|
552
|
+
const code = generateFluentBuilder(infoAsset);
|
|
553
|
+
|
|
554
|
+
// Verify it has the expected parts
|
|
555
|
+
expect(code).toContain("InfoAssetBuilder");
|
|
556
|
+
expect(code).toContain("withPrimaryInfo");
|
|
557
|
+
expect(code).toContain("withTitle");
|
|
558
|
+
expect(code).toContain("withSubtitle");
|
|
559
|
+
expect(code).toContain("Asset | FluentBuilder<Asset, BaseBuildContext>");
|
|
560
|
+
expect(code).toContain("__arrayProperties__");
|
|
561
|
+
expect(code).toContain('"primaryInfo"');
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("generates builder for InputAsset", () => {
|
|
565
|
+
const source = `
|
|
566
|
+
interface Asset<T extends string = string> {
|
|
567
|
+
id: string;
|
|
568
|
+
type: T;
|
|
569
|
+
}
|
|
570
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
571
|
+
|
|
572
|
+
export interface InputAsset extends Asset<"input"> {
|
|
573
|
+
binding: string;
|
|
574
|
+
label: AssetWrapper<Asset>;
|
|
575
|
+
placeholder?: string;
|
|
576
|
+
}
|
|
577
|
+
`;
|
|
578
|
+
|
|
579
|
+
const types = convertTsToXLR(source);
|
|
580
|
+
const inputAsset = types.find(
|
|
581
|
+
(t) => t.name === "InputAsset",
|
|
582
|
+
) as NamedType<ObjectType>;
|
|
583
|
+
|
|
584
|
+
const code = generateFluentBuilder(inputAsset);
|
|
585
|
+
|
|
586
|
+
// Verify it has the expected parts
|
|
587
|
+
expect(code).toContain("InputAssetBuilder");
|
|
588
|
+
expect(code).toContain("withBinding");
|
|
589
|
+
expect(code).toContain("withLabel");
|
|
590
|
+
expect(code).toContain("withPlaceholder");
|
|
591
|
+
expect(code).toContain("Asset | FluentBuilder<Asset, BaseBuildContext>");
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
describe("__arrayProperties__ generation", () => {
|
|
596
|
+
test("generates __arrayProperties__ for types with array properties", () => {
|
|
597
|
+
const source = `
|
|
598
|
+
interface Asset<T extends string = string> {
|
|
599
|
+
id: string;
|
|
600
|
+
type: T;
|
|
601
|
+
}
|
|
602
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
603
|
+
|
|
604
|
+
export interface CollectionAsset extends Asset<"collection"> {
|
|
605
|
+
values: Array<AssetWrapper<Asset>>;
|
|
606
|
+
actions?: Array<AssetWrapper<Asset>>;
|
|
607
|
+
label?: AssetWrapper<Asset>;
|
|
608
|
+
modifiers?: Array<{ type: string }>;
|
|
609
|
+
}
|
|
610
|
+
`;
|
|
611
|
+
|
|
612
|
+
const types = convertTsToXLR(source);
|
|
613
|
+
const collectionAsset = types.find(
|
|
614
|
+
(t) => t.name === "CollectionAsset",
|
|
615
|
+
) as NamedType<ObjectType>;
|
|
616
|
+
|
|
617
|
+
const code = generateFluentBuilder(collectionAsset);
|
|
618
|
+
|
|
619
|
+
// Must have __arrayProperties__ static property
|
|
620
|
+
expect(code).toContain("__arrayProperties__");
|
|
621
|
+
expect(code).toContain("ReadonlySet<string>");
|
|
622
|
+
|
|
623
|
+
// Must include the array properties
|
|
624
|
+
expect(code).toContain('"values"');
|
|
625
|
+
expect(code).toContain('"actions"');
|
|
626
|
+
expect(code).toContain('"modifiers"');
|
|
627
|
+
|
|
628
|
+
// Non-array properties should NOT be in __arrayProperties__
|
|
629
|
+
expect(code).not.toMatch(/__arrayProperties__.*"label"/);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
test("does not generate __arrayProperties__ when no array properties exist", () => {
|
|
633
|
+
const source = `
|
|
634
|
+
interface Asset<T extends string = string> {
|
|
635
|
+
id: string;
|
|
636
|
+
type: T;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export interface TextAsset extends Asset<"text"> {
|
|
640
|
+
value: string;
|
|
641
|
+
optional?: string;
|
|
642
|
+
}
|
|
643
|
+
`;
|
|
644
|
+
|
|
645
|
+
const types = convertTsToXLR(source);
|
|
646
|
+
const textAsset = types.find(
|
|
647
|
+
(t) => t.name === "TextAsset",
|
|
648
|
+
) as NamedType<ObjectType>;
|
|
649
|
+
|
|
650
|
+
const code = generateFluentBuilder(textAsset);
|
|
651
|
+
|
|
652
|
+
// Should NOT have __arrayProperties__ since there are no arrays
|
|
653
|
+
expect(code).not.toContain("__arrayProperties__");
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("array detection works with union types containing arrays", () => {
|
|
657
|
+
const source = `
|
|
658
|
+
interface Asset<T extends string = string> {
|
|
659
|
+
id: string;
|
|
660
|
+
type: T;
|
|
661
|
+
}
|
|
662
|
+
type Binding = string;
|
|
663
|
+
|
|
664
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
665
|
+
value?: string;
|
|
666
|
+
validate?: Array<Binding> | Binding;
|
|
667
|
+
}
|
|
668
|
+
`;
|
|
669
|
+
|
|
670
|
+
const types = convertTsToXLR(source);
|
|
671
|
+
const actionAsset = types.find(
|
|
672
|
+
(t) => t.name === "ActionAsset",
|
|
673
|
+
) as NamedType<ObjectType>;
|
|
674
|
+
|
|
675
|
+
const code = generateFluentBuilder(actionAsset);
|
|
676
|
+
|
|
677
|
+
// validate is Array | string, the Array variant should still be detected
|
|
678
|
+
expect(code).toContain("__arrayProperties__");
|
|
679
|
+
expect(code).toContain('"validate"');
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
describe("Generator configuration", () => {
|
|
684
|
+
test("supports custom fluent import path", () => {
|
|
685
|
+
const source = `
|
|
686
|
+
interface Asset<T extends string = string> {
|
|
687
|
+
id: string;
|
|
688
|
+
type: T;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export interface TextAsset extends Asset<"text"> {
|
|
692
|
+
value: string;
|
|
693
|
+
}
|
|
694
|
+
`;
|
|
695
|
+
|
|
696
|
+
const types = convertTsToXLR(source);
|
|
697
|
+
const textAsset = types.find(
|
|
698
|
+
(t) => t.name === "TextAsset",
|
|
699
|
+
) as NamedType<ObjectType>;
|
|
700
|
+
|
|
701
|
+
const config: GeneratorConfig = {
|
|
702
|
+
fluentImportPath: "../../../gen/common.js",
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const code = generateFluentBuilder(textAsset, config);
|
|
706
|
+
|
|
707
|
+
expect(code).toContain('from "../../../gen/common.js"');
|
|
708
|
+
expect(code).not.toContain('from "@player-tools/fluent"');
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test("supports custom types import path", () => {
|
|
712
|
+
const source = `
|
|
713
|
+
interface Asset<T extends string = string> {
|
|
714
|
+
id: string;
|
|
715
|
+
type: T;
|
|
716
|
+
}
|
|
717
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
718
|
+
|
|
719
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
720
|
+
label?: AssetWrapper<Asset>;
|
|
721
|
+
}
|
|
722
|
+
`;
|
|
723
|
+
|
|
724
|
+
const types = convertTsToXLR(source);
|
|
725
|
+
const actionAsset = types.find(
|
|
726
|
+
(t) => t.name === "ActionAsset",
|
|
727
|
+
) as NamedType<ObjectType>;
|
|
728
|
+
|
|
729
|
+
const config: GeneratorConfig = {
|
|
730
|
+
typesImportPath: "../custom-types.js",
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
const code = generateFluentBuilder(actionAsset, config);
|
|
734
|
+
|
|
735
|
+
expect(code).toContain('from "../custom-types.js"');
|
|
736
|
+
expect(code).not.toContain('from "@player-ui/types"');
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
test("supports custom type import path generator", () => {
|
|
740
|
+
const source = `
|
|
741
|
+
interface Asset<T extends string = string> {
|
|
742
|
+
id: string;
|
|
743
|
+
type: T;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export interface TextAsset extends Asset<"text"> {
|
|
747
|
+
value: string;
|
|
748
|
+
}
|
|
749
|
+
`;
|
|
750
|
+
|
|
751
|
+
const types = convertTsToXLR(source);
|
|
752
|
+
const textAsset = types.find(
|
|
753
|
+
(t) => t.name === "TextAsset",
|
|
754
|
+
) as NamedType<ObjectType>;
|
|
755
|
+
|
|
756
|
+
const config: GeneratorConfig = {
|
|
757
|
+
typeImportPathGenerator: (typeName) =>
|
|
758
|
+
`../types/${typeName.toLowerCase()}.js`,
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const code = generateFluentBuilder(textAsset, config);
|
|
762
|
+
|
|
763
|
+
expect(code).toContain('from "../types/textasset.js"');
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
describe("End-to-end: TypeScript → XLR → Builder → Built Object", () => {
|
|
768
|
+
/**
|
|
769
|
+
* This e2e test validates the entire pipeline:
|
|
770
|
+
* 1. TypeScript interface definition
|
|
771
|
+
* 2. Conversion to XLR via TsConverter
|
|
772
|
+
* 3. Builder generation via FluentBuilderGenerator
|
|
773
|
+
* 4. Builder execution to produce an object
|
|
774
|
+
* 5. Verification that the object matches the original interface structure
|
|
775
|
+
*/
|
|
776
|
+
test("generates working builder for simple asset", () => {
|
|
777
|
+
// Step 1: Define TypeScript interface
|
|
778
|
+
const source = `
|
|
779
|
+
interface Asset<T extends string = string> {
|
|
780
|
+
id: string;
|
|
781
|
+
type: T;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
export interface TextAsset extends Asset<"text"> {
|
|
785
|
+
value: string;
|
|
786
|
+
}
|
|
787
|
+
`;
|
|
788
|
+
|
|
789
|
+
// Step 2: Convert to XLR
|
|
790
|
+
const types = convertTsToXLR(source);
|
|
791
|
+
const textAsset = types.find(
|
|
792
|
+
(t) => t.name === "TextAsset",
|
|
793
|
+
) as NamedType<ObjectType>;
|
|
794
|
+
expect(textAsset).toBeDefined();
|
|
795
|
+
|
|
796
|
+
// Step 3: Generate builder code
|
|
797
|
+
const code = generateFluentBuilder(textAsset);
|
|
798
|
+
|
|
799
|
+
// Step 4: Verify the generated code structure
|
|
800
|
+
// The generated builder should have defaults for id and type
|
|
801
|
+
expect(code).toContain(
|
|
802
|
+
'defaults: Record<string, unknown> = {"type":"text","id":""}',
|
|
803
|
+
);
|
|
804
|
+
// Should have withValue method that accepts TaggedTemplateValue
|
|
805
|
+
expect(code).toContain(
|
|
806
|
+
"withValue(value: string | TaggedTemplateValue<string>)",
|
|
807
|
+
);
|
|
808
|
+
// Should have proper build method
|
|
809
|
+
expect(code).toContain("build(context?: BaseBuildContext): TextAsset");
|
|
810
|
+
|
|
811
|
+
// Step 5: Verify the built object structure (by parsing the generated code)
|
|
812
|
+
// The defaults object shows what fields the builder produces
|
|
813
|
+
const defaultsMatch = code.match(
|
|
814
|
+
/defaults: Record<string, unknown> = ({[^}]+})/,
|
|
815
|
+
);
|
|
816
|
+
expect(defaultsMatch).toBeTruthy();
|
|
817
|
+
const defaults = JSON.parse(defaultsMatch![1]);
|
|
818
|
+
expect(defaults).toEqual({ type: "text", id: "" });
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test("generates working builder for asset with slots", () => {
|
|
822
|
+
// Step 1: Define TypeScript interface with AssetWrapper slots
|
|
823
|
+
const source = `
|
|
824
|
+
interface Asset<T extends string = string> {
|
|
825
|
+
id: string;
|
|
826
|
+
type: T;
|
|
827
|
+
}
|
|
828
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
829
|
+
|
|
830
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
831
|
+
value?: string;
|
|
832
|
+
label?: AssetWrapper<Asset>;
|
|
833
|
+
}
|
|
834
|
+
`;
|
|
835
|
+
|
|
836
|
+
// Step 2: Convert to XLR
|
|
837
|
+
const types = convertTsToXLR(source);
|
|
838
|
+
const actionAsset = types.find(
|
|
839
|
+
(t) => t.name === "ActionAsset",
|
|
840
|
+
) as NamedType<ObjectType>;
|
|
841
|
+
expect(actionAsset).toBeDefined();
|
|
842
|
+
|
|
843
|
+
// Step 3: Generate builder code
|
|
844
|
+
const code = generateFluentBuilder(actionAsset);
|
|
845
|
+
|
|
846
|
+
// Step 4: Verify slot handling
|
|
847
|
+
// The label slot should accept Asset | FluentBuilder, not AssetWrapper
|
|
848
|
+
expect(code).toContain(
|
|
849
|
+
"withLabel(value: Asset | FluentBuilder<Asset, BaseBuildContext>)",
|
|
850
|
+
);
|
|
851
|
+
// Should NOT have AssetWrapper in the parameter type
|
|
852
|
+
expect(code).not.toContain("FluentBuilder<AssetWrapper");
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
test("generates working builder for asset with array properties", () => {
|
|
856
|
+
// Step 1: Define TypeScript interface with array properties
|
|
857
|
+
const source = `
|
|
858
|
+
interface Asset<T extends string = string> {
|
|
859
|
+
id: string;
|
|
860
|
+
type: T;
|
|
861
|
+
}
|
|
862
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
863
|
+
|
|
864
|
+
export interface CollectionAsset extends Asset<"collection"> {
|
|
865
|
+
values?: Array<AssetWrapper<Asset>>;
|
|
866
|
+
actions?: Array<AssetWrapper<Asset>>;
|
|
867
|
+
label?: AssetWrapper<Asset>;
|
|
868
|
+
}
|
|
869
|
+
`;
|
|
870
|
+
|
|
871
|
+
// Step 2: Convert to XLR
|
|
872
|
+
const types = convertTsToXLR(source);
|
|
873
|
+
const collectionAsset = types.find(
|
|
874
|
+
(t) => t.name === "CollectionAsset",
|
|
875
|
+
) as NamedType<ObjectType>;
|
|
876
|
+
expect(collectionAsset).toBeDefined();
|
|
877
|
+
|
|
878
|
+
// Step 3: Generate builder code
|
|
879
|
+
const code = generateFluentBuilder(collectionAsset);
|
|
880
|
+
|
|
881
|
+
// Step 4: Verify __arrayProperties__ is generated correctly
|
|
882
|
+
expect(code).toContain("__arrayProperties__");
|
|
883
|
+
expect(code).toContain('"values"');
|
|
884
|
+
expect(code).toContain('"actions"');
|
|
885
|
+
|
|
886
|
+
// Step 5: Verify array properties are correctly identified
|
|
887
|
+
// The __arrayProperties__ line should include values and actions, but not label
|
|
888
|
+
const arrayPropsMatch = code.match(
|
|
889
|
+
/__arrayProperties__.*new Set\(\[([^\]]+)\]\)/,
|
|
890
|
+
);
|
|
891
|
+
expect(arrayPropsMatch).toBeTruthy();
|
|
892
|
+
const arrayPropsContent = arrayPropsMatch![1];
|
|
893
|
+
expect(arrayPropsContent).toContain('"values"');
|
|
894
|
+
expect(arrayPropsContent).toContain('"actions"');
|
|
895
|
+
expect(arrayPropsContent).not.toContain('"label"');
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
test("generates working builder for asset with nested objects", () => {
|
|
899
|
+
// Step 1: Define TypeScript interface with nested object
|
|
900
|
+
const source = `
|
|
901
|
+
interface Asset<T extends string = string> {
|
|
902
|
+
id: string;
|
|
903
|
+
type: T;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
907
|
+
value?: string;
|
|
908
|
+
confirmation?: {
|
|
909
|
+
message: string;
|
|
910
|
+
affirmativeLabel: string;
|
|
911
|
+
negativeLabel?: string;
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
`;
|
|
915
|
+
|
|
916
|
+
// Step 2: Convert to XLR
|
|
917
|
+
const types = convertTsToXLR(source);
|
|
918
|
+
const actionAsset = types.find(
|
|
919
|
+
(t) => t.name === "ActionAsset",
|
|
920
|
+
) as NamedType<ObjectType>;
|
|
921
|
+
expect(actionAsset).toBeDefined();
|
|
922
|
+
|
|
923
|
+
// Step 3: Generate builder code
|
|
924
|
+
const code = generateFluentBuilder(actionAsset);
|
|
925
|
+
|
|
926
|
+
// Step 4: Verify nested object handling
|
|
927
|
+
// The confirmation property should have inline type with TaggedTemplateValue support
|
|
928
|
+
expect(code).toContain("withConfirmation");
|
|
929
|
+
expect(code).toContain("message: string | TaggedTemplateValue<string>");
|
|
930
|
+
expect(code).toContain(
|
|
931
|
+
"affirmativeLabel: string | TaggedTemplateValue<string>",
|
|
932
|
+
);
|
|
933
|
+
expect(code).toContain(
|
|
934
|
+
"negativeLabel?: string | TaggedTemplateValue<string>",
|
|
935
|
+
);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
test("generates working builder for generic asset", () => {
|
|
939
|
+
// Step 1: Define TypeScript interface with generic parameter
|
|
940
|
+
const source = `
|
|
941
|
+
interface Asset<T extends string = string> {
|
|
942
|
+
id: string;
|
|
943
|
+
type: T;
|
|
944
|
+
}
|
|
945
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
946
|
+
|
|
947
|
+
export interface InputAsset<AnyTextAsset extends Asset = Asset> extends Asset<"input"> {
|
|
948
|
+
binding: string;
|
|
949
|
+
label?: AssetWrapper<AnyTextAsset>;
|
|
950
|
+
}
|
|
951
|
+
`;
|
|
952
|
+
|
|
953
|
+
// Step 2: Convert to XLR
|
|
954
|
+
const types = convertTsToXLR(source);
|
|
955
|
+
const inputAsset = types.find(
|
|
956
|
+
(t) => t.name === "InputAsset",
|
|
957
|
+
) as NamedType<ObjectType>;
|
|
958
|
+
expect(inputAsset).toBeDefined();
|
|
959
|
+
|
|
960
|
+
// Step 3: Generate builder code
|
|
961
|
+
const code = generateFluentBuilder(inputAsset);
|
|
962
|
+
|
|
963
|
+
// Step 4: Verify generic parameter handling
|
|
964
|
+
expect(code).toContain("InputAssetBuilder<AnyTextAsset extends Asset");
|
|
965
|
+
expect(code).toContain("export function input<AnyTextAsset extends Asset");
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
describe("Edge Cases", () => {
|
|
970
|
+
test("handles special types: null, undefined, any, unknown, never, void", () => {
|
|
971
|
+
const source = `
|
|
972
|
+
interface Asset<T extends string = string> {
|
|
973
|
+
id: string;
|
|
974
|
+
type: T;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export interface SpecialTypesAsset extends Asset<"special"> {
|
|
978
|
+
nullValue: null;
|
|
979
|
+
undefinedValue: undefined;
|
|
980
|
+
anyValue: any;
|
|
981
|
+
unknownValue: unknown;
|
|
982
|
+
neverValue: never;
|
|
983
|
+
voidValue: void;
|
|
984
|
+
}
|
|
985
|
+
`;
|
|
986
|
+
|
|
987
|
+
const types = convertTsToXLR(source);
|
|
988
|
+
const specialAsset = types.find(
|
|
989
|
+
(t) => t.name === "SpecialTypesAsset",
|
|
990
|
+
) as NamedType<ObjectType>;
|
|
991
|
+
expect(specialAsset).toBeDefined();
|
|
992
|
+
|
|
993
|
+
const code = generateFluentBuilder(specialAsset);
|
|
994
|
+
|
|
995
|
+
expect(code).toContain("withNullValue(value: null)");
|
|
996
|
+
expect(code).toContain("withUndefinedValue(value: undefined)");
|
|
997
|
+
expect(code).toContain("withAnyValue(value: any)");
|
|
998
|
+
expect(code).toContain("withUnknownValue(value: unknown)");
|
|
999
|
+
expect(code).toContain("withNeverValue(value: never)");
|
|
1000
|
+
expect(code).toContain("withVoidValue(value: void)");
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
test("handles intersection types (AndType)", () => {
|
|
1004
|
+
const source = `
|
|
1005
|
+
interface Asset<T extends string = string> {
|
|
1006
|
+
id: string;
|
|
1007
|
+
type: T;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
interface BaseProps {
|
|
1011
|
+
name: string;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
interface ExtendedProps {
|
|
1015
|
+
description: string;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
export interface IntersectionAsset extends Asset<"intersection"> {
|
|
1019
|
+
combined: BaseProps & ExtendedProps;
|
|
1020
|
+
}
|
|
1021
|
+
`;
|
|
1022
|
+
|
|
1023
|
+
const types = convertTsToXLR(source);
|
|
1024
|
+
const asset = types.find(
|
|
1025
|
+
(t) => t.name === "IntersectionAsset",
|
|
1026
|
+
) as NamedType<ObjectType>;
|
|
1027
|
+
expect(asset).toBeDefined();
|
|
1028
|
+
|
|
1029
|
+
const code = generateFluentBuilder(asset);
|
|
1030
|
+
|
|
1031
|
+
// Should handle intersection type
|
|
1032
|
+
expect(code).toContain("withCombined");
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
test("handles union types containing arrays for __arrayProperties__", () => {
|
|
1036
|
+
const source = `
|
|
1037
|
+
interface Asset<T extends string = string> {
|
|
1038
|
+
id: string;
|
|
1039
|
+
type: T;
|
|
1040
|
+
}
|
|
1041
|
+
type Binding = string;
|
|
1042
|
+
|
|
1043
|
+
export interface ActionAsset extends Asset<"action"> {
|
|
1044
|
+
value?: string;
|
|
1045
|
+
/** Can be single binding or array of bindings */
|
|
1046
|
+
validate?: Array<Binding> | Binding;
|
|
1047
|
+
}
|
|
1048
|
+
`;
|
|
1049
|
+
|
|
1050
|
+
const types = convertTsToXLR(source);
|
|
1051
|
+
const actionAsset = types.find(
|
|
1052
|
+
(t) => t.name === "ActionAsset",
|
|
1053
|
+
) as NamedType<ObjectType>;
|
|
1054
|
+
expect(actionAsset).toBeDefined();
|
|
1055
|
+
|
|
1056
|
+
const code = generateFluentBuilder(actionAsset);
|
|
1057
|
+
|
|
1058
|
+
// The validate property is Array<Binding> | Binding - should be in __arrayProperties__
|
|
1059
|
+
expect(code).toContain("__arrayProperties__");
|
|
1060
|
+
expect(code).toContain('"validate"');
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
test("handles Record types", () => {
|
|
1064
|
+
const source = `
|
|
1065
|
+
interface Asset<T extends string = string> {
|
|
1066
|
+
id: string;
|
|
1067
|
+
type: T;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
export interface DataAsset extends Asset<"data"> {
|
|
1071
|
+
metadata: Record<string, unknown>;
|
|
1072
|
+
counts: Record<string, number>;
|
|
1073
|
+
}
|
|
1074
|
+
`;
|
|
1075
|
+
|
|
1076
|
+
const types = convertTsToXLR(source);
|
|
1077
|
+
const dataAsset = types.find(
|
|
1078
|
+
(t) => t.name === "DataAsset",
|
|
1079
|
+
) as NamedType<ObjectType>;
|
|
1080
|
+
expect(dataAsset).toBeDefined();
|
|
1081
|
+
|
|
1082
|
+
const code = generateFluentBuilder(dataAsset);
|
|
1083
|
+
|
|
1084
|
+
expect(code).toContain("withMetadata(value: Record<string, unknown>)");
|
|
1085
|
+
expect(code).toContain("withCounts(value: Record<string, number");
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
test("handles deeply nested union types", () => {
|
|
1089
|
+
const source = `
|
|
1090
|
+
interface Asset<T extends string = string> {
|
|
1091
|
+
id: string;
|
|
1092
|
+
type: T;
|
|
1093
|
+
}
|
|
1094
|
+
type AssetWrapper<T extends Asset = Asset> = { asset: T };
|
|
1095
|
+
|
|
1096
|
+
export interface ComplexAsset extends Asset<"complex"> {
|
|
1097
|
+
/** Single asset, array of assets, or nothing */
|
|
1098
|
+
content?: AssetWrapper<Asset> | Array<AssetWrapper<Asset>>;
|
|
1099
|
+
}
|
|
1100
|
+
`;
|
|
1101
|
+
|
|
1102
|
+
const types = convertTsToXLR(source);
|
|
1103
|
+
const complexAsset = types.find(
|
|
1104
|
+
(t) => t.name === "ComplexAsset",
|
|
1105
|
+
) as NamedType<ObjectType>;
|
|
1106
|
+
expect(complexAsset).toBeDefined();
|
|
1107
|
+
|
|
1108
|
+
const code = generateFluentBuilder(complexAsset);
|
|
1109
|
+
|
|
1110
|
+
// Should handle the union and include in __arrayProperties__
|
|
1111
|
+
expect(code).toContain("withContent");
|
|
1112
|
+
expect(code).toContain("__arrayProperties__");
|
|
1113
|
+
expect(code).toContain('"content"');
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
test("handles literal union types", () => {
|
|
1117
|
+
const source = `
|
|
1118
|
+
interface Asset<T extends string = string> {
|
|
1119
|
+
id: string;
|
|
1120
|
+
type: T;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
export interface SizeAsset extends Asset<"sized"> {
|
|
1124
|
+
size: "small" | "medium" | "large";
|
|
1125
|
+
alignment?: "left" | "center" | "right";
|
|
1126
|
+
}
|
|
1127
|
+
`;
|
|
1128
|
+
|
|
1129
|
+
const types = convertTsToXLR(source);
|
|
1130
|
+
const sizeAsset = types.find(
|
|
1131
|
+
(t) => t.name === "SizeAsset",
|
|
1132
|
+
) as NamedType<ObjectType>;
|
|
1133
|
+
expect(sizeAsset).toBeDefined();
|
|
1134
|
+
|
|
1135
|
+
const code = generateFluentBuilder(sizeAsset);
|
|
1136
|
+
|
|
1137
|
+
// Should preserve literal union types
|
|
1138
|
+
expect(code).toContain('"small"');
|
|
1139
|
+
expect(code).toContain('"medium"');
|
|
1140
|
+
expect(code).toContain('"large"');
|
|
1141
|
+
});
|
|
1142
|
+
});
|