@the-trybe/formula-engine 1.1.1 → 1.2.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/src/lexer.test.ts CHANGED
@@ -285,4 +285,65 @@ describe('Lexer', () => {
285
285
  expect(() => lexer.tokenize()).toThrow(SyntaxError);
286
286
  });
287
287
  });
288
+
289
+ describe('Braces', () => {
290
+ it('should tokenize left brace', () => {
291
+ const lexer = new Lexer('{');
292
+ const tokens = lexer.tokenize();
293
+
294
+ expect(tokens[0].type).toBe(TokenType.LBRACE);
295
+ expect(tokens[0].value).toBe('{');
296
+ });
297
+
298
+ it('should tokenize right brace', () => {
299
+ const lexer = new Lexer('}');
300
+ const tokens = lexer.tokenize();
301
+
302
+ expect(tokens[0].type).toBe(TokenType.RBRACE);
303
+ expect(tokens[0].value).toBe('}');
304
+ });
305
+
306
+ it('should tokenize object literal tokens', () => {
307
+ const lexer = new Lexer('{ a: 1, b: 2 }');
308
+ const tokens = lexer.tokenize();
309
+
310
+ expect(tokens[0].type).toBe(TokenType.LBRACE);
311
+ expect(tokens[1].type).toBe(TokenType.IDENTIFIER);
312
+ expect(tokens[1].value).toBe('a');
313
+ expect(tokens[2].type).toBe(TokenType.COLON);
314
+ expect(tokens[3].type).toBe(TokenType.NUMBER);
315
+ expect(tokens[3].value).toBe('1');
316
+ expect(tokens[4].type).toBe(TokenType.COMMA);
317
+ expect(tokens[5].type).toBe(TokenType.IDENTIFIER);
318
+ expect(tokens[5].value).toBe('b');
319
+ expect(tokens[6].type).toBe(TokenType.COLON);
320
+ expect(tokens[7].type).toBe(TokenType.NUMBER);
321
+ expect(tokens[7].value).toBe('2');
322
+ expect(tokens[8].type).toBe(TokenType.RBRACE);
323
+ expect(tokens[9].type).toBe(TokenType.EOF);
324
+ });
325
+
326
+ it('should tokenize nested braces', () => {
327
+ const lexer = new Lexer('{ a: { b: 1 } }');
328
+ const tokens = lexer.tokenize();
329
+
330
+ expect(tokens[0].type).toBe(TokenType.LBRACE);
331
+ expect(tokens[3].type).toBe(TokenType.LBRACE);
332
+ expect(tokens[7].type).toBe(TokenType.RBRACE);
333
+ expect(tokens[8].type).toBe(TokenType.RBRACE);
334
+ });
335
+
336
+ it('should tokenize braces with variable values', () => {
337
+ const lexer = new Lexer('{ type: @client.type }');
338
+ const tokens = lexer.tokenize();
339
+
340
+ expect(tokens[0].type).toBe(TokenType.LBRACE);
341
+ expect(tokens[1].type).toBe(TokenType.IDENTIFIER);
342
+ expect(tokens[2].type).toBe(TokenType.COLON);
343
+ expect(tokens[3].type).toBe(TokenType.CONTEXT_VAR);
344
+ expect(tokens[4].type).toBe(TokenType.DOT);
345
+ expect(tokens[5].type).toBe(TokenType.IDENTIFIER);
346
+ expect(tokens[6].type).toBe(TokenType.RBRACE);
347
+ });
348
+ });
288
349
  });
package/src/lexer.ts CHANGED
@@ -110,6 +110,12 @@ export class Lexer {
110
110
  case ']':
111
111
  this.addToken(TokenType.RBRACKET, ']');
112
112
  break;
113
+ case '{':
114
+ this.addToken(TokenType.LBRACE, '{');
115
+ break;
116
+ case '}':
117
+ this.addToken(TokenType.RBRACE, '}');
118
+ break;
113
119
  case ',':
114
120
  this.addToken(TokenType.COMMA, ',');
115
121
  break;
@@ -346,4 +346,113 @@ describe('Parser', () => {
346
346
  expect(() => parser.parse('$a ? $b')).toThrow(UnexpectedTokenError);
347
347
  });
348
348
  });
