@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,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
|
+
});
|