ramda / ramda-fantasy

:ram::sparkles: Fantasy-Land compatible types for easy integration with Ramda.js
MIT License
1.5k stars 95 forks source link

Plans to add Validation type #152

Open OliverJAsh opened 7 years ago

OliverJAsh commented 7 years ago

Are there any plans to add a Validation type, like in Folktale? docs.folktalejs.org/en/latest/api/data/validation/Validation.html

CrossEye commented 7 years ago

This library has grown without much of a plan. If you're interested in creating one, I'm sure people would be interested. If you're looking for others who might be able to build one, you'll have to pique their interests; it would probably help to explain why you think this would be helpful.

ferborva commented 7 years ago

Personally I have found that using Either's disjunction nature you can implement validations with ease. The difference will mainly lie in the wording used for success/failure (left, right). Given its a monad and that it implements the bifunctor spec you have a lot of flexibility as to how you manage the two possible paths.

CrossEye commented 7 years ago

I haven't used Validation except in fiddling around, but my understanding is that it has one significant advantage over Either in that it collects multiple failure messages together. ("password too short" / "password must contain digits") Either doesn't allow this, correct?

ferborva commented 7 years ago

For such a use case you could make an Either of a Tuple where you store the data to be validated and a validation state array where you can store the errors. Upon Left, add error to the array while keeping the value, upon right, pass the previous input along (or viceversa, which ever rocks your boat). Then you'd just pass a checker function configured with a condition to both sides with a bimap call.

There will be other ways do this of course. Any ideas?

OliverJAsh commented 7 years ago

@ferborva Is this what you're talking about? Note I've had to do this using Folktale instead of ramda-fantasy, because this library doesn't expose a Either.fold method:

const Either = require('data.either')

// Either<X, Y>[] => Either<X[], Y[]>
const sequenceEithers = eithers => (
    eithers
        .reduce((acc, either) => (
            acc.fold(
                accLeft => either.fold(
                    left => Either.Left(accLeft.concat(left)),
                    right => Either.Left(accLeft)
                ),
                accRight => either.fold(
                    left => Either.Left([left]),
                    right => Either.Right(accRight.concat(right))
                )
            )
        ), Either.Right([]))
)

{
    const eithers = [
        Either.Left(1),
        Either.Left(2),
    ]

    console.log(sequenceEithers(eithers))
}

{
    const eithers = [
        Either.Right(1),
        Either.Right(2),
    ]

    console.log(sequenceEithers(eithers))
}

{
    const eithers = [
        Either.Left(1),
        Either.Left(2),
        Either.Right(3),
        Either.Right(4),
    ]

    console.log(sequenceEithers(eithers))
}
ferborva commented 7 years ago

Morning @OliverJAsh. I was thinking more a long the lines of something like this:

const Either = require('ramda-fantasy').Either;
const Identity = require('ramda-fantasy').Identity;
const Tuple = require('ramda-fantasy').Tuple;
const R = require('ramda');

// Build a checker function
const checker = (predicate, errorMsg, tuple) => {
  if (predicate(Tuple.fst(tuple))) {
    // The value is valid
  } else {
    // The value is not valid => Add error msg to the array
    tuple.map(arr => arr.push(errorMsg));
  }
  return tuple;
};

// Curry the checker
const curriedChecker = R.curry(checker);

// Validation predicate example
const isGreaterThan40 = x => x > 40;

// Is Number + Msg
const myIsNumber = curriedChecker(R.is(Number), 'Value must be a number');

// Is greater that 40 + Msg
const myGreaterThan40 = curriedChecker(isGreaterThan40, 'Value must be greater than 40');

/*
  To validate a value we put it into a Tuple together with an empty array
  We build a validator function which takes the value and passes
  it through all our checkers

  This could very well be extracted to function that
  takes a value and an array of predicates
 */
const myNumberValidator = (x) => {
  const checkedTuple = Identity.of(Tuple(x, []))
        .map(myIsNumber)
        .map(myGreaterThan40)
        .chain(R.identity);
  if (Tuple.snd(checkedTuple).length) {
    // There are errors
    return Either.Left(checkedTuple);
  }
  return Either.of(checkedTuple);
};

const allGood = 42;
const badValue = 2;
const badNumberAndValue = 'foo';

console.log(myNumberValidator(allGood));
// _Right { value: _Tuple { '0': 42, '1': [], length: 2 } }

console.log(myNumberValidator(badValue));
// _Left { value: _Tuple { '0': 2, '1': [ 'Value must be greater than 40' ], length: 2 } }

console.log(myNumberValidator(badNumberAndValue));
// _Left {
//  value: 
//   _Tuple {
//     '0': 'foo',
//     '1': [ 'Value must be a number', 'Value must be greater than 40' ],
//     length: 2 } }

You put the value into a Tuple inside an Identity container in order to map predicate validations while you accumulate the errors in the second position of the Tuple. Once that is done you can return the checkedTuple as is or check the Tuple of errors and place it into a Left or a Right.

In case you put the Tuple into an Either you can use 'either' static to manage the outcome: