@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.
- package/dist/cjs/index.cjs +2396 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/index.legacy-esm.js +2276 -0
- package/dist/index.mjs +2276 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +38 -0
- package/src/core/base-builder/__tests__/fluent-builder-base.test.ts +2423 -0
- package/src/core/base-builder/__tests__/fluent-partial.test.ts +179 -0
- package/src/core/base-builder/__tests__/id-generator.test.ts +658 -0
- package/src/core/base-builder/__tests__/registry.test.ts +534 -0
- package/src/core/base-builder/__tests__/resolution-mixed-arrays.test.ts +319 -0
- package/src/core/base-builder/__tests__/resolution-pipeline.test.ts +416 -0
- package/src/core/base-builder/__tests__/resolution-switches.test.ts +468 -0
- package/src/core/base-builder/__tests__/resolution-templates.test.ts +255 -0
- package/src/core/base-builder/__tests__/switch.test.ts +815 -0
- package/src/core/base-builder/__tests__/template.test.ts +596 -0
- package/src/core/base-builder/__tests__/value-extraction.test.ts +200 -0
- package/src/core/base-builder/__tests__/value-storage.test.ts +459 -0
- package/src/core/base-builder/conditional/index.ts +64 -0
- package/src/core/base-builder/context.ts +152 -0
- package/src/core/base-builder/errors.ts +69 -0
- package/src/core/base-builder/fluent-builder-base.ts +308 -0
- package/src/core/base-builder/guards.ts +137 -0
- package/src/core/base-builder/id/generator.ts +290 -0
- package/src/core/base-builder/id/registry.ts +152 -0
- package/src/core/base-builder/index.ts +72 -0
- package/src/core/base-builder/resolution/path-resolver.ts +116 -0
- package/src/core/base-builder/resolution/pipeline.ts +103 -0
- package/src/core/base-builder/resolution/steps/__tests__/nested-asset-wrappers.test.ts +206 -0
- package/src/core/base-builder/resolution/steps/asset-id.ts +77 -0
- package/src/core/base-builder/resolution/steps/asset-wrappers.ts +64 -0
- package/src/core/base-builder/resolution/steps/builders.ts +84 -0
- package/src/core/base-builder/resolution/steps/mixed-arrays.ts +95 -0
- package/src/core/base-builder/resolution/steps/nested-asset-wrappers.ts +124 -0
- package/src/core/base-builder/resolution/steps/static-values.ts +35 -0
- package/src/core/base-builder/resolution/steps/switches.ts +71 -0
- package/src/core/base-builder/resolution/steps/templates.ts +40 -0
- package/src/core/base-builder/resolution/value-resolver.ts +333 -0
- package/src/core/base-builder/storage/auxiliary-storage.ts +82 -0
- package/src/core/base-builder/storage/value-storage.ts +282 -0
- package/src/core/base-builder/types.ts +266 -0
- package/src/core/base-builder/utils.ts +10 -0
- package/src/core/flow/__tests__/index.test.ts +292 -0
- package/src/core/flow/index.ts +118 -0
- package/src/core/index.ts +8 -0
- package/src/core/mocks/generated/action.builder.ts +92 -0
- package/src/core/mocks/generated/choice-item.builder.ts +120 -0
- package/src/core/mocks/generated/choice.builder.ts +134 -0
- package/src/core/mocks/generated/collection.builder.ts +93 -0
- package/src/core/mocks/generated/field-collection.builder.ts +86 -0
- package/src/core/mocks/generated/index.ts +10 -0
- package/src/core/mocks/generated/info.builder.ts +64 -0
- package/src/core/mocks/generated/input.builder.ts +63 -0
- package/src/core/mocks/generated/overview-collection.builder.ts +65 -0
- package/src/core/mocks/generated/splash-collection.builder.ts +93 -0
- package/src/core/mocks/generated/text.builder.ts +47 -0
- package/src/core/mocks/index.ts +1 -0
- package/src/core/mocks/types/action.ts +92 -0
- package/src/core/mocks/types/choice.ts +129 -0
- package/src/core/mocks/types/collection.ts +140 -0
- package/src/core/mocks/types/info.ts +7 -0
- package/src/core/mocks/types/input.ts +7 -0
- package/src/core/mocks/types/text.ts +5 -0
- package/src/core/schema/__tests__/index.test.ts +127 -0
- package/src/core/schema/index.ts +195 -0
- package/src/core/schema/types.ts +7 -0
- package/src/core/switch/__tests__/index.test.ts +156 -0
- package/src/core/switch/index.ts +81 -0
- package/src/core/tagged-template/README.md +448 -0
- package/src/core/tagged-template/__tests__/extract-bindings-from-schema.test.ts +207 -0
- package/src/core/tagged-template/__tests__/index.test.ts +190 -0
- package/src/core/tagged-template/__tests__/schema-std-integration.test.ts +580 -0
- package/src/core/tagged-template/binding.ts +95 -0
- package/src/core/tagged-template/expression.ts +92 -0
- package/src/core/tagged-template/extract-bindings-from-schema.ts +120 -0
- package/src/core/tagged-template/index.ts +5 -0
- package/src/core/tagged-template/std.ts +472 -0
- package/src/core/tagged-template/types.ts +123 -0
- package/src/core/template/__tests__/index.test.ts +380 -0
- package/src/core/template/index.ts +196 -0
- package/src/core/utils/index.ts +160 -0
- package/src/fp/README.md +411 -0
- package/src/fp/__tests__/index.test.ts +1178 -0
- package/src/fp/index.ts +386 -0
- package/src/gen/common.ts +15 -0
- package/src/index.ts +5 -0
- package/src/types.ts +203 -0
- package/types/core/base-builder/conditional/index.d.ts +21 -0
- package/types/core/base-builder/context.d.ts +39 -0
- package/types/core/base-builder/errors.d.ts +45 -0
- package/types/core/base-builder/fluent-builder-base.d.ts +147 -0
- package/types/core/base-builder/guards.d.ts +58 -0
- package/types/core/base-builder/id/generator.d.ts +69 -0
- package/types/core/base-builder/id/registry.d.ts +93 -0
- package/types/core/base-builder/index.d.ts +9 -0
- package/types/core/base-builder/resolution/path-resolver.d.ts +15 -0
- package/types/core/base-builder/resolution/pipeline.d.ts +27 -0
- package/types/core/base-builder/resolution/steps/asset-id.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/asset-wrappers.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/builders.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/mixed-arrays.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/nested-asset-wrappers.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/static-values.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/switches.d.ts +15 -0
- package/types/core/base-builder/resolution/steps/templates.d.ts +14 -0
- package/types/core/base-builder/resolution/value-resolver.d.ts +62 -0
- package/types/core/base-builder/storage/auxiliary-storage.d.ts +50 -0
- package/types/core/base-builder/storage/value-storage.d.ts +82 -0
- package/types/core/base-builder/types.d.ts +183 -0
- package/types/core/base-builder/utils.d.ts +2 -0
- package/types/core/flow/index.d.ts +23 -0
- package/types/core/index.d.ts +8 -0
- package/types/core/mocks/index.d.ts +2 -0
- package/types/core/mocks/types/action.d.ts +58 -0
- package/types/core/mocks/types/choice.d.ts +95 -0
- package/types/core/mocks/types/collection.d.ts +102 -0
- package/types/core/mocks/types/info.d.ts +7 -0
- package/types/core/mocks/types/input.d.ts +7 -0
- package/types/core/mocks/types/text.d.ts +5 -0
- package/types/core/schema/index.d.ts +34 -0
- package/types/core/schema/types.d.ts +5 -0
- package/types/core/switch/index.d.ts +21 -0
- package/types/core/tagged-template/binding.d.ts +19 -0
- package/types/core/tagged-template/expression.d.ts +11 -0
- package/types/core/tagged-template/extract-bindings-from-schema.d.ts +7 -0
- package/types/core/tagged-template/index.d.ts +6 -0
- package/types/core/tagged-template/std.d.ts +174 -0
- package/types/core/tagged-template/types.d.ts +69 -0
- package/types/core/template/index.d.ts +97 -0
- package/types/core/utils/index.d.ts +47 -0
- package/types/fp/index.d.ts +149 -0
- package/types/gen/common.d.ts +6 -0
- package/types/index.d.ts +3 -0
- 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
|
+
}
|