@player-tools/fluent 0.12.1--canary.241.6077

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 (134) hide show
  1. package/dist/cjs/index.cjs +2396 -0
  2. package/dist/cjs/index.cjs.map +1 -0
  3. package/dist/index.legacy-esm.js +2276 -0
  4. package/dist/index.mjs +2276 -0
  5. package/dist/index.mjs.map +1 -0
  6. package/package.json +38 -0
  7. package/src/core/base-builder/__tests__/fluent-builder-base.test.ts +2423 -0
  8. package/src/core/base-builder/__tests__/fluent-partial.test.ts +179 -0
  9. package/src/core/base-builder/__tests__/id-generator.test.ts +658 -0
  10. package/src/core/base-builder/__tests__/registry.test.ts +534 -0
  11. package/src/core/base-builder/__tests__/resolution-mixed-arrays.test.ts +319 -0
  12. package/src/core/base-builder/__tests__/resolution-pipeline.test.ts +416 -0
  13. package/src/core/base-builder/__tests__/resolution-switches.test.ts +468 -0
  14. package/src/core/base-builder/__tests__/resolution-templates.test.ts +255 -0
  15. package/src/core/base-builder/__tests__/switch.test.ts +815 -0
  16. package/src/core/base-builder/__tests__/template.test.ts +596 -0
  17. package/src/core/base-builder/__tests__/value-extraction.test.ts +200 -0
  18. package/src/core/base-builder/__tests__/value-storage.test.ts +459 -0
  19. package/src/core/base-builder/conditional/index.ts +64 -0
  20. package/src/core/base-builder/context.ts +152 -0
  21. package/src/core/base-builder/errors.ts +69 -0
  22. package/src/core/base-builder/fluent-builder-base.ts +308 -0
  23. package/src/core/base-builder/guards.ts +137 -0
  24. package/src/core/base-builder/id/generator.ts +290 -0
  25. package/src/core/base-builder/id/registry.ts +152 -0
  26. package/src/core/base-builder/index.ts +72 -0
  27. package/src/core/base-builder/resolution/path-resolver.ts +116 -0
  28. package/src/core/base-builder/resolution/pipeline.ts +103 -0
  29. package/src/core/base-builder/resolution/steps/__tests__/nested-asset-wrappers.test.ts +206 -0
  30. package/src/core/base-builder/resolution/steps/asset-id.ts +77 -0
  31. package/src/core/base-builder/resolution/steps/asset-wrappers.ts +64 -0
  32. package/src/core/base-builder/resolution/steps/builders.ts +84 -0
  33. package/src/core/base-builder/resolution/steps/mixed-arrays.ts +95 -0
  34. package/src/core/base-builder/resolution/steps/nested-asset-wrappers.ts +124 -0
  35. package/src/core/base-builder/resolution/steps/static-values.ts +35 -0
  36. package/src/core/base-builder/resolution/steps/switches.ts +71 -0
  37. package/src/core/base-builder/resolution/steps/templates.ts +40 -0
  38. package/src/core/base-builder/resolution/value-resolver.ts +333 -0
  39. package/src/core/base-builder/storage/auxiliary-storage.ts +82 -0
  40. package/src/core/base-builder/storage/value-storage.ts +282 -0
  41. package/src/core/base-builder/types.ts +266 -0
  42. package/src/core/base-builder/utils.ts +10 -0
  43. package/src/core/flow/__tests__/index.test.ts +292 -0
  44. package/src/core/flow/index.ts +118 -0
  45. package/src/core/index.ts +8 -0
  46. package/src/core/mocks/generated/action.builder.ts +92 -0
  47. package/src/core/mocks/generated/choice-item.builder.ts +120 -0
  48. package/src/core/mocks/generated/choice.builder.ts +134 -0
  49. package/src/core/mocks/generated/collection.builder.ts +93 -0
  50. package/src/core/mocks/generated/field-collection.builder.ts +86 -0
  51. package/src/core/mocks/generated/index.ts +10 -0
  52. package/src/core/mocks/generated/info.builder.ts +64 -0
  53. package/src/core/mocks/generated/input.builder.ts +63 -0
  54. package/src/core/mocks/generated/overview-collection.builder.ts +65 -0
  55. package/src/core/mocks/generated/splash-collection.builder.ts +93 -0
  56. package/src/core/mocks/generated/text.builder.ts +47 -0
  57. package/src/core/mocks/index.ts +1 -0
  58. package/src/core/mocks/types/action.ts +92 -0
  59. package/src/core/mocks/types/choice.ts +129 -0
  60. package/src/core/mocks/types/collection.ts +140 -0
  61. package/src/core/mocks/types/info.ts +7 -0
  62. package/src/core/mocks/types/input.ts +7 -0
  63. package/src/core/mocks/types/text.ts +5 -0
  64. package/src/core/schema/__tests__/index.test.ts +127 -0
  65. package/src/core/schema/index.ts +195 -0
  66. package/src/core/schema/types.ts +7 -0
  67. package/src/core/switch/__tests__/index.test.ts +156 -0
  68. package/src/core/switch/index.ts +81 -0
  69. package/src/core/tagged-template/README.md +448 -0
  70. package/src/core/tagged-template/__tests__/extract-bindings-from-schema.test.ts +207 -0
  71. package/src/core/tagged-template/__tests__/index.test.ts +190 -0
  72. package/src/core/tagged-template/__tests__/schema-std-integration.test.ts +580 -0
  73. package/src/core/tagged-template/binding.ts +95 -0
  74. package/src/core/tagged-template/expression.ts +92 -0
  75. package/src/core/tagged-template/extract-bindings-from-schema.ts +120 -0
  76. package/src/core/tagged-template/index.ts +5 -0
  77. package/src/core/tagged-template/std.ts +472 -0
  78. package/src/core/tagged-template/types.ts +123 -0
  79. package/src/core/template/__tests__/index.test.ts +380 -0
  80. package/src/core/template/index.ts +196 -0
  81. package/src/core/utils/index.ts +160 -0
  82. package/src/fp/README.md +411 -0
  83. package/src/fp/__tests__/index.test.ts +1178 -0
  84. package/src/fp/index.ts +386 -0
  85. package/src/gen/common.ts +15 -0
  86. package/src/index.ts +5 -0
  87. package/src/types.ts +203 -0
  88. package/types/core/base-builder/conditional/index.d.ts +21 -0
  89. package/types/core/base-builder/context.d.ts +39 -0
  90. package/types/core/base-builder/errors.d.ts +45 -0
  91. package/types/core/base-builder/fluent-builder-base.d.ts +147 -0
  92. package/types/core/base-builder/guards.d.ts +58 -0
  93. package/types/core/base-builder/id/generator.d.ts +69 -0
  94. package/types/core/base-builder/id/registry.d.ts +93 -0
  95. package/types/core/base-builder/index.d.ts +9 -0
  96. package/types/core/base-builder/resolution/path-resolver.d.ts +15 -0
  97. package/types/core/base-builder/resolution/pipeline.d.ts +27 -0
  98. package/types/core/base-builder/resolution/steps/asset-id.d.ts +14 -0
  99. package/types/core/base-builder/resolution/steps/asset-wrappers.d.ts +14 -0
  100. package/types/core/base-builder/resolution/steps/builders.d.ts +14 -0
  101. package/types/core/base-builder/resolution/steps/mixed-arrays.d.ts +14 -0
  102. package/types/core/base-builder/resolution/steps/nested-asset-wrappers.d.ts +14 -0
  103. package/types/core/base-builder/resolution/steps/static-values.d.ts +14 -0
  104. package/types/core/base-builder/resolution/steps/switches.d.ts +15 -0
  105. package/types/core/base-builder/resolution/steps/templates.d.ts +14 -0
  106. package/types/core/base-builder/resolution/value-resolver.d.ts +62 -0
  107. package/types/core/base-builder/storage/auxiliary-storage.d.ts +50 -0
  108. package/types/core/base-builder/storage/value-storage.d.ts +82 -0
  109. package/types/core/base-builder/types.d.ts +183 -0
  110. package/types/core/base-builder/utils.d.ts +2 -0
  111. package/types/core/flow/index.d.ts +23 -0
  112. package/types/core/index.d.ts +8 -0
  113. package/types/core/mocks/index.d.ts +2 -0
  114. package/types/core/mocks/types/action.d.ts +58 -0
  115. package/types/core/mocks/types/choice.d.ts +95 -0
  116. package/types/core/mocks/types/collection.d.ts +102 -0
  117. package/types/core/mocks/types/info.d.ts +7 -0
  118. package/types/core/mocks/types/input.d.ts +7 -0
  119. package/types/core/mocks/types/text.d.ts +5 -0
  120. package/types/core/schema/index.d.ts +34 -0
  121. package/types/core/schema/types.d.ts +5 -0
  122. package/types/core/switch/index.d.ts +21 -0
  123. package/types/core/tagged-template/binding.d.ts +19 -0
  124. package/types/core/tagged-template/expression.d.ts +11 -0
  125. package/types/core/tagged-template/extract-bindings-from-schema.d.ts +7 -0
  126. package/types/core/tagged-template/index.d.ts +6 -0
  127. package/types/core/tagged-template/std.d.ts +174 -0
  128. package/types/core/tagged-template/types.d.ts +69 -0
  129. package/types/core/template/index.d.ts +97 -0
  130. package/types/core/utils/index.d.ts +47 -0
  131. package/types/fp/index.d.ts +149 -0
  132. package/types/gen/common.d.ts +6 -0
  133. package/types/index.d.ts +3 -0
  134. package/types/types.d.ts +163 -0
