spex-parser 0.1.1 → 0.1.2
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 +1 -1
- package/.github/workflows/publish.yml +6 -4
- package/package.json +1 -1
- package/src/ast.ts +18 -1
- package/src/index.ts +4 -0
- package/src/visitor.ts +27 -2
- package/tests/visitor.test.ts +144 -10
package/.github/workflows/ci.yml
CHANGED
|
@@ -4,6 +4,10 @@ on:
|
|
|
4
4
|
release:
|
|
5
5
|
types: [published]
|
|
6
6
|
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
id-token: write
|
|
10
|
+
|
|
7
11
|
jobs:
|
|
8
12
|
publish:
|
|
9
13
|
name: Publish to npm
|
|
@@ -16,7 +20,7 @@ jobs:
|
|
|
16
20
|
- name: Setup Node.js
|
|
17
21
|
uses: actions/setup-node@v4
|
|
18
22
|
with:
|
|
19
|
-
node-version: '
|
|
23
|
+
node-version: '22'
|
|
20
24
|
registry-url: 'https://registry.npmjs.org'
|
|
21
25
|
cache: 'npm'
|
|
22
26
|
|
|
@@ -30,6 +34,4 @@ jobs:
|
|
|
30
34
|
run: npm test
|
|
31
35
|
|
|
32
36
|
- name: Publish to npm
|
|
33
|
-
run: npm
|
|
34
|
-
env:
|
|
35
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
37
|
+
run: npm publish --access public
|
package/package.json
CHANGED
package/src/ast.ts
CHANGED
|
@@ -55,10 +55,27 @@ export type ExponentialObject = {
|
|
|
55
55
|
exponent: ObjectExpression
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export type ConstraintReference = {
|
|
59
|
+
kind: 'ConstraintReference'
|
|
60
|
+
name: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type ConstraintText = {
|
|
64
|
+
kind: 'ConstraintText'
|
|
65
|
+
text: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type ConstraintPart = ConstraintReference | ConstraintText
|
|
69
|
+
|
|
70
|
+
export type Constraint = {
|
|
71
|
+
raw: string
|
|
72
|
+
parts: ConstraintPart[]
|
|
73
|
+
}
|
|
74
|
+
|
|
58
75
|
export type SubObject = {
|
|
59
76
|
kind: 'SubObject'
|
|
60
77
|
base: ObjectExpression
|
|
61
|
-
constraint:
|
|
78
|
+
constraint: Constraint
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
export type ArrayObject = {
|
package/src/index.ts
CHANGED
package/src/visitor.ts
CHANGED
|
@@ -11,6 +11,9 @@ import type {
|
|
|
11
11
|
ProductObject,
|
|
12
12
|
SubObject,
|
|
13
13
|
ArrayObject,
|
|
14
|
+
Constraint,
|
|
15
|
+
ConstraintPart,
|
|
16
|
+
ConstraintReference,
|
|
14
17
|
} from './ast.js'
|
|
15
18
|
import { SpexLexer } from './lexer.js'
|
|
16
19
|
import { SpexParser } from './parser.js'
|
|
@@ -18,6 +21,28 @@ import { SpexParser } from './parser.js'
|
|
|
18
21
|
const parserInstance = new SpexParser()
|
|
19
22
|
const BaseSpexVisitor = parserInstance.getBaseCstVisitorConstructor()
|
|
20
23
|
|
|
24
|
+
export const REFERENCE_PATTERN = /@([a-zA-Z_][\w]*(?:\.[a-zA-Z_][\w]*)*)/g
|
|
25
|
+
|
|
26
|
+
export function parseConstraint(raw: string): Constraint {
|
|
27
|
+
const parts: ConstraintPart[] = []
|
|
28
|
+
let lastIndex = 0
|
|
29
|
+
let match: RegExpExecArray | null
|
|
30
|
+
|
|
31
|
+
while ((match = REFERENCE_PATTERN.exec(raw)) !== null) {
|
|
32
|
+
if (match.index > lastIndex) {
|
|
33
|
+
parts.push({ kind: 'ConstraintText', text: raw.slice(lastIndex, match.index) })
|
|
34
|
+
}
|
|
35
|
+
parts.push({ kind: 'ConstraintReference', name: match[1]! })
|
|
36
|
+
lastIndex = match.index + match[0].length
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (lastIndex < raw.length) {
|
|
40
|
+
parts.push({ kind: 'ConstraintText', text: raw.slice(lastIndex) })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { raw, parts }
|
|
44
|
+
}
|
|
45
|
+
|
|
21
46
|
export class SpexParserVisitor extends BaseSpexVisitor implements ICstVisitor<any, any> {
|
|
22
47
|
constructor() {
|
|
23
48
|
super()
|
|
@@ -112,11 +137,11 @@ export class SpexParserVisitor extends BaseSpexVisitor implements ICstVisitor<an
|
|
|
112
137
|
|
|
113
138
|
subObject(ctx: any): SubObject {
|
|
114
139
|
const rawText: string = ctx.SelectBlock[0].image
|
|
115
|
-
const
|
|
140
|
+
const rawConstraint = rawText.slice(1, -1).trim()
|
|
116
141
|
return {
|
|
117
142
|
kind: 'SubObject',
|
|
118
143
|
base: this.visit(ctx.base),
|
|
119
|
-
constraint,
|
|
144
|
+
constraint: parseConstraint(rawConstraint),
|
|
120
145
|
}
|
|
121
146
|
}
|
|
122
147
|
|
package/tests/visitor.test.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { parseToAst } from '../src/visitor.js'
|
|
2
|
+
import { parseToAst, parseConstraint } from '../src/visitor.js'
|
|
3
3
|
import type {
|
|
4
4
|
ObjectDeclaration,
|
|
5
5
|
ImportDeclaration,
|
|
6
6
|
ExportDeclaration,
|
|
7
7
|
GenerateDeclaration,
|
|
8
|
+
Constraint,
|
|
8
9
|
} from '../src/ast.js'
|
|
9
10
|
|
|
10
11
|
describe('SpexParserVisitor', () => {
|
|
@@ -92,7 +93,10 @@ describe('SpexParserVisitor', () => {
|
|
|
92
93
|
p: {
|
|
93
94
|
kind: 'SubObject',
|
|
94
95
|
base: { kind: 'NamedObject', name: 'Number' },
|
|
95
|
-
constraint:
|
|
96
|
+
constraint: {
|
|
97
|
+
raw: 'value is positive',
|
|
98
|
+
parts: [{ kind: 'ConstraintText', text: 'value is positive' }],
|
|
99
|
+
},
|
|
96
100
|
},
|
|
97
101
|
n: { kind: 'NamedObject', name: 'Number' },
|
|
98
102
|
},
|
|
@@ -178,12 +182,18 @@ describe('SpexParserVisitor', () => {
|
|
|
178
182
|
exponent: {
|
|
179
183
|
kind: 'SubObject',
|
|
180
184
|
base: { kind: 'NamedObject', name: 'Number' },
|
|
181
|
-
constraint:
|
|
185
|
+
constraint: {
|
|
186
|
+
raw: 'value is positive',
|
|
187
|
+
parts: [{ kind: 'ConstraintText', text: 'value is positive' }],
|
|
188
|
+
},
|
|
182
189
|
},
|
|
183
190
|
base: {
|
|
184
191
|
kind: 'SubObject',
|
|
185
192
|
base: { kind: 'NamedObject', name: 'Number' },
|
|
186
|
-
constraint:
|
|
193
|
+
constraint: {
|
|
194
|
+
raw: 'value is positive',
|
|
195
|
+
parts: [{ kind: 'ConstraintText', text: 'value is positive' }],
|
|
196
|
+
},
|
|
187
197
|
},
|
|
188
198
|
},
|
|
189
199
|
})
|
|
@@ -199,7 +209,10 @@ describe('SpexParserVisitor', () => {
|
|
|
199
209
|
object: {
|
|
200
210
|
kind: 'SubObject',
|
|
201
211
|
base: { kind: 'NamedObject', name: 'Number' },
|
|
202
|
-
constraint:
|
|
212
|
+
constraint: {
|
|
213
|
+
raw: 'isPositive',
|
|
214
|
+
parts: [{ kind: 'ConstraintText', text: 'isPositive' }],
|
|
215
|
+
},
|
|
203
216
|
},
|
|
204
217
|
})
|
|
205
218
|
})
|
|
@@ -214,7 +227,10 @@ describe('SpexParserVisitor', () => {
|
|
|
214
227
|
object: {
|
|
215
228
|
kind: 'SubObject',
|
|
216
229
|
base: { kind: 'NamedObject', name: 'Number' },
|
|
217
|
-
constraint:
|
|
230
|
+
constraint: {
|
|
231
|
+
raw: 'the number is positive',
|
|
232
|
+
parts: [{ kind: 'ConstraintText', text: 'the number is positive' }],
|
|
233
|
+
},
|
|
218
234
|
},
|
|
219
235
|
})
|
|
220
236
|
})
|
|
@@ -236,7 +252,13 @@ describe('SpexParserVisitor', () => {
|
|
|
236
252
|
s: { kind: 'NamedObject', name: 'String' },
|
|
237
253
|
},
|
|
238
254
|
},
|
|
239
|
-
constraint:
|
|
255
|
+
constraint: {
|
|
256
|
+
raw: '@n is positive',
|
|
257
|
+
parts: [
|
|
258
|
+
{ kind: 'ConstraintReference', name: 'n' },
|
|
259
|
+
{ kind: 'ConstraintText', text: ' is positive' },
|
|
260
|
+
],
|
|
261
|
+
},
|
|
240
262
|
},
|
|
241
263
|
})
|
|
242
264
|
})
|
|
@@ -262,7 +284,10 @@ describe('SpexParserVisitor', () => {
|
|
|
262
284
|
},
|
|
263
285
|
base: { kind: 'NamedObject', name: 'Bool' },
|
|
264
286
|
},
|
|
265
|
-
constraint:
|
|
287
|
+
constraint: {
|
|
288
|
+
raw: 'logs the given input',
|
|
289
|
+
parts: [{ kind: 'ConstraintText', text: 'logs the given input' }],
|
|
290
|
+
},
|
|
266
291
|
},
|
|
267
292
|
})
|
|
268
293
|
})
|
|
@@ -280,9 +305,15 @@ describe('SpexParserVisitor', () => {
|
|
|
280
305
|
base: {
|
|
281
306
|
kind: 'SubObject',
|
|
282
307
|
base: { kind: 'NamedObject', name: 'Number' },
|
|
283
|
-
constraint:
|
|
308
|
+
constraint: {
|
|
309
|
+
raw: 'value is positive',
|
|
310
|
+
parts: [{ kind: 'ConstraintText', text: 'value is positive' }],
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
constraint: {
|
|
314
|
+
raw: 'value is odd',
|
|
315
|
+
parts: [{ kind: 'ConstraintText', text: 'value is odd' }],
|
|
284
316
|
},
|
|
285
|
-
constraint: 'value is odd',
|
|
286
317
|
},
|
|
287
318
|
})
|
|
288
319
|
})
|
|
@@ -460,4 +491,107 @@ describe('SpexParserVisitor', () => {
|
|
|
460
491
|
})
|
|
461
492
|
})
|
|
462
493
|
})
|
|
494
|
+
|
|
495
|
+
describe('parseConstraint', () => {
|
|
496
|
+
it('should parse plain text constraint with no references', () => {
|
|
497
|
+
const result = parseConstraint('value is positive')
|
|
498
|
+
expect(result).toEqual<Constraint>({
|
|
499
|
+
raw: 'value is positive',
|
|
500
|
+
parts: [{ kind: 'ConstraintText', text: 'value is positive' }],
|
|
501
|
+
})
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it('should parse constraint with single reference', () => {
|
|
505
|
+
const result = parseConstraint('@n is positive')
|
|
506
|
+
expect(result).toEqual<Constraint>({
|
|
507
|
+
raw: '@n is positive',
|
|
508
|
+
parts: [
|
|
509
|
+
{ kind: 'ConstraintReference', name: 'n' },
|
|
510
|
+
{ kind: 'ConstraintText', text: ' is positive' },
|
|
511
|
+
],
|
|
512
|
+
})
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('should parse constraint with reference in the middle', () => {
|
|
516
|
+
const result = parseConstraint('use @path for storage')
|
|
517
|
+
expect(result).toEqual<Constraint>({
|
|
518
|
+
raw: 'use @path for storage',
|
|
519
|
+
parts: [
|
|
520
|
+
{ kind: 'ConstraintText', text: 'use ' },
|
|
521
|
+
{ kind: 'ConstraintReference', name: 'path' },
|
|
522
|
+
{ kind: 'ConstraintText', text: ' for storage' },
|
|
523
|
+
],
|
|
524
|
+
})
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
it('should parse constraint with dotted references', () => {
|
|
528
|
+
const result = parseConstraint('return @z.real^2 + @z.imag^2')
|
|
529
|
+
expect(result).toEqual<Constraint>({
|
|
530
|
+
raw: 'return @z.real^2 + @z.imag^2',
|
|
531
|
+
parts: [
|
|
532
|
+
{ kind: 'ConstraintText', text: 'return ' },
|
|
533
|
+
{ kind: 'ConstraintReference', name: 'z.real' },
|
|
534
|
+
{ kind: 'ConstraintText', text: '^2 + ' },
|
|
535
|
+
{ kind: 'ConstraintReference', name: 'z.imag' },
|
|
536
|
+
{ kind: 'ConstraintText', text: '^2' },
|
|
537
|
+
],
|
|
538
|
+
})
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
it('should parse constraint with multiple references', () => {
|
|
542
|
+
const result = parseConstraint('call @LoadTodos using @path')
|
|
543
|
+
expect(result).toEqual<Constraint>({
|
|
544
|
+
raw: 'call @LoadTodos using @path',
|
|
545
|
+
parts: [
|
|
546
|
+
{ kind: 'ConstraintText', text: 'call ' },
|
|
547
|
+
{ kind: 'ConstraintReference', name: 'LoadTodos' },
|
|
548
|
+
{ kind: 'ConstraintText', text: ' using ' },
|
|
549
|
+
{ kind: 'ConstraintReference', name: 'path' },
|
|
550
|
+
],
|
|
551
|
+
})
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
it('should parse constraint with reference at start', () => {
|
|
555
|
+
const result = parseConstraint('@validate the input')
|
|
556
|
+
expect(result).toEqual<Constraint>({
|
|
557
|
+
raw: '@validate the input',
|
|
558
|
+
parts: [
|
|
559
|
+
{ kind: 'ConstraintReference', name: 'validate' },
|
|
560
|
+
{ kind: 'ConstraintText', text: ' the input' },
|
|
561
|
+
],
|
|
562
|
+
})
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
it('should parse constraint with no @ symbols', () => {
|
|
566
|
+
const result = parseConstraint('the user is authenticated')
|
|
567
|
+
expect(result).toEqual<Constraint>({
|
|
568
|
+
raw: 'the user is authenticated',
|
|
569
|
+
parts: [{ kind: 'ConstraintText', text: 'the user is authenticated' }],
|
|
570
|
+
})
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
it('should parse constraint with underscore in reference', () => {
|
|
574
|
+
const result = parseConstraint('@todo_item is valid')
|
|
575
|
+
expect(result).toEqual<Constraint>({
|
|
576
|
+
raw: '@todo_item is valid',
|
|
577
|
+
parts: [
|
|
578
|
+
{ kind: 'ConstraintReference', name: 'todo_item' },
|
|
579
|
+
{ kind: 'ConstraintText', text: ' is valid' },
|
|
580
|
+
],
|
|
581
|
+
})
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
it('should parse empty constraint', () => {
|
|
585
|
+
const result = parseConstraint('')
|
|
586
|
+
expect(result).toEqual<Constraint>({
|
|
587
|
+
raw: '',
|
|
588
|
+
parts: [],
|
|
589
|
+
})
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
it('should preserve raw text exactly', () => {
|
|
593
|
+
const result = parseConstraint(' @foo ')
|
|
594
|
+
expect(result.raw).toBe(' @foo ')
|
|
595
|
+
})
|
|
596
|
+
})
|
|
463
597
|
})
|