@swaggerexpert/arazzo-criterion 1.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.
Files changed (37) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +20 -0
  3. package/README.md +406 -0
  4. package/SECURITY.md +15 -0
  5. package/cjs/apg-lite.cjs +1223 -0
  6. package/cjs/errors/ArazzoCriterionError.cjs +46 -0
  7. package/cjs/errors/ArazzoCriterionEvaluateError.cjs +10 -0
  8. package/cjs/errors/ArazzoCriterionParseError.cjs +10 -0
  9. package/cjs/evaluate/index.cjs +139 -0
  10. package/cjs/grammar.cjs +1111 -0
  11. package/cjs/index.cjs +83 -0
  12. package/cjs/parse/callbacks/cst.cjs +30 -0
  13. package/cjs/parse/index.cjs +42 -0
  14. package/cjs/parse/trace/Expectations.cjs +12 -0
  15. package/cjs/parse/trace/Trace.cjs +37 -0
  16. package/cjs/parse/translators/ASTTranslator/index.cjs +17 -0
  17. package/cjs/parse/translators/ASTTranslator/transformers.cjs +203 -0
  18. package/cjs/parse/translators/CSTTranslator.cjs +49 -0
  19. package/cjs/parse/translators/XMLTranslator.cjs +14 -0
  20. package/cjs/test/index.cjs +20 -0
  21. package/es/errors/ArazzoCriterionError.mjs +40 -0
  22. package/es/errors/ArazzoCriterionEvaluateError.mjs +3 -0
  23. package/es/errors/ArazzoCriterionParseError.mjs +3 -0
  24. package/es/evaluate/index.mjs +132 -0
  25. package/es/grammar.mjs +1105 -0
  26. package/es/index.mjs +11 -0
  27. package/es/parse/callbacks/cst.mjs +24 -0
  28. package/es/parse/index.mjs +35 -0
  29. package/es/parse/trace/Expectations.mjs +6 -0
  30. package/es/parse/trace/Trace.mjs +30 -0
  31. package/es/parse/translators/ASTTranslator/index.mjs +9 -0
  32. package/es/parse/translators/ASTTranslator/transformers.mjs +195 -0
  33. package/es/parse/translators/CSTTranslator.mjs +42 -0
  34. package/es/parse/translators/XMLTranslator.mjs +7 -0
  35. package/es/test/index.mjs +13 -0
  36. package/package.json +95 -0
  37. package/types/index.d.ts +283 -0
