-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #29 from paperhive/feature/either-pattern
Rewrite with pure functional Either<Error, Result> pattern
- Loading branch information
Showing
34 changed files
with
951 additions
and
568 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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']) | ||
) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.