@player-tools/xlr-utils 0.13.0-next.3 → 0.13.0-next.5

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.
@@ -1,14 +1,50 @@
1
1
  import { test, expect, describe } from "vitest";
2
2
  import * as ts from "typescript";
3
- import { NodeType } from "@player-tools/xlr";
3
+ import type {
4
+ NodeType,
5
+ NodeTypeWithGenerics,
6
+ ObjectNode,
7
+ OrType,
8
+ } from "@player-tools/xlr";
4
9
 
5
10
  import {
6
11
  tsStripOptionalType,
7
12
  isExportedDeclaration,
13
+ isExportedModuleDeclaration,
14
+ isNodeExported,
15
+ getReferencedType,
16
+ isTypeScriptLibType,
17
+ buildTemplateRegex,
18
+ fillInGenerics,
8
19
  applyPickOrOmitToNodeType,
9
20
  getStringLiteralsFromUnion,
10
21
  applyPartialOrRequiredToNodeType,
22
+ applyExcludeToNodeType,
11
23
  } from "../ts-helpers";
24
+ import { ScriptTarget } from "typescript";
25
+
26
+ /** Create a TypeChecker from a source string (single file). */
27
+ function createChecker(source: string, fileName = "test.ts"): ts.TypeChecker {
28
+ const sourceFile = ts.createSourceFile(
29
+ fileName,
30
+ source,
31
+ ts.ScriptTarget.Latest,
32
+ true,
33
+ );
34
+ const defaultHost = ts.createCompilerHost({});
35
+ const host: ts.CompilerHost = {
36
+ ...defaultHost,
37
+ getSourceFile: (name) =>
38
+ name === fileName
39
+ ? sourceFile
40
+ : defaultHost.getSourceFile(name, ScriptTarget.ESNext),
41
+ writeFile: () => {},
42
+ getCurrentDirectory: () => "",
43
+ readFile: () => "",
44
+ };
45
+ const program = ts.createProgram([fileName], {}, host);
46
+ return program.getTypeChecker();
47
+ }
12
48
 
