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.
- package/README.md +312 -1
- package/dist/ast.d.ts +70 -2
- package/dist/ast.d.ts.map +1 -1
- package/dist/constants.d.ts +20 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +32 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts +19 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +172 -5
- package/dist/lexer.js.map +1 -1
- package/dist/parser.d.ts +9 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +103 -7
- package/dist/parser.js.map +1 -1
- package/dist/visitor.d.ts +11 -2
- package/dist/visitor.d.ts.map +1 -1
- package/dist/visitor.js +181 -8
- package/dist/visitor.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +93 -0
- package/src/constants.ts +40 -0
- package/src/index.ts +20 -3
- package/src/lexer.ts +164 -6
- package/src/parser.ts +128 -7
- package/src/visitor.ts +195 -11
- package/tests/constants.test.ts +102 -0
- package/tests/e2e.test.ts +15 -3
- package/tests/lexer.test.ts +291 -9
- package/tests/parser.test.ts +489 -0
- package/tests/props/express_todo_web.spex +108 -0
- package/tests/props/express_web_env.spex +16 -0
- package/tests/props/flask_todo_web.spex +111 -0
- package/tests/props/flask_web_env.spex +16 -0
- package/tests/props/python_cli_env.spex +16 -0
- package/tests/props/python_todo_cli.spex +70 -0
- package/tests/props/todo.spex +100 -60
- package/tests/props/typescript_cli_env.spex +16 -0
- package/tests/props/typescript_todo_cli.spex +70 -0
- package/tests/visitor.test.ts +892 -0
package/tests/parser.test.ts
CHANGED
|
@@ -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;
|