@statedelta-libs/expressions 1.2.0 → 2.0.0
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/README.md +119 -414
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +73 -44
- package/dist/index.d.ts +73 -44
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# @statedelta-libs/expressions
|
|
2
2
|
|
|
3
|
-
> Compilador de JSON DSL para funções otimizadas.
|
|
3
|
+
> Compilador de JSON DSL puro para funções otimizadas.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@statedelta-libs/expressions)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
8
|
## Características
|
|
9
9
|
|
|
10
|
+
- **DSL Puro** - 100% JSON-serializável, sem callbacks inline
|
|
10
11
|
- **Compilação** - Compila uma vez, executa milhões de vezes
|
|
11
12
|
- **Alta performance** - ~25-30M ops/s após compilação
|
|
12
13
|
- **Scope externo** - Funções vêm via scope, não hardcoded
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
- **Path syntax** - Suporte a wildcards (`items[*].price`)
|
|
16
17
|
- **Dependency extraction** - Para dirty tracking/reatividade
|
|
17
18
|
- **Conditions nativo** - Condicionais integradas com expressions nos lados
|
|
18
|
-
- **
|
|
19
|
+
- **normalize()** - Helper externo para custom transforms (`$query`, `$mapper`, etc.)
|
|
19
20
|
- **Type-safe** - TypeScript nativo com inferência
|
|
20
21
|
|
|
21
22
|
## Instalação
|
|
@@ -29,18 +30,23 @@ pnpm add @statedelta-libs/expressions
|
|
|
29
30
|
## Quick Start
|
|
30
31
|
|
|
31
32
|
```typescript
|
|
32
|
-
import { compile
|
|
33
|
-
import { filter, map, sum } from '@statedelta-libs/operators';
|
|
33
|
+
import { compile } from '@statedelta-libs/expressions';
|
|
34
34
|
|
|
35
35
|
// Define o scope com funções disponíveis
|
|
36
|
-
const scope = {
|
|
36
|
+
const scope = {
|
|
37
|
+
filter: (pred) => (arr) => arr?.filter(pred),
|
|
38
|
+
map: (fn) => (arr) => arr?.map(fn),
|
|
39
|
+
sum: (arr) => arr?.reduce((a, b) => a + b, 0),
|
|
40
|
+
isActive: (item) => item.active,
|
|
41
|
+
getPrice: (item) => item.price,
|
|
42
|
+
};
|
|
37
43
|
|
|
38
|
-
// Compilar expressão com $pipe
|
|
44
|
+
// Compilar expressão com $pipe (DSL puro)
|
|
39
45
|
const expr = compile({
|
|
40
46
|
$pipe: [
|
|
41
47
|
{ $: "items" },
|
|
42
|
-
{ $fn: "filter", args: [
|
|
43
|
-
{ $fn: "map", args: [
|
|
48
|
+
{ $fn: "filter", args: [{ $fn: "isActive" }] },
|
|
49
|
+
{ $fn: "map", args: [{ $fn: "getPrice" }] },
|
|
44
50
|
{ $fn: "sum" }
|
|
45
51
|
]
|
|
46
52
|
}, { scope });
|
|
@@ -115,16 +121,6 @@ const { fn } = compileAST(
|
|
|
115
121
|
fn(); // usa ctx.get("hp") + ctx.get("mp")
|
|
116
122
|
```
|
|
117
123
|
|
|
118
|
-
Código gerado:
|
|
119
|
-
```js
|
|
120
|
-
(function(scope){
|
|
121
|
-
const{add,accessor}=scope;
|
|
122
|
-
return function(data){
|
|
123
|
-
return add(accessor("hp",data),accessor("mp",data))
|
|
124
|
-
}
|
|
125
|
-
})
|
|
126
|
-
```
|
|
127
|
-
|
|
128
124
|
#### lexicalPrefix (compileAST)
|
|
129
125
|
|
|
130
126
|
Para máxima performance em paths específicos, use `lexicalPrefix` para acesso direto via destructuring:
|
|
@@ -147,22 +143,6 @@ const { fn } = compileAST(
|
|
|
147
143
|
fn({ params: { damage: 25 } }); // ctx.get("hp") + params.damage
|
|
148
144
|
```
|
|
149
145
|
|
|
150
|
-
Código gerado:
|
|
151
|
-
```js
|
|
152
|
-
(function(scope){
|
|
153
|
-
const{add,accessor}=scope;
|
|
154
|
-
return function(data){
|
|
155
|
-
const{params}=data??{}; // destructuring direto (rápido!)
|
|
156
|
-
return add(accessor("hp",data),params?.damage)
|
|
157
|
-
}
|
|
158
|
-
})
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
| Path | Com lexicalPrefix | Sem lexicalPrefix |
|
|
162
|
-
|------|-------------------|-------------------|
|
|
163
|
-
| `params.damage` | `params?.damage` (direto) | `accessor("params.damage", data)` |
|
|
164
|
-
| `hp` | `accessor("hp", data)` | `accessor("hp", data)` |
|
|
165
|
-
|
|
166
146
|
### evaluate() / evaluateAST()
|
|
167
147
|
|
|
168
148
|
Compila e executa em um passo.
|
|
@@ -192,32 +172,30 @@ const deps = extractDeps({
|
|
|
192
172
|
// ["$isVip", "price.vip", "price.regular"]
|
|
193
173
|
```
|
|
194
174
|
|
|
195
|
-
|
|
175
|
+
### normalize()
|
|
196
176
|
|
|
197
|
-
|
|
177
|
+
Normaliza expressões customizadas para DSL puro **antes** da compilação.
|
|
198
178
|
|
|
199
179
|
```typescript
|
|
200
|
-
|
|
201
|
-
name: "John",
|
|
202
|
-
damage: { $: "params.damage" },
|
|
203
|
-
active: { $if: "hp.current", then: true, else: false },
|
|
204
|
-
nested: {
|
|
205
|
-
deep: {
|
|
206
|
-
value: { $fn: "add", args: [{ $: "a" }, { $: "b" }] }
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}, { scope });
|
|
180
|
+
import { normalize, type Transforms } from '@statedelta-libs/expressions';
|
|
210
181
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// }
|
|
218
|
-
```
|
|
182
|
+
const transforms: Transforms = {
|
|
183
|
+
$query: (node) => ({
|
|
184
|
+
$fn: "__query",
|
|
185
|
+
args: [node.$query as string, (node.params ?? {}) as Expression],
|
|
186
|
+
}),
|
|
187
|
+
};
|
|
219
188
|
|
|
220
|
-
|
|
189
|
+
// Converte expressão customizada para DSL puro
|
|
190
|
+
const pure = normalize(
|
|
191
|
+
{ $query: "isAttacked", params: { row: { $: "kingRow" } } },
|
|
192
|
+
transforms
|
|
193
|
+
);
|
|
194
|
+
// → { $fn: "__query", args: ["isAttacked", { row: { $: "kingRow" } }] }
|
|
195
|
+
|
|
196
|
+
// Agora pode compilar normalmente
|
|
197
|
+
const { fn } = compile(pure, { scope });
|
|
198
|
+
```
|
|
221
199
|
|
|
222
200
|
## Tipos de Expressão
|
|
223
201
|
|
|
@@ -260,10 +238,10 @@ Composição com valor inicial - o valor passa por cada função em sequência.
|
|
|
260
238
|
```typescript
|
|
261
239
|
{
|
|
262
240
|
$pipe: [
|
|
263
|
-
{ $: "items" },
|
|
264
|
-
{ $fn: "filter", args: [
|
|
265
|
-
{ $fn: "map", args: [
|
|
266
|
-
{ $fn: "sum" }
|
|
241
|
+
{ $: "items" }, // Valor inicial
|
|
242
|
+
{ $fn: "filter", args: [{ $fn: "isActive" }] }, // Curried filter
|
|
243
|
+
{ $fn: "map", args: [{ $fn: "getPrice" }] }, // Curried map
|
|
244
|
+
{ $fn: "sum" } // Referência à função
|
|
267
245
|
]
|
|
268
246
|
}
|
|
269
247
|
```
|
|
@@ -284,6 +262,10 @@ Chama função do scope com argumentos compilados.
|
|
|
284
262
|
// Com args vazio: chama função sem argumentos
|
|
285
263
|
{ $fn: "getTimestamp", args: [] }
|
|
286
264
|
// → scope.getTimestamp()
|
|
265
|
+
|
|
266
|
+
// Passando função como argumento (referência via $fn)
|
|
267
|
+
{ $fn: "filter", args: [{ $fn: "isActive" }] }
|
|
268
|
+
// → scope.filter(scope.isActive)
|
|
287
269
|
```
|
|
288
270
|
|
|
289
271
|
### Condition
|
|
@@ -315,8 +297,6 @@ Condicionais compiladas internamente. Ambos os lados aceitam qualquer expressão
|
|
|
315
297
|
|
|
316
298
|
**Operadores:** `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `notIn`, `contains`, `notContains`, `exists`, `notExists`, `matches`, `notMatches`, `startsWith`, `endsWith`
|
|
317
299
|
|
|
318
|
-
> **Nota:** Objetos com `op: "set"` ou outros operadores não-condition são tratados como literals.
|
|
319
|
-
|
|
320
300
|
## Scope
|
|
321
301
|
|
|
322
302
|
O scope define quais funções estão disponíveis para `$fn`:
|
|
@@ -333,383 +313,115 @@ const scope = {
|
|
|
333
313
|
filter: R.filter,
|
|
334
314
|
map: R.map,
|
|
335
315
|
sum: R.sum,
|
|
336
|
-
// Funções custom
|
|
316
|
+
// Funções custom (predicados, mappers, etc.)
|
|
337
317
|
double: (n) => n * 2,
|
|
338
|
-
isActive: (item) => item.active
|
|
318
|
+
isActive: (item) => item.active,
|
|
319
|
+
getPrice: (item) => item.price,
|
|
339
320
|
};
|
|
340
321
|
|
|
341
322
|
const { fn } = compile(expression, { scope });
|
|
342
323
|
```
|
|
343
324
|
|
|
344
|
-
##
|
|
345
|
-
|
|
346
|
-
Para objetos que usam interface de acesso customizada (como `ctx.get(path)`), use o `accessor`:
|
|
347
|
-
|
|
348
|
-
```typescript
|
|
349
|
-
// Contexto com método get() customizado
|
|
350
|
-
interface TickContext {
|
|
351
|
-
get(path: string): unknown;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const accessor = (path: string, ctx: TickContext) => ctx.get(path);
|
|
355
|
-
|
|
356
|
-
// Compilação com accessor
|
|
357
|
-
const { fn } = compile<TickContext>(
|
|
358
|
-
{ $: "hp:value" },
|
|
359
|
-
{ accessor }
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
fn(tickContext); // usa ctx.get('hp:value')
|
|
363
|
-
|
|
364
|
-
// Com scope + accessor
|
|
365
|
-
const { fn } = compile<TickContext>(
|
|
366
|
-
{ $fn: "add", args: [{ $: "a" }, { $: "b" }] },
|
|
367
|
-
{ scope, accessor }
|
|
368
|
-
);
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
O accessor é propagado para:
|
|
372
|
-
- Referências (`{ $: "path" }`)
|
|
373
|
-
- Shorthand de `$if` (`{ $if: "path", ... }`)
|
|
374
|
-
- Argumentos de `$fn`
|
|
375
|
-
- Steps de `$pipe`
|
|
376
|
-
- Conditions (compiladas internamente)
|
|
377
|
-
|
|
378
|
-
**Performance:** Igual ao acesso direto (~25-30M ops/s).
|
|
379
|
-
|
|
380
|
-
## Path Syntax
|
|
381
|
-
|
|
382
|
-
| Path | Resultado |
|
|
383
|
-
|------|-----------|
|
|
384
|
-
| `user` | `data.user` |
|
|
385
|
-
| `user.name` | `data.user.name` |
|
|
386
|
-
| `items[0]` | `data.items[0]` |
|
|
387
|
-
| `items[*].price` | `data.items.map(i => i.price)` |
|
|
388
|
-
|
|
389
|
-
## Cache
|
|
390
|
-
|
|
391
|
-
```typescript
|
|
392
|
-
import { cache, cached } from '@statedelta-libs/expressions';
|
|
393
|
-
|
|
394
|
-
// Com scope
|
|
395
|
-
const compiled = cache.get(expression, { scope });
|
|
396
|
-
|
|
397
|
-
// Ou função helper
|
|
398
|
-
const compiled = cached(expression, { scope });
|
|
399
|
-
|
|
400
|
-
// Gerenciar
|
|
401
|
-
cache.size; // Tamanho atual
|
|
402
|
-
cache.clear(); // Limpar
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
## Performance
|
|
406
|
-
|
|
407
|
-
### compile() - Closures
|
|
408
|
-
|
|
409
|
-
| Operação | Velocidade |
|
|
410
|
-
|----------|------------|
|
|
411
|
-
| Compile (literal) | ~6.5M ops/s |
|
|
412
|
-
| Compile (ref) | ~3M ops/s |
|
|
413
|
-
| Compile (fn nested) | ~550K ops/s |
|
|
414
|
-
| Compile (complex) | ~300K ops/s |
|
|
415
|
-
| Execute (ref) | ~28-30M ops/s |
|
|
416
|
-
| Execute (fn nested) | ~9M ops/s |
|
|
417
|
-
| Execute (com accessor) | ~23-30M ops/s |
|
|
418
|
-
|
|
419
|
-
### compileAST() - AST + new Function
|
|
420
|
-
|
|
421
|
-
| Operação | Velocidade |
|
|
422
|
-
|----------|------------|
|
|
423
|
-
| Compile (literal) | ~1.2M ops/s |
|
|
424
|
-
| Compile (ref) | ~800K ops/s |
|
|
425
|
-
| Compile (fn nested) | ~230K ops/s |
|
|
426
|
-
| Compile (complex) | ~130K ops/s |
|
|
427
|
-
| Execute (ref) | ~27M ops/s |
|
|
428
|
-
| Execute (fn nested) | ~25M ops/s ⚡ |
|
|
429
|
-
|
|
430
|
-
### Comparação Execução
|
|
431
|
-
|
|
432
|
-
| Cenário | Closures | AST | Ganho AST |
|
|
433
|
-
|---------|----------|-----|-----------|
|
|
434
|
-
| fn nested | 9M ops/s | 25M ops/s | **+169%** |
|
|
435
|
-
| ref 4 níveis | 16M ops/s | 27M ops/s | **+69%** |
|
|
436
|
-
| conditional nested | 20M ops/s | 25M ops/s | **+25%** |
|
|
325
|
+
## Custom Transforms (normalize)
|
|
437
326
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
## Segurança
|
|
441
|
-
|
|
442
|
-
Ambas as abordagens são **seguras contra injeção de código**:
|
|
443
|
-
|
|
444
|
-
| Método | Proteção |
|
|
445
|
-
|--------|----------|
|
|
446
|
-
| `compile()` | Não gera código string - apenas compõe funções |
|
|
447
|
-
| `compileAST()` | Usa destructuring que valida identificadores automaticamente |
|
|
327
|
+
Para tipos de expressão customizados (`$query`, `$mapper`, etc.), use `normalize()` para convertê-los em DSL puro **antes** da compilação:
|
|
448
328
|
|
|
449
329
|
```typescript
|
|
450
|
-
|
|
451
|
-
{ $fn: "add; console.log('hacked'); //" }
|
|
452
|
-
|
|
453
|
-
// compile(): tenta acessar scope["add; console.log..."] → undefined
|
|
454
|
-
// compileAST(): destructuring inválido → SyntaxError
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
Funções só são acessíveis se existirem no `scope` fornecido pelo desenvolvedor.
|
|
458
|
-
|
|
459
|
-
## Custom Transforms
|
|
460
|
-
|
|
461
|
-
Mecanismo de extensão do walker para tipos de expressão customizados. Permite registrar transforms que interceptam objetos com chaves específicas e os convertem em expressões padrão antes da compilação.
|
|
330
|
+
import { normalize, compile, type Transforms, type Expression } from '@statedelta-libs/expressions';
|
|
462
331
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
```typescript
|
|
466
|
-
const transforms = {
|
|
332
|
+
// Define transforms
|
|
333
|
+
const transforms: Transforms = {
|
|
467
334
|
$query: (node) => ({
|
|
468
335
|
$fn: "__query",
|
|
469
|
-
args: [node.$query, node.params ?? {}]
|
|
336
|
+
args: [node.$query as string, (node.params ?? {}) as Expression],
|
|
470
337
|
}),
|
|
471
338
|
$mapper: (node) => ({
|
|
472
339
|
$fn: "__mapper",
|
|
473
|
-
args: [node.$mapper
|
|
340
|
+
args: [node.$mapper as string],
|
|
474
341
|
}),
|
|
475
342
|
};
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
### Uso com compile() e compileAST()
|
|
479
|
-
|
|
480
|
-
```typescript
|
|
481
|
-
import { compile, compileAST } from '@statedelta-libs/expressions';
|
|
482
343
|
|
|
344
|
+
// Define scope com as funções de runtime
|
|
483
345
|
const scope = {
|
|
484
346
|
__query: (name, params) => queryEngine.run(name, params),
|
|
347
|
+
__mapper: (name) => mapperRegistry.get(name),
|
|
485
348
|
};
|
|
486
349
|
|
|
487
|
-
//
|
|
488
|
-
const
|
|
489
|
-
{ $query: "
|
|
490
|
-
|
|
491
|
-
);
|
|
492
|
-
|
|
493
|
-
// AST (mesmo contrato)
|
|
494
|
-
const { fn } = compileAST(
|
|
495
|
-
{ $query: "isKingAttacked", params: { row: 0, col: 4 } },
|
|
496
|
-
{ scope, transforms }
|
|
350
|
+
// 1. Normalize (converte para DSL puro)
|
|
351
|
+
const pure = normalize(
|
|
352
|
+
{ $query: "isAttacked", params: { row: { $: "kingRow" } } },
|
|
353
|
+
transforms
|
|
497
354
|
);
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
### Expressions dentro de params
|
|
501
355
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
```typescript
|
|
505
|
-
const { fn } = compile(
|
|
506
|
-
{ $query: "check", params: { row: { $: "myRow" }, col: { $: "myCol" } } },
|
|
507
|
-
{ scope, transforms }
|
|
508
|
-
);
|
|
356
|
+
// 2. Compile (DSL puro)
|
|
357
|
+
const { fn } = compile(pure, { scope });
|
|
509
358
|
|
|
510
|
-
|
|
511
|
-
|
|
359
|
+
// 3. Execute
|
|
360
|
+
fn({ kingRow: 0 });
|
|
512
361
|
```
|
|
513
362
|
|
|
514
363
|
### Aninhamento
|
|
515
364
|
|
|
516
|
-
|
|
365
|
+
O `normalize` percorre toda a árvore, então transforms funcionam em qualquer posição:
|
|
517
366
|
|
|
518
367
|
```typescript
|
|
519
|
-
|
|
520
|
-
{
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
368
|
+
normalize(
|
|
369
|
+
{
|
|
370
|
+
$if: {
|
|
371
|
+
left: { $query: "isAttacked", params: { row: 0 } },
|
|
372
|
+
op: "eq",
|
|
373
|
+
right: true
|
|
374
|
+
},
|
|
375
|
+
then: "check",
|
|
376
|
+
else: "safe"
|
|
525
377
|
},
|
|
526
|
-
then: "check",
|
|
527
|
-
else: "safe"
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Dentro de plain object (walk automático)
|
|
531
|
-
{
|
|
532
|
-
damage: { $query: "getDamage", params: { base: 10 } },
|
|
533
|
-
name: "attack"
|
|
534
|
-
}
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
### Extração de deps
|
|
538
|
-
|
|
539
|
-
`extractDeps` também resolve transforms para capturar dependências internas:
|
|
540
|
-
|
|
541
|
-
```typescript
|
|
542
|
-
import { extractDeps } from '@statedelta-libs/expressions';
|
|
543
|
-
|
|
544
|
-
const deps = extractDeps(
|
|
545
|
-
{ $query: "check", params: { row: { $: "myRow" } } },
|
|
546
378
|
transforms
|
|
547
379
|
);
|
|
548
|
-
// ["myRow"]
|
|
549
380
|
```
|
|
550
381
|
|
|
551
|
-
### Proteção contra loops
|
|
382
|
+
### Proteção contra loops
|
|
552
383
|
|
|
553
|
-
Se um transform retornar
|
|
384
|
+
Se um transform retornar objeto com a mesma chave, erro é lançado:
|
|
554
385
|
|
|
555
386
|
```typescript
|
|
556
387
|
const badTransforms = {
|
|
557
388
|
$loop: (node) => ({ $loop: node.$loop }), // mesma chave!
|
|
558
389
|
};
|
|
559
390
|
|
|
560
|
-
|
|
391
|
+
normalize({ $loop: "test" }, badTransforms);
|
|
561
392
|
// Error: Transform "$loop" returned object with same key — infinite loop
|
|
562
393
|
```
|
|
563
394
|
|
|
564
|
-
### Performance
|
|
565
|
-
|
|
566
|
-
Transforms são resolvidos **em tempo de compilação**. Zero overhead em runtime — a função compilada resultante é idêntica a uma expressão `$fn` normal.
|
|
567
|
-
|
|
568
|
-
## Type Detection
|
|
569
|
-
|
|
570
|
-
O compilador detecta automaticamente o tipo de expressão baseado na estrutura do objeto:
|
|
571
|
-
|
|
572
|
-
| Tipo | Detecção | Exemplo |
|
|
573
|
-
|------|----------|---------|
|
|
574
|
-
| Reference | `{ $ }` presente | `{ $: "user.name" }` |
|
|
575
|
-
| Conditional | `{ $if, then }` presentes | `{ $if: cond, then: x, else: y }` |
|
|
576
|
-
| Function | `{ $fn }` presente | `{ $fn: "add", args: [...] }` |
|
|
577
|
-
| Pipe | `{ $pipe }` presente | `{ $pipe: [...] }` |
|
|
578
|
-
| Condition | `{ left, op }` com operador válido | `{ left: { $: "age" }, op: "gte", right: 18 }` |
|
|
579
|
-
| Custom Transform | Chave registrada em `transforms` | `{ $query: "check", params: {...} }` |
|
|
580
|
-
| Literal | Nenhum dos acima | `{ foo: "bar" }`, `42`, `"hello"` |
|
|
581
|
-
|
|
582
|
-
### Distinção entre Conditions e Effect Objects
|
|
583
|
-
|
|
584
|
-
Objetos com `left` e `op` só são tratados como conditions se `op` for um operador válido:
|
|
585
|
-
|
|
586
|
-
```typescript
|
|
587
|
-
// ✓ Condition (op: "eq" é operador válido)
|
|
588
|
-
{ left: { $: "user.age" }, op: "eq", right: 18 }
|
|
589
|
-
|
|
590
|
-
// ✓ Literal object (op: "set" NÃO é operador de condition)
|
|
591
|
-
{ resource: "state", op: "set", path: "currentPlayer", value: "X" }
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
Isso permite que effect objects de handlers (que usam `{ resource, op: "set", path, value }`) sejam processados corretamente sem serem confundidos com conditions.
|
|
595
|
-
|
|
596
395
|
## Builders
|
|
597
396
|
|
|
598
|
-
Funções declarativas para construir expressões
|
|
397
|
+
Funções declarativas para construir expressões:
|
|
599
398
|
|
|
600
399
|
```typescript
|
|
601
|
-
import { builders
|
|
400
|
+
import { builders } from '@statedelta-libs/expressions';
|
|
602
401
|
|
|
603
402
|
const { $, $fn, $if, $pipe, $cond } = builders;
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
### `$` / `ref` - Path Reference
|
|
607
|
-
|
|
608
|
-
```typescript
|
|
609
|
-
$("player.hp") // { $: "player.hp" }
|
|
610
|
-
$("items[0].price") // { $: "items[0].price" }
|
|
611
|
-
$("items[*].price") // { $: "items[*].price" } (wildcard)
|
|
612
|
-
```
|
|
613
|
-
|
|
614
|
-
### `$fn` / `fn` - Function Call
|
|
615
403
|
|
|
616
|
-
|
|
617
|
-
//
|
|
618
|
-
$fn("add", [$("a"), $("b")]) // { $fn: "add", args: [...] }
|
|
619
|
-
$fn("multiply", [10, $("x")]) // literal + expression
|
|
404
|
+
// Path reference
|
|
405
|
+
$("player.hp") // { $: "player.hp" }
|
|
620
406
|
|
|
621
|
-
//
|
|
407
|
+
// Function call
|
|
408
|
+
$fn("add", [$("a"), $("b")]) // { $fn: "add", args: [...] }
|
|
622
409
|
$fn("sum") // { $fn: "sum" }
|
|
623
410
|
|
|
624
|
-
//
|
|
625
|
-
$
|
|
626
|
-
$fn("map", [(item) => item.price])
|
|
627
|
-
```
|
|
628
|
-
|
|
629
|
-
### `$if` - Conditional
|
|
630
|
-
|
|
631
|
-
```typescript
|
|
632
|
-
// Com else
|
|
633
|
-
$if($("isVip"), 0.2, 0)
|
|
634
|
-
// { $if: { $: "isVip" }, then: 0.2, else: 0 }
|
|
635
|
-
|
|
636
|
-
// Sem else
|
|
637
|
-
$if($("active"), "yes")
|
|
638
|
-
// { $if: { $: "active" }, then: "yes" }
|
|
639
|
-
|
|
640
|
-
// Nested
|
|
641
|
-
$if($("level1"), $if($("level2"), "deep", "shallow"), "none")
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
### `$pipe` / `pipe` - Composition
|
|
411
|
+
// Conditional
|
|
412
|
+
$if($("isVip"), 0.2, 0) // { $if: ..., then: 0.2, else: 0 }
|
|
645
413
|
|
|
646
|
-
|
|
414
|
+
// Pipe
|
|
647
415
|
$pipe(
|
|
648
|
-
$("items"), // valor inicial
|
|
649
|
-
$fn("filter", [(x) => x.active]), // transforma
|
|
650
|
-
$fn("map", [(x) => x.price]), // transforma
|
|
651
|
-
$fn("sum") // resultado
|
|
652
|
-
)
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
### `$cond` / `cond` - Condition
|
|
656
|
-
|
|
657
|
-
```typescript
|
|
658
|
-
// Básico
|
|
659
|
-
$cond($("age"), "gte", 18)
|
|
660
|
-
// { left: { $: "age" }, op: "gte", right: 18 }
|
|
661
|
-
|
|
662
|
-
// Exists (sem right)
|
|
663
|
-
$cond($("email"), "exists")
|
|
664
|
-
|
|
665
|
-
// Ref vs Ref
|
|
666
|
-
$cond($("price"), "lte", $("budget"))
|
|
667
|
-
|
|
668
|
-
// Com $fn nos lados
|
|
669
|
-
$cond($fn("add", [$("a"), $("b")]), "gt", 100)
|
|
670
|
-
|
|
671
|
-
// Com $pipe
|
|
672
|
-
$cond($pipe($("name"), $fn("upper")), "eq", "ADMIN")
|
|
673
|
-
```
|
|
674
|
-
|
|
675
|
-
### Exemplo Completo
|
|
676
|
-
|
|
677
|
-
```typescript
|
|
678
|
-
import { builders, compile } from '@statedelta-libs/expressions';
|
|
679
|
-
import { filter, map, sum } from '@statedelta-libs/operators';
|
|
680
|
-
|
|
681
|
-
const { $, $fn, $if, $pipe, $cond } = builders;
|
|
682
|
-
|
|
683
|
-
const scope = { filter, map, sum };
|
|
684
|
-
|
|
685
|
-
// Soma preços de itens ativos com desconto VIP
|
|
686
|
-
const expr = $pipe(
|
|
687
416
|
$("items"),
|
|
688
|
-
$fn("filter", [(
|
|
689
|
-
$fn("map", [(item) => item.price]),
|
|
417
|
+
$fn("filter", [$fn("isActive")]),
|
|
690
418
|
$fn("sum")
|
|
691
|
-
)
|
|
692
|
-
|
|
693
|
-
const { fn } = compile(expr, { scope });
|
|
419
|
+
)
|
|
694
420
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
{ price: 10, active: true },
|
|
698
|
-
{ price: 20, active: false },
|
|
699
|
-
{ price: 30, active: true }
|
|
700
|
-
]
|
|
701
|
-
}); // 40
|
|
421
|
+
// Condition
|
|
422
|
+
$cond($("age"), "gte", 18) // { left: ..., op: "gte", right: 18 }
|
|
702
423
|
```
|
|
703
424
|
|
|
704
|
-
### Aliases
|
|
705
|
-
|
|
706
|
-
| Builder | Alias |
|
|
707
|
-
|---------|-------|
|
|
708
|
-
| `$` | `ref` |
|
|
709
|
-
| `$fn` | `fn` |
|
|
710
|
-
| `$pipe` | `pipe` |
|
|
711
|
-
| `$cond` | `cond` |
|
|
712
|
-
|
|
713
425
|
## TypeScript
|
|
714
426
|
|
|
715
427
|
```typescript
|
|
@@ -728,6 +440,7 @@ import type {
|
|
|
728
440
|
CompileOptions,
|
|
729
441
|
AccessorFn,
|
|
730
442
|
TransformFn,
|
|
443
|
+
Transforms,
|
|
731
444
|
} from '@statedelta-libs/expressions';
|
|
732
445
|
|
|
733
446
|
// Type guards
|
|
@@ -741,64 +454,56 @@ import {
|
|
|
741
454
|
} from '@statedelta-libs/expressions';
|
|
742
455
|
```
|
|
743
456
|
|
|
744
|
-
##
|
|
745
|
-
|
|
746
|
-
### Breaking Changes
|
|
457
|
+
## Performance
|
|
747
458
|
|
|
748
|
-
|
|
749
|
-
```typescript
|
|
750
|
-
// Antes
|
|
751
|
-
compile({ $fn: "add", args: [1, 2] })
|
|
459
|
+
### Comparação Execução
|
|
752
460
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
461
|
+
| Cenário | Closures | AST | Ganho AST |
|
|
462
|
+
|---------|----------|-----|-----------|
|
|
463
|
+
| fn nested | 9M ops/s | 25M ops/s | **+169%** |
|
|
464
|
+
| ref 4 níveis | 16M ops/s | 27M ops/s | **+69%** |
|
|
465
|
+
| conditional nested | 20M ops/s | 25M ops/s | **+25%** |
|
|
756
466
|
|
|
757
|
-
|
|
758
|
-
```typescript
|
|
759
|
-
// Retorna a função
|
|
760
|
-
{ $fn: "sum" }
|
|
467
|
+
**Break-even:** ~8 execuções - após isso, `compileAST()` compensa o tempo extra de compilação.
|
|
761
468
|
|
|
762
|
-
|
|
763
|
-
{ $fn: "getTime", args: [] }
|
|
764
|
-
```
|
|
469
|
+
## Segurança
|
|
765
470
|
|
|
766
|
-
|
|
471
|
+
Ambas as abordagens são **seguras contra injeção de código**:
|
|
767
472
|
|
|
768
|
-
|
|
473
|
+
| Método | Proteção |
|
|
474
|
+
|--------|----------|
|
|
475
|
+
| `compile()` | Não gera código string - apenas compõe funções |
|
|
476
|
+
| `compileAST()` | Usa destructuring que valida identificadores automaticamente |
|
|
769
477
|
|
|
770
|
-
|
|
478
|
+
Funções só são acessíveis se existirem no `scope` fornecido pelo desenvolvedor.
|
|
771
479
|
|
|
772
|
-
|
|
480
|
+
## Migração
|
|
773
481
|
|
|
774
|
-
|
|
482
|
+
### v1.1 → v1.2
|
|
775
483
|
|
|
776
|
-
|
|
484
|
+
1. **DSL Puro**: `FnExpr.args` agora é `Expression[]` (removido suporte a callbacks inline). Funções devem ser passadas via scope:
|
|
777
485
|
```typescript
|
|
778
|
-
|
|
779
|
-
|
|
486
|
+
// Antes (callbacks inline - não funciona mais)
|
|
487
|
+
{ $fn: "filter", args: [(item) => item.active] }
|
|
780
488
|
|
|
781
|
-
|
|
489
|
+
// Depois (referência via scope)
|
|
490
|
+
{ $fn: "filter", args: [{ $fn: "isActive" }] }
|
|
491
|
+
// Com scope: { isActive: (item) => item.active, filter: ... }
|
|
492
|
+
```
|
|
782
493
|
|
|
783
|
-
|
|
494
|
+
2. **Removidos**: `CallbackFn`, `FnArg` types
|
|
784
495
|
|
|
785
|
-
|
|
496
|
+
3. **normalize()**: Custom transforms agora são externos via `normalize()`, não mais dentro de `CompileOptions`:
|
|
786
497
|
```typescript
|
|
787
|
-
|
|
788
|
-
$query: (node) => ({ $fn: "__query", args: [node.$query, node.params ?? {}] }),
|
|
789
|
-
};
|
|
498
|
+
// Antes
|
|
790
499
|
compile(expr, { scope, transforms });
|
|
791
|
-
compileAST(expr, { scope, transforms });
|
|
792
|
-
```
|
|
793
500
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
501
|
+
// Depois
|
|
502
|
+
const pure = normalize(expr, transforms);
|
|
503
|
+
compile(pure, { scope });
|
|
797
504
|
```
|
|
798
505
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
4. **Zero breaking change**: O campo `transforms` é opcional. Sem ele, o comportamento é idêntico ao anterior.
|
|
506
|
+
4. **Novos tipos**: `Transforms` (alias para `Record<string, TransformFn>`)
|
|
802
507
|
|
|
803
508
|
## Licença
|
|
804
509
|
|