Skip to content

Commit 368b221

Browse files
authored
Merge pull request #2 from mkantor/tests
Add tests
2 parents 4726887 + c6b3824 commit 368b221

File tree

7 files changed

+325
-13
lines changed

7 files changed

+325
-13
lines changed

package-lock.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"@matt.kantor/either": "^1.0.0"
66
},
77
"devDependencies": {
8+
"@types/node": "^22.13.14",
89
"typescript": "^5.7.3"
910
},
1011
"files": [
@@ -14,8 +15,10 @@
1415
"main": "dist/index.js",
1516
"repository": "github:mkantor/parsing",
1617
"scripts": {
17-
"clean": "rm -rf dist *.tsbuildinfo",
18-
"build": "tsc --build"
18+
"clean": "rm -rf dist* *.tsbuildinfo",
19+
"build": "tsc --build tsconfig.lib.json",
20+
"build:tests": "tsc --project tsconfig.lib.json --outDir dist-test --declarationDir dist && tsc --build tsconfig.test.json",
21+
"test": "npm run build:tests && node --test"
1922
},
2023
"type": "module"
2124
}

src/parsing.test.ts

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import either, { type Either } from '@matt.kantor/either'
2+
import { strict as assert, AssertionError } from 'node:assert'
3+
import test, { suite } from 'node:test'
4+
import {
5+
as,
6+
butNot,
7+
flatMap,
8+
lazy,
9+
lookaheadNot,
10+
map,
11+
oneOf,
12+
oneOrMore,
13+
sequence,
14+
transformOutput,
15+
zeroOrMore,
16+
} from './combinators.js'
17+
import {
18+
anySingleCharacter,
19+
literal,
20+
nothing,
21+
regularExpression,
22+
} from './constructors.js'
23+
import {
24+
parse,
25+
type InvalidInputError,
26+
type Parser,
27+
type ParserResult,
28+
} from './parser.js'
29+
30+
suite('constructors', _ => {
31+
test('anySingleCharacter', _ => {
32+
assertSuccess(anySingleCharacter('a'), 'a')
33+
assertSuccess(anySingleCharacter('ab'), 'a')
34+
assertFailure(anySingleCharacter(''))
35+
})
36+
37+
test('literal', _ => {
38+
assertSuccess(literal('a')('a'), 'a')
39+
assertSuccess(literal('a')('ab'), 'a')
40+
assertSuccess(literal('')('arbitrary input'), '')
41+
assertFailure(literal('a')('b'))
42+
assertFailure(literal('a')('ba'))
43+
assertFailure(literal('a')(''))
44+
})
45+
46+
test('nothing', _ => {
47+
assertSuccess(nothing('a'), undefined)
48+
assertSuccess(nothing(''), undefined)
49+
})
50+
51+
test('regularExpression', _ => {
52+
assertSuccess(regularExpression(/ab?/)('a'), 'a')
53+
assertSuccess(regularExpression(/ab?/)('ab'), 'ab')
54+
assertSuccess(regularExpression(/ab?/)('abc'), 'ab')
55+
assertSuccess(regularExpression(/.*/)('arbitrary input'), 'arbitrary input')
56+
assertFailure(regularExpression(/ab?/)('bab'))
57+
})
58+
})
59+
60+
suite('combinators', _ => {
61+
test('as', _ => {
62+
assertSuccess(as(literal('a'), 'b')('a'), 'b')
63+
assertFailure(as(literal('a'), 'b')('b'))
64+
})
65+
66+
test('butNot', _ => {
67+
const aOrBButNotB = butNot(regularExpression(/(?:a|b)/), literal('b'), 'b')
68+
assertSuccess(aOrBButNotB('a'), 'a')
69+
assertSuccess(aOrBButNotB('ab'), 'a')
70+
assertFailure(aOrBButNotB('b'))
71+
})
72+
73+
test('flatMap', _ => {
74+
const characterFollowedByItsUppercase = flatMap(
75+
anySingleCharacter,
76+
character => literal(character.toUpperCase()),
77+
)
78+
assertSuccess(characterFollowedByItsUppercase('aA'), 'A')
79+
assertSuccess(characterFollowedByItsUppercase('aAB'), 'A')
80+
assertFailure(characterFollowedByItsUppercase('a'))
81+
assertFailure(characterFollowedByItsUppercase('A'))
82+
assertFailure(characterFollowedByItsUppercase('aa'))
83+
assertFailure(characterFollowedByItsUppercase('aB'))
84+
})
85+
86+
test('lazy', _ => {
87+
const lazyA = lazy(() => a)
88+
const a = literal('a')
89+
assertSuccess(lazyA('a'), 'a')
90+
assertFailure(lazyA('b'))
91+
})
92+
93+
test('lookaheadNot', _ => {
94+
const aNotFollowedByB = lookaheadNot(literal('a'), literal('b'), 'b')
95+
assertSuccess(aNotFollowedByB('a'), 'a')
96+
assertSuccess(aNotFollowedByB('az'), 'a')
97+
assertFailure(aNotFollowedByB('ab'))
98+
assertFailure(aNotFollowedByB('b'))
99+
assertFailure(aNotFollowedByB(''))
100+
})
101+
102+
test('map', _ => {
103+
const characterAsItsUppercase = map(anySingleCharacter, character =>
104+
character.toUpperCase(),
105+
)
106+
assertSuccess(characterAsItsUppercase('a'), 'A')
107+
assertSuccess(characterAsItsUppercase('bb'), 'B')
108+
assertFailure(characterAsItsUppercase(''))
109+
})
110+
111+
test('oneOf', _ => {
112+
const aOrB = oneOf([literal('a'), literal('b')])
113+
assertSuccess(aOrB('a'), 'a')
114+
assertSuccess(aOrB('ba'), 'b')
115+
assertFailure(aOrB('c'))
116+
assertFailure(aOrB(''))
117+
})
118+
119+
test('oneOrMore', _ => {
120+
const oneOrMoreA = oneOrMore(literal('a'))
121+
assertSuccess(oneOrMoreA('a'), ['a'])
122+
assertSuccess(oneOrMoreA('aaab'), ['a', 'a', 'a'])
123+
assertFailure(oneOrMoreA(''))
124+
assertFailure(oneOrMoreA('b'))
125+
})
126+
127+
test('sequence', _ => {
128+
const ab = sequence([literal('a'), literal('b')])
129+
assertSuccess(ab('ab'), ['a', 'b'])
130+
assertSuccess(ab('abc'), ['a', 'b'])
131+
assertFailure(ab('bab'))
132+
})
133+
134+
test('transformOutput', _ => {
135+
const aTransformedToUppercase = transformOutput(literal('a'), a =>
136+
either.makeRight(a.toUpperCase()),
137+
)
138+
assertSuccess(aTransformedToUppercase('a'), 'A')
139+
assertSuccess(aTransformedToUppercase('ab'), 'A')
140+
assertFailure(aTransformedToUppercase('b'))
141+
assertFailure(aTransformedToUppercase(''))
142+
assertFailure(
143+
transformOutput(anySingleCharacter, _ =>
144+
either.makeLeft({ kind: 'invalidInput', input: '', message: '' }),
145+
)(''),
146+
)
147+
})
148+
149+
test('zeroOrMore', _ => {
150+
const zeroOrMoreA = zeroOrMore(literal('a'))
151+
assertSuccess(zeroOrMoreA('a'), ['a'])
152+
assertSuccess(zeroOrMoreA('aaab'), ['a', 'a', 'a'])
153+
assertSuccess(zeroOrMoreA(''), [])
154+
assertSuccess(zeroOrMoreA('b'), [])
155+
})
156+
})
157+
158+
test('parse', _ => {
159+
assertRight(parse(literal('a'), 'a'), 'a')
160+
assertFailure(parse(literal('a'), 'b'))
161+
assertFailure(parse(literal('a'), 'ab'))
162+
})
163+
164+
test('README example', _ => {
165+
const operator = oneOf([literal('+'), literal('-')])
166+
167+
const number = map(
168+
oneOrMore(
169+
oneOf([
170+
literal('0'),
171+
literal('1'),
172+
literal('2'),
173+
literal('3'),
174+
literal('4'),
175+
literal('5'),
176+
literal('6'),
177+
literal('7'),
178+
literal('8'),
179+
literal('9'),
180+
]),
181+
),
182+
Number,
183+
)
184+
185+
const compoundExpression = map(
186+
sequence([number, operator, lazy(() => expression)]),
187+
([a, operator, b]) => {
188+
switch (operator) {
189+
case '+':
190+
return a + b
191+
case '-':
192+
return a - b
193+
}
194+
},
195+
)
196+
197+
const expression: Parser<number> = oneOf([compoundExpression, number])
198+
199+
const evaluate = (input: string) =>
200+
either.flatMap(expression(input), ({ remainingInput, output }) =>
201+
remainingInput.length !== 0
202+
? either.makeLeft('excess content followed valid input')
203+
: either.makeRight(output),
204+
)
205+
206+
assertRight(evaluate('2+2-1'), 3)
207+
})
208+
209+
const adjustStartStackFn = (
210+
error: AssertionError,
211+
stackStartFn: (...args: never) => unknown,
212+
) =>
213+
new AssertionError({
214+
actual: error.actual,
215+
expected: error.expected,
216+
operator: error.operator,
217+
stackStartFn,
218+
...(error.generatedMessage ? {} : { message: error.message }),
219+
})
220+
221+
const customAssertions = (
222+
stackStartFn: (...args: never) => unknown,
223+
functionPerformingAssertions: () => void,
224+
) => {
225+
try {
226+
functionPerformingAssertions()
227+
} catch (error) {
228+
if (!(error instanceof AssertionError)) {
229+
throw error
230+
} else {
231+
throw adjustStartStackFn(error, stackStartFn)
232+
}
233+
}
234+
}
235+
236+
const assertRight = <RightValue>(
237+
actualResult: Either<unknown, RightValue>,
238+
expectedRightValue: RightValue,
239+
) =>
240+
customAssertions(assertRight, () => {
241+
if (either.isLeft(actualResult)) {
242+
assert.fail('result was left; expected right')
243+
}
244+
assert.deepEqual(actualResult.value, expectedRightValue)
245+
})
246+
247+
const assertSuccess = <Output>(
248+
actualResult: ParserResult<Output>,
249+
expectedOutput: Output,
250+
) =>
251+
customAssertions(assertRight, () => {
252+
if (either.isLeft(actualResult)) {
253+
assert.fail('result was failure; expected success')
254+
}
255+
assert.deepEqual(actualResult.value.output, expectedOutput)
256+
})
257+
258+
const assertFailure = <Output>(
259+
actualResult: Either<InvalidInputError, Output>,
260+
) =>
261+
customAssertions(assertRight, () =>
262+
assert(
263+
either.isLeft(actualResult),
264+
'result was successful; expected failure',
265+
),
266+
)

