Skip to content

Commit

Permalink
Merge pull request #29 from paperhive/feature/either-pattern
Browse files Browse the repository at this point in the history
Rewrite with pure functional Either<Error, Result> pattern
  • Loading branch information
andrenarchy committed Mar 30, 2021
2 parents f0127ae + 8d7acbd commit 5031eaa
Show file tree
Hide file tree
Showing 34 changed files with 951 additions and 568 deletions.
233 changes: 163 additions & 70 deletions README.md

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
"mocha": "^8.1.3",
"nyc": "^15.1.0",
"prettier": "^2.2.1",
"ramda": "^0.27.1",
"ts-node": "^9.0.0",
"typescript": "^4.0.3"
},
Expand All @@ -68,5 +67,8 @@
"*.ts": [
"eslint --fix"
]
},
"dependencies": {
"fp-ts": "^2.9.5"
}
}
78 changes: 56 additions & 22 deletions src/array.test.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,70 @@
import { expect } from 'chai'
import { assert } from 'chai'
import { chain } from 'fp-ts/lib/Either'
import { flow } from 'fp-ts/lib/function'

import { FefeError } from './errors'
import { branchError, leafError } from './errors'
import { array } from './array'
import { string } from './string'
import { boolean } from './boolean'
import { failure, success } from './result'

