harmonyc 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/model/model.js CHANGED
@@ -81,6 +81,9 @@ export class Branch {
81
81
  this.parent = undefined;
82
82
  return this;
83
83
  }
84
+ switch(_i) {
85
+ return this;
86
+ }
84
87
  }
85
88
  export class Step extends Branch {
86
89
  constructor(action, responses = [], children, isFork = false) {
@@ -115,6 +118,9 @@ export class Step extends Branch {
115
118
  get isEmpty() {
116
119
  return this.phrases.every((phrase) => phrase.isEmpty);
117
120
  }
121
+ switch(i) {
122
+ return new Step(this.action.switch(i), this.responses.map((r) => r.switch(i)));
123
+ }
118
124
  }
119
125
  export class State {
120
126
  constructor(text = '') {
@@ -167,6 +173,44 @@ export class Word extends Part {
167
173
  return this.text;
168
174
  }
169
175
  }
176
+ export class Repeater extends Part {
177
+ constructor(choices) {
178
+ super();
179
+ this.choices = choices;
180
+ }
181
+ toString() {
182
+ return `{${this.choices.map((ps) => ps.join(' ')).join(' & ')}}`;
183
+ }
184
+ toSingleLineString() {
185
+ return `{${this.choices
186
+ .map((ps) => ps.map((p) => p.toSingleLineString()).join(' '))
187
+ .join(' & ')}}`;
188
+ }
189
+ }
190
+ export class Switch extends Part {
191
+ constructor(choices) {
192
+ super();
193
+ this.choices = choices;
194
+ }
195
+ toString() {
196
+ return `{ ${this.choices.join(' / ')} }`;
197
+ }
198
+ toSingleLineString() {
199
+ return `{ ${this.choices.map((c) => c.toSingleLineString()).join(' / ')} }`;
200
+ }
201
+ }
202
+ export class Router extends Part {
203
+ constructor(choices) {
204
+ super();
205
+ this.choices = choices;
206
+ }
207
+ toString() {
208
+ return `{ ${this.choices.join(' ; ')} }`;
209
+ }
210
+ toSingleLineString() {
211
+ return `{ ${this.choices.map((c) => c.toSingleLineString()).join(' ; ')} }`;
212
+ }
213
+ }
170
214
  export class Arg extends Part {
171
215
  }
172
216
  export class StringLiteral extends Arg {
@@ -243,6 +287,9 @@ export class Phrase {
243
287
  toSingleLineString() {
244
288
  return this.parts.map((p) => p.toSingleLineString()).join(' ');
245
289
  }
290
+ switch(i) {
291
+ return new this.constructor(this.parts.map((p) => (p instanceof Switch ? p.choices[i] : p)));
292
+ }
246
293
  }
247
294
  export class Action extends Phrase {
248
295
  constructor() {
@@ -306,6 +353,9 @@ export class SaveToVariable extends Part {
306
353
  toString() {
307
354
  return `\${${this.variableName}}`;
308
355
  }
356
+ get words() {
357
+ return [];
358
+ }
309
359
  }
310
360
  export class Precondition extends Branch {
311
361
  constructor(state = '') {
@@ -323,7 +373,7 @@ export function makeTests(root) {
323
373
  let ic = routers.getIncompleteCount();
324
374
  let newIc;
325
375
  do {
326
- const newTest = new Test(root, routers.nextWalk());
376
+ const newTest = new Test(routers.nextWalk());
327
377
  newIc = routers.getIncompleteCount();
328
378
  if (newIc < ic)
329
379
  tests.push(newTest);
@@ -340,12 +390,28 @@ export function makeTests(root) {
340
390
  walk(root);
341
391
  tests = tests.filter((t) => t.steps.length > 0);
342
392
  tests.sort((a, b) => branchIndex.get(a.last) - branchIndex.get(b.last));
393
+ resolveSwitches(tests);
343
394
  tests.forEach((test, i) => (test.testNumber = `T${i + 1}`));
344
395
  return tests;
345
396
  }
397
+ function resolveSwitches(tests) {
398
+ for (let i = 0; i < tests.length; ++i) {
399
+ const test = tests[i];
400
+ const phrases = test.steps.flatMap((s) => s.phrases);
401
+ const switches = phrases.flatMap((p) => p.parts.filter((p) => p instanceof Switch));
402
+ if (switches.length === 0)
403
+ continue;
404
+ const count = switches[0].choices.length;
405
+ if (switches.some((s) => s.choices.length !== count)) {
406
+ throw new Error(`all switches in a test case must have the same number of choices: ${test.name} has ${switches.map((s) => s.choices.length)} choices`);
407
+ }
408
+ const newTests = switches[0].choices.map((_, j) => test.switch(j));
409
+ tests.splice(i, 1, ...newTests);
410
+ i += count - 1;
411
+ }
412
+ }
346
413
  export class Test {
347
- constructor(root, branches) {
348
- this.root = root;
414
+ constructor(branches) {
349
415
  this.branches = branches;
350
416
  this.branches = this.branches.filter((b) => !b.isEmpty);
351
417
  this.labels = this.branches
@@ -370,6 +436,9 @@ export class Test {
370
436
  .map((s) => ` - ${s.headToString()}`)
371
437
  .join('\n')}`;
372
438
  }
439
+ switch(j) {
440
+ return new Test(this.branches.map((b) => b.switch(j)));
441
+ }
373
442
  }
374
443
  export function makeGroups(tests) {
375
444
  if (tests.length === 0)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harmonyc",
3
3
  "description": "Harmony Code - model-driven BDD for Vitest",
4
- "version": "0.15.0",
4
+ "version": "0.16.0",
5
5
  "author": "Bernát Kalló",
6
6
  "type": "module",
7
7
  "bin": {
@@ -12,6 +12,11 @@ export var T;
12
12
  T["Space"] = "space";
13
13
  T["OpeningBracket"] = "[";
14
14
  T["ClosingBracket"] = "]";
15
+ T["OpeningBrace"] = "{";
16
+ T["ClosingBrace"] = "}";
17
+ T["And"] = "&";
18
+ T["Slash"] = "/";
19
+ T["Semicolon"] = ";";
15
20
  T["DoubleQuoteString"] = "double-quote string";
16
21
  T["UnclosedDoubleQuoteString"] = "unclosed double-quote string";
17
22
  T["BacktickString"] = "backtick string";
@@ -39,6 +44,11 @@ const rules = [
39
44
  [true, /:(?=\s*(?:\n|$))/y, T.Colon],
40
45
  [true, /\[/y, T.OpeningBracket],
41
46
  [true, /\]/y, T.ClosingBracket],
47
+ [true, /\{/y, T.OpeningBrace],
48
+ [true, /\}/y, T.ClosingBrace],
49
+ [true, /\//y, T.Slash],
50
+ [true, /&/y, T.And],
51
+ [true, /;/y, T.Semicolon],
42
52
  [true, /!!/y, T.ErrorMark],
43
53
  [true, /=>/y, T.ResponseArrow],
44
54
  [
@@ -59,6 +69,6 @@ const rules = [
59
69
  [true, /\$\{[^}\n]*/y, T.UnclosedVariable],
60
70
  [true, /\|(?: .*|(?=\n|$))/y, T.MultilineString],
61
71
  [true, /\|[^ \n]/y, T.InvalidMultilineStringMark],
62
- [true, /.+?(?=[\[\]"`|#]|\/\/|\$\{|$|=>|!!|:\s*$)/my, T.Words],
72
+ [true, /.+?(?=[\[\]"`|#{}&/;]|\$\{|$|=>|!!|:\s*$)/my, T.Words],
63
73
  ];
64
74
  export default rules;
package/parser/parser.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { alt_sc, apply, expectEOF, expectSingleResult, kright, opt_sc, rep_sc, seq, tok, list_sc, kleft, kmid, fail, nil, unableToConsumeToken, } from 'typescript-parsec';
2
2
  import { T, lexer } from "./lexer.js";
3
- import { Action, Response, CodeLiteral, StringLiteral, Section, Step, Docstring, Word, Label, ErrorResponse, SaveToVariable, SetVariable, } from "../model/model.js";
3
+ import { Action, Response, CodeLiteral, StringLiteral, Section, Step, Docstring, Word, Label, ErrorResponse, SaveToVariable, SetVariable, Switch, } from "../model/model.js";
4
4
  export function parse(input, production = TEST_DESIGN) {
5
5
  const tokens = lexer.parse(input);
6
6
  return expectSingleResult(expectEOF(production.parse(tokens)));
@@ -26,7 +26,14 @@ function anythingBut(kind) {
26
26
  },
27
27
  };
28
28
  }
29
- export const NEWLINES = list_sc(tok(T.Newline), nil()), WORDS = apply(tok(T.Words), ({ text }) => new Word(text.trimEnd().split(/\s+/).join(' '))), DOUBLE_QUOTE_STRING = alt_sc(apply(tok(T.DoubleQuoteString), ({ text }) => new StringLiteral(JSON.parse(text))), seq(tok(T.UnclosedDoubleQuoteString), fail('unclosed double-quote string'))), BACKTICK_STRING = apply(tok(T.BacktickString), ({ text }) => new CodeLiteral(text.slice(1, -1))), DOCSTRING = kright(opt_sc(NEWLINES), apply(list_sc(tok(T.MultilineString), tok(T.Newline)), (lines) => new Docstring(lines.map(({ text }) => text.slice(2)).join('\n')))), ERROR_MARK = tok(T.ErrorMark), VARIABLE = apply(tok(T.Variable), ({ text }) => text.slice(2, -1)), PART = alt_sc(WORDS, DOUBLE_QUOTE_STRING, BACKTICK_STRING, DOCSTRING), PHRASE = rep_sc(PART), ARG = alt_sc(DOUBLE_QUOTE_STRING, BACKTICK_STRING, DOCSTRING), SET_VARIABLE = apply(seq(VARIABLE, ARG), ([variable, value]) => new SetVariable(variable, value)), ACTION = alt_sc(SET_VARIABLE, apply(PHRASE, (parts) => new Action(parts))), RESPONSE = apply(seq(PHRASE, opt_sc(VARIABLE)), ([parts, variable]) => new Response(parts, variable !== undefined ? new SaveToVariable(variable) : undefined)), ERROR_RESPONSE = apply(seq(ERROR_MARK, opt_sc(alt_sc(DOUBLE_QUOTE_STRING, DOCSTRING))), ([, parts]) => new ErrorResponse(parts)), SAVE_TO_VARIABLE = apply(VARIABLE, (variable) => new Response([], new SaveToVariable(variable))), ARROW = kright(opt_sc(NEWLINES), tok(T.ResponseArrow)), RESPONSE_ITEM = kright(ARROW, alt_sc(SAVE_TO_VARIABLE, ERROR_RESPONSE, RESPONSE)), STEP = apply(seq(ACTION, rep_sc(RESPONSE_ITEM)), ([action, responses]) => new Step(action, responses).setFork(true)), LABEL = apply(kleft(list_sc(PART, nil()), tok(T.Colon)), (words) => new Label(words.map((w) => w.toString()).join(' '))), SECTION = apply(LABEL, (text) => new Section(text)), BRANCH = alt_sc(SECTION, STEP), // section first, to make sure there is no colon after step
29
+ export const NEWLINES = list_sc(tok(T.Newline), nil()), WORDS = apply(tok(T.Words), ({ text }) => new Word(text.trimEnd().split(/\s+/).join(' '))), DOUBLE_QUOTE_STRING = alt_sc(apply(tok(T.DoubleQuoteString), ({ text }) => new StringLiteral(JSON.parse(text))), seq(tok(T.UnclosedDoubleQuoteString), fail('unclosed double-quote string'))), BACKTICK_STRING = apply(tok(T.BacktickString), ({ text }) => new CodeLiteral(text.slice(1, -1))), DOCSTRING = kright(opt_sc(NEWLINES), apply(list_sc(tok(T.MultilineString), tok(T.Newline)), (lines) => new Docstring(lines.map(({ text }) => text.slice(2)).join('\n')))), ERROR_MARK = tok(T.ErrorMark), VARIABLE = apply(tok(T.Variable), ({ text }) => text.slice(2, -1)), SIMPLE_PART = alt_sc(WORDS, DOUBLE_QUOTE_STRING, BACKTICK_STRING, DOCSTRING),
30
+ //REPEATER = apply(
31
+ //list_sc(rep_sc(SIMPLE_PART), tok(T.And)),
32
+ //(cs) => new Repeater(cs)
33
+ //),
34
+ SWITCH = apply(list_sc(SIMPLE_PART, tok(T.Slash)), (cs) => new Switch(cs)),
35
+ //ROUTER = apply(list_sc(REPEATER, tok(T.Semicolon)), (cs) => new Router(cs)),
36
+ BRACES = kmid(tok(T.OpeningBrace), SWITCH, tok(T.ClosingBrace)), PART = alt_sc(SIMPLE_PART, BRACES), PHRASE = rep_sc(PART), ARG = alt_sc(DOUBLE_QUOTE_STRING, BACKTICK_STRING, DOCSTRING), SET_VARIABLE = apply(seq(VARIABLE, ARG), ([variable, value]) => new SetVariable(variable, value)), ACTION = alt_sc(SET_VARIABLE, apply(PHRASE, (parts) => new Action(parts))), RESPONSE = apply(seq(PHRASE, opt_sc(VARIABLE)), ([parts, variable]) => new Response(parts, variable !== undefined ? new SaveToVariable(variable) : undefined)), ERROR_RESPONSE = apply(seq(ERROR_MARK, opt_sc(alt_sc(DOUBLE_QUOTE_STRING, DOCSTRING))), ([, parts]) => new ErrorResponse(parts)), SAVE_TO_VARIABLE = apply(VARIABLE, (variable) => new Response([], new SaveToVariable(variable))), ARROW = kright(opt_sc(NEWLINES), tok(T.ResponseArrow)), RESPONSE_ITEM = kright(ARROW, alt_sc(SAVE_TO_VARIABLE, ERROR_RESPONSE, RESPONSE)), STEP = apply(seq(ACTION, rep_sc(RESPONSE_ITEM)), ([action, responses]) => new Step(action, responses).setFork(true)), LABEL = apply(kleft(list_sc(PART, nil()), tok(T.Colon)), (words) => new Label(words.map((w) => w.toString()).join(' '))), SECTION = apply(LABEL, (text) => new Section(text)), BRANCH = alt_sc(SECTION, STEP), // section first, to make sure there is no colon after step
30
37
  DENTS = apply(alt_sc(tok(T.Plus), tok(T.Minus)), (seqOrFork) => {
31
38
  return {
32
39
  dent: (seqOrFork.text.length - 2) / 2,