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.
- package/.github/workflows/ci.yml +37 -0
- package/.github/workflows/publish.yml +35 -0
- package/.prettierrc +8 -0
- package/LICENSE +7 -0
- package/README.md +513 -0
- package/dist/ast.d.ts +48 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +2 -0
- package/dist/ast.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer.d.ts +30 -0
- package/dist/lexer.d.ts.map +1 -0
- package/dist/lexer.js +109 -0
- package/dist/lexer.js.map +1 -0
- package/dist/parser.d.ts +18 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +132 -0
- package/dist/parser.js.map +1 -0
- package/dist/visitor.d.ts +22 -0
- package/dist/visitor.d.ts.map +1 -0
- package/dist/visitor.js +154 -0
- package/dist/visitor.js.map +1 -0
- package/eslint.config.js +27 -0
- package/package.json +45 -0
- package/src/ast.ts +67 -0
- package/src/index.ts +13 -0
- package/src/lexer.ts +121 -0
- package/src/parser.ts +170 -0
- package/src/visitor.ts +179 -0
- package/tests/e2e.test.ts +17 -0
- package/tests/lexer.test.ts +123 -0
- package/tests/parser.test.ts +227 -0
- package/tests/props/todo.spex +83 -0
- package/tests/visitor.test.ts +463 -0
- package/tsconfig.json +47 -0
|
@@ -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;
|