describe('array()', () => {
it('should throw if not a array', () => {
const validate = array(string())
expect(() => validate('foo'))
.to.throw(FefeError, 'Not an array.')
.that.deep.include({ value: 'foo', path: [], child: undefined })
it('should return error if not a array', () => {
assert.deepStrictEqual(
array(boolean())('foo'),
failure(leafError('foo', 'Not an array.'))
)
})

it('should throw if nested validation fails', () => {
const validate = array(string())
const value = ['foo', 1]
expect(() => validate(value))
.to.throw(FefeError, 'Not a string.')
.that.deep.include({ value, path: [1] })
it('should return error if nested validation fails', () => {
assert.deepStrictEqual(
array(boolean())([true, 42]),
failure(
branchError(
[true, 42],
[{ key: 1, error: leafError(42, 'Not a boolean.') }]
)
)
)
})

it('should return all errors if nested validation fails', () => {
assert.deepStrictEqual(
array(boolean(), { allErrors: true })([true, 42, 1337]),
failure(
branchError(
[true, 42, 1337],
[
{ key: 1, error: leafError(42, 'Not a boolean.') },
{ key: 2, error: leafError(1337, 'Not a boolean.') },
]
)
)
)
})

it('should return a valid array', () => {
const validate = array(string())
const value = ['foo', 'bar']
expect(validate(value)).to.eql(value)
const value = [true, false]
assert.deepStrictEqual(array(boolean())(value), success(value))
})

it('should return a valid array with allErrors', () => {
const value = [true, false]
assert.deepStrictEqual(
array(boolean(), { allErrors: true })(value),
success(value)
)
})

it('should return a valid array with transformed values', () => {
const validate = array((value) => `transformed: ${string()(value)}`)
expect(validate(['foo', 'bar'])).to.eql([
'transformed: foo',
'transformed: bar',
])
const transform = array(
flow(
boolean(),
chain((v: boolean) => success(`transformed: ${v}`))
)
)
assert.deepStrictEqual(
transform([false, true]),
success(['transformed: false', 'transformed: true'])
)
})
})
43 changes: 27 additions & 16 deletions src/array.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,42 @@
import { FefeError } from './errors'
import { partitionMapWithIndex, traverseWithIndex } from 'fp-ts/lib/Array'
import { either, isLeft, left } from 'fp-ts/Either'

import { branchError, leafError } from './errors'
import { failure, isFailure, success } from './result'
import { Validator } from './validate'

export interface ArrayOptions {
minLength?: number
maxLength?: number
allErrors?: boolean
}

export function array<R>(
elementValidator: Validator<R>,
{ minLength, maxLength }: ArrayOptions = {}
): (value: unknown) => R[] {
{ minLength, maxLength, allErrors }: ArrayOptions = {}
): Validator<R[]> {
const validate = (index: number, element: unknown) => {
const result = elementValidator(element)
if (isFailure(result)) return left({ key: index, error: result.left })
return result
}
return (value: unknown) => {
if (!Array.isArray(value)) throw new FefeError(value, 'Not an array.')
if (!Array.isArray(value)) return failure(leafError(value, 'Not an array.'))
if (minLength !== undefined && value.length < minLength)
throw new FefeError(value, `Has less than ${minLength} elements.`)
return failure(leafError(value, `Has less than ${minLength} elements.`))
if (maxLength !== undefined && value.length > maxLength)
throw new FefeError(value, `Has more than ${maxLength} elements.`)
return failure(leafError(value, `Has more than ${maxLength} elements.`))

if (allErrors) {
const results = partitionMapWithIndex(validate)(value)

if (results.left.length > 0)
return failure(branchError(value, results.left))
return success(results.right)
}

return value.map((element, index) => {
try {
return elementValidator(element)
} catch (error) {
if (error instanceof FefeError) {
throw error.createParentError(value, index)
}
throw error
}
})
const result = traverseWithIndex(either)(validate)(value)
if (isLeft(result)) return failure(branchError(value, [result.left]))
return success(result.right)
}
}
15 changes: 10 additions & 5 deletions src/boolean.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { expect } from 'chai'
import { assert } from 'chai'

import { FefeError } from './errors'
import { boolean } from './boolean'
import { leafError } from './errors'
import { failure, success } from './result'

describe('boolean()', () => {
it('should throw if not a boolean', () => {
expect(() => boolean()('foo')).to.throw(FefeError, 'Not a boolean.')
it('should return an error if not a boolean', () => {
assert.deepStrictEqual(
boolean()('foo'),
failure(leafError('foo', 'Not a boolean.'))
)
})

it('return a valid boolean', () => expect(boolean()(true)).to.equal(true))
it('return a valid boolean', () =>
assert.deepStrictEqual(boolean()(true), success(true)))
})
14 changes: 8 additions & 6 deletions src/boolean.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { FefeError } from './errors'
import { leafError } from './errors'
import { failure, success } from './result'
import { Validator } from './validate'

export function boolean() {
return (value: unknown): boolean => {
// tslint:disable-next-line:strict-type-predicates
if (typeof value !== 'boolean') throw new FefeError(value, 'Not a boolean.')
return value
export function boolean(): Validator<boolean> {
return (value: unknown) => {
if (typeof value !== 'boolean')
return failure(leafError(value, 'Not a boolean.'))
return success(value)
}
}
44 changes: 25 additions & 19 deletions src/date.test.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,40 @@
import { expect } from 'chai'
import { assert } from 'chai'

import { FefeError } from './errors'
import { leafError } from './errors'
import { date } from './date'
import { failure, success } from './result'

describe('date()', () => {
it('should throw if not a date', () => {
expect(() => date()('foo')).to.throw(FefeError, 'Not a date.')
it('should return an error if not a date', () => {
assert.deepStrictEqual(
date()('foo'),
failure(leafError('foo', 'Not a date.'))
)
})

it('should throw if not a valid date', () => {
expect(() => date()(new Date('foo'))).to.throw(
FefeError,
'Not a valid date.'
it('should return an error if not a valid date', () => {
const value = new Date('foo')
assert.deepStrictEqual(
date()(value),
failure(leafError(value, 'Not a valid date.'))
)
})

it('should throw if before min', () => {
it('should return an error if before min', () => {
const validate = date({ min: new Date('2018-10-22T00:00:00.000Z') })
expect(() => validate(new Date('2018-10-21T00:00:00.000Z'))).to.throw(
FefeError,
'Before 2018-10-22T00:00:00.000Z.'
const value = new Date('2018-10-21T00:00:00.000Z')
assert.deepStrictEqual(
validate(value),
failure(leafError(value, 'Before 2018-10-22T00:00:00.000Z.'))
)
})

it('should throw if after max', () => {
it('should return an error if after max', () => {
const validate = date({ max: new Date('2018-10-22T00:00:00.000Z') })
expect(() => validate(new Date('2018-10-23T00:00:00.000Z'))).to.throw(
FefeError,
'After 2018-10-22T00:00:00.000Z.'
const value = new Date('2018-10-23T00:00:00.000Z')
assert.deepStrictEqual(
validate(value),
failure(leafError(value, 'After 2018-10-22T00:00:00.000Z.'))
)
})

Expand All @@ -36,8 +43,7 @@ describe('date()', () => {
min: new Date('2018-10-20T00:00:00.000Z'),
max: new Date('2018-10-22T00:00:00.000Z'),
})
const unsafeDate = new Date('2018-10-21T00:00:00.000Z')
const validatedDate: Date = validate(unsafeDate)
expect(validate(validatedDate)).to.equal(unsafeDate)
const value = new Date('2018-10-21T00:00:00.000Z')
assert.deepStrictEqual(validate(value), success(value))
})
})
20 changes: 12 additions & 8 deletions src/date.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { FefeError } from './errors'
import { leafError } from './errors'
import { failure, success } from './result'
import { Validator } from './validate'

export interface DateOptions {
min?: Date
max?: Date
}

export function date({ min, max }: DateOptions = {}) {
return (value: unknown): Date => {
if (!(value instanceof Date)) throw new FefeError(value, 'Not a date.')
if (isNaN(value.getTime())) throw new FefeError(value, 'Not a valid date.')
export function date({ min, max }: DateOptions = {}): Validator<Date> {
return (value: unknown) => {
if (!(value instanceof Date))
return failure(leafError(value, 'Not a date.'))
if (isNaN(value.getTime()))
return failure(leafError(value, 'Not a valid date.'))
if (min !== undefined && value.getTime() < min.getTime())
throw new FefeError(value, `Before ${min.toISOString()}.`)
return failure(leafError(value, `Before ${min.toISOString()}.`))
if (max !== undefined && value.getTime() > max.getTime())
throw new FefeError(value, `After ${max.toISOString()}.`)
return value
return failure(leafError(value, `After ${max.toISOString()}.`))
return success(value)
}
}
Loading

0 comments on commit 5031eaa

Please sign in to comment.