349
+
350
+ describe('Object Literals', () => {
351
+ it('should parse empty object literal', () => {
352
+ const ast = parser.parse('{}');
353
+
354
+ expect(ast.type).toBe('ObjectLiteral');
355
+ expect((ast as any).properties).toHaveLength(0);
356
+ });
357
+
358
+ it('should parse single-property object literal', () => {
359
+ const ast = parser.parse('{ name: "test" }');
360
+
361
+ expect(ast.type).toBe('ObjectLiteral');
362
+ expect((ast as any).properties).toHaveLength(1);
363
+ expect((ast as any).properties[0].key).toBe('name');
364
+ expect((ast as any).properties[0].value.type).toBe('StringLiteral');
365
+ expect((ast as any).properties[0].value.value).toBe('test');
366
+ });
367
+
368
+ it('should parse multi-property object literal', () => {
369
+ const ast = parser.parse('{ a: 1, b: 2, c: 3 }');
370
+
371
+ expect(ast.type).toBe('ObjectLiteral');
372
+ expect((ast as any).properties).toHaveLength(3);
373
+ expect((ast as any).properties[0].key).toBe('a');
374
+ expect((ast as any).properties[1].key).toBe('b');
375
+ expect((ast as any).properties[2].key).toBe('c');
376
+ });
377
+
378
+ it('should parse object literal with expression values', () => {
379
+ const ast = parser.parse('{ total: $a + $b }');
380
+
381
+ expect(ast.type).toBe('ObjectLiteral');
382
+ expect((ast as any).properties[0].key).toBe('total');
383
+ expect((ast as any).properties[0].value.type).toBe('BinaryOperation');
384
+ expect((ast as any).properties[0].value.operator).toBe('+');
385
+ });
386
+
387
+ it('should parse object literal with context variable values', () => {
388
+ const ast = parser.parse('{ zone: @client.zone }');
389
+
390
+ expect(ast.type).toBe('ObjectLiteral');
391
+ expect((ast as any).properties[0].key).toBe('zone');
392
+ expect((ast as any).properties[0].value.type).toBe('MemberAccess');
393
+ });
394
+
395
+ it('should parse object literal with variable values', () => {
396
+ const ast = parser.parse('{ price: $unitPrice, qty: $quantity }');
397
+
398
+ expect(ast.type).toBe('ObjectLiteral');
399
+ expect((ast as any).properties).toHaveLength(2);
400
+ expect((ast as any).properties[0].value.type).toBe('VariableReference');
401
+ expect((ast as any).properties[0].value.name).toBe('unitPrice');
402
+ expect((ast as any).properties[1].value.type).toBe('VariableReference');
403
+ expect((ast as any).properties[1].value.name).toBe('quantity');
404
+ });
405
+
406
+ it('should parse nested object literals', () => {
407
+ const ast = parser.parse('{ inner: { x: 1 } }');
408
+
409
+ expect(ast.type).toBe('ObjectLiteral');
410
+ expect((ast as any).properties[0].key).toBe('inner');
411
+ expect((ast as any).properties[0].value.type).toBe('ObjectLiteral');
412
+ expect((ast as any).properties[0].value.properties[0].key).toBe('x');
413
+ });
414
+
415
+ it('should parse object literal as function argument', () => {
416
+ const ast = parser.parse('LOOKUP($t, { a: 1 }, "r")');
417
+
418
+ expect(ast.type).toBe('FunctionCall');
419
+ expect((ast as any).name).toBe('LOOKUP');
420
+ expect((ast as any).arguments).toHaveLength(3);
421
+ expect((ast as any).arguments[1].type).toBe('ObjectLiteral');
422
+ expect((ast as any).arguments[1].properties[0].key).toBe('a');
423
+ });
424
+
425
+ it('should parse object literal with boolean and null values', () => {
426
+ const ast = parser.parse('{ active: true, deleted: false, data: null }');
427
+
428
+ expect(ast.type).toBe('ObjectLiteral');
429
+ expect((ast as any).properties).toHaveLength(3);
430
+ expect((ast as any).properties[0].value.type).toBe('BooleanLiteral');
431
+ expect((ast as any).properties[1].value.type).toBe('BooleanLiteral');
432
+ expect((ast as any).properties[2].value.type).toBe('NullLiteral');
433
+ });
434
+
435
+ it('should parse object literal with array value', () => {
436
+ const ast = parser.parse('{ items: [1, 2, 3] }');
437
+
438
+ expect(ast.type).toBe('ObjectLiteral');
439
+ expect((ast as any).properties[0].value.type).toBe('ArrayLiteral');
440
+ });
441
+
442
+ it('should throw on missing colon in object literal', () => {
443
+ expect(() => parser.parse('{ a 1 }')).toThrow(UnexpectedTokenError);
444
+ });
445
+
446
+ it('should throw on non-identifier key', () => {
447
+ expect(() => parser.parse('{ 123: 1 }')).toThrow(UnexpectedTokenError);
448
+ });
449
+
450
+ it('should throw on string key', () => {
451
+ expect(() => parser.parse('{ "key": 1 }')).toThrow(UnexpectedTokenError);
452
+ });
453
+
454
+ it('should throw on unclosed brace', () => {
455
+ expect(() => parser.parse('{ a: 1')).toThrow(UnexpectedTokenError);
456
+ });
457
+ });
349
458
  });
