Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ✨ map support #23

Merged
merged 16 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@

- [ ] I have performed a self-review of my code
- [ ] I have added tests (if possible)

- [ ] I have updated the readme (if necessary)
- [ ] I have updated the demo (if necessary)
7 changes: 6 additions & 1 deletion demo/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,14 @@ import { evaluate, parse } from 'cel-js'
const arrayExpr = '[1, 2]'
console.log(`${arrayExpr} => ${evaluate(arrayExpr)}`) // => [1, 2]

// Map expressions
// TODO - bump version to 0.1.5 and uncomment
// const mapExpr = '{"a": 1, "b": {"c": 2} }'
// console.log(`${mapExpr} => ${evaluate(mapExpr)}`) // => { a: 1, b: { c: 2 } }

// Macro expressions
const macroExpr = 'size([1, 2])'
console.log(`${arrayExpr} => ${evaluate(macroExpr)}`) // => 2
console.log(`${macroExpr} => ${evaluate(macroExpr)}`) // => 2
}

// Parse an expression, useful for validation purposes before persisting
Expand Down
2 changes: 1 addition & 1 deletion demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "A simple usage of the CEL library",
"type": "module",
"dependencies": {
"cel-js": "0.1.2"
"cel-js": "0.1.4"
},
"devDependencies": {
"tsx": "4.7.0"
Expand Down
8 changes: 4 additions & 4 deletions demo/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@
"release": "release-it"
},
"dependencies": {
"chevrotain": "11.0.3"
"chevrotain": "11.0.3",
"ramda": "0.29.1"
},
"devDependencies": {
"@commitlint/cli": "18.4.3",
"@commitlint/config-conventional": "18.4.3",
"@types/ramda": "0.29.11",
"@types/lodash.get": "4.4.9",
"@types/node": "20.9.0",
"@typescript-eslint/eslint-plugin": "6.10.0",
Expand Down
26 changes: 26 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Try out `cel-js` in your browser with the [live demo](https://stackblitz.com/git
- [x] string
- [ ] bytes
- [x] list
- [ ] map
- [x] map
- [x] null
- [x] Conditional Operators
- [ ] Ternary (`condition ? true : false`)
Expand Down
28 changes: 28 additions & 0 deletions src/cst-definitions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,31 @@ export type ListExpressionCstChildren = {
Index?: IndexExpressionCstNode[];
};

export interface MapExpressionCstNode extends CstNode {
name: "mapExpression";
children: MapExpressionCstChildren;
}

export type MapExpressionCstChildren = {
OpenCurlyBracket: IToken[];
keyValues?: MapKeyValuesCstNode[];
CloseCurlyBracket: IToken[];
identifierDotExpression?: IdentifierDotExpressionCstNode[];
identifierIndexExpression?: IndexExpressionCstNode[];
};

export interface MapKeyValuesCstNode extends CstNode {
name: "mapKeyValues";
children: MapKeyValuesCstChildren;
}

export type MapKeyValuesCstChildren = {
key: ExprCstNode[];
Colon: IToken[];
value: ExprCstNode[];
Comma?: IToken[];
};

export interface MacrosExpressionCstNode extends CstNode {
name: "macrosExpression";
children: MacrosExpressionCstChildren;
Expand Down Expand Up @@ -157,6 +182,7 @@ export type AtomicExpressionCstChildren = {
Integer?: IToken[];
ReservedIdentifiers?: IToken[];
listExpression?: ListExpressionCstNode[];
mapExpression?: MapExpressionCstNode[];
macrosExpression?: MacrosExpressionCstNode[];
identifierExpression?: IdentifierExpressionCstNode[];
};
Expand All @@ -171,6 +197,8 @@ export interface ICstNodeVisitor<IN, OUT> extends ICstVisitor<IN, OUT> {
unaryExpression(children: UnaryExpressionCstChildren, param?: IN): OUT;
parenthesisExpression(children: ParenthesisExpressionCstChildren, param?: IN): OUT;
listExpression(children: ListExpressionCstChildren, param?: IN): OUT;
mapExpression(children: MapExpressionCstChildren, param?: IN): OUT;
mapKeyValues(children: MapKeyValuesCstChildren, param?: IN): OUT;
macrosExpression(children: MacrosExpressionCstChildren, param?: IN): OUT;
identifierExpression(children: IdentifierExpressionCstChildren, param?: IN): OUT;
identifierDotExpression(children: IdentifierDotExpressionCstChildren, param?: IN): OUT;
Expand Down
34 changes: 30 additions & 4 deletions src/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
IdentifierDotExpressionCstNode,
IndexExpressionCstNode,
} from './cst-definitions.js'
import { equals } from 'ramda'
ChromeGG marked this conversation as resolved.
Show resolved Hide resolved

export enum CelType {
int = 'int',
Expand Down Expand Up @@ -56,6 +57,9 @@ const isArray = (value: unknown): value is unknown[] =>
const isBoolean = (value: unknown): value is boolean =>
getCelType(value) === CelType.bool

const isMap = (value: unknown): value is Record<string, unknown> =>
getCelType(value) === CelType.map

export const getCelType = (value: unknown): CelType => {
if (value === null) {
return CelType.null
Expand Down Expand Up @@ -194,6 +198,16 @@ const logicalOrOperation = (left: unknown, right: unknown) => {
throw new CelTypeError(Operations.logicalOr, left, right)
}

const comparisonInOperation = (left: unknown, right: unknown) => {
if (isArray(right)) {
return right.includes(left)
}
if (isMap(right)) {
return Object.keys(right).includes(left as string)
}
throw new CelTypeError(Operations.in, left, right)
}

const comparisonOperation = (
operation: Operations,
left: unknown,
Expand All @@ -216,15 +230,15 @@ const comparisonOperation = (
}

if (operation === Operations.equals) {
return left === right
return equals(left, right)
}

if (operation === Operations.notEquals) {
return left !== right
return !equals(left, right)
}

if (operation === Operations.in && isArray(right)) {
return right.includes(left)
if (operation === Operations.in) {
return comparisonInOperation(left, right)
}

throw new CelTypeError(operation, left, right)
Expand Down Expand Up @@ -301,3 +315,15 @@ export const getPosition = (

return ctx.children.OpenBracket[0].startOffset
}

export const size = (arr: unknown) => {
if (isString(arr) || isArray(arr)) {
return arr.length
}

if (isMap(arr)) {
return Object.keys(arr).length
}

throw new CelEvaluationError(`invalid_argument: ${arr}`)
}
54 changes: 44 additions & 10 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import {
OpenBracket,
Comma,
MacrosIdentifier,
OpenCurlyBracket,
CloseCurlyBracket,
Colon,
} from './tokens.js'

export class CelParser extends CstParser {
Expand Down Expand Up @@ -101,6 +104,34 @@ export class CelParser extends CstParser {
})
})

private mapExpression = this.RULE('mapExpression', () => {
this.CONSUME(OpenCurlyBracket)
this.MANY(() => {
this.SUBRULE(this.mapKeyValues, { LABEL: 'keyValues' })
})
this.CONSUME(CloseCurlyBracket)
this.MANY2(() => {
this.OR([
{ ALT: () => this.SUBRULE(this.identifierDotExpression) },
{
ALT: () =>
this.SUBRULE(this.indexExpression, {
LABEL: 'identifierIndexExpression',
}),
},
])
})
})

private mapKeyValues = this.RULE('mapKeyValues', () => {
this.SUBRULE(this.expr, { LABEL: 'key' })
this.CONSUME(Colon)
this.SUBRULE2(this.expr, { LABEL: 'value' })
this.OPTION(() => {
this.CONSUME(Comma)
})
})

private macrosExpression = this.RULE('macrosExpression', () => {
this.CONSUME(MacrosIdentifier)
this.CONSUME(OpenParenthesis)
Expand All @@ -115,24 +146,26 @@ export class CelParser extends CstParser {
this.MANY(() => {
this.OR([
{ ALT: () => this.SUBRULE(this.identifierDotExpression) },
{ ALT: () => this.SUBRULE(this.indexExpression, {LABEL: 'identifierIndexExpression'}) },
{
ALT: () =>
this.SUBRULE(this.indexExpression, {
LABEL: 'identifierIndexExpression',
}),
},
])
})
})
})

private identifierDotExpression = this.RULE('identifierDotExpression', () => {
this.CONSUME(Dot)
this.CONSUME(Identifier)
})

private indexExpression = this.RULE(
'indexExpression',
() => {
this.CONSUME(OpenBracket)
this.SUBRULE(this.expr)
this.CONSUME(CloseBracket)
}
)
private indexExpression = this.RULE('indexExpression', () => {
this.CONSUME(OpenBracket)
this.SUBRULE(this.expr)
this.CONSUME(CloseBracket)
})

private atomicExpression = this.RULE('atomicExpression', () => {
this.OR([
Expand All @@ -144,6 +177,7 @@ export class CelParser extends CstParser {
{ ALT: () => this.CONSUME(Integer) },
{ ALT: () => this.CONSUME(ReservedIdentifiers) },
{ ALT: () => this.SUBRULE(this.listExpression) },
{ ALT: () => this.SUBRULE(this.mapExpression) },
{ ALT: () => this.SUBRULE(this.macrosExpression) },
{ ALT: () => this.SUBRULE(this.identifierExpression) },
])
Expand Down
26 changes: 26 additions & 0 deletions src/spec/macros.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,32 @@ describe('lists expressions', () => {
})
})

describe('map', () => {
it('should return 0 for empty map', () => {
const expr = 'size({})'

const result = evaluate(expr)

expect(result).toBe(0)
})

it('should return 1 for one element map', () => {
const expr = 'size({"a": 1})'

const result = evaluate(expr)

expect(result).toBe(1)
})

it('should return 3 for three element map', () => {
const expr = 'size({"a": 1, "b": 2, "c": 3})'

const result = evaluate(expr)

expect(result).toBe(3)
})
})

describe('string', () => {
it('should return 0 for empty string', () => {
const expr = 'size("")'
Expand Down
Loading
Loading