spex-parser 0.1.5 → 0.6.2

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 (44) hide show
  1. package/README.md +320 -9
  2. package/dist/ast.d.ts +71 -2
  3. package/dist/ast.d.ts.map +1 -1
  4. package/dist/constants.d.ts +20 -0
  5. package/dist/constants.d.ts.map +1 -0
  6. package/dist/constants.js +32 -0
  7. package/dist/constants.js.map +1 -0
  8. package/dist/index.d.ts +4 -2
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +2 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/lexer.d.ts +17 -1
  13. package/dist/lexer.d.ts.map +1 -1
  14. package/dist/lexer.js +159 -5
  15. package/dist/lexer.js.map +1 -1
  16. package/dist/parser.d.ts +9 -0
  17. package/dist/parser.d.ts.map +1 -1
  18. package/dist/parser.js +105 -7
  19. package/dist/parser.js.map +1 -1
  20. package/dist/visitor.d.ts +11 -2
  21. package/dist/visitor.d.ts.map +1 -1
  22. package/dist/visitor.js +182 -8
  23. package/dist/visitor.js.map +1 -1
  24. package/package.json +1 -1
  25. package/src/ast.ts +94 -0
  26. package/src/constants.ts +40 -0
  27. package/src/index.ts +20 -3
  28. package/src/lexer.ts +150 -6
  29. package/src/parser.ts +130 -7
  30. package/src/visitor.ts +196 -11
  31. package/tests/constants.test.ts +102 -0
  32. package/tests/e2e.test.ts +15 -3
  33. package/tests/lexer.test.ts +230 -9
  34. package/tests/parser.test.ts +455 -10
  35. package/tests/props/express_todo_web.spex +108 -0
  36. package/tests/props/express_web_env.spex +16 -0
  37. package/tests/props/flask_todo_web.spex +111 -0
  38. package/tests/props/flask_web_env.spex +16 -0
  39. package/tests/props/python_cli_env.spex +16 -0
  40. package/tests/props/python_todo_cli.spex +70 -0
  41. package/tests/props/todo.spex +99 -68
  42. package/tests/props/typescript_cli_env.spex +16 -0
  43. package/tests/props/typescript_todo_cli.spex +70 -0
  44. package/tests/visitor.test.ts +905 -6
@@ -96,6 +96,13 @@ describe('SpexParser', () => {
96
96
  expect(parser.errors).toHaveLength(0)
97
97
  })
98
98
 
