Skip to content

Commit 78e6bf6

Browse files
authored
Merge pull request #7 from DouglasNeuroInformatics/neverthrow
implement neverthrow in libjs
2 parents 0a53d9a + c7607e1 commit 78e6bf6

19 files changed

+330
-124
lines changed

eslint.config.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
import { config } from '@douglasneuroinformatics/eslint-config';
22

3-
export default config();
3+
export default config(
4+
{},
5+
{
6+
rules: {
7+
'@typescript-eslint/explicit-function-return-type': 'error'
8+
}
9+
}
10+
);

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"test:coverage": "vitest run --coverage"
3131
},
3232
"dependencies": {
33+
"neverthrow": "^8.2.0",
3334
"type-fest": "^4.34.1",
3435
"zod": "^3.22.6"
3536
},

pnpm-lock.yaml

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

src/__tests__/datetime.test.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,12 @@ describe('sleep', () => {
5959

6060
describe('parseDuration', () => {
6161
it('should fail to parse a negative duration', () => {
62-
expect(() => parseDuration(-1)).toThrow('Cannot parse negative length of time: -1');
62+
const result = parseDuration(-1);
63+
expect(result.isErr());
6364
});
6465
it('should parse a duration of zero', () => {
65-
expect(parseDuration(0)).toEqual({
66+
const result = parseDuration(0);
67+
expect(result.isOk() && result.value).toEqual({
6668
days: 0,
6769
hours: 0,
6870
milliseconds: 0,
@@ -71,14 +73,16 @@ describe('parseDuration', () => {
7173
});
7274
});
7375
it('should parse a duration less than a second', () => {
74-
expect(parseDuration(50)).toEqual({
76+
let result = parseDuration(50);
77+
expect(result.isOk() && result.value).toEqual({
7578
days: 0,
7679
hours: 0,
7780
milliseconds: 50,
7881
minutes: 0,
7982
seconds: 0
8083
});
81-
expect(parseDuration(500)).toEqual({
84+
result = parseDuration(500);
85+
expect(result.isOk() && result.value).toEqual({
8286
days: 0,
8387
hours: 0,
8488
milliseconds: 500,
@@ -87,7 +91,8 @@ describe('parseDuration', () => {
8791
});
8892
});
8993
it('should parse a duration less than one minute but more than one second', () => {
90-
expect(parseDuration(11_100)).toEqual({
94+
const result = parseDuration(11_100);
95+
expect(result.isOk() && result.value).toEqual({
9196
days: 0,
9297
hours: 0,
9398
milliseconds: 100,
@@ -96,24 +101,26 @@ describe('parseDuration', () => {
96101
});
97102
});
98103
it('should parse a duration less than one hour but more than one minute', () => {
99-
expect(parseDuration(60_000)).toEqual({
104+
let result = parseDuration(60_000);
105+
expect(result.isOk() && result.value).toEqual({
100106
days: 0,
101107
hours: 0,
102108
milliseconds: 0,
103109
minutes: 1,
104110
seconds: 0
105111
});
106-
expect(parseDuration(62_500)).toEqual({
112+
result = parseDuration(62_500);
113+
expect(result.isOk() && result.value).toEqual({
107114
days: 0,
108115
hours: 0,
109116
milliseconds: 500,
110117
minutes: 1,
111118
seconds: 2
112119
});
113120
});
114-
115121
it('should parse a duration less than one day but more than one hour', () => {
116-
expect(parseDuration(3_600_000)).toEqual({
122+
const result = parseDuration(3_600_000);
123+
expect(result.isOk() && result.value).toEqual({
117124
days: 0,
118125
hours: 1,
119126
milliseconds: 0,
@@ -122,14 +129,16 @@ describe('parseDuration', () => {
122129
});
123130
});
124131
it('should parse a duration greater than one day', () => {
125-
expect(parseDuration(86_400_000)).toEqual({
132+
let result = parseDuration(86_400_000);
133+
expect(result.isOk() && result.value).toEqual({
126134
days: 1,
127135
hours: 0,
128136
milliseconds: 0,
129137
minutes: 0,
130138
seconds: 0
131139
});
132-
expect(parseDuration(4_351_505_030)).toEqual({
140+
result = parseDuration(4_351_505_030);
141+
expect(result.isOk() && result.value).toEqual({
133142
days: 50,
134143
hours: 8,
135144
milliseconds: 30,

src/__tests__/exception.test.ts

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { Simplify } from 'type-fest';
22
import { describe, expect, expectTypeOf, it, test } from 'vitest';
33

4-
import { BaseException, ExceptionBuilder } from '../exception.js';
4+
import { BaseException, ExceptionBuilder, OutOfRangeException, ValueException } from '../exception.js';
55

6-
import type { ExceptionConstructor, ExceptionInstance } from '../exception.js';
6+
import type { ExceptionConstructor } from '../exception.js';
77

88
type ExceptionOptionsWithCode = Simplify<ErrorOptions & { details: { code: number } }>;
99
type ExceptionOptionsWithCause = Simplify<ErrorOptions & { cause: Error }>;
@@ -13,31 +13,28 @@ type ExceptionParams = { name: 'TestException' };
1313
type ExceptionParamsWithMessage = Simplify<ExceptionParams & { message: string }>;
1414

1515
test('ExceptionConstructor', () => {
16-
expectTypeOf<ExceptionConstructor<ExceptionParams, ErrorOptions>>().toEqualTypeOf<
17-
new (message?: string, options?: ErrorOptions) => ExceptionInstance<ExceptionParams, ErrorOptions>
16+
expectTypeOf<ExceptionConstructor<ExceptionParams, ErrorOptions>>().toMatchTypeOf<
17+
new (message?: string, options?: ErrorOptions) => BaseException<ExceptionParams, ErrorOptions>
1818
>();
19-
expectTypeOf<ExceptionConstructor<ExceptionParams, ExceptionOptionsWithCode>>().toEqualTypeOf<
20-
new (
21-
message: string,
22-
options: ExceptionOptionsWithCode
23-
) => ExceptionInstance<ExceptionParams, ExceptionOptionsWithCode>
19+
expectTypeOf<ExceptionConstructor<ExceptionParams, ExceptionOptionsWithCode>>().toMatchTypeOf<
20+
new (message: string, options: ExceptionOptionsWithCode) => BaseException<ExceptionParams, ExceptionOptionsWithCode>
2421
>();
25-
expectTypeOf<ExceptionConstructor<ExceptionParams, ExceptionOptionsWithCause>>().toEqualTypeOf<
22+
expectTypeOf<ExceptionConstructor<ExceptionParams, ExceptionOptionsWithCause>>().toMatchTypeOf<
2623
new (
2724
message: string,
2825
options: ExceptionOptionsWithCause
29-
) => ExceptionInstance<ExceptionParams, ExceptionOptionsWithCause>
26+
) => BaseException<ExceptionParams, ExceptionOptionsWithCause>
3027
>();
31-
expectTypeOf<ExceptionConstructor<ExceptionParams, ExceptionOptionsWithCodeAndCause>>().toEqualTypeOf<
28+
expectTypeOf<ExceptionConstructor<ExceptionParams, ExceptionOptionsWithCodeAndCause>>().toMatchTypeOf<
3229
new (
3330
message: string,
3431
options: ExceptionOptionsWithCodeAndCause
35-
) => ExceptionInstance<ExceptionParams, ExceptionOptionsWithCodeAndCause>
32+
) => BaseException<ExceptionParams, ExceptionOptionsWithCodeAndCause>
3633
>();
37-
expectTypeOf<ExceptionConstructor<ExceptionParamsWithMessage, ExceptionOptionsWithCodeAndCause>>().toEqualTypeOf<
34+
expectTypeOf<ExceptionConstructor<ExceptionParamsWithMessage, ExceptionOptionsWithCodeAndCause>>().toMatchTypeOf<
3835
new (
3936
options: ExceptionOptionsWithCodeAndCause
40-
) => ExceptionInstance<ExceptionParamsWithMessage, ExceptionOptionsWithCodeAndCause>
37+
) => BaseException<ExceptionParamsWithMessage, ExceptionOptionsWithCodeAndCause>
4138
>();
4239
});
4340

@@ -58,12 +55,12 @@ describe('BaseException', () => {
5855

5956
describe('ExceptionBuilder', () => {
6057
it('should return never for the build method if no name is specified', () => {
61-
const fn = () => new ExceptionBuilder().build();
58+
const fn = (): never => new ExceptionBuilder().build();
6259
expect(fn).toThrow('Cannot build exception: params is undefined');
6360
expectTypeOf<ReturnType<typeof fn>>().toBeNever();
6461
});
6562
it('should build an exception with the provided name and message', () => {
66-
const TestException = new ExceptionBuilder().setParams({ name: 'TestException' }).build();
63+
const { TestException } = new ExceptionBuilder().setParams({ name: 'TestException' }).build();
6764
expect(Object.getPrototypeOf(TestException)).toBe(BaseException);
6865
expectTypeOf<Pick<InstanceType<typeof TestException>, 'name'>>().toEqualTypeOf<{ name: 'TestException' }>();
6966
const error = new TestException('This is a test');
@@ -73,8 +70,8 @@ describe('ExceptionBuilder', () => {
7370
});
7471

7572
it('should create distinct constructors', () => {
76-
const TestException = new ExceptionBuilder().setParams({ name: 'TestException' }).build();
77-
const OtherException = new ExceptionBuilder().setParams({ name: 'OtherException' }).build();
73+
const { TestException } = new ExceptionBuilder().setParams({ name: 'TestException' }).build();
74+
const { OtherException } = new ExceptionBuilder().setParams({ name: 'OtherException' }).build();
7875
const e1 = new TestException('This is a test');
7976
const e2 = new OtherException('This is a test');
8077
expect(e1).toBeInstanceOf(BaseException);
@@ -84,7 +81,7 @@ describe('ExceptionBuilder', () => {
8481
});
8582

8683
it('should allow creating an exception with additional details', () => {
87-
const TestException = new ExceptionBuilder()
84+
const { TestException } = new ExceptionBuilder()
8885
.setParams({ name: 'TestException' })
8986
.setOptionsType<{ details: { code: number } }>()
9087
.build();
@@ -96,7 +93,7 @@ describe('ExceptionBuilder', () => {
9693
});
9794

9895
it('should allow creating an error with a custom cause', () => {
99-
const TestException = new ExceptionBuilder()
96+
const { TestException } = new ExceptionBuilder()
10097
.setParams({ name: 'TestException' })
10198
.setOptionsType<{ cause: Error }>()
10299
.build();
@@ -108,7 +105,7 @@ describe('ExceptionBuilder', () => {
108105
});
109106

110107
it('should allow creating an error with a default message', () => {
111-
const TestException = new ExceptionBuilder()
108+
const { TestException } = new ExceptionBuilder()
112109
.setParams({ message: 'Custom message', name: 'TestException' })
113110
.setOptionsType<{ cause: Error }>()
114111
.build();
@@ -117,3 +114,33 @@ describe('ExceptionBuilder', () => {
117114
expectTypeOf<ConstructorParameters<typeof TestException>>().toEqualTypeOf<[options: { cause: Error }]>();
118115
});
119116
});
117+
118+
describe('ValueException', () => {
119+
it('should have the correct prototype', () => {
120+
expect(Object.getPrototypeOf(ValueException)).toBe(BaseException);
121+
});
122+
});
123+
124+
describe('OutOfRangeException', () => {
125+
it('should have the correct prototype', () => {
126+
expect(Object.getPrototypeOf(OutOfRangeException)).toBe(ValueException);
127+
});
128+
describe('constructor', () => {
129+
it('should create the correct message', () => {
130+
const error = new OutOfRangeException({
131+
details: {
132+
max: Infinity,
133+
min: 0,
134+
value: -1
135+
}
136+
});
137+
expect(error.message).toBe('Value -1 is out of range (0 - Infinity)');
138+
});
139+
});
140+
describe('ForNonPositive', () => {
141+
it('should create the correct message', () => {
142+
const error = OutOfRangeException.forNonPositive(-1);
143+
expect(error.message).toBe('Value -1 is out of range (0 - Infinity)');
144+
});
145+
});
146+
});

src/__tests__/object.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { deepFreeze, filterObject, isAllUndefined, isObject, isObjectLike, isPlainObject } from '../object.js';
3+
import {
4+
deepFreeze,
5+
filterObject,
6+
isAllUndefined,
7+
isObject,
8+
isObjectLike,
9+
isPlainObject,
10+
objectify
11+
} from '../object.js';
412

513
describe('deepFreeze', () => {
614
it('should not allow mutating a primitive value', () => {
@@ -96,3 +104,9 @@ describe('filterValues', () => {
96104
});
97105
});
98106
});
107+
108+
describe('objectify', () => {
109+
it('should create a single key map', () => {
110+
expect(objectify('foo', 'bar')).toStrictEqual({ foo: 'bar' });
111+
});
112+
});

src/__tests__/random.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,48 +6,48 @@ describe('randomInt', () => {
66
it('should return an integer value within the range', () => {
77
const min = 5;
88
const max = 8;
9-
const result = randomInt(min, max);
9+
const result = randomInt(min, max)._unsafeUnwrap();
1010
expect(result).toBeGreaterThanOrEqual(min);
1111
expect(result).toBeLessThan(8);
1212
expect(Number.isInteger(result)).toBe(true);
1313
});
14-
it('should throw if the min value is larger than the max', () => {
15-
expect(() => randomInt(10, 5)).toThrow();
14+
it('should return an error if the min value is larger than the max', () => {
15+
expect(randomInt(10, 5).isErr()).toBe(true);
1616
});
17-
it('should throw if the min value equals the max', () => {
18-
expect(() => randomInt(10, 10)).toThrow();
17+
it('should return an error if the min value equals the max', () => {
18+
expect(randomInt(10, 10).isErr()).toBe(true);
1919
});
2020
it('should handle negative values', () => {
2121
const min = -5;
2222
const max = -3;
23-
const result = randomInt(min, max);
23+
const result = randomInt(min, max)._unsafeUnwrap();
2424
expect(result).toBeGreaterThanOrEqual(min);
2525
expect(result).toBeLessThan(8);
2626
expect(Number.isInteger(result)).toBe(true);
27-
expect(() => randomInt(max, min)).toThrow();
27+
expect(randomInt(max, min).isErr()).toBe(true);
2828
});
2929
});
3030

3131
describe('randomDate', () => {
3232
it('should return a date within the range', () => {
3333
const start = new Date(2000, 0, 1);
3434
const end = new Date();
35-
const random = randomDate(start, end);
35+
const random = randomDate(start, end)._unsafeUnwrap();
3636
expect(random.getTime() >= start.getTime()).toBe(true);
3737
expect(random.getTime() <= end.getTime()).toBe(true);
3838
});
39-
it('should throw if the end is before the start', () => {
40-
expect(() => randomDate(new Date(), new Date(2000, 0, 1))).toThrow();
39+
it('should return an error if the end is before the start', () => {
40+
expect(randomDate(new Date(), new Date(2000, 0, 1)).isErr()).toBe(true);
4141
});
4242
});
4343

4444
describe('randomValue', () => {
45-
it('should throw if given an empty array', () => {
46-
expect(() => randomValue([])).toThrow();
45+
it('should return an error if given an empty array', () => {
46+
expect(randomValue([]).isErr()).toBe(true);
4747
});
4848
it('should return a value in the array', () => {
4949
const arr = [-10, -20, -30];
50-
expect(arr.includes(randomValue(arr)!));
50+
expect(arr.includes(randomValue(arr)._unsafeUnwrap()));
5151
});
5252
it('should not mutate the array', () => {
5353
const arr = [-10, -20, -30];

src/__tests__/range.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { range } from '../range.js';
44

55
describe('range', () => {
66
it('should return an array equal in length to the range', () => {
7-
const arr = range(10);
7+
const arr = range(10)._unsafeUnwrap();
88
expect(arr.length).toBe(10);
99
});
10-
it('should throw an error if the start is equal to the end', () => {
11-
expect(() => range(1, 1)).toThrow();
10+
it('should return an error if the start is equal to the end', () => {
11+
expect(range(1, 1).isErr()).toBe(true);
1212
});
1313
});

0 commit comments

Comments
 (0)