Skip to content

Commit

Permalink
refactor!: updating refute type
Browse files Browse the repository at this point in the history
  • Loading branch information
mirek committed Oct 24, 2023
1 parent 6e2cbb3 commit 5d394e2
Show file tree
Hide file tree
Showing 28 changed files with 148 additions and 105 deletions.
8 changes: 4 additions & 4 deletions src/array.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as $ from './index.js'

test('array', () => {
expect($.array($.unknown)({})).toEqual([{}, "expected array"])
expect($.array($.unknown)([])).toEqual([[], undefined])
expect($.array($.number)([1, 2, 3])).toEqual([[1, 2, 3], undefined])
expect($.array($.number)([1, '2', 3])).toEqual(['2', 'at index 1, expected number'])
expect($.array($.unknown)({})).toEqual($.fail({}, 'expected array'))
expect($.array($.unknown)([])).toEqual($.ok([]))
expect($.array($.number)([1, 2, 3])).toEqual($.ok([1, 2, 3]))
expect($.array($.number)([1, '2', 3])).toEqual($.fail('2', 'at index 1, expected number'))
})
6 changes: 6 additions & 0 deletions src/assert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as $ from './index.js'

test('assert', () => {
expect(() => $.assert($.number)('a')).toThrow('Invalid value expected number, got \'a\'.')
expect(() => $.assert($.number, $.reasonWithoutReceived)('a')).toThrow('Invalid value expected number.')
})
6 changes: 3 additions & 3 deletions src/assert.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { failed, failureReason, type Refute } from './prelude.js'
import { failed, reasonWithReceived, type Refute } from './prelude.js'

/** Combinator returning refute as assertion. */
const assert =
<T>(a: Refute<T>) =>
<T>(a: Refute<T>, f = reasonWithReceived) =>
(value: unknown): T => {
const r = a(value)
if (failed(r)) {
throw new TypeError(failureReason(r))
throw new TypeError(f(r))
}
return value as T
}
Expand Down
4 changes: 2 additions & 2 deletions src/bigint.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as $ from './index.js'

test('bigint', () => {
expect($.bigint(0n)).toEqual([0n, undefined])
expect($.bigint(1)).toEqual([1, 'expected bigint'])
expect($.bigint(0n)).toEqual($.ok(0n))
expect($.bigint(1)).toEqual($.fail(1, 'expected bigint'))
})
6 changes: 3 additions & 3 deletions src/boolean.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as $ from './index.js'

test('boolean', () => {
expect($.boolean(true)).toEqual([true, undefined])
expect($.boolean(false)).toEqual([false, undefined])
expect($.boolean(0)).toEqual([0, 'expected boolean'])
expect($.boolean(true)).toEqual($.ok(true))
expect($.boolean(false)).toEqual($.ok(false))
expect($.boolean(0)).toEqual($.fail(0, 'expected boolean'))
})
6 changes: 3 additions & 3 deletions src/calendar-date.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ const range = $.exact({
})

test('not a string', () => {
expect(range({ from: 1, to: 2 })).toEqual([1, 'at key from, expected string'])
expect(range({ from: 1, to: 2 })).toEqual($.fail(1, 'at key from, expected string'))
})

test('valid', () => {
expect(range(JSON.parse('{"from":"2001-01-01","to":"2001-01-02"}'))).toEqual([{
expect(range(JSON.parse('{"from":"2001-01-01","to":"2001-01-02"}'))).toEqual($.ok({
from: '2001-01-01',
to: '2001-01-02'
}, undefined])
}))
})