@@ -0,0 +1,448 @@
1
+ # Tagged Template System
2
+
3
+ A comprehensive type-safe template system for creating dynamic expressions and bindings with full TypeScript support through phantom types and schema integration.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Overview](#overview)
8
+ - [Core Concepts](#core-concepts)
9
+ - [Type Safety & Schema Integration](#type-safety--schema-integration)
10
+ - [API Reference](#api-reference)
11
+ - [Usage Examples](#usage-examples)
12
+ - [Advanced Features](#advanced-features)
13
+ - [Best Practices](#best-practices)
14
+
15
+ ## Overview
16
+
17
+ The tagged-template system provides a type-safe way to create dynamic expressions and data bindings for template rendering. It leverages TypeScript's phantom types to provide compile-time type checking while generating runtime template strings.
18
+
19
+ ### Key Features
20
+
21
+ - **Type-Safe Bindings**: Create data bindings with full TypeScript type checking
22
+ - **Expression Templates**: Build complex logical and arithmetic expressions
23
+ - **Schema Integration**: Automatically extract type-safe bindings from schema definitions
24
+ - **Standard Library**: Rich set of pre-built operations (logical, arithmetic, comparison)
25
+ - **Phantom Types**: Compile-time type information without runtime overhead
26
+
27
+ ## Core Concepts
28
+
29
+ ### TaggedTemplateValue<T>
30
+
31
+ The foundation of the system is `TaggedTemplateValue<T>`, which uses a phantom type `T` to provide type information:
32
+
33
+ ```typescript
34
+ interface TaggedTemplateValue<T = unknown> {
35
+ [TaggedTemplateValueSymbol]: true;
36
+ /** Phantom type marker - not available at runtime */
37
+ readonly _phantomType?: T;
38
+ toValue(): string;
39
+ toRefString(options?: TemplateRefOptions): string;
40
+ toString(): string;
41
+ }
42
+ ```
43
+
44
+ The phantom type `T` acts like a "fat pointer" - it carries type information about the data that the binding targets, enabling TypeScript to perform compile-time type checking on standard library functions and user-defined functions that work with `TaggedTemplateValue<T>`.
45
+
46
+ ### Binding
47
+
48
+ Creates data binding expressions that reference data paths:
49
+
50
+ ```typescript
51
+ import { binding } from "./binding";
52
+
53
+ // Basic binding (backward compatible)
54
+ const userAge = binding`user.age`; // TaggedTemplateValue<unknown>
55
+
56
+ // Type-safe binding with phantom type
57
+ const typedAge = binding<number>`user.age`; // TaggedTemplateValue<number>
58
+
59
+ console.log(userAge.toString()); // "{{user.age}}"
60
+ ```
61
+
62
+ ### Expression
63
+
64
+ Creates executable expressions with syntax validation:
65
+
66
+ ```typescript
67
+ import { expression } from "./expression";
68
+
69
+ // Basic expression
70
+ const calc = expression`user.age + 5`;
71
+
72
+ // Typed expression
73
+ const addNumbers = (
74
+ a: TaggedTemplateValue<number>,
75
+ b: TaggedTemplateValue<number>,
76
+ ) => expression`{{${a}}} + {{${b}}}`;
77
+
78
+ // If we try passing a binding that doens't target a number we get a type error
79
+ ```
80
+
81
+ ### Schema Integration
82
+
83
+ Extract type-safe bindings directly from schema definitions:
84
+
85
+ ```typescript
86
+ import { extractBindingsFromSchema } from "./extract-bindings-from-schema";
87
+
88
+ const schema = {
89
+ ROOT: {
90
+ user: { type: "UserType" },
91
+ count: { type: "NumberType" },
92
+ },
93
+ UserType: {
94
+ name: { type: "StringType" },
95
+ age: { type: "NumberType" },
96
+ preferences: { type: "PreferenceType" },
97
+ },
98
+ PreferenceType: {
99
+ theme: { type: "StringType" },
100
+ notifications: { type: "BooleanType" },
101
+ },
102
+ } as const satisfies Schema.Schema; // <- this is necessary to force Typescript to evaluate keys as string literal
103
+
104
+ const bindings = extractBindingsFromSchema(schema);
105
+
106
+ // Type-safe access with full IntelliSense
107
+ bindings.user.name; // TaggedTemplateValue<string>
108
+ bindings.user.age; // TaggedTemplateValue<number>
109
+ bindings.user.preferences.theme; // TaggedTemplateValue<string>
110
+ bindings.count; // TaggedTemplateValue<number>
111
+ ```
112
+
113
+ ## Type Safety & Schema Integration
114
+
115
+ ### The `as const satisfies Schema.Schema` Requirement
116
+
117
+ For TypeScript to correctly infer types, schemas must be declared with this specific pattern:
118
+
119
+ ```typescript
120
+ // ✅ CORRECT - Enables full type inference
121
+ const schema = {
122
+ ROOT: {
123
+ user: { type: "UserType" },
124
+ },
125
+ UserType: {
126
+ name: { type: "StringType" },
127
+ },
128
+ } as const satisfies Schema.Schema;
129
+
130
+ // ❌ INCORRECT - Type inference will be limited
131
+ const schema: Schema.Schema = {
132
+ ROOT: {
133
+ user: { type: "UserType" },
134
+ },
135
+ UserType: {
136
+ name: { type: "StringType" },
137
+ },
138
+ };
139
+ ```
140
+
141
+ The `as const` assertion preserves literal types, while `satisfies Schema.Schema` ensures the structure is valid. This combination allows TypeScript to:
142
+
143
+ 1. Infer exact property names and types
144
+ 2. Maintain type relationships between schema references
145
+ 3. Generate precise `TaggedTemplateValue<T>` types
146
+ 4. Enable full IntelliSense support
147
+
148
+ ### Phantom Types and Type Checking
149
+
150
+ Phantom types enable the standard library functions to provide type-safe operations:
151
+
152
+ ```typescript
153
+ import { add, equal, greaterThan } from "./std";
154
+
155
+ const userAge = binding<number>`user.age`;
156
+ const minimumAge = binding<number>`settings.minimumAge`;
157
+ const userName = binding<string>`user.name`;
158
+
159
+ // ✅ Type-safe arithmetic (number + number)
160
+ const ageSum = add(userAge, minimumAge);
161
+
162
+ // ✅ Type-safe comparison (number > number)
163
+ const isOldEnough = greaterThan(userAge, minimumAge);
164
+
165
+ // ✅ Type-safe equality (string === string)
166
+ const isAdmin = equal(userName, "admin");
167
+
168
+ // ❌ TypeScript error - can't add string to number
169
+ const invalid = add(userName, userAge); // Type error!
170
+ ```
171
+
172
+ ### Array Handling
173
+
174
+ Arrays in schemas are handled with special binding structures:
175
+
176
+ ```typescript
177
+ const schema = {
178
+ ROOT: {
179
+ tags: { type: "StringType", isArray: true },
180
+ scores: { type: "NumberType", isArray: true },
181
+ users: { type: "UserType", isArray: true },
182
+ },
183
+ UserType: {
184
+ name: { type: "StringType" },
185
+ },
186
+ } as const satisfies Schema.Schema;
187
+
188
+ const bindings = extractBindingsFromSchema(schema);
189
+
190
+ // String arrays have 'name' property
191
+ bindings.tags.name; // TaggedTemplateValue<string> -> "{{tags._current_}}"
192
+
193
+ // Number/Boolean arrays have 'value' property
194
+ bindings.scores.value; // TaggedTemplateValue<number> -> "{{scores._current_}}"
195
+
196
+ // Complex type arrays expose nested structure
197
+ bindings.users.name; // TaggedTemplateValue<string> -> "{{users._current_.name}}"
198
+ ```
199
+
200
+ ## API Reference
201
+
202
+ ### `binding<T>(template): TaggedTemplateValue<T>`
203
+
204
+ Creates a data binding with optional type annotation.
205
+
206
+ ```typescript
207
+ binding`data.path`; // TaggedTemplateValue<unknown>
208
+ binding<string>`user.name`; // TaggedTemplateValue<string>
209
+ binding<number>`user.age`; // TaggedTemplateValue<number>
210
+ ```
211
+
212
+ ### `expression<T>(template): TaggedTemplateValue<T>`
213
+
214
+ Creates an executable expression with syntax validation.
215
+
216
+ ```typescript
217
+ expression`user.age + 1`;
218
+ expression`user.name === "admin"`;
219
+ expression`condition ? value1 : value2`;
220
+ ```
221
+
222
+ ### `extractBindingsFromSchema<S>(schema): ExtractedBindings<S>`
223
+
224
+ Extracts type-safe bindings from a schema definition.
225
+
226
+ ```typescript
227
+ const bindings = extractBindingsFromSchema(schema);
228
+ // Returns fully typed binding structure matching schema
229
+ ```
230
+
231
+ ### Standard Library Functions
232
+
233
+ #### Logical Operations
234
+
235
+ - `and(...args)` - Logical AND (&&)
236
+ - `or(...args)` - Logical OR (||)
237
+ - `not(value)` - Logical NOT (!)
238
+ - `xor(left, right)` - Exclusive OR
239
+ - `nand(...args)` - NOT AND
240
+ - `nor(...args)` - NOT OR
241
+
242
+ #### Comparison Operations
243
+
244
+ - `equal(left, right)` - Loose equality (==)
245
+ - `strictEqual(left, right)` - Strict equality (===)
246
+ - `notEqual(left, right)` - Inequality (!=)
247
+ - `strictNotEqual(left, right)` - Strict inequality (!==)
248
+ - `greaterThan(left, right)` - Greater than (>)
249
+ - `greaterThanOrEqual(left, right)` - Greater than or equal (>=)
250
+ - `lessThan(left, right)` - Less than (<)
251
+ - `lessThanOrEqual(left, right)` - Less than or equal (<=)
252
+
253
+ #### Arithmetic Operations
254
+
255
+ - `add(...args)` - Addition (+)
256
+ - `subtract(left, right)` - Subtraction (-)
257
+ - `multiply(...args)` - Multiplication (\*)
258
+ - `divide(left, right)` - Division (/)
259
+ - `modulo(left, right)` - Modulo (%)
260
+
261
+ #### Control Flow
262
+
263
+ - `conditional(condition, ifTrue, ifFalse)` - Ternary operator (?:)
264
+ - `call(functionName, ...args)` - Function call
265
+ - `literal(value)` - Literal value
266
+
267
+ ## Usage Examples
268
+
269
+ ### Basic Binding and Expression Usage
270
+
271
+ ```typescript
272
+ import { binding, expression } from "./index";
273
+
274
+ // Simple data binding
275
+ const userName = binding<string>`user.name`;
276
+ const userAge = binding<number>`user.age`;
277
+
278
+ // Expression with bindings
279
+ const greeting = expression`"Hello, " + ${userName}`;
280
+ const isAdult = expression`${userAge} >= 18`;
281
+
282
+ console.log(userName.toString()); // "{{user.name}}"
283
+ console.log(greeting.toString()); // "@["Hello, " + {{user.name}}]@"
284
+ console.log(isAdult.toString()); // "@[{{user.age}} >= 18]@"
285
+ ```
286
+
287
+ ### Schema-Driven Development
288
+
289
+ ```typescript
290
+ import { extractBindingsFromSchema } from "./extract-bindings-from-schema";
291
+ import { and, greaterThan, equal } from "./std";
292
+
293
+ const userSchema = {
294
+ ROOT: {
295
+ user: { type: "UserType" },
296
+ settings: { type: "SettingsType" },
297
+ },
298
+ UserType: {
299
+ name: { type: "StringType" },
300
+ age: { type: "NumberType" },
301
+ role: { type: "StringType" },
302
+ },
303
+ SettingsType: {
304
+ minAge: { type: "NumberType" },
305
+ adminRole: { type: "StringType" },
306
+ },
307
+ } as const satisfies Schema.Schema;
308
+
309
+ const data = extractBindingsFromSchema(userSchema);
310
+
311
+ // Create complex type-safe expressions
312
+ const isAuthorizedAdmin = and(
313
+ greaterThan(data.user.age, data.settings.minAge),
314
+ equal(data.user.role, data.settings.adminRole),
315
+ );
316
+
317
+ console.log(isAuthorizedAdmin.toString());
318
+ // "{{data.user.age > data.settings.minAge && data.user.role == data.settings.adminRole}}"
319
+ ```
320
+
321
+ ### Working with Arrays
322
+
323
+ ```typescript
324
+ const listSchema = {
325
+ ROOT: {
326
+ items: { type: "ItemType", isArray: true },
327
+ tags: { type: "StringType", isArray: true },
328
+ scores: { type: "NumberType", isArray: true },
329
+ },
330
+ ItemType: {
331
+ name: { type: "StringType" },
332
+ value: { type: "NumberType" },
333
+ },
334
+ } as const satisfies Schema.Schema;
335
+
336
+ const data = extractBindingsFromSchema(listSchema);
337
+
338
+ // Array item bindings
339
+ data.items.name; // "{{items._current_.name}}"
340
+ data.items.value; // "{{items._current_.value}}"
341
+ data.tags.name; // "{{tags._current_}}" (string arrays use .name)
342
+ data.scores.value; // "{{scores._current_}}" (number arrays use .value)
343
+ ```
344
+
345
+ ### Custom Functions with Type Safety
346
+
347
+ ```typescript
348
+ import type { TaggedTemplateValue } from "./types";
349
+ import { expression } from "./expression";
350
+
351
+ // Custom function that works with typed template values
352
+ function formatCurrency<T extends number>(
353
+ amount: TaggedTemplateValue<T> | T,
354
+ currency: string = "USD",
355
+ ): TaggedTemplateValue<string> {
356
+ const amountExpr = isTaggedTemplateValue(amount)
357
+ ? amount.toRefString({ nestedContext: "expression" })
358
+ : String(amount);
359
+
360
+ return expression`"${currency} " + ${amountExpr}.toFixed(2)` as TaggedTemplateValue<string>;
361
+ }
362
+
363
+ // Usage with type safety
364
+ const price = binding<number>`product.price`;
365
+ const formattedPrice = formatCurrency(price, "EUR");
366
+ // Type is TaggedTemplateValue<string>
367
+ ```
368
+
369
+ ## Advanced Features
370
+
371
+ ### Nested Template Composition
372
+
373
+ Templates can be composed and nested while maintaining proper context:
374
+
375
+ ```typescript
376
+ const baseCondition = expression`user.age > 18`;
377
+ const complexCondition = and(
378
+ baseCondition,
379
+ equal(binding<string>`user.status`, "active"),
380
+ );
381
+
382
+ // Proper nesting with context handling
383
+ const finalExpression = expression`${complexCondition} ? "authorized" : "denied"`;
384
+ ```
385
+
386
+ ### Expression Validation
387
+
388
+ Expressions are validated for syntax errors at creation time:
389
+
390
+ ```typescript
391
+ // ✅ Valid expression
392
+ const valid = expression`user.age + (5 * 2)`;
393
+
394
+ // ❌ Throws error - unbalanced parentheses
395
+ const invalid = expression`user.age + (5 * 2`; // Error: Expected ) at character 15
396
+ ```
397
+
398
+ ## Best Practices
399
+
400
+ ### 1. Always Use Schema Types
401
+
402
+ ```typescript
403
+ // ✅ GOOD - Schema enables full type safety
404
+ const schema = {
405
+ ROOT: { user: { type: "UserType" } },
406
+ UserType: { name: { type: "StringType" } },
407
+ } as const satisfies Schema.Schema;
408
+
409
+ const bindings = extractBindingsFromSchema(schema);
410
+ const userName = bindings.user.name; // TaggedTemplateValue<string>
411
+
412
+ // ❌ AVOID - Manual binding loses type information
413
+ const userName = binding`user.name`; // TaggedTemplateValue<unknown>
414
+ ```
415
+
416
+ ### 2. Leverage Standard Library
417
+
418
+ ```typescript
419
+ // ✅ GOOD - Use type-safe standard library functions
420
+ import { and, greaterThan, equal } from "./std";
421
+
422
+ const condition = and(
423
+ greaterThan(data.user.age, 18),
424
+ equal(data.user.status, "active"),
425
+ );
426
+
427
+ // ❌ AVOID - Manual expression construction
428
+ const condition = expression`${data.user.age} > 18 && ${data.user.status} == "active"`;
429
+ ```
430
+
431
+ ### 3. Type Your Custom Functions
432
+
433
+ ```typescript
434
+ // ✅ GOOD - Properly typed custom function
435
+ function isInRange<T extends number>(
436
+ value: TaggedTemplateValue<T> | T,
437
+ min: number,
438
+ max: number,
439
+ ): TaggedTemplateValue<boolean> {
440
+ return and(greaterThanOrEqual(value, min), lessThanOrEqual(value, max));
441
+ }
442
+
443
+ // Usage maintains type safety
444
+ const userAge = binding<number>`user.age`;
445
+ const isValidAge = isInRange(userAge, 13, 120); // TaggedTemplateValue<boolean>
446
+ ```
447
+
448
+ This system provides a powerful foundation for creating type-safe, dynamic templates while maintaining excellent developer experience through TypeScript integration and comprehensive tooling support.
@@ -0,0 +1,207 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import type { Schema } from "@player-ui/types";
3
+ import { extractBindingsFromSchema } from "../extract-bindings-from-schema";
4
+ import { binding } from "../binding";
5
+
6
+ describe("extractBindingsFromSchema", () => {
7
+ describe("basic schema structure reconstruction", () => {
8
+ test("handles simple type references", () => {
9
+ const schema = {
10
+ ROOT: {
11
+ foo: { type: "fooType" },
12
+ },
13
+ fooType: {
14
+ bar: { type: "StringType" },
15
+ },
16
+ } as const satisfies Schema.Schema;
17
+
18
+ const expected = {
19
+ foo: {
20
+ bar: binding<string>`foo.bar`,
21
+ },
22
+ };
23
+
24
+ const bindings = extractBindingsFromSchema(schema);
25
+
26
+ expect(bindings.foo.bar.toString()).toBe(expected.foo.bar.toString());
27
+ });
28
+
29
+ test("reconstructs complex nested schema structure", () => {
30
+ const schema = {
31
+ ROOT: {
32
+ foo: { type: "fooType" },
33
+ other: { type: "otherType", isArray: true },
34
+ },
35
+ fooType: {
36
+ bar: { type: "barType" },
37
+ },
38
+ barType: {
39
+ baz: { type: "StringType" },
40
+ },
41
+ otherType: {
42
+ item1: { type: "StringType" },
43
+ },
44
+ } as const satisfies Schema.Schema;
45
+
46
+ const expected = {
47
+ foo: {
48
+ bar: {
49
+ baz: binding<string>`foo.bar.baz`,
50
+ },
51
+ },
52
+ other: {
53
+ item1: binding<string>`other._current_.item1`,
54
+ },
55
+ };
56
+
57
+ const bindings = extractBindingsFromSchema(schema);
58
+
59
+ expect(bindings.foo.bar.baz.toString()).toBe(
60
+ expected.foo.bar.baz.toString(),
61
+ );
62
+ expect(bindings.other.item1.toString()).toBe(
63
+ expected.other.item1.toString(),
64
+ );
65
+ });
66
+
67
+ test("handles primitive types directly in ROOT", () => {
68
+ const schema = {
69
+ ROOT: {
70
+ name: { type: "StringType" },
71
+ age: { type: "NumberType" },
72
+ active: { type: "BooleanType" },
73
+ },
74
+ } as const satisfies Schema.Schema;
75
+
76
+ const expected = {
77
+ name: binding<string>`name`,
78
+ age: binding<number>`age`,
79
+ active: binding<boolean>`active`,
80
+ };
81
+
82
+ const bindings = extractBindingsFromSchema(schema);
83
+
84
+ expect(bindings.name.toString()).toBe(expected.name.toString());
85
+ expect(bindings.age.toString()).toBe(expected.age.toString());
86
+ expect(bindings.active.toString()).toBe(expected.active.toString());
87
+ });
88
+ });
89
+
90
+ describe("array handling with _current_ accessor", () => {
91
+ test("converts array access to _current_ path", () => {
92
+ const schema = {
93
+ ROOT: {
94
+ items: { type: "itemType", isArray: true },
95
+ },
96
+ itemType: {
97
+ name: { type: "StringType" },
98
+ value: { type: "NumberType" },
99
+ },
100
+ } as const satisfies Schema.Schema;
101
+
102
+ const expected = {
103
+ items: {
104
+ name: binding<string>`items._current_.name`,
105
+ value: binding<number>`items._current_.value`,
106
+ },
107
+ };
108
+
109
+ const bindings = extractBindingsFromSchema(schema);
110
+
111
+ expect(bindings.items.name.toString()).toBe(
112
+ expected.items.name.toString(),
113
+ );
114
+ expect(bindings.items.value.toString()).toBe(
115
+ expected.items.value.toString(),
116
+ );
117
+ });
118
+
119
+ test("handles arrays of primitive types", () => {
120
+ const schema = {
121
+ ROOT: {
122
+ tags: { type: "StringType", isArray: true },
123
+ numbers: { type: "NumberType", isArray: true },
124
+ },
125
+ } as const satisfies Schema.Schema;
126
+
127
+ const expected = {
128
+ tags: {
129
+ name: binding<string>`tags._current_`,
130
+ },
131
+ numbers: {
132
+ value: binding<number>`numbers._current_`,
133
+ },
134
+ };
135
+
136
+ const bindings = extractBindingsFromSchema(schema);
137
+
138
+ expect(bindings.tags.name.toString()).toBe(expected.tags.name.toString());
139
+ expect(bindings.numbers.value.toString()).toBe(
140
+ expected.numbers.value.toString(),
141
+ );
142
+ });
143
+
144
+ test("handles nested arrays", () => {
145
+ const schema = {
146
+ ROOT: {
147
+ groups: { type: "groupType", isArray: true },
148
+ },
149
+ groupType: {
150
+ name: { type: "StringType" },
151
+ items: { type: "itemType", isArray: true },
152
+ },
153
+ itemType: {
154
+ value: { type: "StringType" },
155
+ },
156
+ } as const satisfies Schema.Schema;
157
+
158
+ const expected = {
159
+ groups: {
160
+ name: binding<string>`groups._current_.name`,
161
+ items: {
162
+ value: binding<string>`groups._current_.items._current_.value`,
163
+ },
164
+ },
165
+ };
166
+
167
+ const bindings = extractBindingsFromSchema(schema);
168
+
169
+ expect(bindings.groups.name.toString()).toBe(
170
+ expected.groups.name.toString(),
171
+ );
172
+ expect(bindings.groups.items.value.toString()).toBe(
173
+ expected.groups.items.value.toString(),
174
+ );
175
+ });
176
+ });
177
+
178
+ describe("record types", () => {
179
+ test("handles record types", () => {
180
+ const schema = {
181
+ ROOT: {
182
+ metadata: { type: "metadataType", isRecord: true },
183
+ },
184
+ metadataType: {
185
+ key1: { type: "StringType" },
186
+ key2: { type: "NumberType" },
187
+ },
188
+ } as const satisfies Schema.Schema;
189
+
190
+ const expected = {
191
+ metadata: {
192
+ key1: binding<string>`metadata.key1`,
193
+ key2: binding<number>`metadata.key2`,
194
+ },
195
+ };
196
+
197
+ const bindings = extractBindingsFromSchema(schema);
198
+
199
+ expect(bindings.metadata.key1.toString()).toBe(
200
+ expected.metadata.key1.toString(),
201
+ );
202
+ expect(bindings.metadata.key2.toString()).toBe(
203
+ expected.metadata.key2.toString(),
204
+ );
205
+ });
206
+ });
207
+ });