@xano/xanoscript-language-server 11.6.5 → 11.7.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 (32) hide show
  1. package/.claude/settings.local.json +4 -1
  2. package/lexer/tokens.js +1 -1
  3. package/onHover/functions.md +9 -3
  4. package/package.json +1 -1
  5. package/parser/clauses/inputClause.js +22 -2
  6. package/parser/clauses/inputClause.spec.js +17 -0
  7. package/parser/clauses/nakedStackFn.js +1 -5
  8. package/parser/clauses/testClause.spec.js +0 -9
  9. package/parser/function_parser.js +4 -1
  10. package/parser/function_parser.spec.js +18 -0
  11. package/parser/functions/api/apiCallFn.js +14 -0
  12. package/parser/functions/api/apiCallFn.spec.js +2 -0
  13. package/parser/functions/api/apiRequestFn.js +1 -1
  14. package/parser/functions/api/apiRequestFn.spec.js +1 -1
  15. package/parser/functions/apiFn.js +2 -5
  16. package/parser/functions/controlFn.js +1 -0
  17. package/parser/functions/controls/conditionalFn.js +22 -7
  18. package/parser/functions/controls/conditionalFn.spec.js +21 -0
  19. package/parser/functions/controls/preconditionFn.spec.js +11 -1
  20. package/parser/functions/expect/unitExpectWithArgumentsFn.spec.js +8 -1
  21. package/parser/functions/schema/schemaFn.js +0 -1
  22. package/parser/functions/schema/schemaParseAttributeFn.spec.js +2 -0
  23. package/parser/functions/schema/schemaParseObjectFn.js +35 -10
  24. package/parser/functions/varFn.spec.js +15 -0
  25. package/parser/functions/workflowExpectFn.js +15 -1
  26. package/parser/generic/expressionFn.js +2 -5
  27. package/parser/generic/functionAttrReq.js +1 -0
  28. package/parser/middleware_parser.js +2 -3
  29. package/parser/middleware_parser.spec.js +29 -2
  30. package/parser/query_parser.spec.js +21 -0
  31. package/parser/workflow_test_parser.js +1 -1
  32. package/parser/workflow_test_parser.spec.js +55 -0
@@ -7,7 +7,10 @@
7
7
  "Bash(git checkout:*)",
8
8
  "Bash(ls:*)",
9
9
  "Bash(npm run lint:*)",
10
- "Bash(grep:*)"
10
+ "Bash(grep:*)",
11
+ "Bash(node --input-type=module:*)",
12
+ "Bash(find:*)",
13
+ "Read(//tmp/**)"
11
14
  ]
12
15
  }
13
16
  }
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({
@@ -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, it allows you to invoke internal APIs within your end-to-end workflows tests.
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`: Custom headers to include in the request.
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@xano/xanoscript-language-server",
3
- "version": "11.6.5",
3
+ "version": "11.7.0",
4
4
  "description": "Language Server Protocol implementation for XanoScript",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -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
- $.AT_LEAST_ONE1(() => $.CONSUME1(NewlineToken)); // at least one new line
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
- $.AT_LEAST_ONE2(() => $.CONSUME2(NewlineToken)); // at least one new line
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(nameToken, "function name must not start with '/'");
32
+ $.addInvalidValueError(
33
+ nameToken,
34
+ "function name must not start with '/'",
35
+ );
33
36
  }
34
37
 
35
38
  $.CONSUME(LCurly); // "{"
@@ -93,4 +93,22 @@ 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
+ api.call "users/{user_id}" verb=GET {
104
+ api_group = "account"
105
+ input = { user_id: 123 }
106
+ } as $user
107
+ }
108
+
109
+ response = null
110
+ }`);
111
+ expect(parser.errors).to.not.be.empty;
112
+ });
96
113
  });
114
+
@@ -18,6 +18,20 @@ 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 = ["workflowTestDeclaration", "toolDeclaration", "testClause"];
24
+ const isInValidContext = validContexts.some((context) =>
25
+ $.sectionStack.includes(context)
26
+ );
27
+
28
+ if (!isInValidContext) {
29
+ $.addInvalidValueError(
30
+ fnToken,
31
+ "api.call can only be used within workflow_test, tool, or test contexts"
32
+ );
33
+ }
34
+
21
35
  $.OR([
22
36
  { ALT: () => $.CONSUME(StringLiteral) }, // "foo/bar"
23
37
  { 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?": "[number]",
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 = 25
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 (options = {}) => {
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
- $.MANY1(() => {
41
- $.AT_LEAST_ONE1(() => $.CONSUME3(NewlineToken));
42
- $.SUBRULE($.conditionalElifFn);
43
- });
44
- $.OPTION(() => {
45
- $.AT_LEAST_ONE2(() => $.CONSUME4(NewlineToken));
46
- $.SUBRULE($.conditionalElseFn);
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
  });
@@ -16,7 +16,7 @@ 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
20
  expect(parser.errors).to.be.empty;
21
21
  });
22
22
 
@@ -27,6 +27,13 @@ describe("unitExpectWithArgumentsFn", () => {
27
27
  expect(parser.errors).to.not.be.empty;
28
28
  });
29
29
 
30
+ it("unitExpectWithArgumentsFn accepts an expression", () => {
31
+ const parser = parse(`to_equal ($response) {
32
+ value = \`12092834098240928304982039840923908402384.09823904823098402834\`
33
+ }`);
34
+ expect(parser.errors).to.be.empty;
35
+ });
36
+
30
37
  it("unitExpectWithArgumentsFn does not accept variables", () => {
31
38
  const parser = parse(`to_equal ($response) {
32
39
  value = $response.x
@@ -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
  ],
@@ -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
 
@@ -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."
@@ -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
  });
@@ -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
  });
@@ -46,7 +46,7 @@ export function workflowTestDeclaration($) {
46
46
  hasStack = true;
47
47
  $.SUBRULE($.stackClause, {
48
48
  ARGS: [
49
- { allowExpectStatements: true, allowCallStatements: true },
49
+ { allowCallStatements: true },
50
50
  ],
51
51
  });
52
52
  },
@@ -68,4 +68,59 @@ describe("workflow_test_parser", () => {
68
68
  }`);
69
69
  expect(parser.errors).to.be.empty;
70
70
  });
71
+
72
+ it("should parse a workflow test with api.call within a group", () => {
73
+ const parser = xanoscriptParser(`workflow_test foo {
74
+ stack {
75
+ group {
76
+ stack {
77
+ api.call "endpoint/{id}" verb=PATCH {
78
+ api_group = "external"
79
+ input = {
80
+ data: {action: "update"}
81
+ id: 123
82
+ }
83
+ } as $result
84
+ }
85
+ }
86
+ }
87
+ }`);
88
+ expect(parser.errors).to.be.empty;
89
+ });
90
+
91
+ it("should parse a workflow test with expect.to_equal within a group", () => {
92
+ const parser = xanoscriptParser(`workflow_test foo {
93
+ stack {
94
+ var $a {
95
+ value = 42
96
+ }
97
+
98
+ group {
99
+ stack {
100
+ function.call "add" {
101
+ input = {a: 20, b: 22}
102
+ } as $sum
103
+
104
+ expect.to_equal ($a) {
105
+ value = 42
106
+ }
107
+ }
108
+ }
109
+
110
+ group {
111
+ stack {
112
+ function.call "add" {
113
+ input = {a: 20, b: 22}
114
+ } as $sum
115
+
116
+ expect.to_equal ($a) {
117
+ value = 42
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }`);
123
+ console.log(parser.errors);
124
+ expect(parser.errors).to.be.empty;
125
+ });
71
126
  });