99
+ it('should parse subobject declaration with a set operation base', () => {
100
+ const testCase =
101
+ 'create MySubobject as from Web intersect TypeScript select { is an express app };'
102
+ const { parser } = parseInput(testCase)
103
+ expect(parser.errors).toHaveLength(0)
104
+ })
105
+
99
106
  it('should parse array type declaration', () => {
100
107
  const testCase = 'create MyArray as string[];'
101
108
  const { parser } = parseInput(testCase)
@@ -145,6 +152,18 @@ describe('SpexParser', () => {
145
152
  expect(parser.errors).toHaveLength(0)
146
153
  })
147
154
 
155
+ it('should parse basic object concept', () => {
156
+ const testCase = 'create MyObject as concept;'
157
+ const { parser } = parseInput(testCase)
158
+ expect(parser.errors).toHaveLength(0)
159
+ })
160
+
161
+ it('should parse basic object environment', () => {
162
+ const testCase = 'create MyObject as environment;'
163
+ const { parser } = parseInput(testCase)
164
+ expect(parser.errors).toHaveLength(0)
165
+ })
166
+
148
167
  it('should parse basic objects in product fields', () => {
149
168
  const testCase = 'create Config as (name: string, count: number, active: bool);'
150
169
  const { parser } = parseInput(testCase)
@@ -174,6 +193,18 @@ describe('SpexParser', () => {
174
193
  const { parser } = parseInput(testCase)
175
194
  expect(parser.errors).not.toHaveLength(0)
176
195
  })
196
+
197
+ it('should not allow overriding basic object concept', () => {
198
+ const testCase = 'create concept as Number;'
199
+ const { parser } = parseInput(testCase)
200
+ expect(parser.errors).not.toHaveLength(0)
201
+ })
202
+
203
+ it('should not allow overriding basic object environment', () => {
204
+ const testCase = 'create environment as Number;'
205
+ const { parser } = parseInput(testCase)
206
+ expect(parser.errors).not.toHaveLength(0)
207
+ })
177
208
  })
178
209
 
179
210
  describe('import declaration', () => {
@@ -212,64 +243,343 @@ describe('SpexParser', () => {
212
243
  })
213
244
  })
214
245
 
246
+ describe('enum object', () => {
247
+ it('should parse enum object declaration', () => {
248
+ const testCase = "create myEnum as enum ('v1', 'v2');"
249
+ const { parser } = parseInput(testCase)
250
+ expect(parser.errors).toHaveLength(0)
251
+ })
252
+
253
+ it('should parse enum object declaration case-insensitively', () => {
254
+ const testCase = "CREATE myEnum AS ENUM ('v1', 'v2');"
255
+ const { parser } = parseInput(testCase)
256
+ expect(parser.errors).toHaveLength(0)
257
+ })
258
+
259
+ it('should parse single-value enum object', () => {
260
+ const testCase = "create Status as enum ('ACTIVE');"
261
+ const { parser } = parseInput(testCase)
262
+ expect(parser.errors).toHaveLength(0)
263
+ })
264
+
265
+ it('should parse enum object with double quoted values', () => {
266
+ const testCase = 'create myEnum as enum ("v1", "v2");'
267
+ const { parser } = parseInput(testCase)
268
+ expect(parser.errors).toHaveLength(0)
269
+ })
270
+
271
+ it('should parse enum object with mixed quote values', () => {
272
+ const testCase = "create myEnum as enum (\"v1\", 'v2');"
273
+ const { parser } = parseInput(testCase)
274
+ expect(parser.errors).toHaveLength(0)
275
+ })
276
+
277
+ it('should parse enum object with escaped values', () => {
278
+ const testCase = "create myEnum as enum ('it\\'s');"
279
+ const { parser } = parseInput(testCase)
280
+ expect(parser.errors).toHaveLength(0)
281
+ })
282
+
283
+ it('should parse enum object inside a product object', () => {
284
+ const testCase = "create Config as (kind: enum ('a', 'b'));"
285
+ const { parser } = parseInput(testCase)
286
+ expect(parser.errors).toHaveLength(0)
287
+ })
288
+
289
+ it('should parse array of enum object', () => {
290
+ const testCase = "create myEnum as enum ('a', 'b')[];"
291
+ const { parser } = parseInput(testCase)
292
+ expect(parser.errors).toHaveLength(0)
293
+ })
294
+
295
+ it('should not parse enum object without values', () => {
296
+ const testCase = 'create myEnum as enum ();'
297
+ const { parser } = parseInput(testCase)
298
+ expect(parser.errors).not.toHaveLength(0)
299
+ })
300
+
301
+ it('should not parse enum object with a trailing comma', () => {
302
+ const testCase = "create myEnum as enum ('v1',);"
303
+ const { parser } = parseInput(testCase)
304
+ expect(parser.errors).not.toHaveLength(0)
305
+ })
306
+
307
+ it('should not parse enum object without parentheses', () => {
308
+ const testCase = 'create myEnum as enum;'
309
+ const { parser } = parseInput(testCase)
310
+ expect(parser.errors).not.toHaveLength(0)
311
+ })
312
+ })
313
+
314
+ describe('literal object', () => {
315
+ it('should parse string literal object', () => {
316
+ const testCase = 'create Foo as "SpexFile";'
317
+ const { parser } = parseInput(testCase)
318
+ expect(parser.errors).toHaveLength(0)
319
+ })
320
+
321
+ it('should parse number literal object', () => {
322
+ const testCase = 'create Foo as 42;'
323
+ const { parser } = parseInput(testCase)
324
+ expect(parser.errors).toHaveLength(0)
325
+ })
326
+
327
+ it('should parse bool literal object', () => {
328
+ const testCase = 'create Foo as true;'
329
+ const { parser } = parseInput(testCase)
330
+ expect(parser.errors).toHaveLength(0)
331
+ })
332
+
333
+ it('should parse product of literal objects', () => {
334
+ const testCase = "create Foo as (name: \"John\", age: 42);"
335
+ const { parser } = parseInput(testCase)
336
+ expect(parser.errors).toHaveLength(0)
337
+ })
338
+
339
+ it('should parse exponential of literal objects', () => {
340
+ const testCase = 'create Foo as "a" -> "b";'
341
+ const { parser } = parseInput(testCase)
342
+ expect(parser.errors).toHaveLength(0)
343
+ })
344
+
345
+ it('should not parse literal with member suffix', () => {
346
+ const testCase = 'create Foo as "a".b;'
347
+ const { parser } = parseInput(testCase)
348
+ expect(parser.errors).not.toHaveLength(0)
349
+ })
350
+ })
351
+
352
+ describe('set object', () => {
353
+ it('should parse union object', () => {
354
+ const testCase = 'create X as A UNION B;'
355
+ const { parser } = parseInput(testCase)
356
+ expect(parser.errors).toHaveLength(0)
357
+ })
358
+
359
+ it('should parse intersect object', () => {
360
+ const testCase = 'create X as A INTERSECT B;'
361
+ const { parser } = parseInput(testCase)
362
+ expect(parser.errors).toHaveLength(0)
363
+ })
364
+
365
+ it('should parse except object', () => {
366
+ const testCase = 'create X as A EXCEPT B;'
367
+ const { parser } = parseInput(testCase)
368
+ expect(parser.errors).toHaveLength(0)
369
+ })
370
+
371
+ it('should parse chained set operations', () => {
372
+ const testCase = 'create X as A UNION B INTERSECT C;'
373
+ const { parser } = parseInput(testCase)
374
+ expect(parser.errors).toHaveLength(0)
375
+ })
376
+
377
+ it('should parse set operations on subobjects', () => {
378
+ const testCase =
379
+ 'create X as from int select { are even } UNION from int select { are positive };'
380
+ const { parser } = parseInput(testCase)
381
+ expect(parser.errors).toHaveLength(0)
382
+ })
383
+
384
+ it('should parse set operations with literals', () => {
385
+ const testCase = 'create X as string EXCEPT "root";'
386
+ const { parser } = parseInput(testCase)
387
+ expect(parser.errors).toHaveLength(0)
388
+ })
389
+
390
+ it('should parse set operations in product fields', () => {
391
+ const testCase = 'create X as (a: A UNION B);'
392
+ const { parser } = parseInput(testCase)
393
+ expect(parser.errors).toHaveLength(0)
394
+ })
395
+
396
+ it('should parse case-insensitively', () => {
397
+ const testCase = 'CREATE X AS A union B;'
398
+ const { parser } = parseInput(testCase)
399
+ expect(parser.errors).toHaveLength(0)
400
+ })
401
+
402
+ it('should bind arrow tighter than set operations', () => {
403
+ const testCase = 'create X as A -> B UNION C;'
404
+ const { parser } = parseInput(testCase)
405
+ expect(parser.errors).toHaveLength(0)
406
+ })
407
+ })
408
+
409
+ describe('coproduct object', () => {
410
+ it('should parse a pipe object', () => {
411
+ const testCase = 'create Shape as Point | Circle;'
412
+ const { parser } = parseInput(testCase)
413
+ expect(parser.errors).toHaveLength(0)
414
+ })
415
+
416
+ it('should parse chained pipes left-associatively', () => {
417
+ const testCase = 'create X as A | B | C;'
418
+ const { parser } = parseInput(testCase)
419
+ expect(parser.errors).toHaveLength(0)
420
+ })
421
+
422
+ it('should bind arrow tighter than pipe', () => {
423
+ const testCase = 'create X as A -> B | C;'
424
+ const { parser } = parseInput(testCase)
425
+ expect(parser.errors).toHaveLength(0)
426
+ })
427
+
428
+ it('should bind pipe tighter than set operations', () => {
429
+ const testCase = 'create X as A UNION B | C;'
430
+ const { parser } = parseInput(testCase)
431
+ expect(parser.errors).toHaveLength(0)
432
+ })
433
+
434
+ it('should parse pipes in product fields', () => {
435
+ const testCase = 'create X as (a: A | B, b: C);'
436
+ const { parser } = parseInput(testCase)
437
+ expect(parser.errors).toHaveLength(0)
438
+ })
439
+
440
+ it('should parse pipes on subobjects', () => {
441
+ const testCase =
442
+ 'create X as from int select { are even } | from int select { are odd };'
443
+ const { parser } = parseInput(testCase)
444
+ expect(parser.errors).toHaveLength(0)
445
+ })
446
+
447
+ it('should parse pipes with literals', () => {
448
+ const testCase = 'create X as string | "number";'
449
+ const { parser } = parseInput(testCase)
450
+ expect(parser.errors).toHaveLength(0)
451
+ })
452
+ })
453
+
454
+ describe('parenthesized object', () => {
455
+ it('should parse a parenthesized coproduct', () => {
456
+ const testCase = 'create X as (A | B);'
457
+ const { parser } = parseInput(testCase)
458
+ expect(parser.errors).toHaveLength(0)
459
+ })
460
+
461
+ it('should override precedence with parentheses', () => {
462
+ const testCase = 'create X as A -> (B | C);'
463
+ const { parser } = parseInput(testCase)
464
+ expect(parser.errors).toHaveLength(0)
465
+ })
466
+
467
+ it('should group set operations with parentheses', () => {
468
+ const testCase = 'create X as (A UNION B) EXCEPT C;'
469
+ const { parser } = parseInput(testCase)
470
+ expect(parser.errors).toHaveLength(0)
471
+ })
472
+
473
+ it('should parse nested parentheses', () => {
474
+ const testCase = 'create X as ((A | B));'
475
+ const { parser } = parseInput(testCase)
476
+ expect(parser.errors).toHaveLength(0)
477
+ })
478
+
479
+ it('should parse arrays of parenthesized objects', () => {
480
+ const testCase = 'create X as (A | B)[];'
481
+ const { parser } = parseInput(testCase)
482
+ expect(parser.errors).toHaveLength(0)
483
+ })
484
+
485
+ it('should still parse products inside parentheses', () => {
486
+ const testCase = 'create X as (a: (B | C));'
487
+ const { parser } = parseInput(testCase)
488
+ expect(parser.errors).toHaveLength(0)
489
+ })
490
+ })
491
+
492
+ describe('pattern object', () => {
493
+ it('should parse a pattern object', () => {
494
+ const testCase = 'create X as /\\d+/;'
495
+ const { parser } = parseInput(testCase)
496
+ expect(parser.errors).toHaveLength(0)
497
+ })
498
+
499
+ it('should parse a pattern object with flags', () => {
500
+ const testCase = 'create X as /create\\b/i;'
501
+ const { parser } = parseInput(testCase)
502
+ expect(parser.errors).toHaveLength(0)
503
+ })
504
+
505
+ it('should parse patterns in product fields', () => {
506
+ const testCase = 'create X as (name: string, pattern: /[a-z]+/i);'
507
+ const { parser } = parseInput(testCase)
508
+ expect(parser.errors).toHaveLength(0)
509
+ })
510
+
511
+ it('should parse patterns in set operations', () => {
512
+ const testCase = 'create X as /\\d+/ UNION /\\w+/;'
513
+ const { parser } = parseInput(testCase)
514
+ expect(parser.errors).toHaveLength(0)
515
+ })
516
+ })
517
+
215
518
  describe('package declaration', () => {
216
519
  it('should parse package executable declaration', () => {
217
- const testCase = 'package executable myapp as Main;'
520
+ const testCase = 'package executable myapp as Main in Python;'
218
521
  const { parser } = parseInput(testCase)
219
522
  expect(parser.errors).toHaveLength(0)
220
523
  })
221
524
 
222
525
  it('should parse package module declaration', () => {
223
- const testCase = 'package module mylib as utils;'
526
+ const testCase = 'package module mylib as utils in Node;'
224
527
  const { parser } = parseInput(testCase)
225
528
  expect(parser.errors).toHaveLength(0)
226
529
  })
227
530
 
228
531
  it('should parse package executable with exponential object', () => {
229
- const testCase = 'package executable cli as (path: string) -> unit;'
532
+ const testCase = 'package executable cli as (path: string) -> unit in Python;'
230
533
  const { parser } = parseInput(testCase)
231
534
  expect(parser.errors).toHaveLength(0)
232
535
  })
233
536
 
234
537
  it('should parse package executable with dotted name', () => {
235
- const testCase = 'package executable myapp as app.Main;'
538
+ const testCase = 'package executable myapp as app.Main in Python;'
236
539
  const { parser } = parseInput(testCase)
237
540
  expect(parser.errors).toHaveLength(0)
238
541
  })
239
542
 
240
543
  it('should parse package module with array type', () => {
241
- const testCase = 'package module mylib as string[];'
544
+ const testCase = 'package module mylib as string[] in Python;'
242
545
  const { parser } = parseInput(testCase)
243
546
  expect(parser.errors).toHaveLength(0)
244
547
  })
245
548
 
246
549
  it('should parse package executable with product type', () => {
247
- const testCase = 'package module mylib as (name: string, count: number);'
550
+ const testCase = 'package module mylib as (name: string, count: number) in Python;'
248
551
  const { parser } = parseInput(testCase)
249
552
  expect(parser.errors).toHaveLength(0)
250
553
  })
251
554
 
252
555
  it('should parse package executable with subobject', () => {
253
- const testCase = 'package executable myapp as from string select { is a valid command };'
556
+ const testCase =
557
+ 'package executable myapp as from string select { is a valid command } in Python;'
254
558
  const { parser } = parseInput(testCase)
255
559
  expect(parser.errors).toHaveLength(0)
256
560
  })
257
561
 
258
562
  it('should parse package declaration case-insensitively', () => {
259
- const testCase = 'PACKAGE EXECUTABLE myapp AS Main;'
563
+ const testCase = 'PACKAGE EXECUTABLE myapp AS Main IN Python;'
260
564
  const { parser } = parseInput(testCase)
261
565
  expect(parser.errors).toHaveLength(0)
262
566
  })
263
567
 
264
568
  it('should parse mixed package and generate declarations', () => {
265
- const testCase = 'package executable myapp as Main;\ngenerate Main;'
569
+ const testCase = 'package executable myapp as Main in Python;\ngenerate Main;'
266
570
  const { parser } = parseInput(testCase)
267
571
  expect(parser.errors).toHaveLength(0)
268
572
  })
269
573
 
270
574
  it('should parse package declaration with complex nested type', () => {
271
575
  const testCase =
272
- 'package module mylib as from (x: number, y: number) -> number select { computes distance };'
576
+ 'package module mylib as from (x: number, y: number) -> number select { computes distance } in Node;'
577
+ const { parser } = parseInput(testCase)
578
+ expect(parser.errors).toHaveLength(0)
579
+ })
580
+
581
+ it('should parse package with complex environment object', () => {
582
+ const testCase = 'package executable myapp as Main in from environment select { python };'
273
583
  const { parser } = parseInput(testCase)
274
584
  expect(parser.errors).toHaveLength(0)
275
585
  })
@@ -338,4 +648,139 @@ describe('SpexParser', () => {
338
648
  expect(parser.errors).toHaveLength(0)
339
649
  })
340
650
  })
651
+
652
+ describe('realize declaration', () => {
653
+ it('should parse a realize declaration', () => {
654
+ const testCase = 'realize Shape as Circle in environment;'
655
+ const { parser } = parseInput(testCase)
656
+ expect(parser.errors).toHaveLength(0)
657
+ })
658
+
659
+ it('should parse a realize declaration case-insensitively', () => {
660
+ const testCase = 'REALIZE Shape AS Circle IN environment;'
661
+ const { parser } = parseInput(testCase)
662
+ expect(parser.errors).toHaveLength(0)
663
+ })
664
+
665
+ it('should parse a realize declaration with product objects', () => {
666
+ const testCase = 'realize (x: number) as (y: string) in environment;'
667
+ const { parser } = parseInput(testCase)
668
+ expect(parser.errors).toHaveLength(0)
669
+ })
670
+
671
+ it('should parse a realize declaration with exponential objects', () => {
672
+ const testCase = 'realize string -> number as string -> string in environment;'
673
+ const { parser } = parseInput(testCase)
674
+ expect(parser.errors).toHaveLength(0)
675
+ })
676
+
677
+ it('should parse a realize declaration with a named environment', () => {
678
+ const testCase = 'realize A as B in MyEnv;'
679
+ const { parser } = parseInput(testCase)
680
+ expect(parser.errors).toHaveLength(0)
681
+ })
682
+
683
+ it('should parse a realize declaration without an environment', () => {
684
+ const testCase = 'realize A as B;'
685
+ const { parser } = parseInput(testCase)
686
+ expect(parser.errors).toHaveLength(0)
687
+ })
688
+
689
+ it('should parse a realize declaration mixed with other declarations', () => {
690
+ const testCase = 'create A as string;\nrealize A as B in MyEnv;\ngenerate A;'
691
+ const { parser } = parseInput(testCase)
692
+ expect(parser.errors).toHaveLength(0)
693
+ })
694
+ })
695
+
696
+ describe('include declaration', () => {
697
+ it('should parse include declaration', () => {
698
+ const testCase = 'include "config.json" as config;'
699
+ const { parser } = parseInput(testCase)
700
+ expect(parser.errors).toHaveLength(0)
701
+ })
702
+
703
+ it('should parse include declaration case-insensitively', () => {
704
+ const testCase = 'INCLUDE "config.json" AS config;'
705
+ const { parser } = parseInput(testCase)
706
+ expect(parser.errors).toHaveLength(0)
707
+ })
708
+
709
+ it('should parse include with single-quoted address', () => {
710
+ const testCase = "include 'data/file.txt' as myfile;"
711
+ const { parser } = parseInput(testCase)
712
+ expect(parser.errors).toHaveLength(0)
713
+ })
714
+
715
+ it('should parse include with folder address', () => {
716
+ const testCase = 'include "images/" as assets;'
717
+ const { parser } = parseInput(testCase)
718
+ expect(parser.errors).toHaveLength(0)
719
+ })
720
+
721
+ it('should parse include mixed with other declarations', () => {
722
+ const testCase =
723
+ 'include "config.json" as config;\ncreate Todo as (id: string);\ngenerate Main;'
724
+ const { parser } = parseInput(testCase)
725
+ expect(parser.errors).toHaveLength(0)
726
+ })
727
+
728
+ it('should not parse include without address', () => {
729
+ const testCase = 'include as config;'
730
+ const { parser } = parseInput(testCase)
731
+ expect(parser.errors).not.toHaveLength(0)
732
+ })
733
+
734
+ it('should not parse include without name', () => {
735
+ const testCase = 'include "config.json" as ;'
736
+ const { parser } = parseInput(testCase)
737
+ expect(parser.errors).not.toHaveLength(0)
738
+ })
739
+ })
740
+
741
+ describe('lambda object', () => {
742
+ it('should parse lambda with product base', () => {
743
+ const testCase = 'create sum as lambda (a: number, b: number) -> number ```python\nreturn a + b\n```;'
744
+ const { parser } = parseInput(testCase)
745
+ expect(parser.errors).toHaveLength(0)
746
+ })
747
+
748
+ it('should parse lambda with named base', () => {
749
+ const testCase = 'create double as lambda number -> number ```python\nreturn @n * 2\n```;'
750
+ const { parser } = parseInput(testCase)
751
+ expect(parser.errors).toHaveLength(0)
752
+ })
753
+
754
+ it('should parse lambda case-insensitively', () => {
755
+ const testCase = 'create sum as LAMBDA (a: number) -> number ```python\nreturn @a\n```;'
756
+ const { parser } = parseInput(testCase)
757
+ expect(parser.errors).toHaveLength(0)
758
+ })
759
+
760
+ it('should parse lambda with pattern blocks', () => {
761
+ const testCase =
762
+ 'create transform as lambda (x: number) -> number ```python\nif @x > 0:\n @{return sin(@x)}\nelse:\n @{return cos(@x)}\n```;'
763
+ const { parser } = parseInput(testCase)
764
+ expect(parser.errors).toHaveLength(0)
765
+ })
766
+
767
+ it('should parse lambda in product field', () => {
768
+ const testCase =
769
+ 'create Config as (handler: lambda (x: string) -> string ```typescript\nreturn x.toUpperCase();\n```, port: number);'
770
+ const { parser } = parseInput(testCase)
771
+ expect(parser.errors).toHaveLength(0)
772
+ })
773
+
774
+ it('should not parse lambda without language', () => {
775
+ const testCase = 'create sum as lambda (a: number) -> number ```\nreturn @a\n```;'
776
+ const { parser } = parseInput(testCase)
777
+ expect(parser.errors).not.toHaveLength(0)
778
+ })
779
+
780
+ it('should not parse lambda without code block', () => {
781
+ const testCase = 'create sum as lambda (a: number) -> number;'
782
+ const { parser } = parseInput(testCase)
783
+ expect(parser.errors).not.toHaveLength(0)
784
+ })
785
+ })
341
786
  })
