spex-parser 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,227 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { SpexParser } from '../src/parser.js'
3
+ import { SpexLexer } from '../src/lexer.js'
4
+
5
+ const parser = new SpexParser()
6
+
7
+ function parseInput(text: string) {
8
+ const lexingResult = SpexLexer.tokenize(text)
9
+ parser.input = lexingResult.tokens
10
+ const cst = parser.spexFile() as any
11
+ return { parser, cst }
12
+ }
13
+
14
+ describe('SpexParser', () => {
15
+ describe('object declaration', () => {
16
+ it('should parse named object declaration', () => {
17
+ const testCase = 'create MyObject as Number;'
18
+ const { parser } = parseInput(testCase)
19
+ expect(parser.errors).toHaveLength(0)
20
+ })
21
+
22
+ it('should parse product object declaration', () => {
23
+ const testCase = 'create MyProduct as (n: Number, s: String);'
24
+ const { parser } = parseInput(testCase)
25
+ expect(parser.errors).toHaveLength(0)
26
+ })
27
+
28
+ it('should parse product object declaration with trailing commas', () => {
29
+ const testCase = 'create MyProduct as (n: Number, s: String,);'
30
+ const { parser } = parseInput(testCase)
31
+ expect(parser.errors).toHaveLength(0)
32
+ })
33
+
34
+ it('should parse product object declaration with exponential objects', () => {
35
+ const testCase = 'create MyProduct as (f: Number -> String, n: Number);'
36
+ const { parser } = parseInput(testCase)
37
+ expect(parser.errors).toHaveLength(0)
38
+ })
39
+
40
+ it('should parse product object declaration with subobjects', () => {
41
+ const testCase =
42
+ 'create MyProduct as (p: from Number select { value is positive }, n: Number);'
43
+ const { parser } = parseInput(testCase)
44
+ expect(parser.errors).toHaveLength(0)
45
+ })
46
+
47
+ it('should parse exponential object declaration with named object', () => {
48
+ const testCase = 'create MyExponential as Number -> Unit;'
49
+ const { parser } = parseInput(testCase)
50
+ expect(parser.errors).toHaveLength(0)
51
+ })
52
+
53
+ it('should parse exponential object declaration with product objects', () => {
54
+ const testCase = 'create MyExponential as (n: Number) -> (s: String);'
55
+ const { parser } = parseInput(testCase)
56
+ expect(parser.errors).toHaveLength(0)
57
+ })
58
+
59
+ it('should parse exponential object declaration with exponential objects', () => {
60
+ const testCase = 'create MyExponential as (f: Number -> String, n: Number) -> String;'
61
+ const { parser } = parseInput(testCase)
62
+ expect(parser.errors).toHaveLength(0)
63
+ })
64
+
65
+ it('should parse exponential object declaration with subobjects', () => {
66
+ const testCase =
67
+ 'create MyExponential as from Number select { value is positive } -> from Number select { value is positive };'
68
+ const { parser } = parseInput(testCase)
69
+ expect(parser.errors).toHaveLength(0)
70
+ })
71
+
72
+ it('should parse subobject declaration with text constraint', () => {
73
+ const testCase = 'create PositiveNumber as from Number select { are positive };'
74
+ const { parser } = parseInput(testCase)
75
+ expect(parser.errors).toHaveLength(0)
76
+ })
77
+
78
+ it('should parse subobject declaration with product objects', () => {
79
+ const testCase =
80
+ 'create MySubobject as from (n: Number, s: String) select { have a positive @n };'
81
+ const { parser } = parseInput(testCase)
82
+ expect(parser.errors).toHaveLength(0)
83
+ })
84
+
85
+ it('should parse subobject declaration with exponential objects', () => {
86
+ const testCase =
87
+ 'create MySubobject as from (n: Number, s: String) -> Bool select { log the given input };'
88
+ const { parser } = parseInput(testCase)
89
+ expect(parser.errors).toHaveLength(0)
90
+ })
91
+
92
+ it('should parse subobject declaration with subobjects', () => {
93
+ const testCase =
94
+ 'create MySubobject as from from Number select { value is positive } select { value is odd };'
95
+ const { parser } = parseInput(testCase)
96
+ expect(parser.errors).toHaveLength(0)
97
+ })
98
+
99
+ it('should parse array type declaration', () => {
100
+ const testCase = 'create MyArray as string[];'
101
+ const { parser } = parseInput(testCase)
102
+ expect(parser.errors).toHaveLength(0)
103
+ })
104
+
105
+ it('should parse array of product type', () => {
106
+ const testCase = 'create MyArray as (n: Number)[];'
107
+ const { parser } = parseInput(testCase)
108
+ expect(parser.errors).toHaveLength(0)
109
+ })
110
+
111
+ it('should parse nested array type', () => {
112
+ const testCase = 'create MyArray as string[][];'
113
+ const { parser } = parseInput(testCase)
114
+ expect(parser.errors).toHaveLength(0)
115
+ })
116
+
117
+ it('should parse object declaration with dotted name', () => {
118
+ const testCase =
119
+ 'create SignUp as (user: types.EmailAddress, pass: types.Password) -> string;'
120
+ const { parser } = parseInput(testCase)
121
+ expect(parser.errors).toHaveLength(0)
122
+ })
123
+
124
+ it('should parse basic object string', () => {
125
+ const testCase = 'create MyObject as string;'
126
+ const { parser } = parseInput(testCase)
127
+ expect(parser.errors).toHaveLength(0)
128
+ })
129
+
130
+ it('should parse basic object number', () => {
131
+ const testCase = 'create MyObject as number;'
132
+ const { parser } = parseInput(testCase)
133
+ expect(parser.errors).toHaveLength(0)
134
+ })
135
+
136
+ it('should parse basic object bool', () => {
137
+ const testCase = 'create MyObject as bool;'
138
+ const { parser } = parseInput(testCase)
139
+ expect(parser.errors).toHaveLength(0)
140
+ })
141
+
142
+ it('should parse basic object unit', () => {
143
+ const testCase = 'create MyObject as unit;'
144
+ const { parser } = parseInput(testCase)
145
+ expect(parser.errors).toHaveLength(0)
146
+ })
147
+
148
+ it('should parse basic objects in product fields', () => {
149
+ const testCase = 'create Config as (name: string, count: number, active: bool);'
150
+ const { parser } = parseInput(testCase)
151
+ expect(parser.errors).toHaveLength(0)
152
+ })
153
+
154
+ it('should not allow overriding basic object string', () => {
155
+ const testCase = 'create string as Number;'
156
+ const { parser } = parseInput(testCase)
157
+ expect(parser.errors).not.toHaveLength(0)
158
+ })
159
+
160
+ it('should not allow overriding basic object number', () => {
161
+ const testCase = 'create number as Number;'
162
+ const { parser } = parseInput(testCase)
163
+ expect(parser.errors).not.toHaveLength(0)
164
+ })
165
+
166
+ it('should not allow overriding basic object bool', () => {
167
+ const testCase = 'create bool as Number;'
168
+ const { parser } = parseInput(testCase)
169
+ expect(parser.errors).not.toHaveLength(0)
170
+ })
171
+
172
+ it('should not allow overriding basic object unit', () => {
173
+ const testCase = 'create unit as Number;'
174
+ const { parser } = parseInput(testCase)
175
+ expect(parser.errors).not.toHaveLength(0)
176
+ })
177
+ })
178
+
179
+ describe('import declaration', () => {
180
+ it('should parse named import', () => {
181
+ const testCase = 'import EmailAddress from "types.spex";'
182
+ const { parser } = parseInput(testCase)
183
+ expect(parser.errors).toHaveLength(0)
184
+ })
185
+
186
+ it('should parse named import with alias', () => {
187
+ const testCase = 'import EmailAddress from "types.spex" as Username;'
188
+ const { parser } = parseInput(testCase)
189
+ expect(parser.errors).toHaveLength(0)
190
+ })
191
+
192
+ it('should parse module import', () => {
193
+ const testCase = 'import "types.spex" as types;'
194
+ const { parser } = parseInput(testCase)
195
+ expect(parser.errors).toHaveLength(0)
196
+ })
197
+ })
198
+
199
+ describe('export declaration', () => {
200
+ it('should parse export declaration', () => {
201
+ const testCase = 'export EmailAddress;'
202
+ const { parser } = parseInput(testCase)
203
+ expect(parser.errors).toHaveLength(0)
204
+ })
205
+ })
206
+
207
+ describe('generate declaration', () => {
208
+ it('should parse generate declaration', () => {
209
+ const testCase = 'generate Main;'
210
+ const { parser } = parseInput(testCase)
211
+ expect(parser.errors).toHaveLength(0)
212
+ })
213
+ })
214
+
215
+ describe('multiple declarations', () => {
216
+ it('should parse multiple declarations', () => {
217
+ const testCase = `
218
+ create Todo as (id: string, title: string, completed: bool);
219
+ create EmailAddress as from string select { are email addresses };
220
+ export EmailAddress;
221
+ generate Main;
222
+ `
223
+ const { parser } = parseInput(testCase)
224
+ expect(parser.errors).toHaveLength(0)
225
+ })
226
+ })
227
+ })
@@ -0,0 +1,83 @@
1
+ create Todo as (
2
+ id: string,
3
+ title: string,
4
+ description: string,
5
+ completed: bool,
6
+ createdAt: string
7
+ );
8
+
9
+ create TodoTitle as from string select {
10
+ are not empty and are shorter than 120 characters
11
+ };
12
+
13
+ create TodoFilePath as from string select {
14
+ represent a valid path to a JSON file storing todos
15
+ };
16
+
17
+ create TodoId as from string select {
18
+ are valid UUIDs
19
+ };
20
+
21
+ create CliArgs as (
22
+ command: string,
23
+ arguments: string[]
24
+ );
25
+
26
+ create LoadTodos as from (path: TodoFilePath) -> Todo[] select {
27
+ 1. read the JSON file at @path
28
+ 2. return an empty list if the file does not exist
29
+ 3. parse the JSON content into todos
30
+ 4. throw an exception if the JSON is invalid
31
+ };
32
+
33
+ create SaveTodos as from (path: TodoFilePath, todos: Todo[]) -> unit select {
34
+ 1. serialize @todos as formatted JSON
35
+ 2. write the JSON to @path
36
+ };
37
+
38
+ create CreateTodo as from (title: TodoTitle) -> Todo select {
39
+ 1. generate a UUID for the todo id
40
+ 2. create a todo with completed set to false
41
+ 3. return the created todo
42
+ };
43
+
44
+ create AddTodo as from (path: TodoFilePath, title: TodoTitle) -> Todo select {
45
+ 1. call @LoadTodos using @path
46
+ 2. call @CreateTodo using @title
47
+ 3. append the new todo to the loaded todos
48
+ 4. call @SaveTodos to persist the updated todos
49
+ 5. return the created todo
50
+ };
51
+
52
+ create ListTodos as from (path: TodoFilePath) -> string select {
53
+ 1. load todos using @LoadTodos
54
+ 2. return a formatted string representation of all todos
55
+ 3. show completed todos with a checkmark
56
+ 4. show incomplete todos with an empty checkbox
57
+ };
58
+
59
+ create CompleteTodo as from (path: TodoFilePath, id: TodoId) -> Todo select {
60
+ 1. load todos using @LoadTodos
61
+ 2. search for the todo matching @id
62
+ 3. throw an exception if the todo does not exist
63
+ 4. set the todo completed status to true
64
+ 5. persist the updated todo list using @SaveTodos
65
+ 6. return the updated todo
66
+ };
67
+
68
+ create ParseCliArgs as from string[] -> CliArgs select {
69
+ 1. parse the command line arguments
70
+ 2. extract the command name
71
+ 3. extract the command arguments
72
+ };
73
+
74
+ create Main as from string[] -> unit select {
75
+ 1. parse process arguments using @ParseCliArgs
76
+ 2. if the command is "add": call @AddTodo
77
+ 3. if the command is "list": call @ListTodos and print the result to stdout
78
+ 4. if the command is "complete": call @CompleteTodo
79
+ 5. print a help message if the command is invalid
80
+ 6. print user-friendly error messages for exceptions
81
+ };
82
+
83
+ generate Main;