test('not valid date string', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/defined.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as $ from './index.js'

test('defined', () => {
expect($.defined(1)).toEqual([1, undefined])
expect($.defined(undefined)).toEqual([undefined, 'expected defined'])
expect($.defined(1)).toEqual($.ok(1))
expect($.defined(undefined)).toEqual($.fail(undefined, 'expected defined'))
})
12 changes: 6 additions & 6 deletions src/exact-partial.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as $ from './index.js'

test('exact partial', () => {
expect($.exactPartial({})({})).toEqual([{}, undefined])
expect($.exactPartial({})(undefined)).toEqual([undefined, 'expected object'])
expect($.exactPartial({})({ a: 1 })).toEqual([{ a: 1 }, 'unexpected key a'])
expect($.exactPartial({ a: $.number })({ a: 1 })).toEqual([{ a: 1 }, undefined])
expect($.exactPartial({ a: $.number })({ a: '1' })).toEqual(['1', 'at key a, expected number'])
expect($.exactPartial({ a: $.number })({ a: undefined })).toEqual([{ a: undefined }, undefined])
expect($.exactPartial({})({})).toEqual($.ok({}))
expect($.exactPartial({})(undefined)).toEqual($.fail(undefined, 'expected object'))
expect($.exactPartial({})({ a: 1 })).toEqual($.fail({ a: 1 }, 'unexpected key a'))
expect($.exactPartial({ a: $.number })({ a: 1 })).toEqual($.ok({ a: 1 }))
expect($.exactPartial({ a: $.number })({ a: '1' })).toEqual($.fail('1', 'at key a, expected number'))
expect($.exactPartial({ a: $.number })({ a: undefined })).toEqual($.ok({ a: undefined }))
})
2 changes: 1 addition & 1 deletion src/exact.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as $ from './index.js'

test('exact', () => {
expect($.exact({})(null)).toEqual([null, 'expected object'])
expect($.exact({})(null)).toEqual($.fail(null, 'expected object'))
})

test('single extra key', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/false.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as $ from './index.js'

test('false', () => {
expect($.false(false)).toEqual([false, undefined])
expect($.false(true)).toEqual([true, 'expected false'])
expect($.false(false)).toEqual($.ok(false))
expect($.false(true)).toEqual($.fail(true, 'expected false'))
})
6 changes: 3 additions & 3 deletions src/finite.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as $ from './index.js'

test('finite', () => {
expect($.finite(1)).toEqual([1, undefined])
expect($.finite(Infinity)).toEqual([Infinity, 'expected finite number'])
expect($.finite(NaN)).toEqual([NaN, 'expected finite number'])
expect($.finite(1)).toEqual($.ok(1))
expect($.finite(Infinity)).toEqual($.fail(Infinity, 'expected finite number'))
expect($.finite(NaN)).toEqual($.fail(NaN, 'expected finite number'))
})
8 changes: 4 additions & 4 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ test('basic', () => {
bar: $.string
})

expect($.reason(refute)({})).toEqual('Invalid value at key foo, expected number.')
expect($.reason(refute)({ foo: 1 })).toEqual('Invalid value at key bar, expected string.')
expect($.reason(refute)({ foo: 'a' })).toEqual('Invalid value at key foo, expected number.')
expect($.reason(refute)({})).toEqual('Invalid value at key foo, expected number, got undefined.')
expect($.reason(refute)({ foo: 1 })).toEqual('Invalid value at key bar, expected string, got undefined.')
expect($.reason(refute)({ foo: 'a' })).toEqual('Invalid value at key foo, expected number, got \'a\'.')

const predicate = $.predicate(refute)
expect(predicate({})).toBe(false)
expect(predicate({ foo: 1, bar: 'a' })).toBe(true)

const assert = $.assert(refute)
expect(() => assert({})).toThrow('Invalid value at key foo, expected number.')
expect(() => assert({})).toThrow('Invalid value at key foo, expected number, got undefined.')
expect(() => assert({ foo: 1, bar: 'a' })).not.toThrow()

})
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import exact from './exact.js'
import exactPartial from './exact-partial.js'
import false_ from './false.js'
import finite from './finite.js'
import is from './is.js'
import lift from './lift.js'
import null_ from './null.js'
import nullishOr from './nullish-or.js'
Expand Down Expand Up @@ -49,6 +50,7 @@ export {
exactPartial,
false_ as false,
finite,
is,
lift,
null_ as null,
nullishOr,
Expand Down
11 changes: 11 additions & 0 deletions src/is.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ok, fail, type Refute } from './index.js'

/** */
const is =
<T>(a: T): Refute<T> =>
(value: unknown) =>
Object.is(value, a) ?
ok(value as T) :
fail(value, `expected ${a}`)

export default is
8 changes: 4 additions & 4 deletions src/lift.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ test('lift', () => {
f: false as const
})