package/src/parser.ts CHANGED
@@ -75,13 +75,15 @@ export class Parser {
75
75
  return this.parseGroupedExpression();
76
76
  case TokenType.LBRACKET:
77
77
  return this.parseArrayLiteral();
78
+ case TokenType.LBRACE:
79
+ return this.parseObjectLiteral();
78
80
  case TokenType.MINUS:
79
81
  case TokenType.NOT:
80
82
  return this.parseUnaryExpression();
81
83
  default:
82
84
  throw new UnexpectedTokenError(
83
85
  String(token.value),
84
- ['number', 'string', 'boolean', 'null', 'variable', 'identifier', '(', '[', '-', '!'],
86
+ ['number', 'string', 'boolean', 'null', 'variable', 'identifier', '(', '[', '{', '-', '!'],
85
87
  token.position
86
88
  );
87
89
  }
@@ -266,6 +268,43 @@ export class Parser {
266
268
  };
267
269
  }
268
270
 
271
+ private parseObjectLiteral(): ASTNode {
272
+ this.advance(); // consume '{'
273
+ const properties: { key: string; value: ASTNode }[] = [];
274
+
275
+ if (this.peek().type !== TokenType.RBRACE) {
276
+ do {
277
+ if (this.peek().type === TokenType.COMMA) {
278
+ this.advance();
279
+ }
280
+ // Key must be an identifier
281
+ const keyToken = this.peek();
282
+ if (keyToken.type !== TokenType.IDENTIFIER) {
283
+ throw new UnexpectedTokenError(
284
+ String(keyToken.value),
285
+ ['identifier (property name)'],
286
+ keyToken.position
287
+ );
288
+ }
289
+ const key = String(this.advance().value);
290
+
291
+ // Expect colon
292
+ this.expect(TokenType.COLON, ':');
293
+
294
+ // Value is any expression
295
+ const value = this.parseExpression(PRECEDENCE.LOWEST);
296
+ properties.push({ key, value });
297
+ } while (this.peek().type === TokenType.COMMA);
298
+ }
299
+
300
+ this.expect(TokenType.RBRACE, '}');
301
+
302
+ return {
303
+ type: 'ObjectLiteral',
304
+ properties,
305
+ };
306
+ }
307
+
269
308
  private parseUnaryExpression(): ASTNode {
270
309
  const token = this.advance();
271
310
  const operator = this.getOperatorSymbol(token.type);
package/src/types.ts CHANGED
@@ -143,6 +143,16 @@ export interface ArrayLiteral {
143
143
  elements: ASTNode[];
144
144
  }
145
145
 
146
+ export interface ObjectLiteralProperty {
147
+ key: string;
148
+ value: ASTNode;
149
+ }
150
+
151
+ export interface ObjectLiteral {
152
+ type: 'ObjectLiteral';
153
+ properties: ObjectLiteralProperty[];
154
+ }
155
+
146
156
  export interface VariableReference {
147
157
  type: 'VariableReference';
148
158
  prefix: '$' | '@';
@@ -194,6 +204,7 @@ export type ASTNode =
194
204
  | BooleanLiteral
195
205
  | NullLiteral
196
206
  | ArrayLiteral
207
+ | ObjectLiteral
197
208
  | VariableReference
198
209
  | BinaryOperation
199
210
  | UnaryOperation
@@ -342,6 +353,8 @@ export enum TokenType {
342
353
  RPAREN = 'RPAREN',
343
354
  LBRACKET = 'LBRACKET',
344
355
  RBRACKET = 'RBRACKET',
356
+ LBRACE = 'LBRACE',
357
+ RBRACE = 'RBRACE',
345
358
  COMMA = 'COMMA',
346
359
  DOT = 'DOT',
347
360
  QUESTION = 'QUESTION',
@@ -1,6 +0,0 @@
1
- {
2
- "enabledMcpjsonServers": [
3
- "trigger"
4
- ],
5
- "enableAllProjectMcpServers": true
6
- }