Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pipe() #31

Merged
merged 5 commits into from
Apr 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}