expect($.reason(refute)({})).toEqual('Invalid value at key str, expected a.')
expect($.reason(refute)({ str: 'a' })).toEqual('Invalid value at key one, expected 1.')
expect($.reason(refute)({ str: 'a', one: 1 })).toEqual('Invalid value at key t, expected true.')
expect($.reason(refute)({ str: 'a', one: 1, t: true })).toEqual('Invalid value at key f, expected false.')
expect($.reason(refute)({})).toEqual('Invalid value at key str, expected a, got undefined.')
expect($.reason(refute)({ str: 'a' })).toEqual('Invalid value at key one, expected 1, got undefined.')
expect($.reason(refute)({ str: 'a', one: 1 })).toEqual('Invalid value at key t, expected true, got undefined.')
expect($.reason(refute)({ str: 'a', one: 1, t: true })).toEqual('Invalid value at key f, expected false, got undefined.')
expect($.reason(refute)({ str: 'a', one: 1, t: true, f: false })).toEqual(undefined)

})
2 changes: 1 addition & 1 deletion src/null.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ok, fail, type Refute } from './prelude.js'

/** @returns refute for `null` type. */
/** @returns ok if value is `null`, failure otherwise. */
const null_: Refute<null> =
(value: unknown) =>
value === null ?
Expand Down
2 changes: 1 addition & 1 deletion src/number.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ok, fail, type Refute } from './prelude.js'

/** @returns refute for `number` type. */
/** @returns ok if value is number, failure otherwise. */
const number_: Refute<number> =
(value: unknown) =>
typeof value === 'number' ?
Expand Down
2 changes: 1 addition & 1 deletion src/or.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ test('or', () => {
'ABC' as const,
$.object({ foo: 1 })
)
expect(() => $.assert(f)({})).toThrow('Invalid value where none of 3 alternatives matched.')
expect(() => $.assert(f)({})).toThrow('Invalid value where none of 3 alternatives matched, got {}.')
expect($.assert(f)('ABC')).toBe('ABC')
expect($.assert(f)(42)).toBe(42)
expect($.assert(f)({ foo: 1 })).toEqual({ foo: 1 })
Expand Down
10 changes: 5 additions & 5 deletions src/partial.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as $ from './index.js'

test('partial', () => {
expect($.partial({ a: $.number })(null)).toEqual([null, 'expected object'])
expect($.partial({ a: $.number })({})).toEqual([{}, undefined])
expect($.partial({ a: $.number })({ b: 1 })).toEqual([{ b: 1 }, undefined])
expect($.partial({ a: $.number })({ a: '1' })).toEqual(['1', 'at key a, expected number'])
expect($.partial({ a: $.number })({ a: 1, b: 2 })).toEqual([{ a: 1, b: 2 }, undefined])
expect($.partial({ a: $.number })(null)).toEqual($.fail(null, 'expected object'))
expect($.partial({ a: $.number })({})).toEqual($.ok({}))
expect($.partial({ a: $.number })({ b: 1 })).toEqual($.ok({ b: 1 }))
expect($.partial({ a: $.number })({ a: '1' })).toEqual($.fail('1', 'at key a, expected number'))
expect($.partial({ a: $.number })({ a: 1, b: 2 })).toEqual($.ok({ a: 1, b: 2 }))
})
70 changes: 44 additions & 26 deletions src/prelude.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
export type Ok<T> = [ value: T, reason: undefined ]

export type Fail = [ value: unknown, reason: string ]

export type Result<T> = Ok<T> | Fail

export type Refute<T> = (value: unknown) => Result<T>

export type Refuted<P> = P extends Refute<infer U> ? U : never
import { inspect } from 'node:util'

export type Ok<T> = {
status: 'ok',
value: T,
}

export type Fail = {
status: 'refuted',
reason: string,
received: unknown
}

export type Result<T> =
| Ok<T>
| Fail

export type Refute<T> =
(value: unknown) =>
Result<T>

export type Refuted<P> =
P extends Refute<infer U> ?
U :
never

export type Primitive =
| undefined
Expand All @@ -19,13 +35,15 @@ export type Primitive =
| symbol
| RegExp

export type Lifted<T> = T extends Refute<infer U> ?
U :
T extends Primitive ?
T :
never
export type Lifted<T> =
T extends Refute<infer U> ?
U :
T extends Primitive ?
T :
never

export type IntersectionOfUnion<T> = (T extends unknown ? (_: T) => unknown : never) extends (_: infer R) => unknown ? R : never
export type IntersectionOfUnion<T> =
(T extends unknown ? (_: T) => unknown : never) extends (_: infer R) => unknown ? R : never

export type Constructor = abstract new (...args: any) => any

Expand All @@ -34,22 +52,22 @@ export const parameter0 = '@prelude/refute:parameter0'
/** @returns success result. */
export const ok =
<T>(value: T): Ok<T> =>
[ value, undefined ]
({ status: 'ok' as const, value })

/** @returns failure result. */
export const fail =
(value: unknown, reason: string): Fail =>
[ value, reason ]
(received: unknown, reason: string): Fail =>
({ status: 'refuted' as const, received, reason })

/** @returns `true` if provided `result` is failure, `false` otherwise. */
export const failed =
(result: Result<unknown>): result is Fail =>
typeof result[1] === 'string'
result.status === 'refuted'

/** Wraps failure with provided `reason` prefix. */
export const refail =
(failure: Fail, reason: string): Fail =>
fail(failure[0], `${reason}, ${failure[1]}`)
fail(failure.received, `${reason}, ${failure.reason}`)

export const nest =
<T>(reason: string) =>
Expand All @@ -61,12 +79,12 @@ export const nest =
r
}

