@xano/xanoscript-language-server 11.6.4 → 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 (33) hide show
  1. package/.claude/settings.local.json +4 -1
  2. package/lexer/control.js +9 -0
  3. package/lexer/tokens.js +1 -1
  4. package/onHover/functions.md +9 -3
  5. package/package.json +1 -1
  6. package/parser/clauses/inputClause.js +22 -2
  7. package/parser/clauses/inputClause.spec.js +17 -0
  8. package/parser/clauses/nakedStackFn.js +1 -5
  9. package/parser/clauses/testClause.spec.js +0 -9
  10. package/parser/function_parser.js +4 -1
  11. package/parser/function_parser.spec.js +18 -0
  12. package/parser/functions/api/apiCallFn.js +14 -0
  13. package/parser/functions/api/apiCallFn.spec.js +2 -0
  14. package/parser/functions/api/apiRequestFn.js +1 -1
  15. package/parser/functions/api/apiRequestFn.spec.js +1 -1
  16. package/parser/functions/apiFn.js +2 -5
  17. package/parser/functions/controlFn.js +1 -0
  18. package/parser/functions/controls/conditionalFn.js +22 -7
  19. package/parser/functions/controls/conditionalFn.spec.js +21 -0
  20. package/parser/functions/controls/preconditionFn.spec.js +11 -1
  21. package/parser/functions/expect/unitExpectWithArgumentsFn.spec.js +8 -1
  22. package/parser/functions/schema/schemaFn.js +0 -1
  23. package/parser/functions/schema/schemaParseAttributeFn.spec.js +2 -0
  24. package/parser/functions/schema/schemaParseObjectFn.js +35 -10
  25. package/parser/functions/varFn.spec.js +29 -0
  26. package/parser/functions/workflowExpectFn.js +15 -1
  27. package/parser/generic/expressionFn.js +4 -5
  28. package/parser/generic/functionAttrReq.js +1 -0
  29. package/parser/middleware_parser.js +2 -3
  30. package/parser/middleware_parser.spec.js +29 -2
  31. package/parser/query_parser.spec.js +21 -0
  32. package/parser/workflow_test_parser.js +1 -1
  33. 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/control.js CHANGED
@@ -302,6 +302,13 @@ export const PipeToken = createUniqToken({
302
302
  label: "|",
303
303
  });
304
304
 
305
+ // ..
306
+ export const DoubleDotToken = createUniqToken({
307
+ name: "DoubleDotToken",
308
+ pattern: /\.\./,
309
+ label: "..",
310
+ });
311
+
305
312
  // ,
306
313
  export const CommaToken = createUniqToken({
307
314
  name: "CommaToken",
@@ -471,6 +478,7 @@ export const JsonSearchToken = createUniqToken({
471
478
  });
472
479
 
473
480
  export const ControlTokens = [
481
+ DoubleDotToken,
474
482
  JsonNotBetweenToken,
475
483
  JsonNotContainsToken,
476
484
  JsonNotILikeToken,
@@ -626,6 +634,7 @@ export function mapTokenToType(token) {
626
634
  case Question.name:
627
635
  case ColonToken.name:
628
636
  case PipeToken.name:
637
+ case DoubleDotToken.name:
629
638
  case CommaToken.name:
630
639
  return "punctuation";
631
640
  default:
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.4",
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
  }
@@ -76,6 +76,20 @@ describe("varFn", () => {
76
76
  expect(parser.errors).to.be.empty;
77
77
  });
78
78
 
79
+ it("varFn allows range expressions", () => {
80
+ const parser = parse(`var $numbers {
81
+ value = (1..100)
82
+ }`);
83
+ expect(parser.errors).to.be.empty;
84
+ });
85
+
86
+ it("varFn allows range expressions with variables", () => {
87
+ const parser = parse(`var $numbers {
88
+ value = ($var.start..$end_var)
89
+ }`);
90
+ expect(parser.errors).to.be.empty;
91
+ });
92
+
79
93
  it("varFn accepts a system env variable", () => {
80
94
  const parser = parse(`var $user {
81
95
  value = $env.$remote_ip
@@ -128,6 +142,14 @@ describe("varFn", () => {
128
142
  expect(parser.errors).to.be.empty;
129
143
  });
130
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
+
131
153
  it("should allow accessing a property from an object defined in place", () => {
132
154
  let parser = parse(`var $config {
133
155
  value = {
@@ -253,6 +275,13 @@ describe("varFn", () => {
253
275
  expect(parser.errors).to.not.be.empty;
254
276
  });
255
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
+
256
285
  // All blacklisted names should be rejected in long form via var.update
257
286
  const allBlacklisted = [
258
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."
@@ -3,6 +3,7 @@ import {
3
3
  AndTestToken,
4
4
  ColonToken,
5
5
  Divide,
6
+ DoubleDotToken,
6
7
  GreaterThan,
7
8
  GreaterThanOrEq,
8
9
  IsEqual,
@@ -234,14 +235,14 @@ export function expressionFn($) {
234
235
  if (hasFilters && hasTests) {
235
236
  $.addInvalidValueError(
236
237
  token,
237
- "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",
238
239
  );
239
240
  }
240
241
 
241
242
  if (options.allowQueryExpression && has_ternary) {
242
243
  $.addInvalidValueError(
243
244
  testToken,
244
- "Ternary expressions are not allowed in a where clause"
245
+ "Ternary expressions are not allowed in a where clause",
245
246
  );
246
247
  }
247
248
  };
@@ -263,6 +264,7 @@ export function expressionTestFn($) {
263
264
  { ALT: () => $.CONSUME(Multiply) }, // "*"
264
265
  { ALT: () => $.CONSUME(Divide) }, // "/"
265
266
  { ALT: () => $.CONSUME(Modulus) }, // "%"
267
+ { ALT: () => $.CONSUME(DoubleDotToken) }, // ".."
266
268
  { ALT: () => $.CONSUME(NullishCoalescingToken) }, // "??"
267
269
  { ALT: () => $.CONSUME(AndTestToken) }, // "&&"
268
270
  { ALT: () => $.CONSUME(OrTestToken) }, // "||"
@@ -371,15 +373,12 @@ export function simpleExpressionFn($) {
371
373
 
372
374
  // Group 2: Complex literals (distinct tokens)
373
375
  {
374
- GATE: () => options.allowExpression,
375
376
  ALT: () => $.CONSUME(MultiLineStringToken),
376
377
  },
377
378
  {
378
- GATE: () => options.allowExpression,
379
379
  ALT: () => $.CONSUME(MultiLineExpressionToken),
380
380
  },
381
381
  {
382
- GATE: () => options.allowExpression,
383
382
  ALT: () => $.CONSUME(ExpressionLiteral),
384
383
  },
385
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
  });