@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,580 @@
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 {
5
+ and,
6
+ or,
7
+ not,
8
+ equal,
9
+ notEqual,
10
+ greaterThan,
11
+ lessThan,
12
+ greaterThanOrEqual,
13
+ add,
14
+ subtract,
15
+ multiply,
16
+ divide,
17
+ conditional,
18
+ literal,
19
+ call,
20
+ xor,
21
+ modulo,
22
+ } from "../std";
23
+
24
+ describe("extractBindingsFromSchema - Refactored", () => {
25
+ describe("unit tests", () => {
26
+ test("handles simple primitive types in ROOT", () => {
27
+ const schema = {
28
+ ROOT: {
29
+ name: { type: "StringType" },
30
+ age: { type: "NumberType" },
31
+ isActive: { type: "BooleanType" },
32
+ },
33
+ } as const satisfies Schema.Schema;
34
+
35
+ const bindings = extractBindingsFromSchema(schema);
36
+
37
+ expect(bindings.name.toString()).toBe("{{name}}");
38
+ expect(bindings.age.toString()).toBe("{{age}}");
39
+ expect(bindings.isActive.toString()).toBe("{{isActive}}");
40
+ });
41
+
42
+ test("handles nested object references", () => {
43
+ const schema = {
44
+ ROOT: {
45
+ user: { type: "UserType" },
46
+ },
47
+ UserType: {
48
+ firstName: { type: "StringType" },
49
+ lastName: { type: "StringType" },
50
+ profile: { type: "ProfileType" },
51
+ },
52
+ ProfileType: {
53
+ bio: { type: "StringType" },
54
+ age: { type: "NumberType" },
55
+ },
56
+ } as const satisfies Schema.Schema;
57
+
58
+ const bindings = extractBindingsFromSchema(schema);
59
+
60
+ expect(bindings.user.firstName.toString()).toBe("{{user.firstName}}");
61
+ expect(bindings.user.lastName.toString()).toBe("{{user.lastName}}");
62
+ expect(bindings.user.profile.bio.toString()).toBe("{{user.profile.bio}}");
63
+ expect(bindings.user.profile.age.toString()).toBe("{{user.profile.age}}");
64
+ });
65
+
66
+ test("handles arrays of primitives with correct structure", () => {
67
+ const schema = {
68
+ ROOT: {
69
+ tags: { type: "StringType", isArray: true },
70
+ scores: { type: "NumberType", isArray: true },
71
+ flags: { type: "BooleanType", isArray: true },
72
+ },
73
+ } as const satisfies Schema.Schema;
74
+
75
+ const bindings = extractBindingsFromSchema(schema);
76
+
77
+ // String arrays have 'name' property
78
+ expect(bindings.tags.name.toString()).toBe("{{tags._current_}}");
79
+
80
+ // Number and boolean arrays have 'value' property
81
+ expect(bindings.scores.value.toString()).toBe("{{scores._current_}}");
82
+ expect(bindings.flags.value.toString()).toBe("{{flags._current_}}");
83
+ });
84
+
85
+ test("handles arrays of complex types", () => {
86
+ const schema = {
87
+ ROOT: {
88
+ users: { type: "UserType", isArray: true },
89
+ },
90
+ UserType: {
91
+ id: { type: "NumberType" },
92
+ name: { type: "StringType" },
93
+ settings: { type: "SettingsType" },
94
+ },
95
+ SettingsType: {
96
+ theme: { type: "StringType" },
97
+ notifications: { type: "BooleanType" },
98
+ },
99
+ } as const satisfies Schema.Schema;
100
+
101
+ const bindings = extractBindingsFromSchema(schema);
102
+
103
+ expect(bindings.users.id.toString()).toBe("{{users._current_.id}}");
104
+ expect(bindings.users.name.toString()).toBe("{{users._current_.name}}");
105
+ expect(bindings.users.settings.theme.toString()).toBe(
106
+ "{{users._current_.settings.theme}}",
107
+ );
108
+ expect(bindings.users.settings.notifications.toString()).toBe(
109
+ "{{users._current_.settings.notifications}}",
110
+ );
111
+ });
112
+
113
+ test("handles nested arrays", () => {
114
+ const schema = {
115
+ ROOT: {
116
+ matrix: { type: "RowType", isArray: true },
117
+ },
118
+ RowType: {
119
+ cells: { type: "NumberType", isArray: true },
120
+ metadata: { type: "MetadataType" },
121
+ },
122
+ MetadataType: {
123
+ label: { type: "StringType" },
124
+ },
125
+ } as const satisfies Schema.Schema;
126
+
127
+ const bindings = extractBindingsFromSchema(schema);
128
+
129
+ expect(bindings.matrix.cells.value.toString()).toBe(
130
+ "{{matrix._current_.cells._current_}}",
131
+ );
132
+ expect(bindings.matrix.metadata.label.toString()).toBe(
133
+ "{{matrix._current_.metadata.label}}",
134
+ );
135
+ });
136
+
137
+ test("handles record types", () => {
138
+ const schema = {
139
+ ROOT: {
140
+ config: { type: "ConfigType", isRecord: true },
141
+ },
142
+ ConfigType: {
143
+ key1: { type: "StringType" },
144
+ key2: { type: "NumberType" },
145
+ nested: { type: "NestedType" },
146
+ },
147
+ NestedType: {
148
+ value: { type: "BooleanType" },
149
+ },
150
+ } as const satisfies Schema.Schema;
151
+
152
+ const bindings = extractBindingsFromSchema(schema);
153
+
154
+ expect(bindings.config.key1.toString()).toBe("{{config.key1}}");
155
+ expect(bindings.config.key2.toString()).toBe("{{config.key2}}");
156
+ expect(bindings.config.nested.value.toString()).toBe(
157
+ "{{config.nested.value}}",
158
+ );
159
+ });
160
+
161
+ test("handles deeply nested structures", () => {
162
+ const schema = {
163
+ ROOT: {
164
+ level1: { type: "Level1Type" },
165
+ },
166
+ Level1Type: {
167
+ level2: { type: "Level2Type" },
168
+ },
169
+ Level2Type: {
170
+ level3: { type: "Level3Type" },
171
+ },
172
+ Level3Type: {
173
+ level4: { type: "Level4Type" },
174
+ },
175
+ Level4Type: {
176
+ value: { type: "StringType" },
177
+ },
178
+ } as const satisfies Schema.Schema;
179
+
180
+ const bindings = extractBindingsFromSchema(schema);
181
+
182
+ expect(bindings.level1.level2.level3.level4.value.toString()).toBe(
183
+ "{{level1.level2.level3.level4.value}}",
184
+ );
185
+ });
186
+ });
187
+
188
+ describe("end-to-end test with all JSON value variants", () => {
189
+ test("comprehensive schema with all variants + std.ts integration", () => {
190
+ // Create a comprehensive schema with all possible JSON value types
191
+ const comprehensiveSchema = {
192
+ ROOT: {
193
+ // Primitive types
194
+ stringField: { type: "StringType" },
195
+ numberField: { type: "NumberType" },
196
+ booleanField: { type: "BooleanType" },
197
+
198
+ // Arrays of primitives
199
+ stringArray: { type: "StringType", isArray: true },
200
+ numberArray: { type: "NumberType", isArray: true },
201
+ booleanArray: { type: "BooleanType", isArray: true },
202
+
203
+ // Complex object
204
+ user: { type: "UserType" },
205
+
206
+ // Array of complex objects
207
+ transactions: { type: "TransactionType", isArray: true },
208
+
209
+ // Record type (dynamic key-value pairs)
210
+ metadata: { type: "MetadataType", isRecord: true },
211
+
212
+ // Nested structures
213
+ organization: { type: "OrganizationType" },
214
+
215
+ // Mixed array (array of mixed types through references)
216
+ mixedItems: { type: "MixedItemType", isArray: true },
217
+ },
218
+
219
+ UserType: {
220
+ id: { type: "NumberType" },
221
+ username: { type: "StringType" },
222
+ email: { type: "StringType" },
223
+ isVerified: { type: "BooleanType" },
224
+ profile: { type: "ProfileType" },
225
+ preferences: { type: "PreferencesType" },
226
+ roles: { type: "StringType", isArray: true },
227
+ scores: { type: "NumberType", isArray: true },
228
+ },
229
+
230
+ ProfileType: {
231
+ firstName: { type: "StringType" },
232
+ lastName: { type: "StringType" },
233
+ bio: { type: "StringType" },
234
+ birthYear: { type: "NumberType" },
235
+ avatar: { type: "AvatarType" },
236
+ addresses: { type: "AddressType", isArray: true },
237
+ },
238
+
239
+ AvatarType: {
240
+ url: { type: "StringType" },
241
+ size: { type: "NumberType" },
242
+ isPublic: { type: "BooleanType" },
243
+ },
244
+
245
+ AddressType: {
246
+ street: { type: "StringType" },
247
+ city: { type: "StringType" },
248
+ zipCode: { type: "StringType" },
249
+ country: { type: "StringType" },
250
+ isPrimary: { type: "BooleanType" },
251
+ coordinates: { type: "CoordinatesType" },
252
+ },
253
+
254
+ CoordinatesType: {
255
+ latitude: { type: "NumberType" },
256
+ longitude: { type: "NumberType" },
257
+ },
258
+
259
+ PreferencesType: {
260
+ theme: { type: "StringType" },
261
+ language: { type: "StringType" },
262
+ emailNotifications: { type: "BooleanType" },
263
+ pushNotifications: { type: "BooleanType" },
264
+ privacy: { type: "PrivacyType" },
265
+ },
266
+
267
+ PrivacyType: {
268
+ profileVisibility: { type: "StringType" },
269
+ showEmail: { type: "BooleanType" },
270
+ showLocation: { type: "BooleanType" },
271
+ },
272
+
273
+ TransactionType: {
274
+ id: { type: "StringType" },
275
+ amount: { type: "NumberType" },
276
+ currency: { type: "StringType" },
277
+ timestamp: { type: "NumberType" },
278
+ status: { type: "StringType" },
279
+ isCompleted: { type: "BooleanType" },
280
+ merchant: { type: "MerchantType" },
281
+ tags: { type: "StringType", isArray: true },
282
+ },
283
+
284
+ MerchantType: {
285
+ name: { type: "StringType" },
286
+ category: { type: "StringType" },
287
+ rating: { type: "NumberType" },
288
+ isVerified: { type: "BooleanType" },
289
+ },
290
+
291
+ MetadataType: {
292
+ version: { type: "StringType" },
293
+ buildNumber: { type: "NumberType" },
294
+ isProduction: { type: "BooleanType" },
295
+ features: { type: "StringType", isArray: true },
296
+ limits: { type: "LimitsType" },
297
+ },
298
+
299
+ LimitsType: {
300
+ maxUsers: { type: "NumberType" },
301
+ maxStorage: { type: "NumberType" },
302
+ maxTransactions: { type: "NumberType" },
303
+ isUnlimited: { type: "BooleanType" },
304
+ },
305
+
306
+ OrganizationType: {
307
+ name: { type: "StringType" },
308
+ employeeCount: { type: "NumberType" },
309
+ isActive: { type: "BooleanType" },
310
+ departments: { type: "DepartmentType", isArray: true },
311
+ headquarters: { type: "AddressType" },
312
+ },
313
+
314
+ DepartmentType: {
315
+ name: { type: "StringType" },
316
+ budget: { type: "NumberType" },
317
+ headCount: { type: "NumberType" },
318
+ isOperational: { type: "BooleanType" },
319
+ projects: { type: "StringType", isArray: true },
320
+ },
321
+
322
+ MixedItemType: {
323
+ type: { type: "StringType" },
324
+ data: { type: "MixedDataType" },
325
+ },
326
+
327
+ MixedDataType: {
328
+ stringValue: { type: "StringType" },
329
+ numberValue: { type: "NumberType" },
330
+ booleanValue: { type: "BooleanType" },
331
+ arrayValue: { type: "StringType", isArray: true },
332
+ },
333
+ } as const satisfies Schema.Schema;
334
+
335
+ // Extract bindings
336
+ const bindings = extractBindingsFromSchema(comprehensiveSchema);
337
+
338
+ // Test primitive bindings
339
+ expect(bindings.stringField.toString()).toBe("{{stringField}}");
340
+ expect(bindings.numberField.toString()).toBe("{{numberField}}");
341
+ expect(bindings.booleanField.toString()).toBe("{{booleanField}}");
342
+
343
+ // Test array bindings
344
+ expect(bindings.stringArray.name.toString()).toBe(
345
+ "{{stringArray._current_}}",
346
+ );
347
+ expect(bindings.numberArray.value.toString()).toBe(
348
+ "{{numberArray._current_}}",
349
+ );
350
+ expect(bindings.booleanArray.value.toString()).toBe(
351
+ "{{booleanArray._current_}}",
352
+ );
353
+
354
+ // Test complex nested bindings
355
+ expect(bindings.user.profile.firstName.toString()).toBe(
356
+ "{{user.profile.firstName}}",
357
+ );
358
+ expect(bindings.user.profile.addresses.city.toString()).toBe(
359
+ "{{user.profile.addresses._current_.city}}",
360
+ );
361
+ expect(
362
+ bindings.user.profile.addresses.coordinates.latitude.toString(),
363
+ ).toBe("{{user.profile.addresses._current_.coordinates.latitude}}");
364
+
365
+ // Test array of complex objects
366
+ expect(bindings.transactions.amount.toString()).toBe(
367
+ "{{transactions._current_.amount}}",
368
+ );
369
+ expect(bindings.transactions.merchant.name.toString()).toBe(
370
+ "{{transactions._current_.merchant.name}}",
371
+ );
372
+ expect(bindings.transactions.tags.name.toString()).toBe(
373
+ "{{transactions._current_.tags._current_}}",
374
+ );
375
+
376
+ // Test record type bindings
377
+ expect(bindings.metadata.version.toString()).toBe("{{metadata.version}}");
378
+ expect(bindings.metadata.limits.maxUsers.toString()).toBe(
379
+ "{{metadata.limits.maxUsers}}",
380
+ );
381
+
382
+ // =================================================================
383
+ // STD.TS INTEGRATION TESTS
384
+ // =================================================================
385
+
386
+ // Test logical operations with boolean bindings
387
+ const userIsVerifiedAndActive = and(
388
+ bindings.user.isVerified,
389
+ bindings.booleanField,
390
+ );
391
+ expect(userIsVerifiedAndActive.toString()).toBe(
392
+ "@[{{user.isVerified}} && {{booleanField}}]@",
393
+ );
394
+
395
+ const userHasNotifications = or(
396
+ bindings.user.preferences.emailNotifications,
397
+ bindings.user.preferences.pushNotifications,
398
+ );
399
+ expect(userHasNotifications.toString()).toBe(
400
+ "@[{{user.preferences.emailNotifications}} || {{user.preferences.pushNotifications}}]@",
401
+ );
402
+
403
+ const privacyIsPrivate = not(bindings.user.preferences.privacy.showEmail);
404
+ expect(privacyIsPrivate.toString()).toBe(
405
+ "@[!{{user.preferences.privacy.showEmail}}]@",
406
+ );
407
+
408
+ // Test comparison operations with string bindings
409
+ const usernameEquals = equal(bindings.user.username, "john_doe");
410
+ expect(usernameEquals.toString()).toBe(
411
+ '@[{{user.username}} == "john_doe"]@',
412
+ );
413
+
414
+ const themeIsNotDark = notEqual(bindings.user.preferences.theme, "dark");
415
+ expect(themeIsNotDark.toString()).toBe(
416
+ '@[{{user.preferences.theme}} != "dark"]@',
417
+ );
418
+
419
+ // Test numeric operations
420
+ const ageIsGreaterThan = greaterThan(
421
+ bindings.user.profile.birthYear,
422
+ 1990,
423
+ );
424
+ expect(ageIsGreaterThan.toString()).toBe(
425
+ "@[{{user.profile.birthYear}} > 1990]@",
426
+ );
427
+
428
+ const transactionIsLarge = greaterThanOrEqual(
429
+ bindings.transactions.amount,
430
+ 1000,
431
+ );
432
+ expect(transactionIsLarge.toString()).toBe(
433
+ "@[{{transactions._current_.amount}} >= 1000]@",
434
+ );
435
+
436
+ const scoresAreLow = lessThan(bindings.user.scores.value, 50);
437
+ expect(scoresAreLow.toString()).toBe(
438
+ "@[{{user.scores._current_}} < 50]@",
439
+ );
440
+
441
+ // Test arithmetic operations
442
+ const totalBudget = add(
443
+ bindings.organization.departments.budget,
444
+ bindings.metadata.limits.maxStorage,
445
+ );
446
+ expect(totalBudget.toString()).toBe(
447
+ "@[{{organization.departments._current_.budget}} + {{metadata.limits.maxStorage}}]@",
448
+ );
449
+
450
+ const priceDifference = subtract(
451
+ bindings.transactions.amount,
452
+ bindings.numberField,
453
+ );
454
+ expect(priceDifference.toString()).toBe(
455
+ "@[{{transactions._current_.amount}} - {{numberField}}]@",
456
+ );
457
+
458
+ const doubleEmployees = multiply(bindings.organization.employeeCount, 2);
459
+ expect(doubleEmployees.toString()).toBe(
460
+ "@[{{organization.employeeCount}} * 2]@",
461
+ );
462
+
463
+ const averageRating = divide(
464
+ bindings.transactions.merchant.rating,
465
+ bindings.user.scores.value,
466
+ );
467
+ expect(averageRating.toString()).toBe(
468
+ "@[{{transactions._current_.merchant.rating}} / {{user.scores._current_}}]@",
469
+ );
470
+
471
+ // Test complex conditional operations
472
+ const displayName = conditional(
473
+ equal(bindings.user.preferences.privacy.profileVisibility, "private"),
474
+ literal("Anonymous"),
475
+ bindings.user.profile.firstName,
476
+ );
477
+ expect(displayName.toString()).toBe(
478
+ '@[{{user.preferences.privacy.profileVisibility}} == "private" ? "Anonymous" : {{user.profile.firstName}}]@',
479
+ );
480
+
481
+ // Test with array element comparisons
482
+ const isPrimaryAddress = equal(
483
+ bindings.user.profile.addresses.isPrimary,
484
+ true,
485
+ );
486
+ expect(isPrimaryAddress.toString()).toBe(
487
+ "@[{{user.profile.addresses._current_.isPrimary}} == true]@",
488
+ );
489
+
490
+ // Test complex nested conditions with mixed types
491
+ const canProcessTransaction = and(
492
+ bindings.organization.isActive,
493
+ greaterThan(bindings.metadata.limits.maxTransactions, 0),
494
+ not(bindings.metadata.isProduction),
495
+ or(
496
+ equal(bindings.transactions.status, "pending"),
497
+ equal(bindings.transactions.status, "processing"),
498
+ ),
499
+ );
500
+ expect(canProcessTransaction.toString()).toBe(
501
+ '@[{{organization.isActive}} && {{metadata.limits.maxTransactions}} > 0 && !{{metadata.isProduction}} && ({{transactions._current_.status}} == "pending" || {{transactions._current_.status}} == "processing")]@',
502
+ );
503
+
504
+ // Test with nested array access
505
+ const hasValidCoordinates = and(
506
+ greaterThan(bindings.user.profile.addresses.coordinates.latitude, -90),
507
+ lessThan(bindings.user.profile.addresses.coordinates.latitude, 90),
508
+ greaterThan(
509
+ bindings.user.profile.addresses.coordinates.longitude,
510
+ -180,
511
+ ),
512
+ lessThan(bindings.user.profile.addresses.coordinates.longitude, 180),
513
+ );
514
+ expect(hasValidCoordinates.toString()).toBe(
515
+ "@[{{user.profile.addresses._current_.coordinates.latitude}} > -90 && {{user.profile.addresses._current_.coordinates.latitude}} < 90 && {{user.profile.addresses._current_.coordinates.longitude}} > -180 && {{user.profile.addresses._current_.coordinates.longitude}} < 180]@",
516
+ );
517
+
518
+ // Test function calls with bindings
519
+ const formatUsername = call(
520
+ "formatName",
521
+ bindings.user.profile.firstName,
522
+ bindings.user.profile.lastName,
523
+ );
524
+ expect(formatUsername.toString()).toBe(
525
+ "@[formatName({{user.profile.firstName}}, {{user.profile.lastName}})]@",
526
+ );
527
+
528
+ // Test mixed item access
529
+ const mixedItemIsValid = and(
530
+ equal(bindings.mixedItems.type, "numeric"),
531
+ greaterThan(bindings.mixedItems.data.numberValue, 0),
532
+ );
533
+ expect(mixedItemIsValid.toString()).toBe(
534
+ '@[{{mixedItems._current_.type}} == "numeric" && {{mixedItems._current_.data.numberValue}} > 0]@',
535
+ );
536
+
537
+ // Test deeply nested arithmetic with multiple array levels
538
+ const complexCalculation = add(
539
+ multiply(
540
+ bindings.organization.departments.headCount,
541
+ bindings.organization.departments.budget,
542
+ ),
543
+ divide(bindings.user.scores.value, bindings.metadata.limits.maxUsers),
544
+ );
545
+ expect(complexCalculation.toString()).toBe(
546
+ "@[{{organization.departments._current_.headCount}} * {{organization.departments._current_.budget}} + {{user.scores._current_}} / {{metadata.limits.maxUsers}}]@",
547
+ );
548
+
549
+ // Test record access with std functions
550
+ const isProductionWithHighVersion = and(
551
+ bindings.metadata.isProduction,
552
+ greaterThan(bindings.metadata.buildNumber, 1000),
553
+ );
554
+ expect(isProductionWithHighVersion.toString()).toBe(
555
+ "@[{{metadata.isProduction}} && {{metadata.buildNumber}} > 1000]@",
556
+ );
557
+
558
+ // Test string array operations
559
+ const hasAdminRole = equal(bindings.user.roles.name, "admin");
560
+ expect(hasAdminRole.toString()).toBe(
561
+ '@[{{user.roles._current_}} == "admin"]@',
562
+ );
563
+
564
+ // Test complex XOR operation
565
+ const exclusiveNotifications = xor(
566
+ bindings.user.preferences.emailNotifications,
567
+ bindings.user.preferences.pushNotifications,
568
+ );
569
+ expect(exclusiveNotifications.toString()).toBe(
570
+ "@[({{user.preferences.emailNotifications}} && !{{user.preferences.pushNotifications}}) || (!{{user.preferences.emailNotifications}} && {{user.preferences.pushNotifications}})]@",
571
+ );
572
+
573
+ // Test modulo operation
574
+ const isEvenScore = equal(modulo(bindings.user.scores.value, 2), 0);
575
+ expect(isEvenScore.toString()).toBe(
576
+ "@[{{user.scores._current_}} % 2 == 0]@",
577
+ );
578
+ });
579
+ });
580
+ });
@@ -0,0 +1,95 @@
1
+ import {
2
+ isTaggedTemplateValue,
3
+ TaggedTemplateValueSymbol,
4
+ type TaggedTemplateValue,
5
+ type TemplateRefOptions,
6
+ } from "./types";
7
+
8
+ /**
9
+ * Tagged template for creating binding expressions with optional type information
10
+ * Used to generate template strings with proper binding syntax
11
+ * @param strings - Template string parts
12
+ * @param expressions - Values to interpolate
13
+ * @returns TaggedTemplateValue with optional phantom type T
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * // Basic usage (backward compatible)
18
+ * const basicBinding = binding`data.count`;
19
+ *
20
+ * // With phantom type for TypeScript type checking
21
+ * const typedBinding = binding<number>`data.count`;
22
+ * ```
23
+ */
24
+ export function binding<T = unknown>(
25
+ strings: TemplateStringsArray,
26
+ ...expressions: Array<unknown>
27
+ ): TaggedTemplateValue<T> {
28
+ // Index counter for replacements
29
+ let indexCounter = 0;
30
+
31
+ /**
32
+ * Replaces index placeholders with unique identifiers
33
+ */
34
+ const processValue = (val: string): string => {
35
+ if (!val.includes("_index_")) return val;
36
+
37
+ return val.replace(/_index_/g, () => {
38
+ const suffix = indexCounter > 0 ? indexCounter : "";
39
+ indexCounter++;
40
+ return `_index${suffix}_`;
41
+ });
42
+ };
43
+
44
+ let result = "";
45
+ const len = strings.length;
46
+
47
+ for (let i = 0; i < len; i++) {
48
+ result += processValue(strings[i]);
49
+
50
+ if (i < expressions.length) {
51
+ const expr = expressions[i];
52
+
53
+ if (isTaggedTemplateValue(expr)) {
54
+ const refStr = expr.toRefString();
55
+ if (refStr.startsWith("@[")) {
56
+ // Expression template in binding context
57
+ result += expr.toRefString({ nestedContext: "binding" });
58
+ } else {
59
+ // Binding template retains braces
60
+ result += expr.toString();
61
+ }
62
+ } else if (typeof expr === "string") {
63
+ // Apply index replacements to string expressions
64
+ result += processValue(expr);
65
+ } else {
66
+ // Convert other values to string
67
+ result += String(expr);
68
+ }
69
+ }
70
+ }
71
+
72
+ const templateValue: TaggedTemplateValue<T> = {
73
+ [TaggedTemplateValueSymbol]: true,
74
+ _phantomType: undefined as T | undefined,
75
+
76
+ toValue(): string {
77
+ return result;
78
+ },
79
+
80
+ toRefString(options?: TemplateRefOptions): string {
81
+ // No additional braces needed in binding context
82
+ if (options?.nestedContext === "binding") {
83
+ return result;
84
+ }
85
+ // Add braces in other contexts
86
+ return `{{${result}}}`;
87
+ },
88
+
89
+ toString(): string {
90
+ return `{{${result}}}`;
91
+ },
92
+ };
93
+
94
+ return templateValue;
95
+ }