/** @return failure reason without interpolating value. */
export const failureReason =
/** @return failure reason without inspecting received value. */
export const reasonWithoutReceived =
(failure: Fail): string =>
`Invalid value ${failure[1]}.`
`Invalid value ${failure.reason}.`

/** @returns failure reason. */
export const unsafeFailureReason =
/** @return failure reason with inspecting received value. */
export const reasonWithReceived =
(failure: Fail): string =>
`Invalid value ${failure[1]}, got ${failure[0]}.`
`Invalid value ${failure.reason}, got ${inspect(failure.received)}.`
6 changes: 3 additions & 3 deletions src/reason.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { failed, failureReason, type Refute } from './prelude.js'
import { failed, reasonWithReceived, type Refute } from './prelude.js'

/** Combinator returning refute reason or `undefined`. */
const reason =
<T>(a: Refute<T>) =>
<T>(a: Refute<T>, f = reasonWithReceived) =>
(value: unknown): undefined | string => {
const r = a(value)
return failed(r) ?
failureReason(r) :
f(r) :
undefined
}

Expand Down
10 changes: 5 additions & 5 deletions src/record.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as $ from './index.js'

test('record', () => {
expect($.record($.string, $.number)(null)).toEqual([null, 'expected object'])
expect($.record($.string, $.number)({})).toEqual([{}, undefined])
expect($.record($.string, $.number)({ a: 1 })).toEqual([{ a: 1 }, undefined])
expect($.record($.string, $.number)({ a: '1' })).toEqual(['1', 'at key a, expected number'])
expect($.record($.regexp(/^[a-z]+$/), $.number)({ a1: '1' })).toEqual(['a1', 'key, expected to match /^[a-z]+$/.'])
expect($.record($.string, $.number)(null)).toEqual($.fail(null, 'expected object'))
expect($.record($.string, $.number)({})).toEqual($.ok({}))
expect($.record($.string, $.number)({ a: 1 })).toEqual($.ok({ a: 1 }))
expect($.record($.string, $.number)({ a: '1' })).toEqual($.fail('1', 'at key a, expected number'))
expect($.record($.regexp(/^[a-z]+$/), $.number)({ a1: '1' })).toEqual($.fail('a1', 'key, expected to match /^[a-z]+$/.'))
})
9 changes: 5 additions & 4 deletions src/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ const record =
return fail(value, 'expected object')
}
let r: Result<unknown>
for (const entry of Object.entries(value)) {
r = k(entry[0])
for (const k_ in value) {
r = k(k_)
if (failed(r)) {
return refail(r, 'key')
}
r = v(entry[1])
const v_ = value[k_]
r = v(v_)
if (failed(r)) {
return refail(r, `at key ${entry[0]}`)
return refail(r, `at key ${k_}`)
}
}
return ok(value as Record<K, V>)
Expand Down
5 changes: 5 additions & 0 deletions src/regexp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as $ from './index.js'

test('regexp', () => {
expect($.regexp(/^foo/dy)('foo')).toEqual($.ok('foo'))
})
Loading

0 comments on commit 5d394e2

Please sign in to comment.