13
49
  test("tsStripOptionalType", () => {
14
50
  const input: ts.TypeNode = ts.factory.createKeywordTypeNode(
@@ -21,6 +57,14 @@ test("tsStripOptionalType", () => {
21
57
  expect(actual).toEqual(expected);
22
58
  });
23
59
 
60
+ test("tsStripOptionalType strips optional type", () => {
61
+ const inner = ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
62
+ const optional = ts.factory.createOptionalTypeNode(inner);
63
+ const actual = tsStripOptionalType(optional);
64
+ expect(actual).toBe(inner);
65
+ expect(ts.isOptionalTypeNode(actual)).toBe(false);
66
+ });
67
+
24
68
  describe("isExportedDeclaration", () => {
25
69
  test("should return false for a non exported Statement", () => {
26
70
  const source = ts.createSourceFile(
@@ -55,16 +99,451 @@ describe("isExportedDeclaration", () => {
55
99
  const result = isExportedDeclaration(node);
56
100
  expect(result).toBe(true);
57
101
  });
102
+
103
+ test("should return false for node without modifiers", () => {
104
+ const source = ts.createSourceFile(
105
+ "test.ts",
106
+ "const x = 1;",
107
+ ts.ScriptTarget.Latest,
108
+ true,
109
+ );
110
+ const node = source.statements[0] as ts.Statement;
111
+ expect(ts.canHaveModifiers(node)).toBe(true);
112
+ const result = isExportedDeclaration(node);
113
+ expect(result).toBe(false);
114
+ });
58
115
  });
59
116
 
60
- test("getStringLiteralsFromUnion", () => {
61
- const input: ts.Node = ts.factory.createUnionTypeNode([
62
- ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral("foo")),
63
- ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral("bar")),
64
- ]);
65
- const expected: Set<string> = new Set(["foo", "bar"]);
66
- const actual = getStringLiteralsFromUnion(input);
67
- expect(actual).toEqual(expected);
117
+ describe("isExportedModuleDeclaration", () => {
118
+ test("returns true for exported module declaration", () => {
119
+ const source = ts.createSourceFile(
120
+ "test.ts",
121
+ "export module M { }",
122
+ ts.ScriptTarget.Latest,
123
+ true,
124
+ );
125
+ const node = source.statements[0] as ts.Statement;
126
+ expect(isExportedModuleDeclaration(node)).toBe(true);
127
+ expect(ts.isModuleDeclaration(node)).toBe(true);
128
+ });
129
+
130
+ test("returns false for non-exported module declaration", () => {
131
+ const source = ts.createSourceFile(
132
+ "test.ts",
133
+ "module M { }",
134
+ ts.ScriptTarget.Latest,
135
+ true,
136
+ );
137
+ const node = source.statements[0] as ts.Statement;
138
+ expect(isExportedModuleDeclaration(node)).toBe(false);
139
+ });
140
+
141
+ test("returns false for exported non-module declaration", () => {
142
+ const source = ts.createSourceFile(
143
+ "test.ts",
144
+ "export interface I { }",
145
+ ts.ScriptTarget.Latest,
146
+ true,
147
+ );
148
+ const node = source.statements[0] as ts.Statement;
149
+ expect(isExportedModuleDeclaration(node)).toBe(false);
150
+ });
151
+ });
152
+
153
+ describe("isNodeExported", () => {
154
+ test("returns true when node has Export modifier", () => {
155
+ const source = ts.createSourceFile(
156
+ "test.ts",
157
+ "export interface I { x: number; }",
158
+ ts.ScriptTarget.Latest,
159
+ true,
160
+ );
161
+ const decl = source.statements[0] as ts.InterfaceDeclaration;
162
+ expect(isNodeExported(decl)).toBe(true);
163
+ });
164
+
165
+ test("returns false when node is nested and has no Export modifier", () => {
166
+ const source = ts.createSourceFile(
167
+ "test.ts",
168
+ "namespace N { interface I { x: number; } }",
169
+ ts.ScriptTarget.Latest,
170
+ true,
171
+ );
172
+ const mod = source.statements[0] as ts.ModuleDeclaration;
173
+ const body = mod.body as ts.ModuleBlock;
174
+ const decl = body.statements[0] as ts.InterfaceDeclaration;
175
+ expect(isNodeExported(decl)).toBe(false);
176
+ });
177
+
178
+ test("returns true when parent is SourceFile (top-level)", () => {
179
+ const source = ts.createSourceFile(
180
+ "test.ts",
181
+ "type T = string;",
182
+ ts.ScriptTarget.Latest,
183
+ true,
184
+ );
185
+ const decl = source.statements[0];
186
+ expect(decl.parent?.kind).toBe(ts.SyntaxKind.SourceFile);
187
+ expect(isNodeExported(decl)).toBe(true);
188
+ });
189
+ });
190
+
191
+ describe("getReferencedType", () => {
192
+ test("returns declaration and exported for interface reference", () => {
193
+ const source = `interface Foo { a: number }
194
+ type Bar = Foo;`;
195
+ const checker = createChecker(source);
196
+ const sourceFile = ts.createSourceFile(
197
+ "test.ts",
198
+ source,
199
+ ts.ScriptTarget.Latest,
200
+ true,
201
+ );
202
+ const typeAlias = sourceFile.statements[1] as ts.TypeAliasDeclaration;
203
+ const typeRef = typeAlias.type as ts.TypeReferenceNode;
204
+ const result = getReferencedType(typeRef, checker);
205
+ expect(result).toBeDefined();
206
+ expect(result!.declaration.kind).toBe(ts.SyntaxKind.InterfaceDeclaration);
207
+ expect(ts.isInterfaceDeclaration(result!.declaration)).toBe(true);
208
+ expect(result!.exported).toBe(true);
209
+ });
210
+
211
+ test("returns declaration for type alias reference", () => {
212
+ const source = `type Foo = { a: number };
213
+ type Bar = Foo;`;
214
+ const checker = createChecker(source);
215
+ const sourceFile = ts.createSourceFile(
216
+ "test.ts",
217
+ source,
218
+ ts.ScriptTarget.Latest,
219
+ true,
220
+ );
221
+ const typeAlias = sourceFile.statements[1] as ts.TypeAliasDeclaration;
222
+ const typeRef = typeAlias.type as ts.TypeReferenceNode;
223
+ const result = getReferencedType(typeRef, checker);
224
+ expect(result).toBeDefined();
225
+ expect(result!.declaration.kind).toBe(ts.SyntaxKind.TypeAliasDeclaration);
226
+ expect(ts.isTypeAliasDeclaration(result!.declaration)).toBe(true);
227
+ });
228
+
229
+ test("returns undefined when reference is to class (not interface/type alias)", () => {
230
+ const source = `class C { }
231
+ type Bar = C;`;
232
+ const checker = createChecker(source);
233
+ const sourceFile = ts.createSourceFile(
234
+ "test.ts",
235
+ source,
236
+ ts.ScriptTarget.Latest,
237
+ true,
238
+ );
239
+ const typeAlias = sourceFile.statements[1] as ts.TypeAliasDeclaration;
240
+ const typeRef = typeAlias.type as ts.TypeReferenceNode;
241
+ const result = getReferencedType(typeRef, checker);
242
+ expect(result).toBeUndefined();
243
+ });
244
+ });
245
+
246
+ describe("isTypeScriptLibType", () => {
247
+ test("returns true for Promise (lib type)", () => {
248
+ const source = "type T = Promise<string>;";
249
+ const checker = createChecker(source);
250
+ const sourceFile = ts.createSourceFile(
251
+ "test.ts",
252
+ source,
253
+ ts.ScriptTarget.Latest,
254
+ true,
255
+ );
256
+ const typeAlias = sourceFile.statements[0] as ts.TypeAliasDeclaration;
257
+ const typeRef = typeAlias.type as ts.TypeReferenceNode;
258
+ expect(isTypeScriptLibType(typeRef, checker)).toBe(true);
259
+ });
260
+
261
+ test("returns false for user-defined type", () => {
262
+ const source = "interface Foo { } type T = Foo;";
263
+ const checker = createChecker(source);
264
+ const sourceFile = ts.createSourceFile(
265
+ "test.ts",
266
+ source,
267
+ ts.ScriptTarget.Latest,
268
+ true,
269
+ );
270
+ const typeAlias = sourceFile.statements[1] as ts.TypeAliasDeclaration;
271
+ const typeRef = typeAlias.type as ts.TypeReferenceNode;
272
+ expect(isTypeScriptLibType(typeRef, checker)).toBe(false);
273
+ });
274
+
275
+ test("returns false when symbol is undefined", () => {
276
+ const source = "type T = NonExistent;";
277
+ const checker = createChecker(source);
278
+ const typeRef = ts.factory.createTypeReferenceNode(
279
+ ts.factory.createIdentifier("NonExistent"),
280
+ );
281
+ expect(isTypeScriptLibType(typeRef, checker)).toBe(false);
282
+ });
283
+ });
284
+
285
+ describe("getStringLiteralsFromUnion", () => {
286
+ test("extracts string literals from union type node", () => {
287
+ const input: ts.Node = ts.factory.createUnionTypeNode([
288
+ ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral("foo")),
289
+ ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral("bar")),
290
+ ]);
291
+ const expected: Set<string> = new Set(["foo", "bar"]);
292
+ const actual = getStringLiteralsFromUnion(input);
293
+ expect(actual).toEqual(expected);
294
+ });
295
+
296
+ test("returns single string literal from LiteralTypeNode", () => {
297
+ const input = ts.factory.createLiteralTypeNode(
298
+ ts.factory.createStringLiteral("only"),
299
+ );
300
+ expect(getStringLiteralsFromUnion(input)).toEqual(new Set(["only"]));
301
+ });
302
+
303
+ test("returns empty set for non-union non-literal node", () => {
304
+ const input = ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
305
+ expect(getStringLiteralsFromUnion(input)).toEqual(new Set());
306
+ });
307
+
308
+ test("union with non-string literal yields empty string in set", () => {
309
+ const input = ts.factory.createUnionTypeNode([
310
+ ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral("a")),
311
+ ts.factory.createLiteralTypeNode(ts.factory.createNumericLiteral(42)),
312
+ ]);
313
+ const actual = getStringLiteralsFromUnion(input);
314
+ expect(actual).toEqual(new Set(["a", ""]));
315
+ });
316
+ });
317
+
318
+ describe("buildTemplateRegex", () => {
319
+ test("builds regex for template literal with string, number, boolean", () => {
320
+ const source = "type T = `pre${string}mid${number}suf`;";
321
+ const checker = createChecker(source);
322
+ const sourceFile = ts.createSourceFile(
323
+ "test.ts",
324
+ source,
325
+ ts.ScriptTarget.Latest,
326
+ true,
327
+ );
328
+ const typeAlias = sourceFile.statements[0] as ts.TypeAliasDeclaration;
329
+ const templateNode = typeAlias.type as ts.TemplateLiteralTypeNode;
330
+ const regex = buildTemplateRegex(templateNode, checker);
331
+ expect(regex).toBe("pre.*mid[0-9]*suf");
332
+ });
333
+
334
+ test("template with one string span", () => {
335
+ const source = "type T = `prefix-${string}-suffix`;";
336
+ const checker = createChecker(source);
337
+ const sourceFile = ts.createSourceFile(
338
+ "test.ts",
339
+ source,
340
+ ts.ScriptTarget.Latest,
341
+ true,
342
+ );
343
+ const typeAlias = sourceFile.statements[0] as ts.TypeAliasDeclaration;
344
+ const templateNode = typeAlias.type as ts.TemplateLiteralTypeNode;
345
+ const regex = buildTemplateRegex(templateNode, checker);
346
+ expect(regex).toBe("prefix-.*-suffix");
347
+ });
348
+ });
349
+
350
+ describe("fillInGenerics", () => {
351
+ test("returns primitive node unchanged when no generics", () => {
352
+ const node: NodeType = { type: "string" };
353
+ expect(fillInGenerics(node)).toBe(node);
354
+ });
355
+
356
+ test("fills ref with value from generics map", () => {
357
+ const refNode: NodeType = {
358
+ type: "ref",
359
+ ref: "T",
360
+ };
361
+ const generics = new Map<string, NodeType>([["T", { type: "string" }]]);
362
+ const result = fillInGenerics(refNode, generics);
363
+ expect(result).toEqual({ type: "string" });
364
+ });
365
+
366
+ test("ref with genericArguments fills each argument", () => {
367
+ const refNode: NodeType = {
368
+ type: "ref",
369
+ ref: "Outer",
370
+ genericArguments: [{ type: "ref", ref: "T" }],
371
+ };
372
+ const generics = new Map<string, NodeType>([["T", { type: "number" }]]);
373
+ const result = fillInGenerics(refNode, generics);
374
+ expect(result).toMatchObject({
375
+ type: "ref",
376
+ ref: "Outer",
377
+ genericArguments: [{ type: "number" }],
378
+ });
379
+ });
380
+
381
+ test("ref not in map returns ref with filled genericArguments", () => {
382
+ const refNode: NodeType = {
383
+ type: "ref",
384
+ ref: "Outer",
385
+ genericArguments: [{ type: "ref", ref: "T" }],
386
+ };
387
+ const generics = new Map<string, NodeType>([["T", { type: "boolean" }]]);
388
+ const result = fillInGenerics(refNode, generics);
389
+ expect(result).toMatchObject({
390
+ type: "ref",
391
+ ref: "Outer",
392
+ genericArguments: [{ type: "boolean" }],
393
+ });
394
+ });
395
+
396
+ test("object properties are filled recursively", () => {
397
+ const obj: NodeType = {
398
+ type: "object",
399
+ properties: {
400
+ p: { required: true, node: { type: "ref", ref: "T" } },
401
+ },
402
+ additionalProperties: false,
403
+ };
404
+ const generics = new Map<string, NodeType>([["T", { type: "string" }]]);
405
+ const result = fillInGenerics(obj, generics);
406
+ expect(result).toMatchObject({
407
+ type: "object",
408
+ properties: {
409
+ p: { required: true, node: { type: "string" } },
410
+ },
411
+ });
412
+ });
413
+
414
+ test("array elementType is filled", () => {
415
+ const arr: NodeType = {
416
+ type: "array",
417
+ elementType: { type: "ref", ref: "T" },
418
+ };
419
+ const generics = new Map<string, NodeType>([["T", { type: "number" }]]);
420
+ const result = fillInGenerics(arr, generics);
421
+ expect(result).toEqual({
422
+ type: "array",
423
+ elementType: { type: "number" },
424
+ });
425
+ });
426
+
427
+ test("or type members are filled", () => {
428
+ const orNode: NodeType = {
429
+ type: "or",
430
+ or: [{ type: "ref", ref: "T" }, { type: "string" }],
431
+ };
432
+ const generics = new Map<string, NodeType>([["T", { type: "number" }]]);
433
+ const result = fillInGenerics(orNode, generics);
434
+ expect(result).toMatchObject({
435
+ type: "or",
436
+ or: [{ type: "number" }, { type: "string" }],
437
+ });
438
+ });
439
+
440
+ test("and type members are filled", () => {
441
+ const andNode: NodeType = {
442
+ type: "and",
443
+ and: [
444
+ { type: "object", properties: {}, additionalProperties: false },
445
+ { type: "ref", ref: "T" },
446
+ ],
447
+ };
448
+ const generics = new Map<string, NodeType>([["T", { type: "null" }]]);
449
+ const result = fillInGenerics(andNode, generics);
450
+ expect(result.type).toBe("and");
451
+ expect((result as { and: NodeType[] }).and).toHaveLength(2);
452
+ expect((result as { and: NodeType[] }).and[1]).toEqual({ type: "null" });
453
+ });
454
+
455
+ test("record keyType and valueType are filled", () => {
456
+ const recordNode: NodeType = {
457
+ type: "record",
458
+ keyType: { type: "ref", ref: "K" },
459
+ valueType: { type: "ref", ref: "V" },
460
+ };
461
+ const generics = new Map<string, NodeType>([
462
+ ["K", { type: "string" }],
463
+ ["V", { type: "number" }],
464
+ ]);
465
+ const result = fillInGenerics(recordNode, generics);
466
+ expect(result).toEqual({
467
+ type: "record",
468
+ keyType: { type: "string" },
469
+ valueType: { type: "number" },
470
+ });
471
+ });
472
+
473
+ test("generic node without map builds defaults from genericTokens", () => {
474
+ const genericObj: NodeTypeWithGenerics<ObjectNode> = {
475
+ type: "object",
476
+ properties: {},
477
+ additionalProperties: false,
478
+ genericTokens: [
479
+ {
480
+ symbol: "T",
481
+ default: { type: "string" },
482
+ constraints: undefined,
483
+ },
484
+ ],
485
+ };
486
+ const result = fillInGenerics(genericObj);
487
+ expect(result).toMatchObject({ type: "object" });
488
+ });
489
+
490
+ test("conditional type with both sides non-ref resolves via resolveConditional", () => {
491
+ const conditionalNode: NodeType = {
492
+ type: "conditional",
493
+ check: {
494
+ left: { type: "string" },
495
+ right: { type: "string" },
496
+ },
497
+ value: {
498
+ true: { type: "number" },
499
+ false: { type: "boolean" },
500
+ },
501
+ };
502
+ const result = fillInGenerics(conditionalNode);
503
+ expect(result).toEqual({ type: "number" });
504
+ });
505
+
506
+ test("conditional type with ref in check returns unresolved conditional", () => {
507
+ const conditionalNode: NodeType = {
508
+ type: "conditional",
509
+ check: {
510
+ left: { type: "ref", ref: "T" },
511
+ right: { type: "string" },
512
+ },
513
+ value: {
514
+ true: { type: "number" },
515
+ false: { type: "boolean" },
516
+ },
517
+ };
518
+ const result = fillInGenerics(conditionalNode);
519
+ expect(result).toMatchObject({
520
+ type: "conditional",
521
+ check: { left: { type: "ref", ref: "T" }, right: { type: "string" } },
522
+ value: { true: { type: "number" }, false: { type: "boolean" } },
523
+ });
524
+ });
525
+
526
+ test("object with genericTokens and extends fills constraints and extends", () => {
527
+ const objWithExtends: NodeTypeWithGenerics<ObjectNode> = {
528
+ type: "object",
529
+ properties: { p: { required: false, node: { type: "ref", ref: "T" } } },
530
+ additionalProperties: false,
531
+ genericTokens: [
532
+ { symbol: "T", default: { type: "string" }, constraints: undefined },
533
+ ],
534
+ extends: { type: "ref", ref: "Base" },
535
+ };
536
+ const generics = new Map<string, NodeType>([
537
+ ["T", { type: "number" }],
538
+ ["Base", { type: "object", properties: {}, additionalProperties: false }],
539
+ ]);
540
+ const result = fillInGenerics(objWithExtends, generics);
541
+ expect(result).toMatchObject({
542
+ type: "object",
543
+ properties: { p: { node: { type: "number" } } },
544
+ extends: { type: "object" },
545
+ });
546
+ });
68
547
  });
69
548
 
70
549
  describe("applyPickOrOmitToNodeType", () => {
@@ -127,6 +606,131 @@ describe("applyPickOrOmitToNodeType", () => {
127
606
 
128
607
  expect(result).toStrictEqual(filteredObject);
129
608
  });
609
+
610
+ test("Pick - no matching properties returns undefined", () => {
611
+ const baseObject: NodeType = {
612
+ type: "object",
613
+ properties: { foo: { node: { type: "string" }, required: false } },
614
+ additionalProperties: false,
615
+ };
616
+ const result = applyPickOrOmitToNodeType(
617
+ baseObject,
618
+ "Pick",
619
+ new Set(["bar"]),
620
+ );
621
+ expect(result).toBeUndefined();
622
+ });
623
+
624
+ test("Omit - all properties with additionalProperties false returns undefined", () => {
625
+ const baseObject: NodeType = {
626
+ type: "object",
627
+ properties: { foo: { node: { type: "string" }, required: false } },
628
+ additionalProperties: false,
629
+ };
630
+ const result = applyPickOrOmitToNodeType(
631
+ baseObject,
632
+ "Omit",
633
+ new Set(["foo"]),
634
+ );
635
+ expect(result).toBeUndefined();
636
+ });
637
+
638
+ test("and type - applies to each member and returns and", () => {
639
+ const baseObject: NodeType = {
640
+ type: "and",
641
+ and: [
642
+ {
643
+ type: "object",
644
+ properties: {
645
+ a: { node: { type: "string" }, required: false },
646
+ b: { node: { type: "string" }, required: false },
647
+ },
648
+ additionalProperties: false,
649
+ },
650
+ {
651
+ type: "object",
652
+ properties: {
653
+ b: { node: { type: "string" }, required: false },
654
+ c: { node: { type: "string" }, required: false },
655
+ },
656
+ additionalProperties: false,
657
+ },
658
+ ],
659
+ };
660
+ const result = applyPickOrOmitToNodeType(
661
+ baseObject,
662
+ "Pick",
663
+ new Set(["b"]),
664
+ );
665
+ expect(result).toMatchObject({ type: "and" });
666
+ const and = (result as { and: NodeType[] }).and;
667
+ expect(and).toHaveLength(2);
668
+ expect(and[0]).toMatchObject({
669
+ type: "object",
670
+ properties: { b: { node: { type: "string" }, required: false } },
671
+ });
672
+ expect(and[1]).toMatchObject({
673
+ type: "object",
674
+ properties: { b: { node: { type: "string" }, required: false } },
675
+ });
676
+ });
677
+
678
+ test("or type - applies to each member and returns or", () => {
679
+ const baseObject: NodeType = {
680
+ type: "or",
681
+ or: [
682
+ {
683
+ type: "object",
684
+ properties: {
685
+ x: { node: { type: "string" }, required: false },
686
+ y: { node: { type: "string" }, required: false },
687
+ },
688
+ additionalProperties: false,
689
+ },
690
+ ],
691
+ };
692
+ const result = applyPickOrOmitToNodeType(
693
+ baseObject,
694
+ "Pick",
695
+ new Set(["x"]),
696
+ );
697
+ expect(result).toMatchObject({
698
+ type: "object",
699
+ properties: { x: { node: { type: "string" }, required: false } },
700
+ });
701
+ });
702
+
703
+ test("or type with multiple members returns single or", () => {
704
+ const baseObject: NodeType = {
705
+ type: "or",
706
+ or: [
707
+ {
708
+ type: "object",
709
+ properties: { a: { node: { type: "string" }, required: false } },
710
+ additionalProperties: false,
711
+ },
712
+ {
713
+ type: "object",
714
+ properties: { b: { node: { type: "string" }, required: false } },
715
+ additionalProperties: false,
716
+ },
717
+ ],
718
+ };
719
+ const result = applyPickOrOmitToNodeType(
720
+ baseObject,
721
+ "Pick",
722
+ new Set(["a", "b"]),
723
+ );
724
+ expect(result).toMatchObject({ type: "or", or: expect.any(Array) });
725
+ expect((result as { or: NodeType[] }).or.length).toBe(2);
726
+ });
727
+
728
+ test("throws when applying Pick/Omit to non-object non-union non-intersection", () => {
729
+ const baseObject: NodeType = { type: "string" };
730
+ expect(() =>
731
+ applyPickOrOmitToNodeType(baseObject, "Pick", new Set(["x"])),
732
+ ).toThrow(/Can not apply Pick to type string/);
733
+ });
130
734
  });
131
735
 
132
736
  describe("applyPartialOrRequiredToNodeType", () => {
@@ -177,4 +781,97 @@ describe("applyPartialOrRequiredToNodeType", () => {
177
781
 
178
782
  expect(result).toStrictEqual(modifiedObject);
179
783
  });
784
+
785
+ test("and type - applies modifier to each member", () => {
786
+ const baseObject: NodeType = {
787
+ type: "and",
788
+ and: [
789
+ {
790
+ type: "object",
791
+ properties: { a: { node: { type: "string" }, required: false } },
792
+ additionalProperties: false,
793
+ },
794
+ ],
795
+ };
796
+ const result = applyPartialOrRequiredToNodeType(baseObject, true);
797
+ expect(result).toMatchObject({ type: "and" });
798
+ expect((result as { and: NodeType[] }).and[0]).toMatchObject({
799
+ type: "object",
800
+ properties: { a: { required: true, node: { type: "string" } } },
801
+ });
802
+ });
803
+
804
+ test("or type - applies modifier to each member", () => {
805
+ const baseObject: NodeType = {
806
+ type: "or",
807
+ or: [
808
+ {
809
+ type: "object",
810
+ properties: { a: { node: { type: "string" }, required: true } },
811
+ additionalProperties: false,
812
+ },
813
+ ],
814
+ };
815
+ const result = applyPartialOrRequiredToNodeType(baseObject, false);
816
+ expect(result).toMatchObject({ type: "or" });
817
+ expect((result as { or: NodeType[] }).or[0]).toMatchObject({
818
+ type: "object",
819
+ properties: { a: { required: false, node: { type: "string" } } },
820
+ });
821
+ });
822
+
823
+ test("throws when applying Partial/Required to non-object non-union non-intersection", () => {
824
+ const baseObject: NodeType = { type: "number" };
825
+ expect(() => applyPartialOrRequiredToNodeType(baseObject, false)).toThrow(
826
+ /Can not apply Partial to type number/,
827
+ );
828
+ });
829
+ });
830
+
831
+ describe("applyExcludeToNodeType", () => {
832
+ test("excludes single type from union", () => {
833
+ const baseObject: OrType = {
834
+ type: "or",
835
+ or: [{ type: "string" }, { type: "number" }, { type: "boolean" }],
836
+ };
837
+ const result = applyExcludeToNodeType(baseObject, { type: "number" });
838
+ expect(result).toMatchObject({
839
+ type: "or",
840
+ or: [{ type: "string" }, { type: "boolean" }],
841
+ });
842
+ });
843
+
844
+ test("excludes with filter union (or)", () => {
845
+ const baseObject: OrType = {
846
+ type: "or",
847
+ or: [{ type: "string" }, { type: "number" }, { type: "boolean" }],
848
+ };
849
+ const filters: OrType = {
850
+ type: "or",
851
+ or: [{ type: "number" }, { type: "boolean" }],
852
+ };
853
+ const result = applyExcludeToNodeType(baseObject, filters);
854
+ expect(result).toEqual({ type: "string" });
855
+ });
856
+
857
+ test("single remaining member returns that member", () => {
858
+ const baseObject: OrType = {
859
+ type: "or",
860
+ or: [{ type: "string" }, { type: "number" }],
861
+ };
862
+ const result = applyExcludeToNodeType(baseObject, { type: "number" });
863
+ expect(result).toEqual({ type: "string" });
864
+ });
865
+
866
+ test("multiple remaining members returns or", () => {
867
+ const baseObject: OrType = {
868
+ type: "or",
869
+ or: [{ type: "string" }, { type: "number" }, { type: "boolean" }],
870
+ };
871
+ const result = applyExcludeToNodeType(baseObject, { type: "number" });
872
+ expect(result).toMatchObject({
873
+ type: "or",
874
+ or: [{ type: "string" }, { type: "boolean" }],
875
+ });
876
+ });
180
877
  });