@statedelta-libs/expressions 1.2.1 → 2.0.1
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 +121 -434
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +75 -92
- package/dist/index.d.ts +75 -92
- 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
|
+
};
|
|
188
|
+
|
|
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" } }] }
|
|
219
195
|
|
|
220
|
-
|
|
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,388 +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).
|
|
325
|
+
## Custom Transforms (normalize)
|
|
379
326
|
|
|
380
|
-
|
|
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
|
|
327
|
+
Para tipos de expressão customizados (`$query`, `$mapper`, etc.), use `normalize()` para convertê-los em DSL puro **antes** da compilação:
|
|
390
328
|
|
|
391
329
|
```typescript
|
|
392
|
-
import {
|
|
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
|
-
```
|
|
330
|
+
import { normalize, compile, type Transforms, type Expression } from '@statedelta-libs/expressions';
|
|
404
331
|
|
|
405
|
-
|
|
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%** |
|
|
437
|
-
|
|
438
|
-
**Break-even:** ~8 execuções - após isso, `compileAST()` compensa o tempo extra de compilação.
|
|
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 |
|
|
448
|
-
|
|
449
|
-
```typescript
|
|
450
|
-
// Tentativa de injeção - FALHA em ambos
|
|
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.
|
|
462
|
-
|
|
463
|
-
### API
|
|
464
|
-
|
|
465
|
-
Transforms retornam `Expression` ou `TransformResult`. Use `TransformResult` quando os args contêm tipos domain-specific (não `FnArg`):
|
|
466
|
-
|
|
467
|
-
```typescript
|
|
468
|
-
import type { TransformFn, TransformResult } from '@statedelta-libs/expressions';
|
|
469
|
-
|
|
470
|
-
const transforms: Record<string, TransformFn> = {
|
|
471
|
-
// TransformResult.args aceita unknown[] — sem casts necessários
|
|
332
|
+
// Define transforms
|
|
333
|
+
const transforms: Transforms = {
|
|
472
334
|
$query: (node) => ({
|
|
473
335
|
$fn: "__query",
|
|
474
|
-
args: [node.$query, node.params ?? {}]
|
|
336
|
+
args: [node.$query as string, (node.params ?? {}) as Expression],
|
|
475
337
|
}),
|
|
476
|
-
$
|
|
477
|
-
$fn: "
|
|
478
|
-
args: [node.$
|
|
338
|
+
$mapper: (node) => ({
|
|
339
|
+
$fn: "__mapper",
|
|
340
|
+
args: [node.$mapper as string],
|
|
479
341
|
}),
|
|
480
342
|
};
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
### Uso com compile() e compileAST()
|
|
484
|
-
|
|
485
|
-
```typescript
|
|
486
|
-
import { compile, compileAST } from '@statedelta-libs/expressions';
|
|
487
343
|
|
|
344
|
+
// Define scope com as funções de runtime
|
|
488
345
|
const scope = {
|
|
489
346
|
__query: (name, params) => queryEngine.run(name, params),
|
|
347
|
+
__mapper: (name) => mapperRegistry.get(name),
|
|
490
348
|
};
|
|
491
349
|
|
|
492
|
-
//
|
|
493
|
-
const
|
|
494
|
-
{ $query: "
|
|
495
|
-
|
|
350
|
+
// 1. Normalize (converte para DSL puro)
|
|
351
|
+
const pure = normalize(
|
|
352
|
+
{ $query: "isAttacked", params: { row: { $: "kingRow" } } },
|
|
353
|
+
transforms
|
|
496
354
|
);
|
|
497
355
|
|
|
498
|
-
//
|
|
499
|
-
const { fn } =
|
|
500
|
-
{ $query: "isKingAttacked", params: { row: 0, col: 4 } },
|
|
501
|
-
{ scope, transforms }
|
|
502
|
-
);
|
|
503
|
-
```
|
|
356
|
+
// 2. Compile (DSL puro)
|
|
357
|
+
const { fn } = compile(pure, { scope });
|
|
504
358
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
Os params do transform são compilados recursivamente — referências e expressões dentro deles são resolvidas normalmente:
|
|
508
|
-
|
|
509
|
-
```typescript
|
|
510
|
-
const { fn } = compile(
|
|
511
|
-
{ $query: "check", params: { row: { $: "myRow" }, col: { $: "myCol" } } },
|
|
512
|
-
{ scope, transforms }
|
|
513
|
-
);
|
|
514
|
-
|
|
515
|
-
fn({ myRow: 5, myCol: 3 });
|
|
516
|
-
// → __query("check", { row: 5, col: 3 })
|
|
359
|
+
// 3. Execute
|
|
360
|
+
fn({ kingRow: 0 });
|
|
517
361
|
```
|
|
518
362
|
|
|
519
363
|
### Aninhamento
|
|
520
364
|
|
|
521
|
-
|
|
365
|
+
O `normalize` percorre toda a árvore, então transforms funcionam em qualquer posição:
|
|
522
366
|
|
|
523
367
|
```typescript
|
|
524
|
-
|
|
525
|
-
{
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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"
|
|
530
377
|
},
|
|
531
|
-
then: "check",
|
|
532
|
-
else: "safe"
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Dentro de plain object (walk automático)
|
|
536
|
-
{
|
|
537
|
-
damage: { $query: "getDamage", params: { base: 10 } },
|
|
538
|
-
name: "attack"
|
|
539
|
-
}
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
### Extração de deps
|
|
543
|
-
|
|
544
|
-
`extractDeps` também resolve transforms para capturar dependências internas:
|
|
545
|
-
|
|
546
|
-
```typescript
|
|
547
|
-
import { extractDeps } from '@statedelta-libs/expressions';
|
|
548
|
-
|
|
549
|
-
const deps = extractDeps(
|
|
550
|
-
{ $query: "check", params: { row: { $: "myRow" } } },
|
|
551
378
|
transforms
|
|
552
379
|
);
|
|
553
|
-
// ["myRow"]
|
|
554
380
|
```
|
|
555
381
|
|
|
556
|
-
### Proteção contra loops
|
|
382
|
+
### Proteção contra loops
|
|
557
383
|
|
|
558
|
-
Se um transform retornar
|
|
384
|
+
Se um transform retornar objeto com a mesma chave, erro é lançado:
|
|
559
385
|
|
|
560
386
|
```typescript
|
|
561
387
|
const badTransforms = {
|
|
562
388
|
$loop: (node) => ({ $loop: node.$loop }), // mesma chave!
|
|
563
389
|
};
|
|
564
390
|
|
|
565
|
-
|
|
391
|
+
normalize({ $loop: "test" }, badTransforms);
|
|
566
392
|
// Error: Transform "$loop" returned object with same key — infinite loop
|
|
567
393
|
```
|
|
568
394
|
|
|
569
|
-
### Performance
|
|
570
|
-
|
|
571
|
-
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.
|
|
572
|
-
|
|
573
|
-
## Type Detection
|
|
574
|
-
|
|
575
|
-
O compilador detecta automaticamente o tipo de expressão baseado na estrutura do objeto:
|
|
576
|
-
|
|
577
|
-
| Tipo | Detecção | Exemplo |
|
|
578
|
-
|------|----------|---------|
|
|
579
|
-
| Reference | `{ $ }` presente | `{ $: "user.name" }` |
|
|
580
|
-
| Conditional | `{ $if, then }` presentes | `{ $if: cond, then: x, else: y }` |
|
|
581
|
-
| Function | `{ $fn }` presente | `{ $fn: "add", args: [...] }` |
|
|
582
|
-
| Pipe | `{ $pipe }` presente | `{ $pipe: [...] }` |
|
|
583
|
-
| Condition | `{ left, op }` com operador válido | `{ left: { $: "age" }, op: "gte", right: 18 }` |
|
|
584
|
-
| Custom Transform | Chave registrada em `transforms` | `{ $query: "check", params: {...} }` |
|
|
585
|
-
| Literal | Nenhum dos acima | `{ foo: "bar" }`, `42`, `"hello"` |
|
|
586
|
-
|
|
587
|
-
### Distinção entre Conditions e Effect Objects
|
|
588
|
-
|
|
589
|
-
Objetos com `left` e `op` só são tratados como conditions se `op` for um operador válido:
|
|
590
|
-
|
|
591
|
-
```typescript
|
|
592
|
-
// ✓ Condition (op: "eq" é operador válido)
|
|
593
|
-
{ left: { $: "user.age" }, op: "eq", right: 18 }
|
|
594
|
-
|
|
595
|
-
// ✓ Literal object (op: "set" NÃO é operador de condition)
|
|
596
|
-
{ resource: "state", op: "set", path: "currentPlayer", value: "X" }
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
Isso permite que effect objects de handlers (que usam `{ resource, op: "set", path, value }`) sejam processados corretamente sem serem confundidos com conditions.
|
|
600
|
-
|
|
601
395
|
## Builders
|
|
602
396
|
|
|
603
|
-
Funções declarativas para construir expressões
|
|
397
|
+
Funções declarativas para construir expressões:
|
|
604
398
|
|
|
605
399
|
```typescript
|
|
606
|
-
import { builders
|
|
400
|
+
import { builders } from '@statedelta-libs/expressions';
|
|
607
401
|
|
|
608
402
|
const { $, $fn, $if, $pipe, $cond } = builders;
|
|
609
|
-
```
|
|
610
|
-
|
|
611
|
-
### `$` / `ref` - Path Reference
|
|
612
|
-
|
|
613
|
-
```typescript
|
|
614
|
-
$("player.hp") // { $: "player.hp" }
|
|
615
|
-
$("items[0].price") // { $: "items[0].price" }
|
|
616
|
-
$("items[*].price") // { $: "items[*].price" } (wildcard)
|
|
617
|
-
```
|
|
618
403
|
|
|
619
|
-
|
|
404
|
+
// Path reference
|
|
405
|
+
$("player.hp") // { $: "player.hp" }
|
|
620
406
|
|
|
621
|
-
|
|
622
|
-
//
|
|
623
|
-
$fn("add", [$("a"), $("b")]) // { $fn: "add", args: [...] }
|
|
624
|
-
$fn("multiply", [10, $("x")]) // literal + expression
|
|
625
|
-
|
|
626
|
-
// Sem argumentos - retorna referência à função (útil em $pipe)
|
|
407
|
+
// Function call
|
|
408
|
+
$fn("add", [$("a"), $("b")]) // { $fn: "add", args: [...] }
|
|
627
409
|
$fn("sum") // { $fn: "sum" }
|
|
628
410
|
|
|
629
|
-
//
|
|
630
|
-
$
|
|
631
|
-
$fn("map", [(item) => item.price])
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
### `$if` - Conditional
|
|
635
|
-
|
|
636
|
-
```typescript
|
|
637
|
-
// Com else
|
|
638
|
-
$if($("isVip"), 0.2, 0)
|
|
639
|
-
// { $if: { $: "isVip" }, then: 0.2, else: 0 }
|
|
640
|
-
|
|
641
|
-
// Sem else
|
|
642
|
-
$if($("active"), "yes")
|
|
643
|
-
// { $if: { $: "active" }, then: "yes" }
|
|
644
|
-
|
|
645
|
-
// Nested
|
|
646
|
-
$if($("level1"), $if($("level2"), "deep", "shallow"), "none")
|
|
647
|
-
```
|
|
648
|
-
|
|
649
|
-
### `$pipe` / `pipe` - Composition
|
|
411
|
+
// Conditional
|
|
412
|
+
$if($("isVip"), 0.2, 0) // { $if: ..., then: 0.2, else: 0 }
|
|
650
413
|
|
|
651
|
-
|
|
414
|
+
// Pipe
|
|
652
415
|
$pipe(
|
|
653
|
-
$("items"), // valor inicial
|
|
654
|
-
$fn("filter", [(x) => x.active]), // transforma
|
|
655
|
-
$fn("map", [(x) => x.price]), // transforma
|
|
656
|
-
$fn("sum") // resultado
|
|
657
|
-
)
|
|
658
|
-
```
|
|
659
|
-
|
|
660
|
-
### `$cond` / `cond` - Condition
|
|
661
|
-
|
|
662
|
-
```typescript
|
|
663
|
-
// Básico
|
|
664
|
-
$cond($("age"), "gte", 18)
|
|
665
|
-
// { left: { $: "age" }, op: "gte", right: 18 }
|
|
666
|
-
|
|
667
|
-
// Exists (sem right)
|
|
668
|
-
$cond($("email"), "exists")
|
|
669
|
-
|
|
670
|
-
// Ref vs Ref
|
|
671
|
-
$cond($("price"), "lte", $("budget"))
|
|
672
|
-
|
|
673
|
-
// Com $fn nos lados
|
|
674
|
-
$cond($fn("add", [$("a"), $("b")]), "gt", 100)
|
|
675
|
-
|
|
676
|
-
// Com $pipe
|
|
677
|
-
$cond($pipe($("name"), $fn("upper")), "eq", "ADMIN")
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
### Exemplo Completo
|
|
681
|
-
|
|
682
|
-
```typescript
|
|
683
|
-
import { builders, compile } from '@statedelta-libs/expressions';
|
|
684
|
-
import { filter, map, sum } from '@statedelta-libs/operators';
|
|
685
|
-
|
|
686
|
-
const { $, $fn, $if, $pipe, $cond } = builders;
|
|
687
|
-
|
|
688
|
-
const scope = { filter, map, sum };
|
|
689
|
-
|
|
690
|
-
// Soma preços de itens ativos com desconto VIP
|
|
691
|
-
const expr = $pipe(
|
|
692
416
|
$("items"),
|
|
693
|
-
$fn("filter", [(
|
|
694
|
-
$fn("map", [(item) => item.price]),
|
|
417
|
+
$fn("filter", [$fn("isActive")]),
|
|
695
418
|
$fn("sum")
|
|
696
|
-
)
|
|
697
|
-
|
|
698
|
-
const { fn } = compile(expr, { scope });
|
|
419
|
+
)
|
|
699
420
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
{ price: 10, active: true },
|
|
703
|
-
{ price: 20, active: false },
|
|
704
|
-
{ price: 30, active: true }
|
|
705
|
-
]
|
|
706
|
-
}); // 40
|
|
421
|
+
// Condition
|
|
422
|
+
$cond($("age"), "gte", 18) // { left: ..., op: "gte", right: 18 }
|
|
707
423
|
```
|
|
708
424
|
|
|
709
|
-
### Aliases
|
|
710
|
-
|
|
711
|
-
| Builder | Alias |
|
|
712
|
-
|---------|-------|
|
|
713
|
-
| `$` | `ref` |
|
|
714
|
-
| `$fn` | `fn` |
|
|
715
|
-
| `$pipe` | `pipe` |
|
|
716
|
-
| `$cond` | `cond` |
|
|
717
|
-
|
|
718
425
|
## TypeScript
|
|
719
426
|
|
|
720
427
|
```typescript
|
|
@@ -733,7 +440,7 @@ import type {
|
|
|
733
440
|
CompileOptions,
|
|
734
441
|
AccessorFn,
|
|
735
442
|
TransformFn,
|
|
736
|
-
|
|
443
|
+
Transforms,
|
|
737
444
|
} from '@statedelta-libs/expressions';
|
|
738
445
|
|
|
739
446
|
// Type guards
|
|
@@ -744,79 +451,59 @@ import {
|
|
|
744
451
|
isPipe,
|
|
745
452
|
isCondition,
|
|
746
453
|
isLiteral,
|
|
747
|
-
isTransformResult,
|
|
748
454
|
} from '@statedelta-libs/expressions';
|
|
749
455
|
```
|
|
750
456
|
|
|
751
|
-
##
|
|
752
|
-
|
|
753
|
-
### Breaking Changes
|
|
754
|
-
|
|
755
|
-
1. **Builtins removidos**: Funções não são mais hardcoded. Passe via `scope`:
|
|
756
|
-
```typescript
|
|
757
|
-
// Antes
|
|
758
|
-
compile({ $fn: "add", args: [1, 2] })
|
|
759
|
-
|
|
760
|
-
// Depois
|
|
761
|
-
compile({ $fn: "add", args: [1, 2] }, { scope: { add } })
|
|
762
|
-
```
|
|
763
|
-
|
|
764
|
-
2. **$fn sem args retorna função**: Para chamar sem argumentos, use `args: []`:
|
|
765
|
-
```typescript
|
|
766
|
-
// Retorna a função
|
|
767
|
-
{ $fn: "sum" }
|
|
457
|
+
## Performance
|
|
768
458
|
|
|
769
|
-
|
|
770
|
-
{ $fn: "getTime", args: [] }
|
|
771
|
-
```
|
|
459
|
+
### Comparação Execução
|
|
772
460
|
|
|
773
|
-
|
|
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%** |
|
|
774
466
|
|
|
775
|
-
|
|
467
|
+
**Break-even:** ~8 execuções - após isso, `compileAST()` compensa o tempo extra de compilação.
|
|
776
468
|
|
|
777
|
-
|
|
469
|
+
## Segurança
|
|
778
470
|
|
|
779
|
-
|
|
471
|
+
Ambas as abordagens são **seguras contra injeção de código**:
|
|
780
472
|
|
|
781
|
-
|
|
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 |
|
|
782
477
|
|
|
783
|
-
|
|
784
|
-
```typescript
|
|
785
|
-
$cond($fn("add", [$("a"), $("b")]), "gt", 100)
|
|
786
|
-
```
|
|
478
|
+
Funções só são acessíveis se existirem no `scope` fornecido pelo desenvolvedor.
|
|
787
479
|
|
|
788
|
-
|
|
480
|
+
## Migração
|
|
789
481
|
|
|
790
482
|
### v1.1 → v1.2
|
|
791
483
|
|
|
792
|
-
1. **
|
|
484
|
+
1. **DSL Puro**: `FnExpr.args` agora é `Expression[]` (removido suporte a callbacks inline). Funções devem ser passadas via scope:
|
|
793
485
|
```typescript
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
};
|
|
797
|
-
compile(expr, { scope, transforms });
|
|
798
|
-
compileAST(expr, { scope, transforms });
|
|
799
|
-
```
|
|
486
|
+
// Antes (callbacks inline - não funciona mais)
|
|
487
|
+
{ $fn: "filter", args: [(item) => item.active] }
|
|
800
488
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
489
|
+
// Depois (referência via scope)
|
|
490
|
+
{ $fn: "filter", args: [{ $fn: "isActive" }] }
|
|
491
|
+
// Com scope: { isActive: (item) => item.active, filter: ... }
|
|
804
492
|
```
|
|
805
493
|
|
|
806
|
-
|
|
494
|
+
2. **Removidos**: `CallbackFn`, `FnArg` types
|
|
807
495
|
|
|
808
|
-
|
|
496
|
+
3. **normalize()**: Custom transforms agora são externos via `normalize()`, não mais dentro de `CompileOptions`:
|
|
809
497
|
```typescript
|
|
810
|
-
//
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
};
|
|
498
|
+
// Antes
|
|
499
|
+
compile(expr, { scope, transforms });
|
|
500
|
+
|
|
501
|
+
// Depois
|
|
502
|
+
const pure = normalize(expr, transforms);
|
|
503
|
+
compile(pure, { scope });
|
|
817
504
|
```
|
|
818
505
|
|
|
819
|
-
|
|
506
|
+
4. **Novos tipos**: `Transforms` (alias para `Record<string, TransformFn>`)
|
|
820
507
|
|
|
821
508
|
## Licença
|
|
822
509
|
|