tsconfig.base.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"composite": true,
4+
"exactOptionalPropertyTypes": true,
5+
"module": "node16",
6+
"noPropertyAccessFromIndexSignature": true,
7+
"noUncheckedIndexedAccess": true,
8+
"noUncheckedSideEffectImports": true,
9+
"rootDir": "./src",
10+
"skipLibCheck": true,
11+
"strict": true,
12+
"target": "es2022",
13+
"verbatimModuleSyntax": true
14+
}
15+
}

tsconfig.json

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
{
2-
"compilerOptions": {
3-
"declaration": true,
4-
"exactOptionalPropertyTypes": true,
5-
"module": "node16",
6-
"noUncheckedIndexedAccess": true,
7-
"outDir": "dist",
8-
"rootDir": "src",
9-
"strict": true,
10-
"target": "es2020",
11-
"verbatimModuleSyntax": true
12-
}
2+
"references": [
3+
{ "path": "./tsconfig.test.json" },
4+
{ "path": "./tsconfig.lib.json" }
5+
]
136
}

tsconfig.lib.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "./tsconfig.base.json",
3+
"compilerOptions": {
4+
"types": [],
5+
"outDir": "./dist"
6+
},
7+
"include": ["./src/**/*.ts"],
8+
"exclude": ["./src/**/*.test.ts"]
9+
}

tsconfig.test.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "./tsconfig.base.json",
3+
"compilerOptions": {
4+
"outDir": "./dist-test"
5+
},
6+
"include": ["./src/**/*.test.ts"],
7+
"references": [{ "path": "./tsconfig.lib.json" }]
8+
}

0 commit comments

Comments
 (0)