Skip to content

Commit a96a1b1

Browse files
feat(web_core): implement v0.9 Generic Binder and refactor Basic Functions (google#848)
1 parent c95cf7a commit a96a1b1

12 files changed

Lines changed: 1266 additions & 350 deletions

File tree

renderers/web_core/src/v0_9/basic_catalog/functions/basic_functions.test.ts

Lines changed: 65 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -21,79 +21,85 @@ import { BASIC_FUNCTIONS } from "./basic_functions.js";
2121
import { DataModel } from "../../state/data-model.js";
2222
import { DataContext } from "../../rendering/data-context.js";
2323

24+
function invoke(name: string, args: Record<string, any>, context: DataContext) {
25+
const fn = BASIC_FUNCTIONS.find(f => f.name === name);
26+
if (!fn) throw new Error(`Function ${name} not found`);
27+
return fn.execute(fn.schema.parse(args), context);
28+
}
29+
2430
describe("BASIC_FUNCTIONS", () => {
2531
const dataModel = new DataModel({ a: 10, b: 20 });
26-
const context = new DataContext(dataModel, "/");
32+
const context = new DataContext(dataModel, "/", () => null);
2733

2834
describe("Arithmetic", () => {
2935
it("add", () => {
30-
assert.strictEqual(BASIC_FUNCTIONS.add({ a: 1, b: 2 }, context), 3);
31-
assert.strictEqual(BASIC_FUNCTIONS.add({ a: "1", b: "2" }, context), 3);
36+
assert.strictEqual(invoke("add", { a: 1, b: 2 }, context), 3);
37+
assert.strictEqual(invoke("add", { a: "1", b: "2" }, context), 3);
3238
});
3339
it("subtract", () => {
34-
assert.strictEqual(BASIC_FUNCTIONS.subtract({ a: 5, b: 3 }, context), 2);
40+
assert.strictEqual(invoke("subtract", { a: 5, b: 3 }, context), 2);
3541
});
3642
it("multiply", () => {
37-
assert.strictEqual(BASIC_FUNCTIONS.multiply({ a: 4, b: 2 }, context), 8);
43+
assert.strictEqual(invoke("multiply", { a: 4, b: 2 }, context), 8);
3844
});
3945
it("divide", () => {
40-
assert.strictEqual(BASIC_FUNCTIONS.divide({ a: 10, b: 2 }, context), 5);
46+
assert.strictEqual(invoke("divide", { a: 10, b: 2 }, context), 5);
4147
assert.strictEqual(
42-
BASIC_FUNCTIONS.divide({ a: 10, b: 0 }, context),
48+
invoke("divide", { a: 10, b: 0 }, context),
4349
Infinity,
4450
);
4551
assert.ok(
46-
Number.isNaN(BASIC_FUNCTIONS.divide({ a: 10, b: undefined }, context)),
52+
Number.isNaN(invoke("divide", { a: 10, b: undefined }, context)),
4753
);
4854
assert.ok(
49-
Number.isNaN(BASIC_FUNCTIONS.divide({ a: undefined, b: 10 }, context)),
55+
Number.isNaN(invoke("divide", { a: undefined, b: 10 }, context)),
5056
);
5157
assert.ok(
5258
Number.isNaN(
53-
BASIC_FUNCTIONS.divide({ a: undefined, b: undefined }, context),
59+
invoke("divide", { a: undefined, b: undefined }, context),
5460
),
5561
);
5662
assert.ok(
57-
Number.isNaN(BASIC_FUNCTIONS.divide({ a: 10, b: null }, context)),
63+
Number.isNaN(invoke("divide", { a: 10, b: null }, context)),
5864
);
5965
assert.ok(
60-
Number.isNaN(BASIC_FUNCTIONS.divide({ a: 10, b: "invalid" }, context)),
66+
Number.isNaN(invoke("divide", { a: 10, b: "invalid" }, context)),
6167
);
62-
assert.strictEqual(BASIC_FUNCTIONS.divide({ a: 10, b: "2" }, context), 5);
68+
assert.strictEqual(invoke("divide", { a: 10, b: "2" }, context), 5);
6369
assert.strictEqual(
64-
BASIC_FUNCTIONS.divide({ a: "10", b: "2" }, context),
70+
invoke("divide", { a: "10", b: "2" }, context),
6571
5,
6672
);
6773
});
6874
});
6975

7076
describe("Comparison", () => {
7177
it("equals", () => {
72-
assert.strictEqual(BASIC_FUNCTIONS.equals({ a: 1, b: 1 }, context), true);
78+
assert.strictEqual(invoke("equals", { a: 1, b: 1 }, context), true);
7379
assert.strictEqual(
74-
BASIC_FUNCTIONS.equals({ a: 1, b: 2 }, context),
80+
invoke("equals", { a: 1, b: 2 }, context),
7581
false,
7682
);
7783
});
7884
it("not_equals", () => {
7985
assert.strictEqual(
80-
BASIC_FUNCTIONS.not_equals({ a: 1, b: 2 }, context),
86+
invoke("not_equals", { a: 1, b: 2 }, context),
8187
true,
8288
);
8389
});
8490
it("greater_than", () => {
8591
assert.strictEqual(
86-
BASIC_FUNCTIONS.greater_than({ a: 5, b: 3 }, context),
92+
invoke("greater_than", { a: 5, b: 3 }, context),
8793
true,
8894
);
8995
assert.strictEqual(
90-
BASIC_FUNCTIONS.greater_than({ a: 3, b: 5 }, context),
96+
invoke("greater_than", { a: 3, b: 5 }, context),
9197
false,
9298
);
9399
});
94100
it("less_than", () => {
95101
assert.strictEqual(
96-
BASIC_FUNCTIONS.less_than({ a: 3, b: 5 }, context),
102+
invoke("less_than", { a: 3, b: 5 }, context),
97103
true,
98104
);
99105
});
@@ -103,49 +109,49 @@ describe("BASIC_FUNCTIONS", () => {
103109
it("and", () => {
104110
// Checks args['values'] array OR args['a'] && args['b'].
105111
assert.strictEqual(
106-
BASIC_FUNCTIONS.and({ values: [true, true] }, context),
112+
invoke("and", { values: [true, true] }, context),
107113
true,
108114
);
109115
assert.strictEqual(
110-
BASIC_FUNCTIONS.and({ values: [true, false] }, context),
116+
invoke("and", { values: [true, false] }, context),
111117
false,
112118
);
113119
assert.strictEqual(
114-
BASIC_FUNCTIONS.and({ a: true, b: true }, context),
120+
invoke("and", { a: true, b: true }, context),
115121
true,
116122
);
117123
});
118124
it("or", () => {
119125
assert.strictEqual(
120-
BASIC_FUNCTIONS.or({ values: [false, true] }, context),
126+
invoke("or", { values: [false, true] }, context),
121127
true,
122128
);
123129
assert.strictEqual(
124-
BASIC_FUNCTIONS.or({ values: [false, false] }, context),
130+
invoke("or", { values: [false, false] }, context),
125131
false,
126132
);
127133
assert.strictEqual(
128-
BASIC_FUNCTIONS.or({ a: false, b: true }, context),
134+
invoke("or", { a: false, b: true }, context),
129135
true,
130136
);
131137
});
132138
it("not", () => {
133-
assert.strictEqual(BASIC_FUNCTIONS.not({ value: false }, context), true);
134-
assert.strictEqual(BASIC_FUNCTIONS.not({ value: true }, context), false);
139+
assert.strictEqual(invoke("not", { value: false }, context), true);
140+
assert.strictEqual(invoke("not", { value: true }, context), false);
135141
});
136142
});
137143

138144
describe("String", () => {
139145
it("contains", () => {
140146
assert.strictEqual(
141-
BASIC_FUNCTIONS.contains(
147+
invoke("contains",
142148
{ string: "hello world", substring: "world" },
143149
context,
144150
),
145151
true,
146152
);
147153
assert.strictEqual(
148-
BASIC_FUNCTIONS.contains(
154+
invoke("contains",
149155
{ string: "hello world", substring: "foo" },
150156
context,
151157
),
@@ -154,13 +160,13 @@ describe("BASIC_FUNCTIONS", () => {
154160
});
155161
it("starts_with", () => {
156162
assert.strictEqual(
157-
BASIC_FUNCTIONS.starts_with({ string: "hello", prefix: "he" }, context),
163+
invoke("starts_with", { string: "hello", prefix: "he" }, context),
158164
true,
159165
);
160166
});
161167
it("ends_with", () => {
162168
assert.strictEqual(
163-
BASIC_FUNCTIONS.ends_with({ string: "hello", suffix: "lo" }, context),
169+
invoke("ends_with", { string: "hello", suffix: "lo" }, context),
164170
true,
165171
);
166172
});
@@ -169,74 +175,74 @@ describe("BASIC_FUNCTIONS", () => {
169175
describe("Validation", () => {
170176
it("required", () => {
171177
assert.strictEqual(
172-
BASIC_FUNCTIONS.required({ value: "a" }, context),
178+
invoke("required", { value: "a" }, context),
173179
true,
174180
);
175181
assert.strictEqual(
176-
BASIC_FUNCTIONS.required({ value: "" }, context),
182+
invoke("required", { value: "" }, context),
177183
false,
178184
);
179185
assert.strictEqual(
180-
BASIC_FUNCTIONS.required({ value: null }, context),
186+
invoke("required", { value: null }, context),
181187
false,
182188
);
183189
});
184190

185191
it("length", () => {
186192
assert.strictEqual(
187-
BASIC_FUNCTIONS.length({ value: "abc", min: 2 }, context),
193+
invoke("length", { value: "abc", min: 2 }, context),
188194
true,
189195
);
190196
assert.strictEqual(
191-
BASIC_FUNCTIONS.length({ value: "abc", max: 2 }, context),
197+
invoke("length", { value: "abc", max: 2 }, context),
192198
false,
193199
);
194200
});
195201

196202
it("numeric", () => {
197203
assert.strictEqual(
198-
BASIC_FUNCTIONS.numeric({ value: 10, min: 5, max: 15 }, context),
204+
invoke("numeric", { value: 10, min: 5, max: 15 }, context),
199205
true,
200206
);
201207
assert.strictEqual(
202-
BASIC_FUNCTIONS.numeric({ value: 3, min: 5 }, context),
208+
invoke("numeric", { value: 3, min: 5 }, context),
203209
false,
204210
);
205211
});
206212

207213
it("email", () => {
208214
assert.strictEqual(
209-
BASIC_FUNCTIONS.email({ value: "test@example.com" }, context),
215+
invoke("email", { value: "test@example.com" }, context),
210216
true,
211217
);
212218
assert.strictEqual(
213-
BASIC_FUNCTIONS.email({ value: "invalid" }, context),
219+
invoke("email", { value: "invalid" }, context),
214220
false,
215221
);
216222
});
217223

218224
it("regex", () => {
219225
assert.strictEqual(
220-
BASIC_FUNCTIONS.regex({ value: "abc", pattern: "^[a-z]+$" }, context),
226+
invoke("regex", { value: "abc", pattern: "^[a-z]+$" }, context),
221227
true,
222228
);
223229
assert.strictEqual(
224-
BASIC_FUNCTIONS.regex({ value: "123", pattern: "^[a-z]+$" }, context),
230+
invoke("regex", { value: "123", pattern: "^[a-z]+$" }, context),
225231
false,
226232
);
227233
});
228234

229235
it("regex handles invalid pattern", () => {
230236
assert.strictEqual(
231-
BASIC_FUNCTIONS.regex({ value: "abc", pattern: "[" }, context),
237+
invoke("regex", { value: "abc", pattern: "[" }, context),
232238
false, // fallback when regex throws
233239
);
234240
});
235241
});
236242

237243
describe("Formatting", () => {
238244
it("formatString (static literal)", (_, done) => {
239-
const result = BASIC_FUNCTIONS.formatString(
245+
const result = invoke("formatString",
240246
{ value: "hello world" },
241247
context,
242248
) as import("@preact/signals-core").Signal<string>;
@@ -254,7 +260,7 @@ describe("BASIC_FUNCTIONS", () => {
254260

255261
it("formatString (with data binding)", (_, done) => {
256262
// Assuming dataModel has { "a": 10 } from setup
257-
const result = BASIC_FUNCTIONS.formatString(
263+
const result = invoke("formatString",
258264
{ value: "Value: ${a}" },
259265
context,
260266
) as import("@preact/signals-core").Signal<string>;
@@ -293,7 +299,7 @@ describe("BASIC_FUNCTIONS", () => {
293299
return null;
294300
});
295301

296-
const result = BASIC_FUNCTIONS.formatString(
302+
const result = invoke("formatString",
297303
{ value: "Result: ${add(a: 5, b: 7)}" },
298304
ctxWithInvoker,
299305
) as import("@preact/signals-core").Signal<string>;
@@ -311,7 +317,7 @@ describe("BASIC_FUNCTIONS", () => {
311317

312318
it("formatNumber", () => {
313319
// Test basic output as Intl behavior varies by environment.
314-
const result = BASIC_FUNCTIONS.formatNumber(
320+
const result = invoke("formatNumber",
315321
{ value: 1234.56, decimals: 1 },
316322
context,
317323
);
@@ -324,7 +330,7 @@ describe("BASIC_FUNCTIONS", () => {
324330
});
325331

326332
it("formatCurrency", () => {
327-
const result = BASIC_FUNCTIONS.formatCurrency(
333+
const result = invoke("formatCurrency",
328334
{ value: 1234.56, currency: "USD" },
329335
context,
330336
);
@@ -334,30 +340,30 @@ describe("BASIC_FUNCTIONS", () => {
334340
});
335341

336342
it("formatDate", () => {
337-
const result = BASIC_FUNCTIONS.formatDate(
343+
const result = invoke("formatDate",
338344
{ value: "2025-01-01T00:00:00Z" },
339345
context,
340346
);
341347
assert.ok(typeof result === "string");
342348
assert.ok(result.length > 0);
343349

344-
const resultISO = BASIC_FUNCTIONS.formatDate(
350+
const resultISO = invoke("formatDate",
345351
{ value: "2025-01-01T00:00:00Z", format: "ISO" },
346352
context,
347353
);
348354
assert.strictEqual(resultISO, "2025-01-01T00:00:00.000Z");
349355
});
350356

351357
it("formatDate handles invalid dates", () => {
352-
const result = BASIC_FUNCTIONS.formatDate(
358+
const result = invoke("formatDate",
353359
{ value: "invalid-date" },
354360
context,
355361
);
356362
assert.strictEqual(result, "");
357363
});
358364

359365
it("formatDate uses options properly", () => {
360-
const result = BASIC_FUNCTIONS.formatDate(
366+
const result = invoke("formatDate",
361367
{
362368
value: "2025-01-01T00:00:00Z",
363369
options: { year: "numeric", timeZone: "UTC" },
@@ -369,7 +375,7 @@ describe("BASIC_FUNCTIONS", () => {
369375
});
370376

371377
it("formatDate fallback on formatting error", () => {
372-
const result = BASIC_FUNCTIONS.formatDate(
378+
const result = invoke("formatDate",
373379
{ value: "2025-01-01T00:00:00Z", locale: "invalid-locale-!!!11123" },
374380
context,
375381
);
@@ -378,7 +384,7 @@ describe("BASIC_FUNCTIONS", () => {
378384
});
379385

380386
it("formatCurrency fallback on formatting error", () => {
381-
const result = BASIC_FUNCTIONS.formatCurrency(
387+
const result = invoke("formatCurrency",
382388
{ value: 1234.56, currency: "INVALID-CURRENCY", decimals: 2 },
383389
context,
384390
);
@@ -388,14 +394,14 @@ describe("BASIC_FUNCTIONS", () => {
388394

389395
it("pluralize", () => {
390396
assert.strictEqual(
391-
BASIC_FUNCTIONS.pluralize(
397+
invoke("pluralize",
392398
{ value: 1, one: "apple", other: "apples" },
393399
context,
394400
),
395401
"apple",
396402
);
397403
assert.strictEqual(
398-
BASIC_FUNCTIONS.pluralize(
404+
invoke("pluralize",
399405
{ value: 2, one: "apple", other: "apples" },
400406
context,
401407
),
@@ -416,11 +422,11 @@ describe("BASIC_FUNCTIONS", () => {
416422
};
417423

418424
try {
419-
BASIC_FUNCTIONS.openUrl({ url: "https://google.com" }, context);
425+
invoke("openUrl", { url: "https://google.com" }, context);
420426
assert.strictEqual(openedUrl, "https://google.com");
421427
} finally {
422428
(global as any).window = originalWindow;
423429
}
424430
});
425431
});
426-
});
432+
});

0 commit comments

Comments
 (0)