package/es/index.mjs ADDED
@@ -0,0 +1,11 @@
1
+ export { default as Grammar } from "./grammar.mjs";
2
+ export { default as parse } from "./parse/index.mjs";
3
+ export { default as test } from "./test/index.mjs";
4
+ export { default as evaluate } from "./evaluate/index.mjs";
5
+ export { default as CSTTranslator } from "./parse/translators/CSTTranslator.mjs";
6
+ export { default as ASTTranslator } from "./parse/translators/ASTTranslator/index.mjs";
7
+ export { default as XMLTranslator } from "./parse/translators/XMLTranslator.mjs";
8
+ export { default as Trace } from "./parse/trace/Trace.mjs";
9
+ export { default as ArazzoCriterionError } from "./errors/ArazzoCriterionError.mjs";
10
+ export { default as ArazzoCriterionParseError } from "./errors/ArazzoCriterionParseError.mjs";
11
+ export { default as ArazzoCriterionEvaluateError } from "./errors/ArazzoCriterionEvaluateError.mjs";
@@ -0,0 +1,24 @@
1
+ import { utilities, identifiers } from 'apg-lite';
2
+ const cst = nodeType => {
3
+ return (state, chars, phraseIndex, phraseLength, data) => {
4
+ if (state === identifiers.SEM_PRE) {
5
+ const node = {
6
+ type: nodeType,
7
+ text: utilities.charsToString(chars, phraseIndex, phraseLength),
8
+ start: phraseIndex,
9
+ length: phraseLength,
10
+ children: []
11
+ };
12
+ if (data.stack.length > 0) {
13
+ data.stack[data.stack.length - 1].children.push(node);
14
+ } else {
15
+ data.root = node;
16
+ }
17
+ data.stack.push(node);
18
+ }
19
+ if (state === identifiers.SEM_POST) {
20
+ data.stack.pop();
21
+ }
22
+ };
23
+ };
24
+ export default cst;
@@ -0,0 +1,35 @@
1
+ import { Parser, Stats } from 'apg-lite';
2
+ import Grammar from "../grammar.mjs";
3
+ import ASTTranslator from "./translators/ASTTranslator/index.mjs";
4
+ import Trace from "./trace/Trace.mjs";
5
+ import ArazzoCriterionParseError from "../errors/ArazzoCriterionParseError.mjs";
6
+ const grammar = new Grammar();
7
+ const parse = (condition, {
8
+ startRule = 'condition',
9
+ stats = false,
10
+ trace = false,
11
+ translator = new ASTTranslator()
12
+ } = {}) => {
13
+ if (typeof condition !== 'string') {
14
+ throw new TypeError('Criterion condition must be a string');
15
+ }
16
+ try {
17
+ const parser = new Parser();
18
+ if (translator) parser.ast = translator;
19
+ if (stats) parser.stats = new Stats();
20
+ if (trace) parser.trace = new Trace();
21
+ const result = parser.parse(grammar, startRule, condition);
22
+ return {
23
+ result,
24
+ tree: result.success && translator ? parser.ast.getTree() : undefined,
25
+ stats: parser.stats,
26
+ trace: parser.trace
27
+ };
28
+ } catch (error) {
29
+ throw new ArazzoCriterionParseError('Unexpected error during Arazzo Criterion parsing', {
30
+ cause: error,
31
+ condition
32
+ });
33
+ }
34
+ };
35
+ export default parse;
@@ -0,0 +1,6 @@
1
+ class Expectations extends Array {
2
+ toString() {
3
+ return this.map(c => `"${String(c)}"`).join(', ');
4
+ }
5
+ }
6
+ export default Expectations;
@@ -0,0 +1,30 @@
1
+ import { Trace as BaseTrace } from 'apg-lite';
2
+ import Expectations from "./Expectations.mjs";
3
+ class Trace extends BaseTrace {
4
+ inferExpectations() {
5
+ const lines = this.displayTrace().split('\n');
6
+ const expectations = new Set();
7
+ let lastMatchedIndex = -1;
8
+ for (let i = 0; i < lines.length; i++) {
9
+ const line = lines[i];
10
+
11
+ // capture the max match line (first one that ends in a single character match)
12
+ if (line.includes('M|')) {
13
+ const textMatch = line.match(/]'(.*)'$/);
14
+ if (textMatch && textMatch[1]) {
15
+ lastMatchedIndex = i;
16
+ }
17
+ }
18
+
19
+ // collect terminal failures after the deepest successful match
20
+ if (i > lastMatchedIndex) {
21
+ const terminalFailMatch = line.match(/N\|\[TLS\(([^)]+)\)]/);
22
+ if (terminalFailMatch) {
23
+ expectations.add(terminalFailMatch[1]);
24
+ }
25
+ }
26
+ }
27
+ return new Expectations(...expectations);
28
+ }
29
+ }
30
+ export default Trace;
@@ -0,0 +1,9 @@
1
+ import CSTTranslator from "../CSTTranslator.mjs";
2
+ import transformers, { transformCSTtoAST } from "./transformers.mjs";
3
+ class ASTTranslator extends CSTTranslator {
4
+ getTree() {
5
+ const cst = super.getTree();
6
+ return transformCSTtoAST(cst, transformers);
7
+ }
8
+ }
9
+ export default ASTTranslator;
@@ -0,0 +1,195 @@
1
+ import { parse as parseRuntimeExpression } from '@swaggerexpert/arazzo-runtime-expression';
2
+ import parse from "../../index.mjs";
3
+ export const transformCSTtoAST = (node, transformerMap) => {
4
+ const transformer = transformerMap[node.type];
5
+ if (!transformer) {
6
+ throw new Error(`No transformer for CST node type: ${node.type}`);
7
+ }
8
+ return transformer(node);
9
+ };
10
+
11
+ // A basic-expr is not a registered CST rule, so its alternatives
12
+ // (paren-expr / comparison-expr / test-expr) and the literal alternatives
13
+ // (number / string / boolean / null) surface directly as children.
14
+ const OPERAND_TYPES = new Set(['paren-expr', 'comparison-expr', 'test-expr', 'runtime-expression-operand', 'number', 'string', 'boolean', 'null']);
15
+
16
+ /**
17
+ * Split an operand token at the boundary between its runtime-expression base and the
18
+ * trailing criterion navigation, returning both as text. The base is the longest
19
+ * leading substring that parses as a runtime expression.
20
+ *
21
+ * The grammar cannot find this boundary itself - a runtime expression and the
22
+ * criterion accessors both use "." - so it is resolved here by delegating to
23
+ * @swaggerexpert/arazzo-runtime-expression.
24
+ */
25
+ const splitAtRuntimeExpression = operand => {
26
+ const full = parseRuntimeExpression(operand);
27
+ if (full.result.success) {
28
+ return {
29
+ expressionText: operand,
30
+ navigationText: ''
31
+ };
32
+ }
33
+
34
+ // A failing parse reports how far it matched; walk down from there to the
35
+ // longest prefix that parses cleanly (robust against stopping mid-rule).
36
+ for (let n = full.result.maxMatched; n > 0; n -= 1) {
37
+ if (parseRuntimeExpression(operand.slice(0, n)).result.success) {
38
+ return {
39
+ expressionText: operand.slice(0, n),
40
+ navigationText: operand.slice(n)
41
+ };
42
+ }
43
+ }
44
+ throw new Error(`Operand is not a valid runtime expression: "${operand}"`);
45
+ };
46
+
47
+ /**
48
+ * Parse a criterion navigation remainder (e.g. ".data[0].id") into a MemberAccess /
49
+ * IndexAccess path by parsing it against the grammar's `runtime-expression-navigation`
50
+ * rule (a secondary entry point); the ASTTranslator produces the path array directly.
51
+ * Throws if the remainder is not a valid accessor sequence.
52
+ */
53
+ const parseNavigation = navigationText => {
54
+ const {
55
+ result,
56
+ tree
57
+ } = parse(navigationText, {
58
+ startRule: 'runtime-expression-navigation'
59
+ });
60
+ if (!result.success) {
61
+ throw new Error(`Invalid criterion navigation: "${navigationText}"`);
62
+ }
63
+ return tree;
64
+ };
65
+
66
+ /**
67
+ * Parse an operand token into its runtime-expression base and criterion navigation,
68
+ * both already parsed. `navigation` is an empty array when there are no accessors.
69
+ */
70
+ const parseOperand = operand => {
71
+ const {
72
+ expressionText,
73
+ navigationText
74
+ } = splitAtRuntimeExpression(operand);
75
+ const runtimeExpression = {
76
+ type: 'RuntimeExpression',
77
+ text: expressionText,
78
+ expression: parseRuntimeExpression(expressionText).tree
79
+ };
80
+ const navigation = navigationText === '' ? [] : parseNavigation(navigationText);
81
+ return {
82
+ runtimeExpression,
83
+ navigation
84
+ };
85
+ };
86
+ const foldLeftAssociative = (operands, operator) => operands.reduce((left, right) => ({
87
+ type: 'LogicalExpression',
88
+ operator,
89
+ left,
90
+ right
91
+ }));
92
+ const transformers = {
93
+ ['condition'](node) {
94
+ const child = node.children.find(c => c.type === 'logical-or-expr');
95
+ return transformCSTtoAST(child, transformers);
96
+ },
97
+ ['logical-or-expr'](node) {
98
+ const operands = node.children.filter(c => c.type === 'logical-and-expr').map(c => transformCSTtoAST(c, transformers));
99
+ return operands.length === 1 ? operands[0] : foldLeftAssociative(operands, '||');
100
+ },
101
+ ['logical-and-expr'](node) {
102
+ const operands = node.children.filter(c => OPERAND_TYPES.has(c.type)).map(c => transformCSTtoAST(c, transformers));
103
+ return operands.length === 1 ? operands[0] : foldLeftAssociative(operands, '&&');
104
+ },
105
+ ['paren-expr'](node) {
106
+ const inner = node.children.find(c => c.type === 'logical-or-expr');
107
+ const argument = transformCSTtoAST(inner, transformers);
108
+ const negated = node.children.some(c => c.type === 'logical-not-op');
109
+ return negated ? {
110
+ type: 'UnaryExpression',
111
+ operator: '!',
112
+ argument
113
+ } : argument;
114
+ },
115
+ ['test-expr'](node) {
116
+ const operandNode = node.children.find(c => OPERAND_TYPES.has(c.type));
117
+ const argument = transformCSTtoAST(operandNode, transformers);
118
+ const negated = node.children.some(c => c.type === 'logical-not-op');
119
+ return negated ? {
120
+ type: 'UnaryExpression',
121
+ operator: '!',
122
+ argument
123
+ } : argument;
124
+ },
125
+ ['comparison-expr'](node) {
126
+ const operands = node.children.filter(c => OPERAND_TYPES.has(c.type));
127
+ const opNode = node.children.find(c => c.type === 'comparison-op');
128
+ return {
129
+ type: 'BinaryExpression',
130
+ operator: opNode.text,
131
+ left: transformCSTtoAST(operands[0], transformers),
132
+ right: transformCSTtoAST(operands[1], transformers)
133
+ };
134
+ },
135
+ ['runtime-expression-operand'](node) {
136
+ const {
137
+ runtimeExpression,
138
+ navigation
139
+ } = parseOperand(node.text);
140
+ if (navigation.length === 0) return runtimeExpression;
141
+ return {
142
+ type: 'RuntimeExpressionNavigation',
143
+ expression: runtimeExpression,
144
+ navigation
145
+ };
146
+ },
147
+ ['runtime-expression-navigation'](node) {
148
+ return node.children.filter(c => c.type === 'member-access' || c.type === 'index-access').map(c => transformCSTtoAST(c, transformers));
149
+ },
150
+ ['member-access'](node) {
151
+ const nameNode = node.children.find(c => c.type === 'member-name');
152
+ return {
153
+ type: 'MemberAccess',
154
+ name: nameNode.text
155
+ };
156
+ },
157
+ ['index-access'](node) {
158
+ const indexNode = node.children.find(c => c.type === 'index');
159
+ return {
160
+ type: 'IndexAccess',
161
+ value: Number(indexNode.text)
162
+ };
163
+ },
164
+ ['number'](node) {
165
+ return {
166
+ type: 'Literal',
167
+ valueType: 'number',
168
+ value: Number(node.text)
169
+ };
170
+ },
171
+ ['string'](node) {
172
+ // strip surrounding single quotes, then collapse doubled '' into a single '
173
+ const inner = node.text.slice(1, -1).replace(/''/g, "'");
174
+ return {
175
+ type: 'Literal',
176
+ valueType: 'string',
177
+ value: inner
178
+ };
179
+ },
180
+ ['boolean'](node) {
181
+ return {
182
+ type: 'Literal',
183
+ valueType: 'boolean',
184
+ value: node.text === 'true'
185
+ };
186
+ },
187
+ ['null'](node) {
188
+ return {
189
+ type: 'Literal',
190
+ valueType: 'null',
191
+ value: null
192
+ };
193
+ }
194
+ };
195
+ export default transformers;
@@ -0,0 +1,42 @@
1
+ import { Ast as AST } from 'apg-lite';
2
+ import cstCallback from "../callbacks/cst.mjs";
3
+ class CSTTranslator extends AST {
4
+ constructor() {
5
+ super();
6
+
7
+ // logical / comparison spine
8
+ this.callbacks['condition'] = cstCallback('condition');
9
+ this.callbacks['logical-or-expr'] = cstCallback('logical-or-expr');
10
+ this.callbacks['logical-and-expr'] = cstCallback('logical-and-expr');
11
+ this.callbacks['paren-expr'] = cstCallback('paren-expr');
12
+ this.callbacks['comparison-expr'] = cstCallback('comparison-expr');
13
+ this.callbacks['test-expr'] = cstCallback('test-expr');
14
+ this.callbacks['logical-not-op'] = cstCallback('logical-not-op');
15
+ this.callbacks['comparison-op'] = cstCallback('comparison-op');
16
+
17
+ // comparable operand (runtime expression + navigation), captured as one token
18
+ this.callbacks['runtime-expression-operand'] = cstCallback('runtime-expression-operand');
19
+
20
+ // navigation (secondary entry point; parses the accessor remainder of an operand)
21
+ this.callbacks['runtime-expression-navigation'] = cstCallback('runtime-expression-navigation');
22
+ this.callbacks['member-access'] = cstCallback('member-access');
23
+ this.callbacks['index-access'] = cstCallback('index-access');
24
+ this.callbacks['member-name'] = cstCallback('member-name');
25
+ this.callbacks['index'] = cstCallback('index');
26
+
27
+ // literals
28
+ this.callbacks['number'] = cstCallback('number');
29
+ this.callbacks['string'] = cstCallback('string');
30
+ this.callbacks['boolean'] = cstCallback('boolean');
31
+ this.callbacks['null'] = cstCallback('null');
32
+ }
33
+ getTree() {
34
+ const data = {
35
+ stack: [],
36
+ root: null
37
+ };
38
+ this.translate(data);
39
+ return data.root;
40
+ }
41
+ }
42
+ export default CSTTranslator;
@@ -0,0 +1,7 @@
1
+ import CSTTranslator from "./CSTTranslator.mjs";
2
+ class XMLTranslator extends CSTTranslator {
3
+ getTree() {
4
+ return this.toXml();
5
+ }
6
+ }
7
+ export default XMLTranslator;
@@ -0,0 +1,13 @@
1
+ import parse from "../parse/index.mjs";
2
+ const test = condition => {
3
+ if (typeof condition !== 'string') return false;
4
+ try {
5
+ const {
6
+ result
7
+ } = parse(condition);
8
+ return result.success;
9
+ } catch {
10
+ return false;
11
+ }
12
+ };
13
+ export default test;
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "@swaggerexpert/arazzo-criterion",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "1.0.0",
7
+ "description": "Arazzo Criterion Object (simple type) parser, validator and evaluator.",
8
+ "main": "./cjs/index.cjs",
9
+ "types": "./types/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./types/index.d.ts",
13
+ "import": "./es/index.mjs",
14
+ "require": "./cjs/index.cjs"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "watch": {
19
+ "test": "{src,test}/*.js"
20
+ },
21
+ "scripts": {
22
+ "prepublishOnly": "npm run build",
23
+ "grammar:compile": "node ./scripts/apg-js.js --lite --in=./src/grammar.bnf --out=./src/grammar.js",
24
+ "build": "npm run grammar:compile && npm run build:es && npm run build:cjs && npm run build:cjs:apg-lite",
25
+ "build:es": "cross-env BABEL_ENV=es babel src --out-dir es --extensions '.js' --out-file-extension '.mjs'",
26
+ "build:cjs": "cross-env BABEL_ENV=cjs babel src --out-dir cjs --extensions '.js' --out-file-extension '.cjs'",
27
+ "build:cjs:apg-lite": "cross-env BABEL_ENV=cjs babel node_modules/apg-lite/lib/parser.js --out-file ./cjs/apg-lite.cjs",
28
+ "test": "cross-env UPDATE_SNAPSHOT=new mocha",
29
+ "test:watch": "npm-watch test",
30
+ "watch": "npm-watch"
31
+ },
32
+ "engines": {
33
+ "node": ">=16.14.0"
34
+ },
35
+ "type": "module",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/swaggerexpert/arazzo-criterion.git"
39
+ },
40
+ "keywords": [
41
+ "arazzo",
42
+ "criterion",
43
+ "simple",
44
+ "condition",
45
+ "parser",
46
+ "validator",
47
+ "evaluator"
48
+ ],
49
+ "author": "Vladimír Gorej <vladimir.gorej@gmail.com>",
50
+ "license": "Apache-2.0",
51
+ "bugs": {
52
+ "url": "https://github.com/swaggerexpert/arazzo-criterion/issues"
53
+ },
54
+ "homepage": "https://github.com/swaggerexpert/arazzo-criterion#readme",
55
+ "files": [
56
+ "es/",
57
+ "cjs/",
58
+ "types/",
59
+ "LICENSE",
60
+ "NOTICE",
61
+ "package.json",
62
+ "README.md",
63
+ "SECURITY.md"
64
+ ],
65
+ "dependencies": {
66
+ "@swaggerexpert/arazzo-runtime-expression": "^3.1.0",
67
+ "apg-lite": "^1.0.5"
68
+ },
69
+ "devDependencies": {
70
+ "@babel/cli": "=8.0.1",
71
+ "@babel/core": "=8.0.1",
72
+ "@babel/preset-env": "=8.0.2",
73
+ "@commitlint/cli": "=20.5.3",
74
+ "@commitlint/config-conventional": "=20.5.3",
75
+ "@semantic-release/commit-analyzer": "^13.0.1",
76
+ "@semantic-release/git": "^10.0.1",
77
+ "@semantic-release/github": "^12.0.2",
78
+ "@semantic-release/npm": "^13.1.3",
79
+ "@semantic-release/release-notes-generator": "^14.1.0",
80
+ "apg-js": "^4.4.0",
81
+ "babel-plugin-module-resolver": "^5.0.2",
82
+ "chai": "=6.2.2",
83
+ "cross-env": "^10.0.0",
84
+ "husky": "=9.1.7",
85
+ "mocha": "=11.7.6",
86
+ "mocha-expect-snapshot": "^8.0.0",
87
+ "npm-watch": "^0.13.0",
88
+ "prettier": "^3.1.1",
89
+ "semantic-release": "^25.0.2"
90
+ },
91
+ "overrides": {
92
+ "jest-snapshot": "^30.4.1",
93
+ "@babel/core": "$@babel/core"
94
+ }
95
+ }