@@ -0,0 +1,108 @@
1
+ -- The Express realization of the Todo Web app.
2
+ --
3
+ -- Imports the shared TodoWeb representation, the todo operations, and the
4
+ -- Express environment. PostgreSQL storage, the route handlers, and the main
5
+ -- exponential are defined as separate objects, and the concept is realized
6
+ -- as a product of them in the realize statement. Express handlers receive a
7
+ -- request and a response and write to the response.
8
+
9
+ import TodoWeb from "todo.spex";
10
+ import Todo from "todo.spex";
11
+ import TodoId from "todo.spex";
12
+ import TodoTitle from "todo.spex";
13
+ import AddTodo from "todo.spex";
14
+ import ListTodos from "todo.spex";
15
+ import CompleteTodo from "todo.spex";
16
+ import HttpRequest from "todo.spex";
17
+ import HttpResponse from "todo.spex";
18
+ import ExpressWebEnv from "express_web_env.spex";
19
+
20
+ -- TODO: realize the shared HttpRequest and HttpResponse objects using the
21
+ -- types provided by the express dependency in ExpressWebEnv.
22
+
23
+ /* PostgreSQL storage. */
24
+ create PgPool as (
25
+ connectionString: string
26
+ );
27
+
28
+ create ConnectPostgres as
29
+ from (connectionString: string) -> PgPool
30
+ select {
31
+ 1. create a connection pool to the PostgreSQL database
32
+ 2. create the todos table if it does not exist
33
+ };
34
+
35
+ create SelectTodos as
36
+ from (pool: PgPool) -> Todo[]
37
+ select {
38
+ 1. query the id, title, and completed columns from the todos table
39
+ 2. map each row to a @Todo
40
+ };
41
+
42
+ create InsertTodo as
43
+ from (pool: PgPool, todo: Todo) -> unit
44
+ select {
45
+ 1. insert the id, title, and completed values of @todo into the todos table
46
+ };
47
+
48
+ create UpdateTodo as
49
+ from (pool: PgPool, todo: Todo) -> unit
50
+ select {
51
+ 1. update the completed value of the row matching the id of @todo
52
+ };
53
+
54
+ /* Route handlers. */
55
+ create ListTodosHandler as
56
+ from (request: HttpRequest, response: HttpResponse) -> unit
57
+ select {
58
+ 1. query the todos with @SelectTodos
59
+ 2. write the todos serialized as JSON to @response with status 200
60
+ };
61
+
62
+ create AddTodoHandler as
63
+ from (request: HttpRequest, response: HttpResponse) -> unit
64
+ select {
65
+ 1. parse the JSON body of @request into a @TodoTitle
66
+ 2. create the todo with @AddTodo
67
+ 3. persist the todo with @InsertTodo
68
+ 4. write the created todo serialized as JSON to @response with status 201
69
+ };
70
+
71
+ create CompleteTodoHandler as
72
+ from (request: HttpRequest, response: HttpResponse) -> unit
73
+ select {
74
+ 1. extract the todo id from the path of @request
75
+ 2. load the todos with @SelectTodos
76
+ 3. find the matching @Todo and complete it with @CompleteTodo
77
+ 4. persist it with @UpdateTodo
78
+ 5. write the updated todo serialized as JSON to @response with status 200
79
+ };
80
+
81
+ /* The main entry exponential. */
82
+ create ExpressTodoWeb as
83
+ from unit -> unit
84
+ select {
85
+ 1. create the express app object
86
+ 2. add the JSON body parser middleware
87
+ 3. mount @ListTodosHandler on the GET /todos route
88
+ 4. mount @AddTodoHandler on the POST /todos route
89
+ 5. mount @CompleteTodoHandler on the PATCH /todos/:id route
90
+ 6. connect to PostgreSQL
91
+ 7. listen on the configured port
92
+ };
93
+
94
+ realize TodoWeb as (
95
+ list: ListTodosHandler,
96
+ add: AddTodoHandler,
97
+ complete: CompleteTodoHandler,
98
+ storage: (
99
+ connect: ConnectPostgres,
100
+ query: SelectTodos,
101
+ insert: InsertTodo,
102
+ update: UpdateTodo
103
+ ),
104
+ main: ExpressTodoWeb
105
+ ) in ExpressWebEnv;
106
+
107
+ generate ExpressTodoWeb;
108
+ package executable expresstodo as ExpressTodoWeb in ExpressWebEnv;
@@ -0,0 +1,16 @@
1
+ -- The Express web environment.
2
+ --
3
+ -- A subobject of the shared Web environment intersected with the shared
4
+ -- TypeScript environment.
5
+
6
+ import Web from "todo.spex";
7
+ import TypeScript from "todo.spex";
8
+
9
+ create ExpressWebEnv as
10
+ from Web intersect TypeScript
11
+ select {
12
+ dependencies: express,
13
+ database: PostgreSQL
14
+ };
15
+
16
+ export ExpressWebEnv;