Skip to content

Commit

Permalink
Merge pull request #35 from paperhive/feature/object-map
Browse files Browse the repository at this point in the history
Add objectMap()
  • Loading branch information
andrenarchy committed Sep 19, 2021
2 parents 4e8d246 + a0f1de1 commit 69aed7e
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 0 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,14 @@ You can use the following helpers:
* `optional(validator: Validator<T>)`: generates an optional key validator with the given `validator`.
* `defaultTo(validator: Validator<T>, default: D | () => D`: generates a validator that defaults to `default()` if it is a function and `default` otherwise.
### `objectMap(valueValidator, options?): Validator<{ [k: string]?: T }>`
Returns a validator that returns a map with value type T if all values pass the `valueValidator`, otherwise it returns an error. A new object is returned that has the results of the validator functions as values.
Options:
* `valueValidator: Validator<T>`: validator that is applied to each value.
* `options.allErrors?: boolean`: set to `true` to return all errors instead of only the first.
### `pipe(validator1: Transformer<A, B>): Pipe<A, B>`
Returns a transformer that offers a `.pipe(validator2: Transformer<B, C>): Pipe<A, C>` method.
Expand Down
5 changes: 5 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('Integration tests', () => {
joinedAt: fefe.date(),
favoriteDishes: fefe.array(fefe.string()),
notifications: fefe.enumerate('immediately', 'daily', 'never'),
emails: fefe.objectMap(fefe.string()),
})

type Person = fefe.ValidatorReturnType<typeof validatePerson>
Expand All @@ -29,6 +30,10 @@ describe('Integration tests', () => {
joinedAt: new Date(),
favoriteDishes: ['Pho Bo', 'Sushi'],
notifications: 'daily',
emails: {
work: 'andre@work.com',
home: 'andre@home.org',
},
}

it('validates a person', () =>
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from './enumerate'
export * from './map-object-keys'
export * from './number'
export * from './object'
export * from './object-map'
export * from './parse-boolean'
export * from './parse-date'
export * from './parse-json'
Expand Down
52 changes: 52 additions & 0 deletions src/object-map.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { assert } from 'chai'

import { objectMap } from './object-map'
import { string } from './string'
import { branchError, leafError } from './errors'
import { failure, success } from './result'

describe('objectMap()', () => {
it('should return an error if value is not an object', () =>
assert.deepStrictEqual(
objectMap(string())(null),
failure(leafError(null, 'Not an object.'))
))

it('should return an error if object has a value that does not validate', () => {
const value = { foo: 1337 }
assert.deepStrictEqual(
objectMap(string())(value),
failure(
branchError(value, [
{ key: 'foo', error: leafError(1337, 'Not a string.') },
])
)
)
})

it('should return all errors if requested and object has two value that do not validate', () => {
const value = { foo: 1337, bar: true }
assert.deepStrictEqual(
objectMap(string(), { allErrors: true })(value),
failure(
branchError(value, [
{ key: 'foo', error: leafError(1337, 'Not a string.') },
{ key: 'bar', error: leafError(true, 'Not a string.') },
])
)
)
})

it('should validate an object', () => {
const value = { foo: 'bar' }
assert.deepStrictEqual(objectMap(string())(value), success(value))
})

it('should validate an object with allErrors', () => {
const value = { foo: 'bar' }
assert.deepStrictEqual(
objectMap(string(), { allErrors: true })(value),
success(value)
)
})
})
42 changes: 42 additions & 0 deletions src/object-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { partitionMap, traverse } from 'fp-ts/lib/Array'
import { Either, either, isLeft, left, right } from 'fp-ts/lib/Either'
import { branchError, ChildError, success } from '.'
import { leafError } from './errors'
import { failure } from './result'
import { Validator } from './transformer'

export interface ObjectMapOptions {
allErrors?: boolean
}

export function objectMap<R>(
valueValidator: Validator<R>,
{ allErrors }: ObjectMapOptions = {}
): Validator<{ [k in string]?: R }> {
function validateEntry([key, v]: [string, unknown]): Either<
ChildError,
[string, R]
> {
const result = valueValidator(v)
if (isLeft(result)) return left({ key, error: result.left })
return right([key, result.right])
}

return (value) => {
if (typeof value !== 'object' || value === null)
return failure(leafError(value, 'Not an object.'))

const entries = Object.entries(value)

if (allErrors) {
const results = partitionMap(validateEntry)(entries)
if (results.left.length > 0)
return failure(branchError(value, results.left))
return success(Object.fromEntries(results.right))
}

const result = traverse(either)(validateEntry)(entries)
if (isLeft(result)) return failure(branchError(value, [result.left]))
return success(Object.fromEntries(result.right))
}
}

0 comments on commit 69aed7e

Please sign in to comment.