Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/paperhive/fefe

Validate, sanitize and transform values with proper TypeScript types and zero dependencies.
https://github.com/paperhive/fefe

functional parse parsing sanitization sanitize schema transform typescript validate validation

Last synced: about 2 months ago
JSON representation

Validate, sanitize and transform values with proper TypeScript types and zero dependencies.

Awesome Lists containing this project

README

        

# fefe

[![npm version](https://badge.fury.io/js/fefe.svg)](https://badge.fury.io/js/fefe)
[![Test status](https://github.com/paperhive/fefe/actions/workflows/test.yaml/badge.svg)](https://github.com/paperhive/fefe/actions/workflows/test.yaml)
[![codecov](https://codecov.io/gh/paperhive/fefe/branch/main/graph/badge.svg?token=OZcHEYFYrQ)](https://codecov.io/gh/paperhive/fefe)

Validate, sanitize and transform values with proper TypeScript types and with a single dependency ([fp-ts](https://www.npmjs.com/package/fp-ts)).

**🔎  Validation:** checks a value (example: check if value is string)

**:nut_and_bolt:  Sanitization:** if a value is not valid, try to transform it (example: transform value to `Date`)

**🛠️  Transformation:** transforms a value (example: parse JSON)

**🔌  Everything is a function**: functional approach makes it easy to extend – just plug in your own function anywhere!

**↔️  Based on `Either`:** explicit and type-safe error handling – `left` path is a (typed!) error, `right` path is a valid value (see below).

## Installation

```bash
npm install fefe
```

## Usage

### 🔎 Validation example

Validation checks the provided value and returns it with proper types.

```typescript
import { object, string } from 'fefe'

const validatePerson = object({ name: string() })

const result = validatePerson({ name: 'Leia' })
if (isFailure(result)) {
return console.error(result.left)

// result is of type { name: string }
const person = result.right
```

☝️ You can also use `fefe` to define your types easily:

```typescript
import { ValidatorReturnType } from 'fefe'
type Person = ValidatorReturnType // { name: string }
```

### ⚙️ Basic transformation example

#### Parse a value

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, pipe, string, ValidatorReturnType } from 'fefe'

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

// { title: string, releasedAt: Date }
type Movie = ValidatorReturnType

const movie: Movie = sanitizeMovie({
title: 'Star Wars',
releasedAt: '1977-05-25T12:00:00.000Z'
})
```

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, pipe, union } from 'fefe'

const sanitizeDate = union(
date(),
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 `pipe` to compose functions. Here, we also add a custom function that splits a string into an array.

```typescript
import { object, parseJson, pipe, string, success } from 'fefe'

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

// { gcloudCredentials: { secret: string }, whitelist: string[] }
type Config = ValidatorReturnType

const config: Config = parseConfig({
gcloudCredentials: '{"secret":"foobar"}',
whitelist: 'alice,bob'
})
```

Then `config.right` will equal `{ gcloudCredentials: { secret: 'foobar'}, whitelist: ['alice', 'bob'] }`.

## Documentation

### Transformer

A transformer is a function that accepts some value of type `V` (it could be `unknown`) and returns a type `T`:
```typescript
type Transform = (v: V) => Result
```
The result can either be a `FefeError` (see below) or the validated value as type `T`:
```typescript
type Result = Either
```

`fefe` uses the `Either` pattern with types and functions from [fp-ts](https://www.npmjs.com/package/fp-ts). `Either` can either represent an error (the "left" path) or the successfully validated value (the "right" path). This results in type-safe errors and explicit error-handling. Example:

```typescript
import { isFailure } from 'fefe'

const result: Result = ...
if (isFailure(result)) {
console.error(result.left)
process.exit(1)
}
const name = result.right
```

You may wonder why `fefe` does not just throw an error and the answer is:
1. Throwing an error is a side-effect which goes against pure functional programming.
2. Lack of type-safety: A thrown error can be anything and needs run-time checking before it can be used in a meaningful way.

You can read more about it [here](https://medium.com/nmc-techblog/functional-error-handling-in-js-8b7f7e4fa092).

For simplifying the transition from a 2.x codebase you can use the `toThrow(t: Transformer)` function that returns a funtion `(v: V) => T` that returns the value directly and throws instead of returning a `FefeError` in the case of an error. Note that the thrown `FefeThrowError` has a different structure than the pre-3.x `FefeError`.

### Validator

A validator is just a special (but common) case of a transformer where the input is `unknown`:

```typescript
type Validator = Transformer
```

### `FefeError`

`fefe` validators return a `FefeError` if a value can't be validated/transformed. Note that `FefeError` is *not* derived from the JavaScript `Error` object but is a simple object.

If an error occurs it will allow you to pinpoint where exactly the error(s) occured and why. The structure is the following:

```typescript
type FefeError = LeafError | BranchError
```

#### `LeafError`

A `LeafError` can be seen as the source of an error which can happen deep in a nested object and it carries both the value that failed and a human-readable reason describing why it failed.

```typescript
interface LeafError {
type: 'leaf'
value: unknown
reason: string
}
```

#### `BranchError`

A `BranchError` is the encapsulation of one or more errors on a higher level.

```typescript
interface BranchError {
type: 'branch'
value: unknown
childErrors: ChildError[]
}

interface ChildError {
key: Key
error: FefeError
}
```

Imagine an array of values where the values at position 2 and 5 fail. This would result in two `childErrors`: one with `key` equal to 2 and `key` equal to 5. The `error` property is again a `FefeError` so this is a full error tree.

#### `getErrorString(error: FefeError): string`

To simplify handling of errors, you can use `getErrorString()` which traverses the tree and returns a human-readable error message for each `LeafError` – along with the paths and reasons.

Example error message: `user.id: Not a string.`

### `array(elementValidator, options?): Validator`

Returns a validator that checks that the given value is an array and that runs `elementValidator` on all elements. A new array with the results is returned as `Result`.

Options:
* `elementValidator: Validator`: validator that is applied to each element. The return values are returned as a new array.
* `options.minLength?: number`, `options.maxLength?: number`: restrict length of array
* `options.allErrors?: boolean`: set to `true` to return all errors instead of only the first.

### `boolean(): Validator`

Returns a validator that returns `value` if it is a boolean and returns an error otherwise.

### `date(options?): Validator`

Returns a validator that returns `value` if it is a Date and returns an error otherwise.

Options:
* `options.min?: Date`, `options.max?: Date`: restrict date

### `discriminatedUnion(key, definition, options?): Validator>`

Returns a validator that returns `value` if:
* it is an object and
* the `value[key]` is a key of `definition`
* `value` (without `key`) passes the validation as specified in `definition[key]`.
Otherwise it returns an error. A new object is returned that has the results of the validator functions as values.

Options: see `object()`.

### `enumerate(value1, value2, ...): Validator`

Returns a validator that returns `value` if if equals one of the strings `value1`, `value2`, .... and returns an error otherwise.

### `mapObjectKeys(map): Transformer`

Returns a transformer that takes the input object and returns a new object with the keys of `map`. For each key `k` the resulting object's value is the value for the key `map[k]` of the input object.

Options:
* `map: Record`: maps output object keys to input object keys.

This function is very useful in combination with `object()`:

```typescript
const validateEnv = pipe(
object({
FOO: string(),
BAR: optional(pipe(string()).pipe(parseNumber())),
})
)
.pipe(mapObjectKeys({ foo: 'FOO', bar: 'BAR' }))

const result = validatEnv({ FOO: 'str', BAR: '1337' })
```
Then `isSuccess(result)` will be `true` and `result.right` equals to `{ foo: 'str', bar: 1337 }`.

### `number(options?): Validator`

Returns a validator that returns `value` if it is a number and returns an error otherwise.

Options:
* `options.min?: number`, `options.max?: number`: restrict number
* `options.integer?: boolean`: require number to be an integer (default: `false`)
* `options.allowNaN?: boolean`, `options.allowInfinity?: boolean`: allow `NaN` or `infinity` (default: `false`)

### `object(definition, options?): Validator>`

Returns a validator that returns `value` if it is an object and all values pass the validation as specified in `definition`, otherwise it returns an error. A new object is returned that has the results of the validator functions as values.

Options:
* `definition: ObjectDefinition`: an object where each value is a `Validator`.
* `allowExcessProperties?: boolean`: allow excess properties in `value` (default: `false`). Excess properties are not copied to the returned object.
* `allErrors?: boolean`: set to `true` to return all errors instead of only the first (default: `false`).

You can use the following helpers:
* `optional(validator: Validator)`: generates an optional key validator with the given `validator`.
* `defaultTo(validator: Validator, 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`: 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): Pipe`

Returns a transformer that offers a `.pipe(validator2: Transformer): Pipe` method.

### `string(options?): Validator`

Returns a validator that returns `value` if it is a string and returns an error otherwise.

Options:
* `options.minLength?: number`, `options.maxLength?: number`: restrict length of string
* `options.regex?: RegExp`: require string to match regex

### `union(validator1, validator2, ...): Validator`

Returns a validator that returns the return value of the first validator called with `value` that does not return an error. The function returns an error if all validators return an error. All arguments are validators (e.g., `validator1: Validator, validator2: Validator, ...`)

### `parseBoolean(): Transformer`

Returns a transformer that parses a string as a boolean.

### `parseDate(options?): Transformer`

Returns a transformer that parses a string as a date.

Options:
* `options.iso?: boolean`: require value to be an ISO 8601 string.

### `parseJson(): Transformer`

Returns a transformer that parses a JSON string. Since parsed JSON can in turn be almost anything, it is usually combined with another validator like `object({ ... })`.

### `parseNumber(): Transformer`

Returns a transformer that parses a number string.