Skip to content

Commit

Permalink
Merge pull request #31 from paperhive/feature/compose
Browse files Browse the repository at this point in the history
Add pipe()
  • Loading branch information
andrenarchy committed Apr 5, 2021
2 parents dc2e205 + c4b087d commit 49322eb
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 41 deletions.
40 changes: 18 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,14 @@ type Person = ValidatorReturnType<typeof validatePerson> // { name: string }
#### Parse a value
In this example a `string` needs to be parsed as a `Date`. Chaining functions can be achieved by the standard functional tools like `flow` and `chain` in [fp-ts](https://www.npmjs.com/package/fp-ts).
In this example a `string` needs to be parsed as a `Date`. You can use `pipe()` to pass a value through multiple functions:
```typescript
import { object, parseDate, string, ValidatorReturnType } from 'fefe'
import { chain } from 'fp-ts/lib/Either'
import { flow } from 'fp-ts/lib/function'
import { object, parseDate, pipe, string, ValidatorReturnType } from 'fefe'

const sanitizeMovie = object({
title: string(),
releasedAt: flow(string(), chain(parseDate()))
releasedAt: pipe(string()).pipe(parseDate())
})

// { title: string, releasedAt: Date }
Expand All @@ -73,40 +71,34 @@ const movie: Movie = sanitizeMovie({
Then `movie.right` equals `{ title: 'Star Wars', releasedAt: Date(1977-05-25T12:00:00.000Z) }` (`releasedAt` now is a date).
**Note:** Chaining functions can also be achieved by the standard functional tools like `flow` and `chain` in [fp-ts](https://www.npmjs.com/package/fp-ts).
#### Parse a value on demand (sanitize)
Sometimes a value might already be of the right type. In the following example we use `union()` to create a sanitizer that returns a provided value if it is a `Date` already and parse it otherwise. If it can't be parsed either the function will throw:
```typescript
import { date, parseDate, union } from 'fefe'
import { chain } from 'fp-ts/lib/Either'
import { flow } from 'fp-ts/lib/function'
import { date, parseDate, pipe, union } from 'fefe'

const sanitizeDate = union(
date(),
flow(string(), chain(parseDate()))
pipe(string()).pipe(parseDate())
)
```
### 🛠️ Complex transformation example
This is a more complex example that can be applied to parsing environment variables or query string parameters. Again, we use `flow` and `chain` to compose functions. Here, we also add a custom function that splits a string into an array.
This is a more complex example that can be applied to parsing environment variables or query string parameters. Again, we use `pipe` to compose functions. Here, we also add a custom function that splits a string into an array.
```typescript
import { object, parseJson, string, success } from 'fefe'
import { chain } from 'fp-ts/lib/Either'
import { flow } from 'fp-ts/lib/function'
import { object, parseJson, pipe, string, success } from 'fefe'

const parseConfig = object({
gcloudCredentials: flow(
string()
chain(parseJson()),
chain(object({ secret: string() }))
),
whitelist: flow(
string(),
chain(secret => success(str.split(',')))
)
gcloudCredentials: pipe(string())
.pipe(parseJson())
.pipe(object({ secret: string() })),
whitelist: pipe(string()
.pipe(secret => success(str.split(',')))
})

// { gcloudCredentials: { secret: string }, whitelist: string[] }
Expand Down Expand Up @@ -255,6 +247,10 @@ 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.
### `pipe(validator1: Transformer<A, B>): Pipe<A, B>`
Returns a transformer that offers a `.pipe(validator2: Transformer<B, C>): Pipe<A, C>` method.
### `string(options?): Validator<string>`
Returns a validator that returns `value` if it is a string and returns an error otherwise.
Expand Down
8 changes: 2 additions & 6 deletions src/array.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { assert } from 'chai'
import { chain } from 'fp-ts/lib/Either'
import { flow } from 'fp-ts/lib/function'

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

describe('array()', () => {
it('should return error if not a array', () => {
Expand Down Expand Up @@ -57,10 +56,7 @@ describe('array()', () => {

it('should return a valid array with transformed values', () => {
const transform = array(
flow(
boolean(),
chain((v: boolean) => success(`transformed: ${v}`))
)
pipe(boolean()).pipe((v: boolean) => success(`transformed: ${v}`))
)
assert.deepStrictEqual(
transform([false, true]),
Expand Down
22 changes: 9 additions & 13 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { assert } from 'chai'
import { chain } from 'fp-ts/lib/Either'
import { flow } from 'fp-ts/lib/function'

import * as fefe from '.'

Expand Down Expand Up @@ -63,7 +61,7 @@ describe('Integration tests', () => {
describe('Basic transformation (sanitization)', () => {
const sanitizeMovie = fefe.object({
title: fefe.string(),
releasedAt: flow(fefe.string(), chain(fefe.parseDate())),
releasedAt: fefe.pipe(fefe.string()).pipe(fefe.parseDate()),
})

it('validates a movie and parses the date string', () => {
Expand Down Expand Up @@ -99,7 +97,7 @@ describe('Integration tests', () => {
describe('Basic transformation (on-demand sanitization)', () => {
const sanitizeDate = fefe.union(
fefe.date(),
flow(fefe.string(), chain(fefe.parseDate()))
fefe.pipe(fefe.string()).pipe(fefe.parseDate())
)
const date = new Date()

Expand All @@ -126,15 +124,13 @@ describe('Integration tests', () => {

describe('Complex transformation and validation', () => {
const parseConfig = fefe.object({
gcloudCredentials: flow(
fefe.string(),
chain(fefe.parseJson()),
chain(fefe.object({ key: fefe.string() }))
),
whitelist: flow(
fefe.string(),
chain((value) => fefe.success(value.split(',')))
),
gcloudCredentials: fefe
.pipe(fefe.string())
.pipe(fefe.parseJson())
.pipe(fefe.object({ key: fefe.string() })),
whitelist: fefe
.pipe(fefe.string())
.pipe((value) => fefe.success(value.split(','))),
})

type Config = fefe.ValidatorReturnType<typeof parseConfig>
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './errors'
export * from './result'
export * from './throw'
export * from './transformer'

export * from './array'
Expand All @@ -12,5 +13,6 @@ export * from './parse-boolean'
export * from './parse-date'
export * from './parse-json'
export * from './parse-number'
export * from './pipe'
export * from './string'
export * from './union'
41 changes: 41 additions & 0 deletions src/pipe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { assert } from 'chai'

import { branchError, leafError } from './errors'
import { object } from './object'
import { parseJson } from './parse-json'
import { pipe } from './pipe'
import { failure, success } from './result'
import { string } from './string'

describe('pipe()', () => {
const validate = pipe(string())
.pipe(parseJson())
.pipe(object({ foo: string() }))

it('should return an error if first transformer fails', () =>
assert.deepStrictEqual(validate(1), failure(leafError(1, 'Not a string.'))))

it('should return an error if second transformer fails', () =>
assert.deepStrictEqual(
validate('{]'),
failure(
leafError(
'{]',
'Invalid JSON: Unexpected token ] in JSON at position 1.'
)
)
))

it('should return an error if third transformer fails', () =>
assert.deepStrictEqual(
validate('{"foo":1}'),
failure(
branchError({ foo: 1 }, [
{ key: 'foo', error: leafError(1, 'Not a string.') },
])
)
))

it('return a valid boolean', () =>
assert.deepStrictEqual(validate('{"foo":"bar"}'), success({ foo: 'bar' })))
})
17 changes: 17 additions & 0 deletions src/pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { isFailure } from './result'
import { Transformer } from './transformer'

export type Pipe<V, T> = Transformer<V, T> & {
pipe<S>(t: Transformer<T, S>): Pipe<V, S>
}

export function pipe<V, T>(transformer: Transformer<V, T>): Pipe<V, T> {
const pipedTransformer = ((v: V) => transformer(v)) as Pipe<V, T>
pipedTransformer.pipe = <S>(nextTransformer: Transformer<T, S>) =>
pipe((v: V) => {
const result = transformer(v)
if (isFailure(result)) return result
return nextTransformer(result.right)
})
return pipedTransformer
}

0 comments on commit 49322eb

Please sign in to comment.