Flexible validation with fp-ts

molok

Alessio Bolognino

Posted on February 29, 2020

Flexible validation with fp-ts

The Either monad is a good way to represent the result of a computation, you either have the happy path (conventionally on the Right) or the error (on the Left).

I've been working on a problem where I had a third case: the function could succeed with non-fatal warnings.

One way to model this in fp-ts is fp-ts/lib/These, from the docs:

These is a data structure providing "inclusive-or" as opposed to Either's "exclusive-or". If you interpret Either<E, A> as suggesting the computation may either fail or succeed (exclusively), then These<E, A> may fail, succeed, or do both at the same time.

On the Left a ReadOnlyNonEmptyArray that will contain our Warnings, on the Right a password provided by the user. If both values are present it means that it passed the validation but there are some warnings the user should know about.

This example is meant to be a variation of a post (Either vs Validation) by Giulio Canti, the author of fp-ts.

I'm publishing this snippet because I haven't found many (if any) public code bases that make use of These, pipeable, getMonad and it could be useful to someone else (or myself in a few months).

import * as E from 'fp-ts/lib/Either'
import * as TH from 'fp-ts/lib/These';
import {pipe, pipeable} from "fp-ts/lib/pipeable";
import * as RNEA from 'fp-ts/lib/ReadonlyNonEmptyArray';
import { Semigroup } from 'fp-ts/lib/Semigroup';

type Password = string;
type Warning = string
type Warnings = RNEA.ReadonlyNonEmptyArray<Warning>

const minLength = (s: string): E.Either<string, string> =>
  s.length >= 6 ? E.right(s) : E.left('at least 6 characters')

const atLeastOneCapital = (arg: Password): TH.These<Warnings, Password> => {
    if (!/[A-Z]/g.test(arg)) {
        return TH.both(["at least one capital"], arg + "Z")
    }
    return TH.right(arg);
};

const trimSpaces = (s: Password): TH.These<Warnings, Password> => {
    const res = s.trim();
    return (res === s) 
           ? TH.right(res) 
           : TH.both(["no trailing spaces allowed"], res)
}

const lift = (f: (s: string) => E.Either<string, string>): (s: string) => E.Either<Warnings, Password> => {
    return (a: Password): E.Either<Warnings, Password> => {
        return pipe(
            f(a),
            E.mapLeft(warning => [warning]));
    }
};

const {chain: theseChain} = pipeable(TH.getMonad(RNEA.getSemigroup() as Semigroup<Warnings>));

const validate = (s: Password) => {
    return pipe( 
        TH.right(s),
        theseChain(trimSpaces),
        theseChain(lift(minLength)),
        theseChain(atLeastOneCapital),
        TH.fold(
            warnings => `invalid password: ${warnings.join(", ")}`,
            pass => `password accepted`,
            (warnings, pass) => `this is your new password: "${pass}" (we had to improve it: ${warnings.join(", ")})`
        )
    )
}

console.log(validate("pass "));
// invalid password: no trailing spaces allowed, at least 6 characters

console.log(validate(" pass123!"));
// this is your new password: "pass123!Z" (we had to improve it: 
// no trailing spaces allowed, at least one capital)

console.log(validate("Correct Horse"));
// password accepted
šŸ’– šŸ’Ŗ šŸ™… šŸš©
molok
Alessio Bolognino

Posted on February 29, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Reader monads
functional Reader monads

June 4, 2023

Task monads
functional Task monads

December 18, 2022

The Option monad
functional The Option monad

December 6, 2022

The Either monad
functional The Either monad

November 28, 2022

Functor and Monad
functional Functor and Monad

November 2, 2022