spex-parser 0.1.4 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +312 -1
  2. package/dist/ast.d.ts +70 -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 +19 -1
  13. package/dist/lexer.d.ts.map +1 -1
  14. package/dist/lexer.js +172 -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 +103 -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 +181 -8
  23. package/dist/visitor.js.map +1 -1
  24. package/package.json +1 -1
  25. package/src/ast.ts +93 -0
  26. package/src/constants.ts +40 -0
  27. package/src/index.ts +20 -3
  28. package/src/lexer.ts +164 -6
  29. package/src/parser.ts +128 -7
  30. package/src/visitor.ts +195 -11
  31. package/tests/constants.test.ts +102 -0
  32. package/tests/e2e.test.ts +15 -3
  33. package/tests/lexer.test.ts +291 -9
  34. package/tests/parser.test.ts +489 -0
  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 +100 -60
  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 +892 -0
@@ -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,6 +243,278 @@ 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
520
  const testCase = 'package executable myapp as Main;'
@@ -275,6 +578,57 @@ describe('SpexParser', () => {
275
578
  })
276
579
  })
277
580
 
581
+ describe('comments', () => {
582
+ it('should parse declarations separated by single-line comments', () => {
583
+ const testCase = `
584
+ -- domain model
585
+ create Todo as (id: string, title: string);
586
+ -- create a new todo
587
+ create CreateTodo as (title: string) -> Todo;
588
+ `
589
+ const { parser } = parseInput(testCase)
590
+ expect(parser.errors).toHaveLength(0)
591
+ })
592
+
593
+ it('should parse declarations separated by block comments', () => {
594
+ const testCase = `
595
+ /* storage layer */
596
+ create LoadTodos as (path: string) -> Todo[];
597
+ /* entry point */
598
+ create Main as string[] -> unit;
599
+ `
600
+ const { parser } = parseInput(testCase)
601
+ expect(parser.errors).toHaveLength(0)
602
+ })
603
+
604
+ it('should parse inline block comments within a product declaration', () => {
605
+ const testCase = 'create Config as (name: string /* the name */, count: number);'
606
+ const { parser } = parseInput(testCase)
607
+ expect(parser.errors).toHaveLength(0)
608
+ })
609
+
610
+ it('should parse a file containing only comments', () => {
611
+ const testCase = `
612
+ -- this file is intentionally empty
613
+ /* apart from these comments */
614
+ `
615
+ const { parser } = parseInput(testCase)
616
+ expect(parser.errors).toHaveLength(0)
617
+ })
618
+
619
+ it('should parse a block comment spanning a single-line comment', () => {
620
+ const testCase = `
621
+ create Foo as string;
622
+ /* comment one
623
+ -- not a real comment, still part of the block
624
+ comment two */
625
+ generate Main;
626
+ `
627
+ const { parser } = parseInput(testCase)
628
+ expect(parser.errors).toHaveLength(0)
629
+ })
630
+ })
631
+
278
632
  describe('multiple declarations', () => {
279
633
  it('should parse multiple declarations', () => {
280
634
  const testCase = `
@@ -287,4 +641,139 @@ describe('SpexParser', () => {
287
641
  expect(parser.errors).toHaveLength(0)
288
642
  })
289
643
  })
644
+
645
+ describe('realize declaration', () => {
646
+ it('should parse a realize declaration', () => {
647
+ const testCase = 'realize Shape as Circle in environment;'
648
+ const { parser } = parseInput(testCase)
649
+ expect(parser.errors).toHaveLength(0)
650
+ })
651
+
652
+ it('should parse a realize declaration case-insensitively', () => {
653
+ const testCase = 'REALIZE Shape AS Circle IN environment;'
654
+ const { parser } = parseInput(testCase)
655
+ expect(parser.errors).toHaveLength(0)
656
+ })
657
+
658
+ it('should parse a realize declaration with product objects', () => {
659
+ const testCase = 'realize (x: number) as (y: string) in environment;'
660
+ const { parser } = parseInput(testCase)
661
+ expect(parser.errors).toHaveLength(0)
662
+ })
663
+
664
+ it('should parse a realize declaration with exponential objects', () => {
665
+ const testCase = 'realize string -> number as string -> string in environment;'
666
+ const { parser } = parseInput(testCase)
667
+ expect(parser.errors).toHaveLength(0)
668
+ })
669
+
670
+ it('should parse a realize declaration with a named environment', () => {
671
+ const testCase = 'realize A as B in MyEnv;'
672
+ const { parser } = parseInput(testCase)
673
+ expect(parser.errors).toHaveLength(0)
674
+ })
675
+
676
+ it('should parse a realize declaration without an environment', () => {
677
+ const testCase = 'realize A as B;'
678
+ const { parser } = parseInput(testCase)
679
+ expect(parser.errors).toHaveLength(0)
680
+ })
681
+
682
+ it('should parse a realize declaration mixed with other declarations', () => {
683
+ const testCase = 'create A as string;\nrealize A as B in MyEnv;\ngenerate A;'
684
+ const { parser } = parseInput(testCase)
685
+ expect(parser.errors).toHaveLength(0)
686
+ })
687
+ })
688
+
689
+ describe('include declaration', () => {
690
+ it('should parse include declaration', () => {
691
+ const testCase = 'include "config.json" as config;'
692
+ const { parser } = parseInput(testCase)
693
+ expect(parser.errors).toHaveLength(0)
694
+ })
695
+
696
+ it('should parse include declaration case-insensitively', () => {
697
+ const testCase = 'INCLUDE "config.json" AS config;'
698
+ const { parser } = parseInput(testCase)
699
+ expect(parser.errors).toHaveLength(0)
700
+ })
701
+
702
+ it('should parse include with single-quoted address', () => {
703
+ const testCase = "include 'data/file.txt' as myfile;"
704
+ const { parser } = parseInput(testCase)
705
+ expect(parser.errors).toHaveLength(0)
706
+ })
707
+
708
+ it('should parse include with folder address', () => {
709
+ const testCase = 'include "images/" as assets;'
710
+ const { parser } = parseInput(testCase)
711
+ expect(parser.errors).toHaveLength(0)
712
+ })
713
+
714
+ it('should parse include mixed with other declarations', () => {
715
+ const testCase =
716
+ 'include "config.json" as config;\ncreate Todo as (id: string);\ngenerate Main;'
717
+ const { parser } = parseInput(testCase)
718
+ expect(parser.errors).toHaveLength(0)
719
+ })
720
+
721
+ it('should not parse include without address', () => {
722
+ const testCase = 'include as config;'
723
+ const { parser } = parseInput(testCase)
724
+ expect(parser.errors).not.toHaveLength(0)
725
+ })
726
+
727
+ it('should not parse include without name', () => {
728
+ const testCase = 'include "config.json" as ;'
729
+ const { parser } = parseInput(testCase)
730
+ expect(parser.errors).not.toHaveLength(0)
731
+ })
732
+ })
733
+
734
+ describe('lambda object', () => {
735
+ it('should parse lambda with product base', () => {
736
+ const testCase = 'create sum as lambda (a: number, b: number) -> number ```python\nreturn a + b\n```;'
737
+ const { parser } = parseInput(testCase)
738
+ expect(parser.errors).toHaveLength(0)
739
+ })
740
+
741
+ it('should parse lambda with named base', () => {
742
+ const testCase = 'create double as lambda number -> number ```python\nreturn @n * 2\n```;'
743
+ const { parser } = parseInput(testCase)
744
+ expect(parser.errors).toHaveLength(0)
745
+ })
746
+
747
+ it('should parse lambda case-insensitively', () => {
748
+ const testCase = 'create sum as LAMBDA (a: number) -> number ```python\nreturn @a\n```;'
749
+ const { parser } = parseInput(testCase)
750
+ expect(parser.errors).toHaveLength(0)
751
+ })
752
+
753
+ it('should parse lambda with pattern blocks', () => {
754
+ const testCase =
755
+ 'create transform as lambda (x: number) -> number ```python\nif @x > 0:\n @{return sin(@x)}\nelse:\n @{return cos(@x)}\n```;'
756
+ const { parser } = parseInput(testCase)
757
+ expect(parser.errors).toHaveLength(0)
758
+ })
759
+
760
+ it('should parse lambda in product field', () => {
761
+ const testCase =
762
+ 'create Config as (handler: lambda (x: string) -> string ```typescript\nreturn x.toUpperCase();\n```, port: number);'
763
+ const { parser } = parseInput(testCase)
764
+ expect(parser.errors).toHaveLength(0)
765
+ })
766
+
767
+ it('should not parse lambda without language', () => {
768
+ const testCase = 'create sum as lambda (a: number) -> number ```\nreturn @a\n```;'
769
+ const { parser } = parseInput(testCase)
770
+ expect(parser.errors).not.toHaveLength(0)
771
+ })
772
+
773
+ it('should not parse lambda without code block', () => {
774
+ const testCase = 'create sum as lambda (a: number) -> number;'
775
+ const { parser } = parseInput(testCase)
776
+ expect(parser.errors).not.toHaveLength(0)
777
+ })
778
+ })
290
779
  })
@@ -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;
@@ -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;