spex-parser 0.1.5 → 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 +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 +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 +150 -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 +230 -9
  34. package/tests/parser.test.ts +438 -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 +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 +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;'
@@ -338,4 +641,139 @@ describe('SpexParser', () => {
338
641
  expect(parser.errors).toHaveLength(0)
339
642
  })
340
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
+ })
341
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;
@@ -0,0 +1,111 @@
1
+ -- The Flask realization of the Todo Web app.
2
+ --
3
+ -- Imports the shared TodoWeb representation, the todo operations, and the
4
+ -- Flask environment. SQLite 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. Flask handlers receive a
7
+ -- request and return a 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 FlaskWebEnv from "flask_web_env.spex";
19
+
20
+ -- TODO: realize the shared HttpRequest and HttpResponse objects using the
21
+ -- types provided by the flask dependency in FlaskWebEnv.
22
+
23
+ /* SQLite storage. */
24
+ create SqliteConnection as (
25
+ path: string
26
+ );
27
+
28
+ create OpenSqlite as
29
+ from (path: string) -> SqliteConnection
30
+ select {
31
+ 1. open the SQLite database at @path
32
+ 2. create the todos table if it does not exist
33
+ };
34
+
35
+ create SelectTodos as
36
+ from (connection: SqliteConnection) -> Todo[]
37
+ select {
38
+ 1. select 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 (connection: SqliteConnection, todo: Todo) -> unit
44
+ select {
45
+ 1. insert the id, title, and completed values of @todo into the todos table
46
+ 2. commit the transaction
47
+ };
48
+
49
+ create UpdateTodo as
50
+ from (connection: SqliteConnection, todo: Todo) -> unit
51
+ select {
52
+ 1. update the completed value of the row matching the id of @todo
53
+ 2. commit the transaction
54
+ };
55
+
56
+ /* Route handlers. */
57
+ create ListTodosHandler as
58
+ from (request: HttpRequest) -> HttpResponse
59
+ select {
60
+ 1. open the database with @OpenSqlite
61
+ 2. load the todos with @SelectTodos
62
+ 3. return a 200 response with the todos serialized as JSON
63
+ };
64
+
65
+ create AddTodoHandler as
66
+ from (request: HttpRequest) -> HttpResponse
67
+ select {
68
+ 1. parse the JSON body of @request into a @TodoTitle
69
+ 2. create the todo with @AddTodo
70
+ 3. open the database with @OpenSqlite
71
+ 4. persist the todo with @InsertTodo
72
+ 5. return a 201 response with the created todo serialized as JSON
73
+ };
74
+
75
+ create CompleteTodoHandler as
76
+ from (request: HttpRequest) -> HttpResponse
77
+ select {
78
+ 1. extract the todo id from the path of @request
79
+ 2. open the database with @OpenSqlite
80
+ 3. load the todos with @SelectTodos
81
+ 4. find the matching @Todo and complete it with @CompleteTodo
82
+ 5. persist it with @UpdateTodo
83
+ 6. return a 200 response with the updated todo serialized as JSON
84
+ };
85
+
86
+ /* The main entry exponential. */
87
+ create FlaskTodoWeb as
88
+ from unit -> unit
89
+ select {
90
+ 1. create the Flask application object
91
+ 2. register @ListTodosHandler on the GET /todos route
92
+ 3. register @AddTodoHandler on the POST /todos route
93
+ 4. register @CompleteTodoHandler on the PATCH /todos/:id route
94
+ 5. run the application
95
+ };
96
+
97
+ realize TodoWeb as (
98
+ list: ListTodosHandler,
99
+ add: AddTodoHandler,
100
+ complete: CompleteTodoHandler,
101
+ storage: (
102
+ open: OpenSqlite,
103
+ query: SelectTodos,
104
+ insert: InsertTodo,
105
+ update: UpdateTodo
106
+ ),
107
+ main: FlaskTodoWeb
108
+ ) in FlaskWebEnv;
109
+
110
+ generate FlaskTodoWeb;
111
+ package executable flasktodo as FlaskTodoWeb;