@xano/xanoscript-language-server 11.6.5 → 11.7.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/.claude/settings.local.json +6 -1
- package/lexer/tokens.js +1 -1
- package/onCompletion/contentAssist.js +27 -23
- package/onHover/functions.md +9 -3
- package/package.json +1 -1
- package/parser/clauses/inputClause.js +22 -2
- package/parser/clauses/inputClause.spec.js +17 -0
- package/parser/clauses/nakedStackFn.js +1 -5
- package/parser/clauses/testClause.spec.js +0 -9
- package/parser/function_parser.js +4 -1
- package/parser/function_parser.spec.js +21 -0
- package/parser/functions/api/apiCallFn.js +18 -0
- package/parser/functions/api/apiCallFn.spec.js +2 -0
- package/parser/functions/api/apiRequestFn.js +1 -1
- package/parser/functions/api/apiRequestFn.spec.js +1 -1
- package/parser/functions/apiFn.js +2 -5
- package/parser/functions/controlFn.js +1 -0
- package/parser/functions/controls/conditionalFn.js +22 -7
- package/parser/functions/controls/conditionalFn.spec.js +21 -0
- package/parser/functions/controls/preconditionFn.spec.js +11 -1
- package/parser/functions/expect/unitExpectToBeWithinFn.spec.js +4 -2
- package/parser/functions/expect/unitExpectWithArgumentsFn.js +8 -2
- package/parser/functions/expect/unitExpectWithArgumentsFn.spec.js +15 -1
- package/parser/functions/expect/workflowExpectToBeWithinFn.js +2 -0
- package/parser/functions/expect/workflowExpectWithArgumentsFn.js +2 -0
- package/parser/functions/expect/workflowExpectWithArgumentsFn.spec.js +7 -0
- package/parser/functions/expect/workflowExpectWithoutArgumentsFn.js +2 -0
- package/parser/functions/schema/schemaFn.js +1 -2
- package/parser/functions/schema/schemaParseAttributeFn.spec.js +11 -9
- package/parser/functions/schema/schemaParseObjectFn.js +35 -10
- package/parser/functions/varFn.spec.js +15 -0
- package/parser/functions/workflowExpectFn.js +15 -1
- package/parser/functions/workflowExpectFn.spec.js +33 -0
- package/parser/generic/chainedIdentifier.js +2 -1
- package/parser/generic/expressionFn.js +2 -5
- package/parser/generic/functionAttrReq.js +1 -0
- package/parser/middleware_parser.js +2 -3
- package/parser/middleware_parser.spec.js +29 -2
- package/parser/parser.js +1 -0
- package/parser/query_parser.spec.js +21 -0
- package/parser/workflow_test_parser.js +1 -3
- package/parser/workflow_test_parser.spec.js +68 -2
package/lexer/tokens.js
CHANGED
|
@@ -153,7 +153,7 @@ export const NullToken = createTokenByName("null", {
|
|
|
153
153
|
export const RegExpToken = createToken({
|
|
154
154
|
name: "RegExpToken",
|
|
155
155
|
label: "regexp",
|
|
156
|
-
pattern: /\/(?:[^/\n\\]|\\.)
|
|
156
|
+
pattern: /\/(?:[^/\n\\ ]|\\.)(?:[^/\n\\]|\\.)*\//,
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
export const WhiteSpace = createToken({
|
|
@@ -127,6 +127,11 @@ const filterSuggestions = Object.freeze(
|
|
|
127
127
|
})
|
|
128
128
|
);
|
|
129
129
|
|
|
130
|
+
// Maximum number of tokens to pass to computeContentAssist.
|
|
131
|
+
// Chevrotain's content assist has exponential complexity with complex grammars;
|
|
132
|
+
// beyond this threshold it can consume gigabytes of memory and OOM the process.
|
|
133
|
+
const MAX_CONTENT_ASSIST_TOKENS = 40;
|
|
134
|
+
|
|
130
135
|
export function getContentAssistSuggestions(text, scheme) {
|
|
131
136
|
try {
|
|
132
137
|
const lexResult = lexDocument(text);
|
|
@@ -138,32 +143,31 @@ export function getContentAssistSuggestions(text, scheme) {
|
|
|
138
143
|
return filterSuggestions;
|
|
139
144
|
}
|
|
140
145
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
);
|
|
158
|
-
} else if (scheme === "task") {
|
|
159
|
-
syntacticSuggestions = parser.computeContentAssist(
|
|
160
|
-
"taskDeclaration",
|
|
161
|
-
partialTokenVector
|
|
162
|
-
);
|
|
163
|
-
} else {
|
|
146
|
+
// Skip computeContentAssist for large token vectors to prevent OOM.
|
|
147
|
+
// Chevrotain explores all possible parse paths which grows exponentially
|
|
148
|
+
// with deeply nested grammars (e.g., arrays inside function blocks).
|
|
149
|
+
if (partialTokenVector.length > MAX_CONTENT_ASSIST_TOKENS) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const ruleNameByScheme = {
|
|
154
|
+
db: "tableDeclaration",
|
|
155
|
+
api: "queryDeclaration",
|
|
156
|
+
function: "functionDeclaration",
|
|
157
|
+
task: "taskDeclaration",
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const ruleName = ruleNameByScheme[scheme];
|
|
161
|
+
if (!ruleName) {
|
|
164
162
|
return [];
|
|
165
163
|
}
|
|
166
164
|
|
|
165
|
+
parser.reset();
|
|
166
|
+
const syntacticSuggestions = parser.computeContentAssist(
|
|
167
|
+
ruleName,
|
|
168
|
+
partialTokenVector
|
|
169
|
+
);
|
|
170
|
+
|
|
167
171
|
// The suggestions also include the context, we are only interested
|
|
168
172
|
// in the TokenTypes in this example.
|
|
169
173
|
const tokenTypesSuggestions = syntacticSuggestions
|
package/onHover/functions.md
CHANGED
|
@@ -398,12 +398,14 @@ api.call "account/{account_id}" verb=POST {
|
|
|
398
398
|
} as $account_update_response
|
|
399
399
|
```
|
|
400
400
|
|
|
401
|
-
Calls an internal Xano API endpoint
|
|
401
|
+
Calls an internal Xano API endpoint from within a `workflow_test` or `test` clause. This allows you to invoke internal APIs as part of your end-to-end workflow tests or unit tests within queries, functions, and tasks.
|
|
402
|
+
|
|
403
|
+
**Note:** `api.call` is only available within `workflow_test`, `test` (in queries/functions/tasks/middleware), and `tool` contexts, including within control structures like `group`, `conditional`, loops, etc.
|
|
402
404
|
|
|
403
405
|
- `api.call` signature is similar to the `query` definition it's invoking (e.g., `query <URL> verb=<METHOD> {...}`).
|
|
404
406
|
- `api_group`: The API group to which the endpoint belongs.
|
|
405
|
-
- `headers`:
|
|
406
|
-
- `input`: The request payload.
|
|
407
|
+
- `headers`: Optional custom headers to include in the request.
|
|
408
|
+
- `input`: The request payload containing parameters for the API call.
|
|
407
409
|
|
|
408
410
|
# tool.call
|
|
409
411
|
|
|
@@ -2387,6 +2389,8 @@ test "value should always be greater than 0" {
|
|
|
2387
2389
|
|
|
2388
2390
|
Defines a unit-test with a descriptive name (e.g., `"value should always be greater than 0"`). Each test can contain many `expect` blocks asserting conditions on the `$response` variable. The test will pass if all expectations are met. An optional `input` parameter can be passed to the stack being tested as its calling `input`.
|
|
2389
2391
|
|
|
2392
|
+
**Note:** `expect` statements are available within `test` clauses and `workflow_test` declarations. They cannot be used in regular queries or functions.
|
|
2393
|
+
|
|
2390
2394
|
# expect.to_equal
|
|
2391
2395
|
|
|
2392
2396
|
```xs
|
|
@@ -2397,6 +2401,8 @@ expect.to_equal ($response.some_property) {
|
|
|
2397
2401
|
|
|
2398
2402
|
Checks if the value of `$response.some_property` equals the specified `value`. Useful for basic equality checks in test assertions.
|
|
2399
2403
|
|
|
2404
|
+
**Note:** `expect` statements are only available within `workflow_test` declarations and `test` clauses. They cannot be used in regular queries or functions.
|
|
2405
|
+
|
|
2400
2406
|
# expect.to_start_with
|
|
2401
2407
|
|
|
2402
2408
|
```xs
|
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MismatchedTokenException } from "chevrotain";
|
|
1
2
|
import { LCurly, RCurly } from "../../lexer/control.js";
|
|
2
3
|
import { InputToken, NewlineToken } from "../../lexer/tokens.js";
|
|
3
4
|
|
|
@@ -7,12 +8,19 @@ import { InputToken, NewlineToken } from "../../lexer/tokens.js";
|
|
|
7
8
|
export function inputClause($) {
|
|
8
9
|
return () => {
|
|
9
10
|
$.sectionStack.push("inputClause");
|
|
11
|
+
|
|
12
|
+
let hasStatement = false;
|
|
13
|
+
let hasNewLine = false;
|
|
14
|
+
|
|
10
15
|
$.CONSUME(InputToken); // "input"
|
|
11
16
|
// unlike schema, input can be mentioned without any fields
|
|
12
17
|
$.OPTION(() => {
|
|
13
18
|
$.CONSUME(LCurly); // "{"
|
|
14
19
|
$.MANY(() => {
|
|
15
|
-
$.
|
|
20
|
+
$.MANY1(() => {
|
|
21
|
+
$.CONSUME1(NewlineToken);
|
|
22
|
+
hasNewLine = true;
|
|
23
|
+
}); // at least one new line
|
|
16
24
|
$.SUBRULE($.optionalCommentBlockFn);
|
|
17
25
|
|
|
18
26
|
$.OR([
|
|
@@ -26,8 +34,20 @@ export function inputClause($) {
|
|
|
26
34
|
ALT: () => $.SUBRULE($.dbLinkColumnDefinition),
|
|
27
35
|
},
|
|
28
36
|
]);
|
|
37
|
+
|
|
38
|
+
if (hasStatement && !hasNewLine) {
|
|
39
|
+
$.SAVE_ERROR(
|
|
40
|
+
new MismatchedTokenException(
|
|
41
|
+
"Each statement in the input clause must be on a new line",
|
|
42
|
+
$.LA(0),
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
hasNewLine = false;
|
|
48
|
+
hasStatement = true;
|
|
29
49
|
});
|
|
30
|
-
$.
|
|
50
|
+
$.MANY2(() => $.CONSUME2(NewlineToken)); // can have new lines at the end of the input clause
|
|
31
51
|
$.CONSUME(RCurly); // "}"
|
|
32
52
|
});
|
|
33
53
|
$.sectionStack.pop();
|
|
@@ -12,6 +12,23 @@ function parse(inputText) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
describe("inputClause", () => {
|
|
15
|
+
it("inputClause accepts empty input", () => {
|
|
16
|
+
const parser = parse(`input {}`);
|
|
17
|
+
expect(parser.errors).to.be.empty;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("inputClause accepts a single input as one liner", () => {
|
|
21
|
+
const parser = parse(`input { email email_input? filters=trim|lower }`);
|
|
22
|
+
expect(parser.errors).to.be.empty;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("inputClause cannot have 2 statement on the same line", () => {
|
|
26
|
+
const parser = parse(
|
|
27
|
+
`input { email email_input? filters=trim|lower text name }`,
|
|
28
|
+
);
|
|
29
|
+
expect(parser.errors).to.not.be.empty;
|
|
30
|
+
});
|
|
31
|
+
|
|
15
32
|
it("inputClause accepts filters and field definition", () => {
|
|
16
33
|
const parser = parse(`input {
|
|
17
34
|
email email_input? filters=trim|lower
|
|
@@ -4,7 +4,6 @@ import { LCurly, RCurly } from "../../lexer/control.js";
|
|
|
4
4
|
import { NewlineToken } from "../../lexer/tokens.js";
|
|
5
5
|
|
|
6
6
|
export const DEFAULT_OPTIONS = {
|
|
7
|
-
allowExpectStatements: false,
|
|
8
7
|
allowCallStatements: false,
|
|
9
8
|
};
|
|
10
9
|
|
|
@@ -56,10 +55,7 @@ export function nakedStackFn($) {
|
|
|
56
55
|
GATE: () => options.allowCallStatements,
|
|
57
56
|
ALT: () => $.SUBRULE($.middlewareCallFn),
|
|
58
57
|
},
|
|
59
|
-
{
|
|
60
|
-
GATE: () => options.allowExpectStatements,
|
|
61
|
-
ALT: () => $.SUBRULE($.workflowExpectFn),
|
|
62
|
-
},
|
|
58
|
+
{ ALT: () => $.SUBRULE($.workflowExpectFn) },
|
|
63
59
|
]);
|
|
64
60
|
});
|
|
65
61
|
$.AT_LEAST_ONE2(() => $.CONSUME2(NewlineToken));
|
|
@@ -20,15 +20,6 @@ describe("testClause", () => {
|
|
|
20
20
|
expect(parser.errors).to.be.empty;
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
it("testClause accepts a comment", () => {
|
|
24
|
-
const parser = parse(`test "should add numbers" {
|
|
25
|
-
expect.to_equal ($response) {
|
|
26
|
-
value = 5
|
|
27
|
-
}
|
|
28
|
-
}`);
|
|
29
|
-
expect(parser.errors).to.be.empty;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
23
|
it("testClause requires accepts an optional datasource", () => {
|
|
33
24
|
const parser = parse(`test "should add numbers" {
|
|
34
25
|
datasource = "live"
|
|
@@ -29,7 +29,10 @@ export function functionDeclaration($) {
|
|
|
29
29
|
]);
|
|
30
30
|
|
|
31
31
|
if (nameToken?.image && getVarName(nameToken).startsWith("/")) {
|
|
32
|
-
$.addInvalidValueError(
|
|
32
|
+
$.addInvalidValueError(
|
|
33
|
+
nameToken,
|
|
34
|
+
"function name must not start with '/'",
|
|
35
|
+
);
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
$.CONSUME(LCurly); // "{"
|
|
@@ -93,4 +93,25 @@ describe("function_parser", () => {
|
|
|
93
93
|
}`);
|
|
94
94
|
expect(parser.errors).to.not.be.empty;
|
|
95
95
|
});
|
|
96
|
+
|
|
97
|
+
it("should not allow api.call in a function stack", () => {
|
|
98
|
+
const parser = xanoscriptParser(`function foo {
|
|
99
|
+
input {
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
stack {
|
|
103
|
+
group {
|
|
104
|
+
stack {
|
|
105
|
+
api.call "users/{user_id}" verb=GET {
|
|
106
|
+
api_group = "account"
|
|
107
|
+
input = { user_id: 123 }
|
|
108
|
+
} as $user
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
response = null
|
|
114
|
+
}`);
|
|
115
|
+
expect(parser.errors).to.not.be.empty;
|
|
116
|
+
});
|
|
96
117
|
});
|
|
@@ -18,6 +18,24 @@ export function apiCallFn($) {
|
|
|
18
18
|
return () => {
|
|
19
19
|
$.sectionStack.push("apiCallFn");
|
|
20
20
|
const fnToken = $.CONSUME(CallToken); // "call"
|
|
21
|
+
|
|
22
|
+
// Validate that api.call is only used in allowed contexts
|
|
23
|
+
const validContexts = [
|
|
24
|
+
"workflowTestDeclaration",
|
|
25
|
+
"toolDeclaration",
|
|
26
|
+
"testClause",
|
|
27
|
+
];
|
|
28
|
+
const isInValidContext = validContexts.some((context) =>
|
|
29
|
+
$.sectionStack.includes(context),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (!isInValidContext) {
|
|
33
|
+
$.addInvalidValueError(
|
|
34
|
+
fnToken,
|
|
35
|
+
"api.call can only be used within workflow_test, tool, or test contexts",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
$.OR([
|
|
22
40
|
{ ALT: () => $.CONSUME(StringLiteral) }, // "foo/bar"
|
|
23
41
|
{ ALT: () => $.CONSUME(Identifier) }, // foo
|
|
@@ -7,6 +7,8 @@ function parse(inputText) {
|
|
|
7
7
|
parser.reset();
|
|
8
8
|
const lexResult = lexDocument(inputText);
|
|
9
9
|
parser.input = lexResult.tokens;
|
|
10
|
+
// Simulate being inside a test context for api.call validation
|
|
11
|
+
parser.sectionStack.push("testClause");
|
|
10
12
|
parser.apiCallFn();
|
|
11
13
|
return parser;
|
|
12
14
|
}
|
|
@@ -29,7 +29,7 @@ export function apiRequestFn($) {
|
|
|
29
29
|
"disabled?": "[boolean]",
|
|
30
30
|
"params?": "[expression]",
|
|
31
31
|
"headers?": "[expression]",
|
|
32
|
-
"timeout?": "[
|
|
32
|
+
"timeout?": "[expression]",
|
|
33
33
|
"follow_location?": "[expression]",
|
|
34
34
|
"ca_certificate?": "[string]",
|
|
35
35
|
"certificate?": "[string]",
|
|
@@ -53,7 +53,7 @@ describe("apiRequestFn", () => {
|
|
|
53
53
|
method = "POST"
|
|
54
54
|
params = {}|set:"foo":"bar"
|
|
55
55
|
headers = []|push:"Set-Cookie: sessionId=e8bb43229de9; Domain=foo.example.com"
|
|
56
|
-
timeout =
|
|
56
|
+
timeout = $input.timeout
|
|
57
57
|
ca_certificate = "what is this"
|
|
58
58
|
certificate = """
|
|
59
59
|
-----BEGIN CERTIFICATE-----
|
|
@@ -5,15 +5,12 @@ import { DotToken } from "../../lexer/tokens.js";
|
|
|
5
5
|
* @param {import('../base_parser.js').XanoBaseParser} $
|
|
6
6
|
*/
|
|
7
7
|
export function apiFn($) {
|
|
8
|
-
return (
|
|
8
|
+
return () => {
|
|
9
9
|
$.sectionStack.push("api");
|
|
10
10
|
$.CONSUME(ApiToken); // "api"
|
|
11
11
|
$.CONSUME(DotToken); // "."
|
|
12
12
|
$.OR([
|
|
13
|
-
{
|
|
14
|
-
GATE: () => options.allowCallStatements,
|
|
15
|
-
ALT: () => $.SUBRULE($.apiCallFn), // "api.call"
|
|
16
|
-
},
|
|
13
|
+
{ ALT: () => $.SUBRULE($.apiCallFn) }, // "api.call"
|
|
17
14
|
{ ALT: () => $.SUBRULE($.apiLambdaFn) }, // "api.lambda"
|
|
18
15
|
{ ALT: () => $.SUBRULE($.apiRequestFn) }, // "api.request"
|
|
19
16
|
{ ALT: () => $.SUBRULE($.apiRealtimeEventFn) }, // "api.realtime_event"
|
|
@@ -6,6 +6,7 @@ import { BreakToken, ContinueToken } from "../../lexer/control.js";
|
|
|
6
6
|
*/
|
|
7
7
|
export function controlFn($) {
|
|
8
8
|
return (options = {}) => {
|
|
9
|
+
$.sectionStack.push("controlFn");
|
|
9
10
|
$.OR([
|
|
10
11
|
{ ALT: () => $.CONSUME(BreakToken) }, // "break"
|
|
11
12
|
{ ALT: () => $.CONSUME(ContinueToken) }, // "continue"
|
|
@@ -10,6 +10,7 @@ export function conditionalFn($) {
|
|
|
10
10
|
let hasIfStatement = false;
|
|
11
11
|
let hasDescription = false;
|
|
12
12
|
let hasDisabled = false;
|
|
13
|
+
let hasElseStatement = false;
|
|
13
14
|
|
|
14
15
|
$.sectionStack.push("conditionalFn");
|
|
15
16
|
const parent = $.CONSUME(ConditionalToken); // "conditional"
|
|
@@ -37,13 +38,27 @@ export function conditionalFn($) {
|
|
|
37
38
|
ALT: () => {
|
|
38
39
|
hasIfStatement = true;
|
|
39
40
|
$.SUBRULE($.conditionalIfFn);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
|
|
42
|
+
// as soon as we hit an else statement, we know there can be no more elif statements, so we only check for the absence of an else statement
|
|
43
|
+
$.MANY1({
|
|
44
|
+
GATE: () => !hasElseStatement,
|
|
45
|
+
DEF: () => {
|
|
46
|
+
$.AT_LEAST_ONE1(() => $.CONSUME3(NewlineToken));
|
|
47
|
+
$.SUBRULE($.optionalCommentBlockFn);
|
|
48
|
+
$.OR1([
|
|
49
|
+
{
|
|
50
|
+
ALT: () => {
|
|
51
|
+
hasElseStatement = true;
|
|
52
|
+
$.SUBRULE($.conditionalElseFn);
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
ALT: () => {
|
|
57
|
+
$.SUBRULE($.conditionalElifFn);
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
},
|
|
47
62
|
});
|
|
48
63
|
},
|
|
49
64
|
},
|
|
@@ -108,6 +108,27 @@ describe("conditionalFn", () => {
|
|
|
108
108
|
expect(parser.errors).to.be.empty;
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
it("conditionalFn accepts a comment above the else", () => {
|
|
112
|
+
const parser = parse(`conditional {
|
|
113
|
+
// If destination address is a system account.
|
|
114
|
+
if ($some_condition) {
|
|
115
|
+
// Getting the final destination
|
|
116
|
+
var $destination {
|
|
117
|
+
value = "https://example.com/default_destination/x"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Set the final destination URL
|
|
122
|
+
else {
|
|
123
|
+
// Getting the final destination
|
|
124
|
+
var $destination {
|
|
125
|
+
value = "https://example.com/default_destination"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}`);
|
|
129
|
+
expect(parser.errors).to.be.empty;
|
|
130
|
+
});
|
|
131
|
+
|
|
111
132
|
it("conditionalFn accepts a description", () => {
|
|
112
133
|
const parser = parse(`conditional {
|
|
113
134
|
description = "foo"
|
|
@@ -28,9 +28,19 @@ describe("preconditionFn", () => {
|
|
|
28
28
|
expect(parser.errors).to.be.empty;
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
it("should accept a complex precondition expression", () => {
|
|
32
|
+
const parser =
|
|
33
|
+
parse(`precondition (($a_rate >= ($rate_token.x_rate * (1 - ($env.x_rate_all / 100)))) && ($a_rate <= ($rate_token.x_rate * (1 + ($env.x_rate_all / 100))))) {
|
|
34
|
+
error_type = "badrequest"
|
|
35
|
+
error = "ohoh"
|
|
36
|
+
payload = "The rate is off"
|
|
37
|
+
}`);
|
|
38
|
+
expect(parser.errors).to.be.empty;
|
|
39
|
+
});
|
|
40
|
+
|
|
31
41
|
it("preconditionFn accept an escaped string", () => {
|
|
32
42
|
const parser = parse(
|
|
33
|
-
`precondition ($escaped == '\\"hello this is chris\\\\\\' test\\"')
|
|
43
|
+
`precondition ($escaped == '\\"hello this is chris\\\\\\' test\\"')`,
|
|
34
44
|
);
|
|
35
45
|
expect(parser.errors).to.be.empty;
|
|
36
46
|
});
|
|
@@ -29,8 +29,10 @@ describe("unitExpectToBeWithinFn", () => {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
it("unitExpectToBeWithinFn does not accepts filters on value argument", () => {
|
|
32
|
-
const parser =
|
|
33
|
-
|
|
32
|
+
const parser = parse(`to_be_within (($response|get:foo:"bar")
|
|
33
|
+
|first
|
|
34
|
+
|concat:bar
|
|
35
|
+
) {
|
|
34
36
|
min = 11
|
|
35
37
|
max = 13
|
|
36
38
|
}`);
|
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
ToStartWithToken,
|
|
11
11
|
} from "../../../lexer/expect.js";
|
|
12
12
|
import { DotToken, Identifier } from "../../../lexer/tokens.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
ResponseVariable,
|
|
15
|
+
ShortFormVariable,
|
|
16
|
+
} from "../../../lexer/variables.js";
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* @param {import('../../base_parser.js').XanoBaseParser} $
|
|
@@ -32,7 +35,10 @@ export function unitExpectWithArgumentsFn($) {
|
|
|
32
35
|
$.CONSUME(ResponseVariable); // "$response"
|
|
33
36
|
$.MANY(() => {
|
|
34
37
|
$.CONSUME(DotToken); // "."
|
|
35
|
-
$.
|
|
38
|
+
$.OR1([
|
|
39
|
+
{ ALT: () => $.CONSUME(Identifier) }, // "x", "users", etc.
|
|
40
|
+
{ ALT: () => $.CONSUME(ShortFormVariable) }, // "$xyz"
|
|
41
|
+
]);
|
|
36
42
|
});
|
|
37
43
|
$.CONSUME(RParent); // ")"
|
|
38
44
|
|
|
@@ -16,7 +16,14 @@ describe("unitExpectWithArgumentsFn", () => {
|
|
|
16
16
|
it("unitExpectWithArgumentsFn to_equal accepts a $response variable", () => {
|
|
17
17
|
const parser = parse(`to_equal ($response) {
|
|
18
18
|
value = "foo"
|
|
19
|
-
}
|
|
19
|
+
}`);
|
|
20
|
+
expect(parser.errors).to.be.empty;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("unitExpectWithArgumentsFn to_equal accepts a $response subpath with $ prefix", () => {
|
|
24
|
+
const parser = parse(`to_equal ($response.$xyz) {
|
|
25
|
+
value = "foo"
|
|
26
|
+
}`);
|
|
20
27
|
expect(parser.errors).to.be.empty;
|
|
21
28
|
});
|
|
22
29
|
|
|
@@ -27,6 +34,13 @@ describe("unitExpectWithArgumentsFn", () => {
|
|
|
27
34
|
expect(parser.errors).to.not.be.empty;
|
|
28
35
|
});
|
|
29
36
|
|
|
37
|
+
it("unitExpectWithArgumentsFn accepts an expression", () => {
|
|
38
|
+
const parser = parse(`to_equal ($response) {
|
|
39
|
+
value = \`12092834098240928304982039840923908402384.09823904823098402834\`
|
|
40
|
+
}`);
|
|
41
|
+
expect(parser.errors).to.be.empty;
|
|
42
|
+
});
|
|
43
|
+
|
|
30
44
|
it("unitExpectWithArgumentsFn does not accept variables", () => {
|
|
31
45
|
const parser = parse(`to_equal ($response) {
|
|
32
46
|
value = $response.x
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LParent, RParent } from "../../../lexer/control.js";
|
|
2
2
|
import { ToBeWithinToken } from "../../../lexer/expect.js";
|
|
3
|
+
import { NewlineToken } from "../../../lexer/tokens.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @param {import('../../base_parser.js').XanoBaseParser} $
|
|
@@ -13,6 +14,7 @@ export function workflowExpectToBeWithinFn($) {
|
|
|
13
14
|
const fnToken = $.CONSUME(ToBeWithinToken); // "to_be_within"
|
|
14
15
|
$.CONSUME(LParent); // "("
|
|
15
16
|
$.SUBRULE($.expressionFn); // "$foo|get:bar:null"
|
|
17
|
+
$.MANY(() => $.CONSUME(NewlineToken)); // allow newlines between the expression and the attributes
|
|
16
18
|
$.CONSUME(RParent); // ")"
|
|
17
19
|
$.SUBRULE($.functionAttrReq, {
|
|
18
20
|
ARGS: [fnToken, requiredAttrs, optionalAttrs],
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
ToNotEqualToken,
|
|
10
10
|
ToStartWithToken,
|
|
11
11
|
} from "../../../lexer/expect.js";
|
|
12
|
+
import { NewlineToken } from "../../../lexer/tokens.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @param {import('../../base_parser.js').XanoBaseParser} $
|
|
@@ -31,6 +32,7 @@ export function workflowExpectWithArgumentsFn($) {
|
|
|
31
32
|
]);
|
|
32
33
|
$.CONSUME(LParent); // "("
|
|
33
34
|
$.SUBRULE($.expressionFn); // "$foo|get:bar:null"
|
|
35
|
+
$.MANY(() => $.CONSUME(NewlineToken)); // allow newlines between the expression and the attributes
|
|
34
36
|
$.CONSUME(RParent); // ")"
|
|
35
37
|
$.SUBRULE($.functionAttrReq, {
|
|
36
38
|
ARGS: [fnToken, requiredAttrs, optionalAttrs],
|
|
@@ -27,6 +27,13 @@ describe("workflowExpectWithArgumentsFn", () => {
|
|
|
27
27
|
expect(parser.errors).to.be.empty;
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
it("accept string a named attribute", () => {
|
|
31
|
+
const parser = parse(`to_equal ($attr.sub."some value") {
|
|
32
|
+
value = "enabled"
|
|
33
|
+
}`);
|
|
34
|
+
expect(parser.errors).to.be.empty;
|
|
35
|
+
});
|
|
36
|
+
|
|
30
37
|
it("workflowExpectWithArgumentsFn accepts a filtered variable", () => {
|
|
31
38
|
// foo is not in quotes
|
|
32
39
|
let parser = parse(`to_equal ($x|get:foo:"bar") {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
ToNotBeDefinedToken,
|
|
11
11
|
ToNotBeNullToken,
|
|
12
12
|
} from "../../../lexer/expect.js";
|
|
13
|
+
import { NewlineToken } from "../../../lexer/tokens.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* @param {import('../../base_parser.js').XanoBaseParser} $
|
|
@@ -30,6 +31,7 @@ export function workflowExpectWithoutArgumentsFn($) {
|
|
|
30
31
|
]);
|
|
31
32
|
$.CONSUME(LParent); // "("
|
|
32
33
|
$.SUBRULE($.expressionFn); // "$foo|get:bar:null"
|
|
34
|
+
$.MANY(() => $.CONSUME(NewlineToken)); // allow newlines between the expression and the attributes
|
|
33
35
|
$.CONSUME(RParent); // ")"
|
|
34
36
|
$.SUBRULE($.functionAttrReq, {
|
|
35
37
|
ARGS: [fnToken, [], ["description", "disabled"]],
|
|
@@ -64,7 +64,7 @@ export function schemaFn($) {
|
|
|
64
64
|
schemaExpectOneOf(
|
|
65
65
|
schema,
|
|
66
66
|
["string", "number", "boolean", "timestamp"],
|
|
67
|
-
captured
|
|
67
|
+
captured,
|
|
68
68
|
),
|
|
69
69
|
ALT: () => {
|
|
70
70
|
$.SUBRULE($.schemaParseImmutableFn, {
|
|
@@ -130,7 +130,6 @@ export function schemaFn($) {
|
|
|
130
130
|
$.SUBRULE($.nakedStackFn, {
|
|
131
131
|
ARGS: [
|
|
132
132
|
{
|
|
133
|
-
allowExpectStatements: isTest,
|
|
134
133
|
allowCallStatements: isTest,
|
|
135
134
|
},
|
|
136
135
|
],
|
|
@@ -14,7 +14,7 @@ export function parserExtension() {
|
|
|
14
14
|
const name = this.CONSUME(Identifier);
|
|
15
15
|
this.CONSUME(EqualToken); // "="
|
|
16
16
|
this.SUBRULE(this.schemaParseAttributeFn, { ARGS: [name, schema] });
|
|
17
|
-
}
|
|
17
|
+
},
|
|
18
18
|
);
|
|
19
19
|
|
|
20
20
|
this.schemaParseAttributeFn_expression = this.RULE(
|
|
@@ -26,7 +26,7 @@ export function parserExtension() {
|
|
|
26
26
|
};
|
|
27
27
|
const name = this.CONSUME(Identifier);
|
|
28
28
|
this.SUBRULE(this.schemaParseAttributeFn, { ARGS: [name, schema] });
|
|
29
|
-
}
|
|
29
|
+
},
|
|
30
30
|
);
|
|
31
31
|
|
|
32
32
|
// this rule requires a foo field to be defined
|
|
@@ -36,7 +36,7 @@ export function parserExtension() {
|
|
|
36
36
|
const schema = { "disabled?": "[boolean]", "description?": "[string]" };
|
|
37
37
|
const name = this.CONSUME(Identifier);
|
|
38
38
|
this.SUBRULE(this.schemaParseAttributeFn, { ARGS: [name, schema] });
|
|
39
|
-
}
|
|
39
|
+
},
|
|
40
40
|
);
|
|
41
41
|
|
|
42
42
|
this.schemaParseAttributeFn_deep = this.RULE(
|
|
@@ -52,7 +52,7 @@ export function parserExtension() {
|
|
|
52
52
|
const name = this.CONSUME(Identifier);
|
|
53
53
|
this.CONSUME(EqualToken); // "="
|
|
54
54
|
this.SUBRULE(this.schemaParseAttributeFn, { ARGS: [name, schema] });
|
|
55
|
-
}
|
|
55
|
+
},
|
|
56
56
|
);
|
|
57
57
|
|
|
58
58
|
this.schemaParseAttributeFn_stack = this.RULE(
|
|
@@ -65,7 +65,7 @@ export function parserExtension() {
|
|
|
65
65
|
};
|
|
66
66
|
const name = this.CONSUME(Identifier);
|
|
67
67
|
this.SUBRULE(this.schemaParseAttributeFn, { ARGS: [name, schema] });
|
|
68
|
-
}
|
|
68
|
+
},
|
|
69
69
|
);
|
|
70
70
|
|
|
71
71
|
this.schemaParseAttributeFn_optional = this.RULE(
|
|
@@ -80,7 +80,7 @@ export function parserExtension() {
|
|
|
80
80
|
const name = this.CONSUME(Identifier);
|
|
81
81
|
this.CONSUME(EqualToken); // "="
|
|
82
82
|
this.SUBRULE(this.schemaParseAttributeFn, { ARGS: [name, schema] });
|
|
83
|
-
}
|
|
83
|
+
},
|
|
84
84
|
);
|
|
85
85
|
|
|
86
86
|
this.schemaParseAttributeFn_multiple = this.RULE(
|
|
@@ -93,7 +93,7 @@ export function parserExtension() {
|
|
|
93
93
|
const name = this.CONSUME(Identifier);
|
|
94
94
|
this.CONSUME(EqualToken); // "="
|
|
95
95
|
this.SUBRULE(this.schemaParseAttributeFn, { ARGS: [name, schema] });
|
|
96
|
-
}
|
|
96
|
+
},
|
|
97
97
|
);
|
|
98
98
|
|
|
99
99
|
this.schemaParseAttributeFn_disabled = this.RULE(
|
|
@@ -108,7 +108,7 @@ export function parserExtension() {
|
|
|
108
108
|
const name = this.CONSUME(Identifier);
|
|
109
109
|
this.CONSUME(EqualToken); // "="
|
|
110
110
|
this.SUBRULE(this.schemaParseAttributeFn, { ARGS: [name, schema] });
|
|
111
|
-
}
|
|
111
|
+
},
|
|
112
112
|
);
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -116,6 +116,8 @@ function parse(inputText) {
|
|
|
116
116
|
parser.reset();
|
|
117
117
|
const lexResult = lexDocument(inputText);
|
|
118
118
|
parser.input = lexResult.tokens;
|
|
119
|
+
// Simulate being inside a test context for api.call validation
|
|
120
|
+
parser.sectionStack.push("testClause");
|
|
119
121
|
return parser;
|
|
120
122
|
}
|
|
121
123
|
|
|
@@ -131,7 +133,7 @@ describe("schemaParseAttributeFn", () => {
|
|
|
131
133
|
expect(parser.errors).to.be.empty;
|
|
132
134
|
|
|
133
135
|
parse(
|
|
134
|
-
`value = { something = 123.2, other_thing = 44 }
|
|
136
|
+
`value = { something = 123.2, other_thing = 44 }`,
|
|
135
137
|
).schemaParseAttributeFn_flat();
|
|
136
138
|
expect(parser.errors).to.be.empty;
|
|
137
139
|
});
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "../../../lexer/control.js";
|
|
9
9
|
import { StringLiteral } from "../../../lexer/literal.js";
|
|
10
10
|
import { DotToken, Identifier, NewlineToken } from "../../../lexer/tokens.js";
|
|
11
|
+
import { InputVariable, ShortFormVariable } from "../../../lexer/variables.js";
|
|
11
12
|
import {
|
|
12
13
|
canBeDisabledAttribute,
|
|
13
14
|
isMultiAttribute,
|
|
@@ -40,7 +41,7 @@ export function schemaParseObjectFn($) {
|
|
|
40
41
|
some(objectKeys, (k) => isSchemaGenericType(k))
|
|
41
42
|
) {
|
|
42
43
|
throw new Error(
|
|
43
|
-
"schemaParseObjectFn supports only one generic type when multiple keys are defined"
|
|
44
|
+
"schemaParseObjectFn supports only one generic type when multiple keys are defined",
|
|
44
45
|
);
|
|
45
46
|
}
|
|
46
47
|
});
|
|
@@ -55,7 +56,7 @@ export function schemaParseObjectFn($) {
|
|
|
55
56
|
if (needSeparator) {
|
|
56
57
|
$.addInvalidValueError(
|
|
57
58
|
lastToken,
|
|
58
|
-
"Expected a comma, a new line or closing bracket"
|
|
59
|
+
"Expected a comma, a new line or closing bracket",
|
|
59
60
|
);
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -82,25 +83,49 @@ export function schemaParseObjectFn($) {
|
|
|
82
83
|
return objectKeyToken.image.slice(1, -1);
|
|
83
84
|
},
|
|
84
85
|
},
|
|
86
|
+
{
|
|
87
|
+
// dynamic key using $input.property
|
|
88
|
+
ALT: () => {
|
|
89
|
+
objectKeyToken = $.CONSUME(InputVariable);
|
|
90
|
+
const tokens = [objectKeyToken];
|
|
91
|
+
$.MANY4(() => {
|
|
92
|
+
tokens.push($.CONSUME1(DotToken));
|
|
93
|
+
tokens.push($.CONSUME2(Identifier));
|
|
94
|
+
});
|
|
95
|
+
return tokens.map((t) => t.image).join(".");
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
// dynamic key using $variable or $variable.property
|
|
100
|
+
ALT: () => {
|
|
101
|
+
objectKeyToken = $.CONSUME(ShortFormVariable);
|
|
102
|
+
const tokens = [objectKeyToken];
|
|
103
|
+
$.MANY5(() => {
|
|
104
|
+
tokens.push($.CONSUME2(DotToken));
|
|
105
|
+
tokens.push($.CONSUME3(Identifier));
|
|
106
|
+
});
|
|
107
|
+
return tokens.map((t) => t.image).join(".");
|
|
108
|
+
},
|
|
109
|
+
},
|
|
85
110
|
]);
|
|
86
111
|
|
|
87
112
|
const subSchemaKey = valueMatchRequirements(keyValue, objectKeys);
|
|
88
113
|
if (!subSchemaKey) {
|
|
89
114
|
$.addInvalidValueError(
|
|
90
115
|
objectKeyToken,
|
|
91
|
-
`The key '${keyValue}' is not valid in this context
|
|
116
|
+
`The key '${keyValue}' is not valid in this context`,
|
|
92
117
|
);
|
|
93
118
|
} else if (disabledToken && !canBeDisabledAttribute(subSchemaKey)) {
|
|
94
119
|
$.addInvalidValueError(
|
|
95
120
|
disabledToken,
|
|
96
|
-
`The key '${keyValue}' cannot be disabled
|
|
121
|
+
`The key '${keyValue}' cannot be disabled`,
|
|
97
122
|
);
|
|
98
123
|
}
|
|
99
124
|
|
|
100
125
|
if (keyNames.includes(keyValue) && !isMultiAttribute(subSchemaKey)) {
|
|
101
126
|
$.addInvalidValueError(
|
|
102
127
|
objectKeyToken,
|
|
103
|
-
`Duplicate key '${keyValue}' found
|
|
128
|
+
`Duplicate key '${keyValue}' found`,
|
|
104
129
|
);
|
|
105
130
|
} else {
|
|
106
131
|
keyNames.push(keyValue);
|
|
@@ -133,25 +158,25 @@ export function schemaParseObjectFn($) {
|
|
|
133
158
|
|
|
134
159
|
if (missingKeys.length) {
|
|
135
160
|
const missingKeyNames = missingKeys.filter(
|
|
136
|
-
(k) => !isOptionalAttribute(k)
|
|
161
|
+
(k) => !isOptionalAttribute(k),
|
|
137
162
|
);
|
|
138
163
|
if (missingKeyNames.length > 1) {
|
|
139
164
|
$.addMissingError(
|
|
140
165
|
lastToken || token,
|
|
141
166
|
`{} is missing the following required entries: ${missingKeyNames.join(
|
|
142
|
-
", "
|
|
143
|
-
)}
|
|
167
|
+
", ",
|
|
168
|
+
)}`,
|
|
144
169
|
);
|
|
145
170
|
} else if (missingKeyNames.length === 1) {
|
|
146
171
|
if (isSchemaGenericType(missingKeyNames[0])) {
|
|
147
172
|
$.addMissingError(
|
|
148
173
|
lastToken || token,
|
|
149
|
-
`{} requires a least one entry
|
|
174
|
+
`{} requires a least one entry`,
|
|
150
175
|
);
|
|
151
176
|
} else {
|
|
152
177
|
$.addMissingError(
|
|
153
178
|
lastToken || token,
|
|
154
|
-
`{} is missing required key '${missingKeyNames[0]}'
|
|
179
|
+
`{} is missing required key '${missingKeyNames[0]}'`,
|
|
155
180
|
);
|
|
156
181
|
}
|
|
157
182
|
}
|
|
@@ -142,6 +142,14 @@ describe("varFn", () => {
|
|
|
142
142
|
expect(parser.errors).to.be.empty;
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
+
it("var.update should allow $input reference in its filters", () => {
|
|
146
|
+
const parser = parse(`var.update $object_for_update {
|
|
147
|
+
value = $object_for_update
|
|
148
|
+
|merge_recursive:{$input.key:$input.value}
|
|
149
|
+
}`);
|
|
150
|
+
expect(parser.errors).to.be.empty;
|
|
151
|
+
});
|
|
152
|
+
|
|
145
153
|
it("should allow accessing a property from an object defined in place", () => {
|
|
146
154
|
let parser = parse(`var $config {
|
|
147
155
|
value = {
|
|
@@ -267,6 +275,13 @@ describe("varFn", () => {
|
|
|
267
275
|
expect(parser.errors).to.not.be.empty;
|
|
268
276
|
});
|
|
269
277
|
|
|
278
|
+
it("accepts a backtick expression as value", () => {
|
|
279
|
+
const parser = parse(`var $current_time {
|
|
280
|
+
value = \`12316546543321321.3213521654654\`
|
|
281
|
+
}`);
|
|
282
|
+
expect(parser.errors).to.be.empty;
|
|
283
|
+
});
|
|
284
|
+
|
|
270
285
|
// All blacklisted names should be rejected in long form via var.update
|
|
271
286
|
const allBlacklisted = [
|
|
272
287
|
"auth",
|
|
@@ -7,7 +7,21 @@ import { DotToken } from "../../lexer/tokens.js";
|
|
|
7
7
|
export function workflowExpectFn($) {
|
|
8
8
|
return () => {
|
|
9
9
|
$.sectionStack.push("workflowExpect");
|
|
10
|
-
$.CONSUME(ExpectToken); // "expect"
|
|
10
|
+
const expectToken = $.CONSUME(ExpectToken); // "expect"
|
|
11
|
+
|
|
12
|
+
// Validate that expect statements are only used in allowed contexts
|
|
13
|
+
const validContexts = ["workflowTestDeclaration", "testClause"];
|
|
14
|
+
const isInValidContext = validContexts.some((context) =>
|
|
15
|
+
$.sectionStack.includes(context),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
if (!isInValidContext) {
|
|
19
|
+
$.addInvalidValueError(
|
|
20
|
+
expectToken,
|
|
21
|
+
"expect statements can only be used within workflow_test or test contexts",
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
$.CONSUME(DotToken); // "."
|
|
12
26
|
$.OR([
|
|
13
27
|
{ ALT: () => $.SUBRULE($.workflowExpectWithArgumentsFn) }, // "expect.to_equal, expect.to_start_with, etc."
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { describe, it } from "mocha";
|
|
3
|
+
import { lexDocument } from "../../lexer/lexer.js";
|
|
4
|
+
import { parser } from "../test_parser.js";
|
|
5
|
+
|
|
6
|
+
function parse(inputText) {
|
|
7
|
+
parser.reset();
|
|
8
|
+
const lexResult = lexDocument(inputText);
|
|
9
|
+
parser.input = lexResult.tokens;
|
|
10
|
+
parser.sectionStack.push("workflowTestDeclaration");
|
|
11
|
+
parser.workflowExpectFn();
|
|
12
|
+
return parser;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("workflowExpectFn", () => {
|
|
16
|
+
it("workflowExpectFn accepts a string literal as value", () => {
|
|
17
|
+
const parser = parse(`expect.to_equal ($response.email) {
|
|
18
|
+
value = "email@example.com"
|
|
19
|
+
}`);
|
|
20
|
+
expect(parser.errors).to.be.empty;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("workflowExpectFn accepts filtered values", () => {
|
|
24
|
+
const parser = parse(`expect.to_equal ($total
|
|
25
|
+
|subtract:$tax
|
|
26
|
+
|subtract:$cost
|
|
27
|
+
) {
|
|
28
|
+
value = $som_value
|
|
29
|
+
}`);
|
|
30
|
+
console.log(parser.errors);
|
|
31
|
+
expect(parser.errors).to.be.empty;
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IntegerLiteral } from "../../lexer/literal.js";
|
|
1
|
+
import { IntegerLiteral, StringLiteral } from "../../lexer/literal.js";
|
|
2
2
|
import { DotToken, Identifier } from "../../lexer/tokens.js";
|
|
3
3
|
import { ShortFormVariable } from "../../lexer/variables.js";
|
|
4
4
|
|
|
@@ -38,6 +38,7 @@ export function singleChainedIdentifier($) {
|
|
|
38
38
|
}, // e.g., .bar
|
|
39
39
|
{ ALT: () => $.CONSUME(Identifier) }, // e.g., .bar
|
|
40
40
|
{ ALT: () => $.CONSUME2(IntegerLiteral) }, // e.g., .3
|
|
41
|
+
{ ALT: () => $.CONSUME(StringLiteral) }, // e.g., ."some value"
|
|
41
42
|
{ ALT: () => $.SUBRULE($.bracketAccessor) }, // e.g., .["key"]
|
|
42
43
|
]);
|
|
43
44
|
},
|
|
@@ -235,14 +235,14 @@ export function expressionFn($) {
|
|
|
235
235
|
if (hasFilters && hasTests) {
|
|
236
236
|
$.addInvalidValueError(
|
|
237
237
|
token,
|
|
238
|
-
"An expression should be wrapped in parentheses when combining filters and tests"
|
|
238
|
+
"An expression should be wrapped in parentheses when combining filters and tests",
|
|
239
239
|
);
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
if (options.allowQueryExpression && has_ternary) {
|
|
243
243
|
$.addInvalidValueError(
|
|
244
244
|
testToken,
|
|
245
|
-
"Ternary expressions are not allowed in a where clause"
|
|
245
|
+
"Ternary expressions are not allowed in a where clause",
|
|
246
246
|
);
|
|
247
247
|
}
|
|
248
248
|
};
|
|
@@ -373,15 +373,12 @@ export function simpleExpressionFn($) {
|
|
|
373
373
|
|
|
374
374
|
// Group 2: Complex literals (distinct tokens)
|
|
375
375
|
{
|
|
376
|
-
GATE: () => options.allowExpression,
|
|
377
376
|
ALT: () => $.CONSUME(MultiLineStringToken),
|
|
378
377
|
},
|
|
379
378
|
{
|
|
380
|
-
GATE: () => options.allowExpression,
|
|
381
379
|
ALT: () => $.CONSUME(MultiLineExpressionToken),
|
|
382
380
|
},
|
|
383
381
|
{
|
|
384
|
-
GATE: () => options.allowExpression,
|
|
385
382
|
ALT: () => $.CONSUME(ExpressionLiteral),
|
|
386
383
|
},
|
|
387
384
|
|
|
@@ -24,6 +24,7 @@ export function functionAttrReq($) {
|
|
|
24
24
|
* @param {string[]} options.allowQueryExpression special attriutes that will allow query expressions
|
|
25
25
|
**/
|
|
26
26
|
return (parent, required, optional, options = {}) => {
|
|
27
|
+
$.sectionStack.push("functionAttrReq");
|
|
27
28
|
// setup basic defaults
|
|
28
29
|
required = required || [];
|
|
29
30
|
optional = optional || [];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CommentToken } from "../lexer/comment.js";
|
|
1
2
|
import { EqualToken, LCurly, RCurly } from "../lexer/control.js";
|
|
2
3
|
import { StringLiteral } from "../lexer/literal.js";
|
|
3
4
|
import {
|
|
@@ -34,9 +35,7 @@ export function middlewareDeclaration($) {
|
|
|
34
35
|
$.MANY(() => {
|
|
35
36
|
$.AT_LEAST_ONE(() => $.CONSUME(NewlineToken)); // at least one new line
|
|
36
37
|
$.OR2([
|
|
37
|
-
{
|
|
38
|
-
ALT: () => $.SUBRULE($.commentBlockFn),
|
|
39
|
-
},
|
|
38
|
+
{ ALT: () => $.CONSUME(CommentToken) },
|
|
40
39
|
{
|
|
41
40
|
GATE: () => !hasDescription,
|
|
42
41
|
ALT: () => {
|
|
@@ -60,6 +60,33 @@ describe("middleware_parser", () => {
|
|
|
60
60
|
expect(parser.errors).to.not.be.empty;
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
it("middleware should accept a test", () => {
|
|
64
|
+
const parser = xanoscriptParser(`middleware foo {
|
|
65
|
+
input {
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
stack {
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
response = null
|
|
72
|
+
response_strategy = "replace"
|
|
73
|
+
exception_policy = "rethrow"
|
|
74
|
+
|
|
75
|
+
// test cases
|
|
76
|
+
test "testset " {
|
|
77
|
+
input = {
|
|
78
|
+
adoption_application_id: ""
|
|
79
|
+
user_id : ""
|
|
80
|
+
completed_only : ""
|
|
81
|
+
scheduled_only : ""
|
|
82
|
+
page : ""
|
|
83
|
+
per_page : ""
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}`);
|
|
87
|
+
expect(parser.errors).to.be.empty;
|
|
88
|
+
});
|
|
89
|
+
|
|
63
90
|
describe("ResponseStrategy", () => {
|
|
64
91
|
it("should parse middleware with response_strategy = 'merge'", () => {
|
|
65
92
|
const parser = xanoscriptParser(`middleware test_merge {
|
|
@@ -109,7 +136,7 @@ describe("middleware_parser", () => {
|
|
|
109
136
|
expect(parser.errors).to.not.be.empty;
|
|
110
137
|
expect(parser.errors[0].message).to.include('Invalid value "invalid"');
|
|
111
138
|
expect(parser.errors[0].message).to.include(
|
|
112
|
-
'Must be one of: "merge", "replace"'
|
|
139
|
+
'Must be one of: "merge", "replace"',
|
|
113
140
|
);
|
|
114
141
|
});
|
|
115
142
|
});
|
|
@@ -179,7 +206,7 @@ describe("middleware_parser", () => {
|
|
|
179
206
|
expect(parser.errors).to.not.be.empty;
|
|
180
207
|
expect(parser.errors[0].message).to.include('Invalid value "invalid"');
|
|
181
208
|
expect(parser.errors[0].message).to.include(
|
|
182
|
-
'Must be one of: "silent", "rethrow", "critical"'
|
|
209
|
+
'Must be one of: "silent", "rethrow", "critical"',
|
|
183
210
|
);
|
|
184
211
|
});
|
|
185
212
|
});
|
package/parser/parser.js
CHANGED
|
@@ -15,6 +15,7 @@ export function xanoscriptParser(text, scheme, preTokenized = null) {
|
|
|
15
15
|
scheme = getSchemeFromContent(text);
|
|
16
16
|
}
|
|
17
17
|
const lexResult = preTokenized || lexDocument(text);
|
|
18
|
+
parser.reset();
|
|
18
19
|
parser.input = lexResult.tokens;
|
|
19
20
|
switch (scheme.toLowerCase()) {
|
|
20
21
|
case "addon":
|
|
@@ -151,4 +151,25 @@ describe("query_parser", () => {
|
|
|
151
151
|
}`);
|
|
152
152
|
expect(parser.errors).to.be.empty;
|
|
153
153
|
});
|
|
154
|
+
|
|
155
|
+
it("should not allow api.call within a group in a query stack", () => {
|
|
156
|
+
const parser = xanoscriptParser(`query foo verb=GET {
|
|
157
|
+
input {
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
stack {
|
|
161
|
+
group {
|
|
162
|
+
stack {
|
|
163
|
+
api.call bar verb=POST {
|
|
164
|
+
api_group = "internal"
|
|
165
|
+
input = { x: 1 }
|
|
166
|
+
} as $result
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
response = null
|
|
172
|
+
}`);
|
|
173
|
+
expect(parser.errors).to.not.be.empty;
|
|
174
|
+
});
|
|
154
175
|
});
|
|
@@ -39,8 +39,10 @@ describe("workflow_test_parser", () => {
|
|
|
39
39
|
input = {a: 5, b: 10}
|
|
40
40
|
} as $sums
|
|
41
41
|
|
|
42
|
-
expect.to_equal ($sum
|
|
43
|
-
|
|
42
|
+
expect.to_equal ($sum
|
|
43
|
+
|subtract:5
|
|
44
|
+
) {
|
|
45
|
+
value = 10
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
}`);
|
|
@@ -68,4 +70,68 @@ describe("workflow_test_parser", () => {
|
|
|
68
70
|
}`);
|
|
69
71
|
expect(parser.errors).to.be.empty;
|
|
70
72
|
});
|
|
73
|
+
|
|
74
|
+
it("should parse a workflow test with api.call within a group", () => {
|
|
75
|
+
const parser = xanoscriptParser(`workflow_test foo {
|
|
76
|
+
stack {
|
|
77
|
+
group {
|
|
78
|
+
stack {
|
|
79
|
+
api.call "endpoint/{id}" verb=PATCH {
|
|
80
|
+
api_group = "external"
|
|
81
|
+
input = {
|
|
82
|
+
data: {action: "update"}
|
|
83
|
+
id: 123
|
|
84
|
+
}
|
|
85
|
+
} as $result
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}`);
|
|
90
|
+
expect(parser.errors).to.be.empty;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should parse a workflow test with expect.to_equal within a group", () => {
|
|
94
|
+
const parser = xanoscriptParser(`workflow_test foo {
|
|
95
|
+
stack {
|
|
96
|
+
var $a {
|
|
97
|
+
value = 42
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function.run woot {
|
|
101
|
+
input = {scope: 12}
|
|
102
|
+
} as $my_val
|
|
103
|
+
|
|
104
|
+
group {
|
|
105
|
+
stack {
|
|
106
|
+
function.call "add" {
|
|
107
|
+
input = {a: 20, b: 22}
|
|
108
|
+
} as $sum
|
|
109
|
+
|
|
110
|
+
expect.to_equal ($a) {
|
|
111
|
+
value = 42
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
group {
|
|
117
|
+
stack {
|
|
118
|
+
function.call "add" {
|
|
119
|
+
input = {a: 20, b: 22}
|
|
120
|
+
} as $sum
|
|
121
|
+
|
|
122
|
+
api.call "users/{user_id}" verb=GET {
|
|
123
|
+
api_group = "account"
|
|
124
|
+
input = { user_id: 123 }
|
|
125
|
+
} as $user
|
|
126
|
+
|
|
127
|
+
expect.to_equal ($a) {
|
|
128
|
+
value = 42
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}`);
|
|
134
|
+
console.log(parser.errors);
|
|
135
|
+
expect(parser.errors).to.be.empty;
|
|
136
|
+
});
|
|
71
137
|
});
|