@qrvey/formula-lang 0.0.2 → 0.1.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 CHANGED
@@ -1,51 +1,68 @@
1
1
  # Qrvey Formula Language
2
+ [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
3
+
2
4
 
3
5
  ## Usage
4
6
 
5
- Use it inside a javascript module
7
+ ### Transpile function
8
+ ```js
9
+ import { ENGINES, Transpile } from "@qrvey/formula-lang";
6
10
 
11
+ const program = "MID(MID(\"This is a test\", 1, 5), 1, 2)"
12
+ const transpiled = Transpile(program, ENGINES.ELASTICSEARCH)
13
+ console.log(transpiled)
14
+ ```
15
+ the output will be
16
+ ```
17
+ 'This is a test'.substring(1, 5).substring(1, 2)
7
18
  ```
8
- import { QFormulaLang, calculateAST } from "@qrvey/formula-lang"
9
- import { iRootAST, ENGINES, Transpiler } from '../../qrvey-lang/transpiler/lib/constants/interfaces';
10
-
11
- // input expression
12
- const expression = 'MID("this is an ex", 1)';
13
-
14
- // get the parser from QFormulaLang
15
- const parser = QFormulaLang.parser;
16
-
17
- // get the tree from input expression
18
- const tree = parser.parse(expression);
19
19
 
20
- // transform the tree into an understandable AST
21
- const ast = calculateAST(expression, tree.topNode);
20
+ ### TranspileAST function
22
21
 
23
- // transpile the code to a specific target Engine
24
- const transpilated = calculateTranspilation(ast, ENGINES.ELASTICSEARCH)
22
+ ```js
23
+ import { ENGINES, TranspileAST } from "@qrvey/formula-lang";
25
24
 
26
- calculateTranspilation = (ast: iRootAST, engine: ENGINES) => {
27
- let result;
28
- try {
29
- const transpiler = new Transpiler(ast, engine)
30
- result = transpiler.get()
31
- } catch (error) {
32
- result = JSON.stringify(error, undefined, 4)
25
+ const ast = {
26
+ "type": "Program",
27
+ "exp": "1 + 2",
28
+ "lang": "QrveyLang",
29
+ "version": "0.0.0",
30
+ "body": {
31
+ "operator": "+",
32
+ "type": "BinaryExpression",
33
+ "left": {
34
+ "type": "Literal",
35
+ "dataType": "number",
36
+ "value": 1
37
+ },
38
+ "right": {
39
+ "type": "Literal",
40
+ "dataType": "number",
41
+ "value": 2
42
+ }
33
43
  }
34
- return result
35
44
  }
45
+ const transpiled = TranspileAST(ast, ENGINES.ELASTICSEARCH)
46
+ console.log(transpiled)
36
47
  ```
37
-
38
- Use inside codemirror editor
48
+ the output will be
49
+ ```
50
+ (1 + 2)
39
51
  ```
52
+
53
+
54
+ ### codemirror editor
55
+ ```js
40
56
  import { EditorState } from "@codemirror/state";
41
- import { QFormula, calculateAST } from "@qrvey/formula-lang"
57
+ import { basicSetup } from "codemirror";
58
+ import { FormulaHighlight, calculateAST } from "@qrvey/formula-lang"
42
59
 
43
60
  // set the editor state with the QFormula plugin. This adds the support for
44
61
  this._state = EditorState.create({
45
- doc: options?.text ?? '',
62
+ doc: 'Test formula',
46
63
  extensions: [
47
64
  basicSetup,
48
- QFormula(),
65
+ FormulaHighlight(),
49
66
  ...
50
67
  ]
51
68
  })
@@ -0,0 +1,17 @@
1
+ import { AST_TYPES } from '../../src/constants';
2
+ import { calculateAST, FormulaLang } from '../../src';
3
+ import { BinaryExpressionAST } from '../../src/constants/interfaces';
4
+
5
+ describe('Parser tests suite', () => {
6
+ test('Should parse a single node', () => {
7
+ const program = '1 + 2';
8
+ const parser = FormulaLang.parser;
9
+ const { topNode } = parser.parse(program);
10
+ const ast = calculateAST(program, topNode);
11
+ expect(ast?.body).not.toBeUndefined();
12
+
13
+ const body = ast?.body as BinaryExpressionAST;
14
+ expect(body.operator).toBe('+');
15
+ expect(body.type).toBe(AST_TYPES.binaryExpression);
16
+ });
17
+ });
@@ -0,0 +1,120 @@
1
+ import { ENGINES } from '../../src/constants';
2
+ import { ProgramAST } from '../../src/constants/interfaces';
3
+ import { Transpiler } from '../../src/transpiler';
4
+ import { Transpile } from '../../src/index';
5
+
6
+ describe('Transpiler tests suit', () => {
7
+ test('Should resolve basic arithmetic operation', () => {
8
+ const AST: ProgramAST = {
9
+ type: 'Program',
10
+ exp: '1 + 1',
11
+ lang: 'QrveyLang',
12
+ version: '0.0.0',
13
+ body: {
14
+ operator: '+',
15
+ type: 'BinaryExpression',
16
+ left: {
17
+ type: 'Literal',
18
+ dataType: 'number',
19
+ value: 1,
20
+ },
21
+ right: {
22
+ type: 'Literal',
23
+ dataType: 'number',
24
+ value: 2,
25
+ },
26
+ },
27
+ } as ProgramAST;
28
+
29
+ const transpiler = new Transpiler(AST, ENGINES.ELASTICSEARCH);
30
+ const result = transpiler.get();
31
+ expect(result.expression).toBe('(1 + 2)');
32
+ });
33
+
34
+ test('Should resolve MID function', () => {
35
+ const AST: ProgramAST = {
36
+ type: 'Program',
37
+ exp: 'MID("string", 2, 3)',
38
+ lang: 'QrveyLang',
39
+ version: '0.0.0',
40
+ body: {
41
+ type: 'FunctionCall',
42
+ name: 'MID',
43
+ arguments: [
44
+ {
45
+ type: 'Literal',
46
+ dataType: 'string',
47
+ value: 'string',
48
+ },
49
+ {
50
+ type: 'Literal',
51
+ dataType: 'number',
52
+ value: 2,
53
+ },
54
+ {
55
+ type: 'Literal',
56
+ dataType: 'number',
57
+ value: 3,
58
+ },
59
+ ],
60
+ },
61
+ } as ProgramAST;
62
+
63
+ const transpiler = new Transpiler(AST, ENGINES.ELASTICSEARCH);
64
+ const result = transpiler.get();
65
+ expect(result.expression).toBe(`'string'.substring(2, 3)`);
66
+ });
67
+
68
+ test('Should throw a exception because is a invalid expression (function does not exists)', () => {
69
+ const AST: ProgramAST = {
70
+ type: 'Program',
71
+ exp: 'MID1("string", 2, 3)',
72
+ lang: 'QrveyLang',
73
+ version: '0.0.0',
74
+ body: {
75
+ type: 'FunctionCall',
76
+ name: 'MID1',
77
+ arguments: [
78
+ {
79
+ type: 'Literal',
80
+ dataType: 'string',
81
+ value: 'string',
82
+ },
83
+ {
84
+ type: 'Literal',
85
+ dataType: 'number',
86
+ value: 2,
87
+ },
88
+ {
89
+ type: 'Literal',
90
+ dataType: 'number',
91
+ value: 3,
92
+ },
93
+ ],
94
+ },
95
+ } as ProgramAST;
96
+
97
+ const transpiler = new Transpiler(AST, ENGINES.ELASTICSEARCH);
98
+ const result = transpiler.get();
99
+ expect(result.valid).toBe(false);
100
+ expect(result.error).not.toBeUndefined();
101
+ });
102
+
103
+ test('should transpile a function to elasticsearch', () => {
104
+ const program = 'MID(MID("This is a test", 1, 5), 1, 2)';
105
+ const result = Transpile(program, ENGINES.ELASTICSEARCH);
106
+ expect(result?.valid).toBe(true);
107
+ expect(result?.expression).toBe(
108
+ "'This is a test'.substring(1, 5).substring(1, 2)",
109
+ );
110
+ });
111
+
112
+ test('should transpile a function to snowflake', () => {
113
+ const program = 'MID(MID("This is a test", 1, 5), 1, 2)';
114
+ const result = Transpile(program, ENGINES.SNOWFLAKE);
115
+ expect(result?.valid).toBe(true);
116
+ expect(result?.expression).toBe(
117
+ "SUBSTRING(SUBSTRING('This is a test', 1, 5), 1, 2)",
118
+ );
119
+ });
120
+ });
@@ -0,0 +1,32 @@
1
+ export declare enum ENGINES {
2
+ ELASTICSEARCH = "elasticsearch",
3
+ SNOWFLAKE = "snowflake"
4
+ }
5
+ export declare enum AST_TYPES {
6
+ program = "Program",
7
+ binaryExpression = "BinaryExpression",
8
+ unaryExpression = "UnaryExpression",
9
+ functionCall = "FunctionCall",
10
+ variable = "Variable",
11
+ literal = "Literal"
12
+ }
13
+ export declare enum ERROR_LIST {
14
+ arguments = "arguments",
15
+ nulls = "nulls",
16
+ floatingPoint = "floatingPoint",
17
+ missingString = "missingString",
18
+ missingNumber = "missingNumber",
19
+ greaterThanZero = "greaterThanZero",
20
+ missingArg = "missingArg",
21
+ unknownFunction = "unknownFunction",
22
+ unknown = "unknown"
23
+ }
24
+ type iErrorsType = {
25
+ [key in ERROR_LIST]: IErrorDictionary;
26
+ };
27
+ interface IErrorDictionary {
28
+ message: string;
29
+ code: string;
30
+ }
31
+ export declare const ERROR_DICTIONARY: iErrorsType;
32
+ export {};
@@ -0,0 +1,65 @@
1
+ export var ENGINES;
2
+ (function (ENGINES) {
3
+ ENGINES["ELASTICSEARCH"] = "elasticsearch";
4
+ ENGINES["SNOWFLAKE"] = "snowflake";
5
+ // REDSHIFT = 'redshift',
6
+ })(ENGINES || (ENGINES = {}));
7
+ export var AST_TYPES;
8
+ (function (AST_TYPES) {
9
+ AST_TYPES["program"] = "Program";
10
+ AST_TYPES["binaryExpression"] = "BinaryExpression";
11
+ AST_TYPES["unaryExpression"] = "UnaryExpression";
12
+ AST_TYPES["functionCall"] = "FunctionCall";
13
+ AST_TYPES["variable"] = "Variable";
14
+ AST_TYPES["literal"] = "Literal";
15
+ })(AST_TYPES || (AST_TYPES = {}));
16
+ export var ERROR_LIST;
17
+ (function (ERROR_LIST) {
18
+ ERROR_LIST["arguments"] = "arguments";
19
+ ERROR_LIST["nulls"] = "nulls";
20
+ ERROR_LIST["floatingPoint"] = "floatingPoint";
21
+ ERROR_LIST["missingString"] = "missingString";
22
+ ERROR_LIST["missingNumber"] = "missingNumber";
23
+ ERROR_LIST["greaterThanZero"] = "greaterThanZero";
24
+ ERROR_LIST["missingArg"] = "missingArg";
25
+ ERROR_LIST["unknownFunction"] = "unknownFunction";
26
+ ERROR_LIST["unknown"] = "unknown";
27
+ })(ERROR_LIST || (ERROR_LIST = {}));
28
+ export const ERROR_DICTIONARY = {
29
+ [ERROR_LIST.arguments]: {
30
+ message: 'Ilegal number of arguments for function',
31
+ code: 'NUMBER_ARGS',
32
+ },
33
+ [ERROR_LIST.nulls]: {
34
+ message: 'Can not apply function with null values',
35
+ code: 'NULL_FOUNDS',
36
+ },
37
+ [ERROR_LIST.floatingPoint]: {
38
+ message: 'Invalid floating point operation to integer',
39
+ code: 'FLOATING_POINT',
40
+ },
41
+ [ERROR_LIST.missingString]: {
42
+ message: 'Element has to be a String',
43
+ code: 'MISSING_STRING',
44
+ },
45
+ [ERROR_LIST.missingNumber]: {
46
+ message: 'Element has to be a Number',
47
+ code: 'MISSING_NUMBER',
48
+ },
49
+ [ERROR_LIST.greaterThanZero]: {
50
+ message: 'Element has to be greater than 0',
51
+ code: 'GREATER_THAN_ZERO',
52
+ },
53
+ [ERROR_LIST.missingArg]: {
54
+ message: 'Missing arguments',
55
+ code: 'MISSING_ARG',
56
+ },
57
+ [ERROR_LIST.unknownFunction]: {
58
+ message: 'Unknown function',
59
+ code: 'UNKNOWN_FUNCTION',
60
+ },
61
+ [ERROR_LIST.unknown]: {
62
+ message: 'Unknown error',
63
+ code: 'UNKNOWN',
64
+ },
65
+ };
@@ -0,0 +1,60 @@
1
+ import { ENGINES, AST_TYPES } from '.';
2
+ interface FunctionElementDefinition {
3
+ identifier: string;
4
+ name: string;
5
+ }
6
+ export interface FunctionDefinition extends FunctionElementDefinition {
7
+ transpiler: TranspilerEnginesFunction;
8
+ parameters?: Array<FunctionParameters>;
9
+ }
10
+ type TranspilerEnginesFunction = {
11
+ [key in ENGINES]: Function;
12
+ };
13
+ interface FunctionParameters extends FunctionElementDefinition {
14
+ type: string;
15
+ optional: boolean;
16
+ validator: Array<ValidatorFunctionParameter>;
17
+ }
18
+ interface ValidatorFunctionParameter {
19
+ (args: any): ValidatorResponse;
20
+ }
21
+ export interface ValidatorResponse {
22
+ valid: boolean;
23
+ code?: string;
24
+ message?: string;
25
+ }
26
+ export interface TranspilationResponse {
27
+ valid: boolean;
28
+ expression?: string;
29
+ error?: any;
30
+ }
31
+ export interface FunctionList {
32
+ [identifier: string]: FunctionDefinition;
33
+ }
34
+ export interface CommonAST {
35
+ type: AST_TYPES;
36
+ }
37
+ export interface ProgramAST extends CommonAST {
38
+ exp: string;
39
+ lang: string;
40
+ version: string;
41
+ body?: CommonAST;
42
+ }
43
+ export interface UnaryExpressionAST extends CommonAST {
44
+ operator: string;
45
+ right: CommonAST;
46
+ }
47
+ export interface BinaryExpressionAST extends UnaryExpressionAST {
48
+ left: CommonAST;
49
+ }
50
+ export interface FunctionCallAST extends CommonAST {
51
+ name: string;
52
+ arguments: Array<CommonAST>;
53
+ }
54
+ export interface VariableAST extends CommonAST {
55
+ value: string | boolean | number | CommonAST[];
56
+ }
57
+ export interface LiteralAST extends VariableAST {
58
+ dataType: string;
59
+ }
60
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ export declare class BaseError extends Error {
2
+ code: string;
3
+ constructor(code: string, message: string);
4
+ toString(): {
5
+ code: string;
6
+ message: string;
7
+ };
8
+ }
9
+ export declare class GenericError extends BaseError {
10
+ constructor(message?: string);
11
+ }
@@ -0,0 +1,18 @@
1
+ import { ERROR_DICTIONARY, ERROR_LIST } from '../constants';
2
+ export class BaseError extends Error {
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ }
7
+ toString() {
8
+ return {
9
+ code: this.code,
10
+ message: this.message,
11
+ };
12
+ }
13
+ }
14
+ export class GenericError extends BaseError {
15
+ constructor(message = ERROR_DICTIONARY[ERROR_LIST.unknown].message) {
16
+ super(ERROR_DICTIONARY[ERROR_LIST.unknown].code, message);
17
+ }
18
+ }
@@ -0,0 +1,24 @@
1
+ import { ERROR_LIST } from '../constants';
2
+ export declare const errorRegex: ({
3
+ unknownFunction: {
4
+ regex: RegExp;
5
+ value: ERROR_LIST;
6
+ };
7
+ missingArguments: {
8
+ regex: RegExp;
9
+ value: ERROR_LIST;
10
+ };
11
+ } | {
12
+ arguments: {
13
+ regex: RegExp;
14
+ value: ERROR_LIST;
15
+ };
16
+ state: {
17
+ regex: RegExp;
18
+ value: ERROR_LIST;
19
+ };
20
+ null_pointer: {
21
+ regex: RegExp;
22
+ value: ERROR_LIST;
23
+ };
24
+ })[];
@@ -0,0 +1,26 @@
1
+ import { ERROR_LIST } from '../constants';
2
+ const reasonsRegex = {
3
+ unknownFunction: {
4
+ regex: /Unknown call \[.*\] with/,
5
+ value: ERROR_LIST.unknownFunction,
6
+ },
7
+ missingArguments: {
8
+ regex: /static method \[.*\] not found/,
9
+ value: ERROR_LIST.arguments,
10
+ },
11
+ };
12
+ const typesRegex = {
13
+ arguments: {
14
+ regex: /illegal_argument_exception/,
15
+ value: ERROR_LIST.arguments,
16
+ },
17
+ state: {
18
+ regex: /illegal_state_exception/,
19
+ value: ERROR_LIST.nulls,
20
+ },
21
+ null_pointer: {
22
+ regex: /null_pointer_exception/,
23
+ value: ERROR_LIST.nulls,
24
+ },
25
+ };
26
+ export const errorRegex = [reasonsRegex, typesRegex];
@@ -0,0 +1,5 @@
1
+ import { ValidatorResponse } from '../constants/interfaces';
2
+ export declare function engineErrors(messages: Array<string>, regexs: Array<object>): ValidatorResponse;
3
+ export declare function isStringParam(param: any): ValidatorResponse;
4
+ export declare function isNumberParam(param: any): ValidatorResponse;
5
+ export declare function isPositiveNumberParam(param: any): ValidatorResponse;
@@ -0,0 +1,38 @@
1
+ import { ERROR_LIST, ERROR_DICTIONARY } from '../constants';
2
+ export function engineErrors(messages, regexs) {
3
+ const loopParams = (obj, str) => {
4
+ for (const key in obj) {
5
+ const regex = new RegExp(obj[key].regex, 'g');
6
+ if (regex.test(str)) {
7
+ return obj[key].value;
8
+ }
9
+ }
10
+ };
11
+ let value;
12
+ messages.forEach((message) => {
13
+ regexs.forEach((regex) => {
14
+ value = loopParams(regex, message);
15
+ });
16
+ });
17
+ if (!value)
18
+ value = ERROR_LIST.unknown;
19
+ return Object.assign({ valid: false }, ERROR_DICTIONARY[value]);
20
+ }
21
+ export function isStringParam(param) {
22
+ const valid = typeof param === 'string';
23
+ if (!valid)
24
+ return Object.assign({ valid }, ERROR_DICTIONARY[ERROR_LIST.missingString]);
25
+ return { valid };
26
+ }
27
+ export function isNumberParam(param) {
28
+ const valid = typeof param === 'number';
29
+ if (!valid)
30
+ return Object.assign({ valid }, ERROR_DICTIONARY[ERROR_LIST.missingNumber]);
31
+ return { valid };
32
+ }
33
+ export function isPositiveNumberParam(param) {
34
+ const valid = param >= 0;
35
+ if (!valid)
36
+ return Object.assign({ valid }, ERROR_DICTIONARY[ERROR_LIST.greaterThanZero]);
37
+ return { valid };
38
+ }
@@ -0,0 +1,15 @@
1
+ import { ERROR_LIST } from '../constants';
2
+ export declare const errorRegex: {
3
+ missingArguments: {
4
+ regex: RegExp;
5
+ value: ERROR_LIST;
6
+ };
7
+ floatingPoint: {
8
+ regex: RegExp;
9
+ value: ERROR_LIST;
10
+ };
11
+ unknownFunction: {
12
+ regex: RegExp;
13
+ value: ERROR_LIST;
14
+ };
15
+ }[];
@@ -0,0 +1,16 @@
1
+ import { ERROR_LIST } from '../constants';
2
+ const reasonsRegex = {
3
+ missingArguments: {
4
+ regex: /not enough arguments for function/,
5
+ value: ERROR_LIST.arguments,
6
+ },
7
+ floatingPoint: {
8
+ regex: /Invalid floating point operation/,
9
+ value: ERROR_LIST.floatingPoint,
10
+ },
11
+ unknownFunction: {
12
+ regex: /Unknown function/,
13
+ value: ERROR_LIST.unknownFunction,
14
+ },
15
+ };
16
+ export const errorRegex = [reasonsRegex];
@@ -0,0 +1,2 @@
1
+ import { FunctionList } from '../constants/interfaces';
2
+ export declare const functionList: FunctionList;
@@ -0,0 +1,4 @@
1
+ import { MID } from './mid';
2
+ export const functionList = {
3
+ MID,
4
+ };
@@ -0,0 +1,2 @@
1
+ import { FunctionDefinition } from '../constants/interfaces';
2
+ export declare const MID: FunctionDefinition;
@@ -0,0 +1,47 @@
1
+ import * as errorList from '../errors';
2
+ export const MID = {
3
+ identifier: 'MID',
4
+ name: 'MID',
5
+ parameters: [
6
+ {
7
+ identifier: 'CURRENT',
8
+ name: 'current',
9
+ type: 'String',
10
+ optional: false,
11
+ validator: [errorList.isStringParam],
12
+ },
13
+ {
14
+ identifier: 'STARTING_AT',
15
+ name: 'starting_at',
16
+ type: 'Number',
17
+ optional: false,
18
+ validator: [errorList.isNumberParam, customIsPositive],
19
+ },
20
+ {
21
+ identifier: 'EXTRACT_LENGTH',
22
+ name: 'extract_length',
23
+ type: 'Number',
24
+ optional: true,
25
+ validator: [customIsPositive],
26
+ },
27
+ ],
28
+ transpiler: {
29
+ elasticsearch,
30
+ snowflake,
31
+ },
32
+ };
33
+ //TODO esto no sirve para nada, es solo un ejemplo de implementacion.
34
+ function customIsPositive(param) {
35
+ const isPositive = errorList.isPositiveNumberParam(param);
36
+ if (!isPositive.valid)
37
+ return isPositive;
38
+ return { valid: true };
39
+ }
40
+ function elasticsearch(current, start, length) {
41
+ const optional = length ? `, ${length}` : '';
42
+ return `${current}.substring(${start}${optional})`;
43
+ }
44
+ function snowflake(current, start, length) {
45
+ const optional = length ? `, ${length}` : '';
46
+ return `SUBSTRING(${current}, ${start}${optional})`;
47
+ }
@@ -1 +1 @@
1
- export * from "./generated/qformula.lang.js";
1
+ export * from './generated/qformula.lang.js';
@@ -1 +1 @@
1
- export * from "./generated/qformula.lang.js";
1
+ export * from './generated/qformula.lang.js';
package/dist/index.d.ts CHANGED
@@ -1 +1,8 @@
1
- export * from "./grammar/qformula.grammar";
1
+ import { ENGINES } from './constants/index';
2
+ import { TranspilationResponse } from './constants/interfaces';
3
+ export { QFormula as FormulaHighlight, QFormulaLang as FormulaLang, } from './grammar/qformula.grammar';
4
+ export { calculateAST } from './parser/json-parser';
5
+ export { ENGINES } from './constants/index';
6
+ export { TranspileAST } from './transpiler/index';
7
+ export { ProgramAST } from './constants/interfaces';
8
+ export declare function Transpile(program: string, engine: ENGINES): TranspilationResponse | undefined;
package/dist/index.js CHANGED
@@ -1 +1,17 @@
1
- export * from "./grammar/qformula.grammar";
1
+ import { QFormulaLang } from './grammar/qformula.grammar';
2
+ import { calculateAST } from './parser/json-parser';
3
+ import { TranspileAST } from './transpiler/index';
4
+ export { QFormula as FormulaHighlight, QFormulaLang as FormulaLang, } from './grammar/qformula.grammar';
5
+ export { calculateAST } from './parser/json-parser';
6
+ export { ENGINES } from './constants/index';
7
+ export { TranspileAST } from './transpiler/index';
8
+ export function Transpile(program, engine) {
9
+ const parser = QFormulaLang.parser;
10
+ const { topNode } = parser.parse(program);
11
+ if (!topNode)
12
+ return;
13
+ const ast = calculateAST(program, topNode);
14
+ if (!ast)
15
+ return;
16
+ return TranspileAST(ast, engine);
17
+ }
@@ -0,0 +1,29 @@
1
+ export interface AstNode {
2
+ type: string;
3
+ }
4
+ export interface AstBinaryExpressionNode extends AstNode {
5
+ operator: string;
6
+ left: AstNode;
7
+ right: AstNode;
8
+ }
9
+ export interface AstUnaryExpressionNode extends AstNode {
10
+ operator: string;
11
+ right: AstNode;
12
+ }
13
+ export interface AstValueNode extends AstNode {
14
+ value: string | boolean | number | AstNode[];
15
+ dataType: string;
16
+ }
17
+ export interface AstVariableNode extends AstNode {
18
+ value: string;
19
+ }
20
+ export interface AstFunctionNode extends AstNode {
21
+ name: string;
22
+ arguments: AstNode[];
23
+ }
24
+ export interface AstProgramNode extends AstNode {
25
+ exp: string;
26
+ lang: string;
27
+ version: string;
28
+ body: AstNode;
29
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { SyntaxNode } from '@lezer/common';
2
+ import { ProgramAST } from '../constants/interfaces';
3
+ export declare function calculateAST(program: string, startNode: SyntaxNode): ProgramAST | undefined;
@@ -0,0 +1,92 @@
1
+ import { AST_TYPES } from '../constants';
2
+ import { getNodeValue } from '../utils';
3
+ const PARSER_VERSION = '0.0.0';
4
+ const LANG_NAME = 'QrveyLang';
5
+ export function calculateAST(program, startNode) {
6
+ if (!startNode)
7
+ return;
8
+ return {
9
+ type: AST_TYPES.program,
10
+ exp: program,
11
+ lang: LANG_NAME,
12
+ version: PARSER_VERSION,
13
+ body: transformNode(program, startNode),
14
+ };
15
+ }
16
+ function transformNode(program, node) {
17
+ switch (node.name) {
18
+ case 'BinaryExpression':
19
+ return transformBinaryExpression(program, node);
20
+ case 'UnaryExpression':
21
+ return transformUnaryExpression(program, node);
22
+ case 'Function':
23
+ return transformFunctionExpression(program, node);
24
+ case 'Number':
25
+ return {
26
+ type: 'Literal',
27
+ dataType: 'number',
28
+ value: parseFloat(getNodeValue(program, node)),
29
+ };
30
+ case 'String':
31
+ return {
32
+ type: 'Literal',
33
+ dataType: 'string',
34
+ value: getNodeValue(program, node).slice(1, -1),
35
+ };
36
+ case 'Boolean':
37
+ return {
38
+ type: 'Literal',
39
+ dataType: 'boolean',
40
+ value: getNodeValue(program, node) === 'true',
41
+ };
42
+ case 'Array':
43
+ return {
44
+ type: 'Literal',
45
+ dataType: 'array',
46
+ value: node.getChildren('Value').map((node) => {
47
+ return transformNode(program, node);
48
+ }),
49
+ };
50
+ case 'Variable':
51
+ return {
52
+ type: 'Variable',
53
+ value: getNodeValue(program, node).slice(1, -1),
54
+ };
55
+ default:
56
+ return node.firstChild
57
+ ? transformNode(program, node.firstChild)
58
+ : undefined;
59
+ }
60
+ }
61
+ function transformBinaryExpression(program, node) {
62
+ const [left, right] = node.getChildren('Expression');
63
+ const operator = node.getChild('ArithOp');
64
+ return {
65
+ operator: getNodeValue(program, operator),
66
+ type: AST_TYPES.binaryExpression,
67
+ left: transformNode(program, left),
68
+ right: transformNode(program, right),
69
+ };
70
+ }
71
+ function transformUnaryExpression(program, node) {
72
+ const [right] = node.getChildren('Expression');
73
+ const operator = node.getChild('ArithOp');
74
+ return {
75
+ operator: getNodeValue(program, operator),
76
+ type: AST_TYPES.unaryExpression,
77
+ right: transformNode(program, right),
78
+ };
79
+ }
80
+ function transformFunctionExpression(program, node) {
81
+ const args = node.getChild('FunctionArguments');
82
+ const nodeValue = node.getChild('FunctionIdentifier');
83
+ return {
84
+ type: AST_TYPES.functionCall,
85
+ name: getNodeValue(program, nodeValue),
86
+ arguments: args
87
+ ? args
88
+ .getChildren('Expression')
89
+ .map((node) => transformNode(program, node))
90
+ : [],
91
+ };
92
+ }
@@ -0,0 +1,15 @@
1
+ import { ENGINES } from '../constants';
2
+ import { CommonAST, ProgramAST, BinaryExpressionAST, UnaryExpressionAST, FunctionCallAST, LiteralAST, FunctionDefinition, TranspilationResponse } from '../constants/interfaces';
3
+ export declare class Transpiler {
4
+ readonly engine: ENGINES;
5
+ private ast;
6
+ constructor(ast: ProgramAST, engine: ENGINES);
7
+ get(): TranspilationResponse;
8
+ processNode(expression: CommonAST): string;
9
+ literal({ value, dataType }: LiteralAST): string | number | boolean | CommonAST[];
10
+ unaryExpression({ operator, right }: UnaryExpressionAST): string;
11
+ binaryExpression({ left, operator, right }: BinaryExpressionAST): string;
12
+ functionCall(node: FunctionCallAST): string;
13
+ executer(func: FunctionDefinition, args: Array<any>, node: FunctionCallAST): string;
14
+ }
15
+ export declare function TranspileAST(ast: ProgramAST, engine: ENGINES): TranspilationResponse;
@@ -0,0 +1,77 @@
1
+ import { AST_TYPES, ERROR_DICTIONARY, ERROR_LIST } from '../constants';
2
+ import { GenericError } from '../errors/definitions';
3
+ import { functionList } from '../functions';
4
+ export class Transpiler {
5
+ constructor(ast, engine) {
6
+ this.ast = ast;
7
+ this.engine = engine;
8
+ }
9
+ get() {
10
+ try {
11
+ const { body } = this.ast;
12
+ return {
13
+ valid: body !== undefined,
14
+ expression: body ? this.processNode(body) : undefined,
15
+ };
16
+ }
17
+ catch (error) {
18
+ return {
19
+ valid: false,
20
+ error: new GenericError(error.message),
21
+ };
22
+ }
23
+ }
24
+ processNode(expression) {
25
+ const { type } = expression;
26
+ const nodes = {
27
+ [AST_TYPES.variable]: this.literal,
28
+ [AST_TYPES.literal]: this.literal,
29
+ [AST_TYPES.unaryExpression]: this.unaryExpression,
30
+ [AST_TYPES.binaryExpression]: this.binaryExpression,
31
+ [AST_TYPES.functionCall]: this.functionCall,
32
+ };
33
+ return nodes[type].call(this, expression);
34
+ }
35
+ literal({ value, dataType }) {
36
+ if (dataType === 'string')
37
+ value = `'${value}'`;
38
+ return value;
39
+ }
40
+ unaryExpression({ operator, right }) {
41
+ const rightResult = this.processNode(right);
42
+ return `(${operator} ${rightResult})`;
43
+ }
44
+ binaryExpression({ left, operator, right }) {
45
+ const leftResult = this.processNode(left);
46
+ const rightResult = this.processNode(right);
47
+ return `(${leftResult} ${operator} ${rightResult})`;
48
+ }
49
+ functionCall(node) {
50
+ const { name, arguments: args } = node;
51
+ const func = functionList[name];
52
+ if (!func)
53
+ throw Object.assign({ node }, ERROR_DICTIONARY[ERROR_LIST.unknownFunction]);
54
+ const parameters = args.map((param) => this.processNode(param));
55
+ return this.executer(func, parameters, node);
56
+ }
57
+ executer(func, args, node) {
58
+ const params = (func === null || func === void 0 ? void 0 : func.parameters) ? [...func.parameters] : [];
59
+ for (const [index, element] of params.entries()) {
60
+ const value = args[index];
61
+ if (!value && element.optional)
62
+ continue;
63
+ if (!value && !element.optional)
64
+ throw Object.assign({ node }, ERROR_DICTIONARY[ERROR_LIST.missingArg]);
65
+ element.validator.forEach((validatorFunction) => {
66
+ const validator = validatorFunction(value);
67
+ if (!validator.valid)
68
+ throw Object.assign({ node }, validator);
69
+ });
70
+ }
71
+ return func.transpiler[this.engine](...args);
72
+ }
73
+ }
74
+ export function TranspileAST(ast, engine) {
75
+ const transpiler = new Transpiler(ast, engine);
76
+ return transpiler.get();
77
+ }
@@ -0,0 +1,4 @@
1
+ export declare function getNodeValue(str: string, { from, to }: {
2
+ from: number;
3
+ to: number;
4
+ }): string;
@@ -0,0 +1,3 @@
1
+ export function getNodeValue(str, { from, to }) {
2
+ return str.slice(from, to);
3
+ }
@@ -0,0 +1,4 @@
1
+ export declare function getNodeValue(str: string, { from, to }: {
2
+ from: number;
3
+ to: number;
4
+ }): string;
@@ -0,0 +1,3 @@
1
+ export function getNodeValue(str, { from, to }) {
2
+ return str.slice(from, to);
3
+ }
package/package.json CHANGED
@@ -1,16 +1,21 @@
1
1
  {
2
2
  "name": "@qrvey/formula-lang",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "description": "QFormula support for qrvey projects",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "type": "module",
8
8
  "scripts": {
9
- "test": "echo \"Error: no test specified\" && exit 1",
9
+ "test": "jest __tests__",
10
10
  "build": "tsc",
11
11
  "build:watch": "tsc -w",
12
12
  "prepublishOnly": "npm run build",
13
- "prepare-lang": "rollup -c --bundleConfigAsCjs"
13
+ "cm": "cz",
14
+ "prepare-lang": "rollup -c --bundleConfigAsCjs",
15
+ "lint": "eslint . --ext .ts",
16
+ "lint:fix": "eslint . --fix --ext .ts",
17
+ "prettier:fix": "prettier --write ./src",
18
+ "prepare:husky": "husky install"
14
19
  },
15
20
  "repository": {
16
21
  "type": "git",
@@ -25,14 +30,35 @@
25
30
  "author": "Qrvey Inc",
26
31
  "homepage": "https://bitbucket.org/qrvey/qrvey_formula_lang#readme",
27
32
  "dependencies": {
28
- "@codemirror/language": "^6.6.0",
29
- "@lezer/highlight": "^1.1.4"
33
+ "@codemirror/language": "6.6.0",
34
+ "@lezer/common": "1.0.2",
35
+ "@lezer/highlight": "1.1.4"
30
36
  },
31
37
  "devDependencies": {
38
+ "@commitlint/cli": "^17.5.1",
39
+ "@commitlint/config-conventional": "^17.4.4",
32
40
  "@lezer/generator": "^1.2.2",
41
+ "@types/jest": "^29.5.0",
42
+ "@typescript-eslint/eslint-plugin": "^5.58.0",
43
+ "@typescript-eslint/parser": "^5.58.0",
44
+ "commitizen": "^4.3.0",
45
+ "cz-conventional-changelog": "^3.3.0",
46
+ "eslint": "^8.38.0",
47
+ "eslint-config-prettier": "^8.8.0",
48
+ "eslint-plugin-prettier": "^4.2.1",
49
+ "husky": "^8.0.3",
50
+ "jest": "^29.5.0",
51
+ "lint-staged": "^13.2.1",
52
+ "prettier": "^2.8.7",
33
53
  "rollup": "^3.20.2",
34
54
  "rollup-plugin-dts": "^5.3.0",
35
55
  "rollup-plugin-ts": "^3.2.0",
56
+ "ts-jest": "^29.1.0",
36
57
  "typescript": "^5.0.4"
58
+ },
59
+ "config": {
60
+ "commitizen": {
61
+ "path": "./node_modules/cz-conventional-changelog"
62
+ }
37
63
  }
38
- }
64
+ }