diff --git a/core/src/main/scala/pear/form/Definition.scala b/core/src/main/scala/pear/form/Definition.scala index 23bd554..57482f6 100644 --- a/core/src/main/scala/pear/form/Definition.scala +++ b/core/src/main/scala/pear/form/Definition.scala @@ -1,45 +1,59 @@ package pear package form -import scala.language.higherKinds -import scala.language.implicitConversions -import matryoshka.{CorecursiveT, Delay} -import scalaz.{Applicative, Cord, Functor, Kleisli, Show, Traverse, \/} - import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter +import scala.language.{higherKinds, implicitConversions} + +import matryoshka.{CorecursiveT, Delay} +import scalaz.{Applicative, Cord, Functor, Show, Traverse} object Definition { type Form = matryoshka.data.Fix[FormF] - sealed trait FormF[A] extends Product with Serializable - + sealed trait FormF[A] extends Product with Serializable + final case class Empty[A]() extends FormF[A] final case class Optional[A](form: A) extends FormF[A] final case class Fields[A](fields: Vector[(String, A)]) extends FormF[A] final case class Choice[A](alternatives: Vector[(String, A)]) extends FormF[A] - final case class Sequence[A](element: A) extends FormF[A] - final case class Value[A](constraint: Constraint) extends FormF[A] + final case class Sequence[A](element: Vector[A]) extends FormF[A] + final case class Number[A]() extends FormF[A] + final case class IsoDateTime[A]() extends FormF[A] + final case class MinLength[A](minLength: Int) extends FormF[A] + final case class MaxLength[A](maxLength: Int) extends FormF[A] + final case class Min[A](min: BigDecimal) extends FormF[A] + final case class Max[A](max: BigDecimal) extends FormF[A] + final case class After[A](start: ZonedDateTime) extends FormF[A] + final case class Before[A](end: ZonedDateTime) extends FormF[A] + final case class AndThen[A](lhs: A, rhs: A) extends FormF[A] - implicit def formFTraverse[L]: Traverse[FormF[?]] = new Traverse[FormF[?]] { + implicit def formFTraverse[L]: Traverse[FormF] = new Traverse[FormF] { import scalaz.std.vector.vectorInstance import scalaz.syntax.traverse._ def traverseImpl[G[_], A, B](fa: FormF[A])(f: A => G[B])(implicit G: Applicative[G]): G[FormF[B]] = fa match { - case Optional(v) => Functor[G].map(f(v))(Optional.apply) + case Empty() => G.point(Empty[B]()) + case Optional(v) => G.map(f(v))(Optional.apply) case Fields(fs) => val (names, values) = fs.unzip - Functor[G].map(values traverse f)(vs => Fields(names zip vs)) + G.map(values traverse f)(vs => Fields(names zip vs)) case Choice(alts) => val (names, values) = alts.unzip - Functor[G].map(values traverse f)(as => Choice(names zip as)) - case Sequence(elem) => - Functor[G].map(f(elem))(Sequence.apply) - case Value(c) => Applicative[G].point(Value[B](c)) + G.map(values traverse f)(as => Choice(names zip as)) + case Sequence(elems) => G.map(elems traverse f)(Sequence.apply) + case Number() => G.point(Number[B]()) + case IsoDateTime() => G.point(IsoDateTime[B]()) + case MinLength(min) => G.point(MinLength[B](min)) + case MaxLength(max) => G.point(MaxLength[B](max)) + case Min(min) => G.point(Min[B](min)) + case Max(max) => G.point(Max[B](max)) + case After(start) => G.point(After[B](start)) + case Before(end) => G.point(Before[B](end)) + case AndThen(lhs, rhs) => G.apply2(f(lhs), f(rhs))(AndThen.apply) } } - implicit def formShow[L](implicit showC: Show[Constraint]) = new Delay[Show, FormF[?]] { + implicit def formShow: Delay[Show, FormF] = new Delay[Show, FormF] { def apply[A](showA: Show[A]): Show[FormF[A]] = new Show[FormF[A]] { override def show(f: FormF[A]): Cord = f match { case Optional(a) => @@ -50,18 +64,13 @@ object Definition { case Choice(alt) => Cord("[ ") ++ Cord.mkCord(Cord(" | "), alt.map { case (k, v) => Cord(s"$k:") ++ showA.show(v) }: _*) ++ Cord( " ]") - case Sequence(elem) => - showA.show(elem) ++ Cord("*") - case Value(c) => - showC.show(c) + case Sequence(elems) => + Cord.mkCord(Cord(", "), elems.map(showA.show): _*) ++ Cord("*") + case terminal => Cord(terminal.toString) } } } - private def parse[O](unsafeParse: String => form.FormValue): form.Constraint = - Kleisli[\/[String, ?], String, FormValue]((s: String) => - \/.fromTryCatchNonFatal(unsafeParse(s)).leftMap(e => s"For input string $s, got ${e.getClass}: ${e.getMessage}")) - def optional[T[_[_]], L](form: T[FormF])(implicit T: CorecursiveT[T]): T[FormF] = T.embedT[FormF](Optional(form)) def mapping[T[_[_]], L](fields: (String, T[FormF])*)(implicit T: CorecursiveT[T]): T[FormF] = @@ -69,13 +78,11 @@ object Definition { def choice[T[_[_]], L](alternatives: (String, T[FormF])*)(implicit T: CorecursiveT[T]): T[FormF] = T.embedT[FormF](Choice(alternatives.toVector)) def sequence[T[_[_]]](element: T[FormF])(implicit T: CorecursiveT[T]): T[FormF] = - T.embedT[FormF](Sequence(element)) - - def int: Constraint = parse(s => ValueNum(s.toInt)) + T.embedT[FormF](Sequence(Vector(element))) - def isoDateTime = parse(s => ValueDate(ZonedDateTime.parse(s, DateTimeFormatter.ISO_DATE_TIME))) + def int[T[_[_]]](implicit T: CorecursiveT[T]) = T.embedT[FormF](Number()) - implicit def lift[T[_[_]]](f: Constraint)(implicit T: CorecursiveT[T]): T[Definition.FormF] = - T.embedT[Definition.FormF](Definition.Value(f)) + def isoDateTime[T[_[_]]](implicit T: CorecursiveT[T]) = T.embedT[FormF](IsoDateTime()) + def andThen[T[_[_]]](lhs: T[FormF], rhs: T[FormF])(implicit T: CorecursiveT[T]) = T.embedT[FormF](AndThen(lhs, rhs)) } diff --git a/core/src/main/scala/pear/form/FormValue.scala b/core/src/main/scala/pear/form/FormValue.scala index 9441aea..b16c5b5 100644 --- a/core/src/main/scala/pear/form/FormValue.scala +++ b/core/src/main/scala/pear/form/FormValue.scala @@ -5,13 +5,17 @@ import java.time.ZonedDateTime sealed trait FormValue extends Product with Serializable final case class ValueObject(fields: Map[String, FormValue]) extends FormValue -final case class ValueList(elements: List[FormValue]) extends FormValue +final case class ValueList(elements: Vector[FormValue]) extends FormValue final case class ValueStr(value: String) extends FormValue -final case class ValueNum(value: Int) extends FormValue +final case class ValueNum(value: BigDecimal) extends FormValue final case class ValueDate(value: ZonedDateTime) extends FormValue final case class ValueBool(value: Boolean) extends FormValue case object ValueNull extends FormValue +object FormValue { + def empty: FormValue = ValueNull +} + final case class Error(path: Path, message: String) { override def toString: String = s"$path: $message" } diff --git a/core/src/main/scala/pear/form/Validation.scala b/core/src/main/scala/pear/form/Validation.scala deleted file mode 100644 index 7a68bdf..0000000 --- a/core/src/main/scala/pear/form/Validation.scala +++ /dev/null @@ -1,52 +0,0 @@ -package pear -package form - -import scala.language.higherKinds - -import scalaz.{Functor, Traverse, Applicative, Show, Cord} -import matryoshka.Delay - -object Validation { - - sealed trait FormF[A] extends Product with Serializable - - final case class Optional[A](inner: A) extends FormF[A] - final case class Fields[A](fields: Vector[(String, A)]) extends FormF[A] - final case class Sequence[A](element: Vector[A]) extends FormF[A] - final case class Value[A](constraint: Constraint) extends FormF[A] - final case class Selection[A](inner: A) extends FormF[A] - final case class Erroneous[A]() extends FormF[A] - - implicit def formFTraverse[L]: Traverse[FormF[?]] = new Traverse[FormF[?]] { - import scalaz.std.vector.vectorInstance - import scalaz.syntax.traverse._ - - def traverseImpl[G[_], A, B](fa: FormF[A])(f: A => G[B])(implicit G: Applicative[G]): G[FormF[B]] = fa match { - case Erroneous() => Applicative[G].point(Erroneous()) - case Optional(i) => Functor[G].map(f(i))(Optional.apply) - case Selection(i) => Functor[G].map(f(i))(Selection.apply) - case Fields(fs) => - val (names, values) = fs.unzip - Functor[G].map(values traverse f)(vs => Fields(names zip vs)) - case Sequence(elements) => - Functor[G].map(elements traverse f)(Sequence.apply) - case Value(c) => Applicative[G].point(Value[B](c)) - } - } - - implicit def formShow[L](implicit showC: Show[Constraint]) = new Delay[Show, FormF] { - def apply[A](showA: Show[A]): Show[FormF[A]] = new Show[FormF[A]] { - override def show(f: FormF[A]): Cord = f match { - case Erroneous() => Cord("!!ERROR!!") - case Optional(i) => Cord("optional<") ++ showA.show(i) ++ Cord(">") - case Selection(i) => showA.show(i) - case Fields(fields) => - Cord("{ ") ++ Cord - .mkCord(Cord(", "), fields.map { case (n, v) => Cord(s"$n : ") ++ showA.show(v) }: _*) ++ Cord(" }") - case Sequence(elems) => Cord("[") ++ Cord.mkCord(Cord(", "), elems.map(showA.show): _*) ++ Cord("*") - case Value(c) => showC.show(c) - } - } - } - -} diff --git a/core/src/main/scala/pear/form/package.scala b/core/src/main/scala/pear/form/package.scala index f983aaa..e29b7a8 100644 --- a/core/src/main/scala/pear/form/package.scala +++ b/core/src/main/scala/pear/form/package.scala @@ -2,16 +2,17 @@ package pear import scala.language.higherKinds -import matryoshka.{AlgebraM, BirecursiveT, RecursiveT, CoalgebraM} +import matryoshka._ import matryoshka.implicits._ import matryoshka.patterns.EnvT -import scalaz.{\/, IList, Kleisli, NonEmptyList, StateT, State, Id} -import scalaz.syntax.either._ +import scalaz._ +import Scalaz._ import java.net.URLDecoder package form { case class Path(elements: Vector[String]) extends AnyVal { + def ++(other: Path) = Path(elements ++ other.elements) def /(elem: String) = Path(elements :+ elem) def isChildOf(other: Path) = (this != other) && elements.startsWith(other.elements) def relativeTo(other: Path) = Path(elements.drop(other.elements.size)) @@ -25,23 +26,28 @@ package form { final case class DecodedForm(map: Map[Path, String]) extends AnyVal { def valueAt(path: Path): Option[String] = map.get(path) - def listSize(path: Path): Int = { + def getList(path: Path): Vector[Path] = { map.keys .filter(_.isChildOf(path)) .map(_.relativeTo(path)) - .map(_.elements.head) - .filter(_.forall(_.isDigit)) - .map(_.toInt) - .max + .filter(_.elements.head.forall(_.isDigit)) + .map(path / _.elements.head) + .toVector } } + + final case class EvaluationContext[T[_[_]]](form: DecodedForm, + path: Path, + result: T[EnvT[NonEmptyList[Error] \/ FormValue, Definition.FormF, ?]]) + + object EvaluationContext { + def init[T[_[_]]](input: DecodedForm)(implicit T: CorecursiveT[T]): EvaluationContext[T] = + EvaluationContext(input, Path.empty, T.embedT[form.Validated](EnvT((\/-(ValueNull), Definition.Empty())))) + } + } package object form { - - type Decorated[A] = EnvT[Path, Validation.FormF, A] - type Seed[T[_[_]]] = (Path, T[Definition.FormF]) - type Errors[A] = State[List[List[Error]], A] - type Constraint = Kleisli[\/[String, ?], String, FormValue] + import Definition._ implicit class ParsingOps(input: String) { def parseFormUrlEncoded: DecodedForm = @@ -57,132 +63,219 @@ package object form { ) } - implicit class FormOps[T[_[_]]](f: T[Definition.FormF])(implicit T: BirecursiveT[T]) { - def validate(value: String): NonEmptyList[List[Error]] \/ FormValue = { - val (errors, result) = - validationTraversal((Path.empty, f), value.parseFormUrlEncoded).run(Nil) - if (errors.isEmpty) result.right - else NonEmptyList.nel(errors.head, IList.fromList(errors.tail)).left + implicit class FormOps[T[_[_]]](f: T[FormF])(implicit T: BirecursiveT[T]) { + def validate(value: String): T[Validated] = { + val input = value.parseFormUrlEncoded + val validator = + validationTraversal(f) + validator(EvaluationContext.init(input)).result } } - /** - * Starting from a Seed[T], eg. a pair (Path, T[Definition.FormF]), traverse the - * definition top-down to produce a T[Validation.FormF] with each node decorated - * with the path of the corresponding value. - * - * During that process, every Choice is determined according to the input. - * - * We abstract over T which can be any fix-point type, provided there is a RecursiveT - * instance for it (that provides us with the projectT method that extract the F - * from a T[F]). - */ - def decorate[T[_[_]]](input: DecodedForm)(implicit T: RecursiveT[T]): CoalgebraM[Errors, Decorated, Seed[T]] = { - case a @ (v, t) => - (v, T.projectT[Definition.FormF](t)) match { - case (path, Definition.Fields(fs)) => - // Decorate each field with a path built by concatenating this Fields' path - // and the field's name - val subs: Vector[(String, Seed[T])] = fs.map { - case (key, subForm) => - key -> (path / key -> subForm) - } - StateT.stateT(EnvT((v, Validation.Fields(subs)))) - case (path, Definition.Value(c)) => - // Nothing much to do, just translate to Validation.Value - StateT.stateT(EnvT((path, Validation.Value(c)))) - case (path, Definition.Optional(x)) => - // Optional is just a "marker" layer, so we just push the same path - // down to the inner form - StateT.stateT(EnvT((path, Validation.Optional(path -> x)))) - case (path, Definition.Sequence(elem)) => - // Here we need to expand a Definition.Sequence that only contains the - // *schema* of an element to a Validation.Sequence that contains the - // actual *instances* of this schema. - // So we basically end up copying [[elem]] as many times as there are elements - // for this sequence in the input. - // - // NOTE: the astute reader would have noticed that we only compute the size - // of the input list, without verifying that there is an element for every - // index. That is OK because if there is no value for a given index, this will - // be caught by the [[evaluate]] algebra. - StateT.stateT( - EnvT( - (path, Validation.Sequence((0 to input.listSize(path)).map(i => (path / i.toString) -> elem).toVector)))) - case (path, Definition.Choice(alt)) => - // This is somehow the dual of the Sequence case. Definition.Choice contains - // several alternatives, but we need to choose only one to construct a - // Validation.Selection - (for { - selection <- input.valueAt(path) - definition <- alt.toMap.get(selection) - } yield - StateT.stateT[Id.Id, List[List[Error]], Decorated[Seed[T]]]( // type inference sucks! - EnvT(path -> Validation.Selection(Path(selection) -> definition)))) - .getOrElse( - State(errors => (List(Error(path, "invalid choice")) :: errors, EnvT((path, Validation.Erroneous())))) - ) + type Validated[A] = EnvT[\/[NonEmptyList[Error], FormValue], FormF, A] - } - } + type Validator[T[_[_]]] = EvaluationContext[T] => EvaluationContext[T] + + import java.time.ZonedDateTime + import java.time.format.DateTimeFormatter - /** - * Starting from a T[Decorated], wich is basically a T[Validation.FormF] where each - * layer has a Path attached to it, construct a FormValue by tearing it down bottom up. - * Along the way, accumulates errors in a State monad. - */ - def evaluate[T[_[_]]](input: DecodedForm): AlgebraM[Errors, Decorated, FormValue] = { env => - (env.ask, env.lower) match { - case (_, Validation.Fields(f)) => - // Nothing more to do than wrapping the f:Vector[(String, FormValue)] - // into a ValueObject - StateT.stateT(ValueObject(f.toMap)) - case (_, Validation.Sequence(l)) => - // Nothing more to do than wrapping the f:Vector[FormValue] - // into a ValueList - StateT.stateT(ValueList(l.toList)) - case (path, Validation.Value(c)) => - // Here comes the real validation. We did all the above fiddling with path - // just so we know here how to grab a value from the input. - input.valueAt(path) match { - case None => - State(errors => (List(Error(path, "missing mandatory value")) :: errors, ValueNull)) - case Some(s) => - c.run(s) - .fold( - err => State(errors => (List(Error(path, err)) :: errors, ValueNull)), - value => State(errors => (errors, value)) - ) + @inline def terminal[T[_[_]]](f: FormF[T[Validated]]): FormF[T[Validated]] = f + @inline def missingValue[T[_[_]]](path: Path, f: FormF[T[Validated]])(implicit T: CorecursiveT[T]): T[Validated] = + T.embedT[Validated](EnvT((-\/(NonEmptyList.nels(Error(path, "missing.value"))), terminal(f)))) + + def evaluate[T[_[_]]](T: BirecursiveT[T]): Algebra[FormF, Validator[T]] = { + case Empty() => { ctx => + ctx + } + case Optional(v) => { ctx => + val EvaluationContext(form, _, res) = v(ctx) + ctx.copy( + result = T.embedT[Validated]( + EnvT((T.projectT[Validated](res).ask.fold(_ => \/-(ValueNull), identity(_).right), Optional(res))))) + } + case Fields(fields) => { ctx => + val subs = fields.map { case (k, v) => k -> v(ctx.copy(path = ctx.path / k)).result } + val decorations = + subs + .map { case (k, e) => T.projectT[Validated](e).ask.map(f => Vector(k -> f)) } + .suml + .map(v => ValueObject(v.toMap)) + val tree: FormF[T[Validated]] = Fields(subs) + ctx.copy(result = T.embedT[Validated](EnvT((decorations, tree)))) + } + case Choice(alternatives) => { ctx => + ctx.form + .valueAt(ctx.path) + .flatMap { selection => + alternatives.toMap + .get(selection) + .map { validator => + validator(ctx.copy(path = Path(selection))) + } + } + .getOrElse { + val rebuiltChoices = alternatives.map { case (k, v) => k -> v(ctx).result } + ctx.copy(result = missingValue(ctx.path, Choice(rebuiltChoices))(T)) + } + } + case Sequence(elems) => { ctx => + val paths = ctx.form.getList(ctx.path) + val subEnvs = + paths.map(path => T.projectT[Validated](elems.head(ctx.copy(path = path)).result)) + val elements = subEnvs.map(_.ask.map(fv => Vector(fv))).suml.map(elems => ValueList(elems)) + EvaluationContext(ctx.form, + ctx.path, + T.embedT[Validated](EnvT((elements, Sequence(subEnvs.map(T.embedT[Validated])))))) + } + case Number() => { ctx => + val result = ctx.form + .valueAt(ctx.path) + .map { (str: String) => + val result = \/.fromTryCatchNonFatal(ValueNum(BigDecimal(str))).leftMap(_ => + NonEmptyList.nels(Error(ctx.path, "malformed.number"))) + T.embedT[Validated](EnvT((result, terminal(Number())))) } - case (_, Validation.Optional(ValueNull)) => - // Something obviously went wrong at the lower layer - // but since Optional means that we tolerate null values - // we just have to remove that error from the stack - State(errors => (errors.drop(1), ValueNull)) - case (path, Validation.Optional(x)) => - // Here we don't know yet if anything went wrong one layer bellow - State { errors => - // So let's try and remove errors that happened one level bellow - val prunedErrors = errors.filterNot(_.forall(_.path.isChildOf(path))) - // If there was any, just remove them and return ValueNull - if (prunedErrors.size < errors.size) (prunedErrors, ValueNull) - // Otherwise, everything was fine in the first place! - else (errors, x) + .getOrElse { + missingValue(ctx.path, Number())(T) } - case (_, Validation.Erroneous()) => - // An Erroneous node was emmited with an error during the - // top-down phase, nothing more to do here - StateT.stateT(ValueNull) - case (_, Validation.Selection(r)) => - // Selection is only there so that the structure of Validation.FormF matches the - // one of Definition.FormF enough so that we don't need to look at multiple - // layers of Definition during de top-down phase, so here we just need to - // peel it off. - StateT.stateT(r) + ctx.copy(result = result) + } + case IsoDateTime() => { ctx => + val result = ctx.form + .valueAt(ctx.path) + .map { str => + val result = \/.fromTryCatchNonFatal(ValueDate(ZonedDateTime.parse(str, DateTimeFormatter.ISO_DATE_TIME))) + .leftMap(_ => NonEmptyList.nels(Error(ctx.path, "malformed.date"))) + T.embedT[Validated](EnvT((result, terminal(IsoDateTime())))) + } + .getOrElse { + missingValue(ctx.path, IsoDateTime())(T) + } + EvaluationContext(ctx.form, ctx.path, result) + } + case MinLength(min) => { ctx => + T.projectT[Validated](ctx.result).ask match { + case errs @ -\/(_) => + EvaluationContext(ctx.form, ctx.path, T.embedT[Validated](EnvT((errs, terminal(MinLength(min)))))) + case valid @ \/-(ValueStr(s)) if s.length >= min => + EvaluationContext(ctx.form, ctx.path, T.embedT[Validated](EnvT((valid, terminal(MinLength(min)))))) + case \/-(ValueStr(_)) => + EvaluationContext( + ctx.form, + ctx.path, + T.embedT[Validated]( + EnvT((NonEmptyList.nels(Error(ctx.path, "min.length.not.met")).left, terminal(MinLength(min)))))) + case \/-(x) => + EvaluationContext( + ctx.form, + ctx.path, + T.embedT[Validated]( + EnvT((NonEmptyList.nels(Error(ctx.path, "incoherent.definition")).left, terminal(MinLength(min)))))) + } + } + case MaxLength(max) => { ctx => + T.projectT[Validated](ctx.result).ask match { + case errs @ -\/(_) => + EvaluationContext(ctx.form, ctx.path, T.embedT[Validated](EnvT((errs, terminal(MaxLength(max)))))) + case valid @ \/-(ValueStr(s)) if s.length < max => + EvaluationContext(ctx.form, ctx.path, T.embedT[Validated](EnvT((valid, terminal(MaxLength(max)))))) + case \/-(ValueStr(_)) => + EvaluationContext( + ctx.form, + ctx.path, + T.embedT[Validated]( + EnvT((NonEmptyList.nels(Error(ctx.path, "max.length.exceeded")).left, terminal(MaxLength(max)))))) + case \/-(x) => + EvaluationContext( + ctx.form, + ctx.path, + T.embedT[Validated]( + EnvT((NonEmptyList.nels(Error(ctx.path, "incoherent.definition")).left, terminal(MaxLength(max)))))) + } + } + case Min(min) => { ctx => + T.projectT[Validated](ctx.result).ask match { + case errs @ -\/(_) => + EvaluationContext(ctx.form, ctx.path, T.embedT[Validated](EnvT((errs, terminal(Min(min)))))) + case valid @ \/-(ValueNum(s)) if s >= min => + EvaluationContext(ctx.form, ctx.path, T.embedT[Validated](EnvT((valid, terminal(Min(min)))))) + case \/-(ValueNum(_)) => + EvaluationContext(ctx.form, + ctx.path, + T.embedT[Validated]( + EnvT((NonEmptyList.nels(Error(ctx.path, "min.value.not.met")).left, terminal(Min(min)))))) + case \/-(x) => + EvaluationContext( + ctx.form, + ctx.path, + T.embedT[Validated]( + EnvT((NonEmptyList.nels(Error(ctx.path, "incoherent.definition")).left, terminal(Min(min)))))) + } + } + case Max(max) => { ctx => + T.projectT[Validated](ctx.result).ask match { + case errs @ -\/(_) => + EvaluationContext(ctx.form, ctx.path, T.embedT[Validated](EnvT((errs, terminal(Max(max)))))) + case valid @ \/-(ValueNum(s)) if s < max => + EvaluationContext(ctx.form, ctx.path, T.embedT[Validated](EnvT((valid, terminal(Max(max)))))) + case \/-(ValueNum(_)) => + EvaluationContext( + ctx.form, + ctx.path, + T.embedT[Validated]( + EnvT((NonEmptyList.nels(Error(ctx.path, "max.value.exceeded")).left, terminal(Max(max)))))) + case \/-(x) => + EvaluationContext( + ctx.form, + ctx.path, + T.embedT[Validated]( + EnvT((NonEmptyList.nels(Error(ctx.path, "incoherent.definition")).left, terminal(Max(max)))))) + } + } + case After(start) => { ctx => + T.projectT[Validated](ctx.result).ask match { + case errs @ -\/(_) => + EvaluationContext(ctx.form, ctx.path, T.embedT[Validated](EnvT((errs, terminal(After(start)))))) + case valid @ \/-(ValueDate(s)) if s.isAfter(start) => + EvaluationContext(ctx.form, ctx.path, T.embedT[Validated](EnvT((valid, terminal(After(start)))))) + case \/-(ValueDate(_)) => + EvaluationContext( + ctx.form, + ctx.path, + T.embedT[Validated](EnvT((NonEmptyList.nels(Error(ctx.path, "too.soon")).left, terminal(After(start)))))) + case \/-(x) => + EvaluationContext( + ctx.form, + ctx.path, + T.embedT[Validated]( + EnvT((NonEmptyList.nels(Error(ctx.path, "incoherent.definition")).left, terminal(After(start)))))) + } + } + case Before(end) => { ctx => + T.projectT[Validated](ctx.result).ask match { + case errs @ -\/(_) => + EvaluationContext(ctx.form, ctx.path, T.embedT[Validated](EnvT((errs, terminal(Before(end)))))) + case valid @ \/-(ValueDate(s)) if s.isBefore(end) => + EvaluationContext(ctx.form, ctx.path, T.embedT[Validated](EnvT((valid, terminal(Before(end)))))) + case \/-(ValueDate(_)) => + EvaluationContext( + ctx.form, + ctx.path, + T.embedT[Validated](EnvT((NonEmptyList.nels(Error(ctx.path, "too.late")).left, terminal(Before(end)))))) + case \/-(x) => + EvaluationContext( + ctx.form, + ctx.path, + T.embedT[Validated]( + EnvT((NonEmptyList.nels(Error(ctx.path, "incoherent.definition")).left, terminal(Before(end)))))) + } + } + case AndThen(lhs, rhs) => { ctx => + rhs(lhs(ctx)) } } - def validationTraversal[T[_[_]]](seed: Seed[T], input: DecodedForm)(implicit T: BirecursiveT[T]): Errors[FormValue] = - seed.hyloM[Errors, Decorated, FormValue](evaluate[T](input), decorate[T](input)) + def validationTraversal[T[_[_]]](form: T[FormF])(implicit T: BirecursiveT[T]): Validator[T] = + form.cata(evaluate(T)) } diff --git a/core/src/test/scala/pear/form/ValidationSpec.scala b/core/src/test/scala/pear/form/ValidationSpec.scala index 4b074c6..d3dc57c 100644 --- a/core/src/test/scala/pear/form/ValidationSpec.scala +++ b/core/src/test/scala/pear/form/ValidationSpec.scala @@ -3,7 +3,7 @@ package form import org.scalatest.{EitherValues, WordSpec, Matchers} import scalaz.\/- -import scalaz.syntax.either._ +import scalaz.syntax.compose._ class ValidationSpec extends WordSpec with Matchers with EitherValues { @@ -14,18 +14,18 @@ class ValidationSpec extends WordSpec with Matchers with EitherValues { "singleInt" should { "accept 'foo=42'" in { - val result = singleInt.validate("foo=42") - result.toEither should be('right) - result should be(\/-(ValueObject(Map("foo" -> ValueNum(42))))) + val result = singleInt.validate("foo=42").unFix + result.ask.toEither should be('right) + result.ask should be(\/-(ValueObject(Map("foo" -> ValueNum(42))))) } "reject 'foo=bar'" in { - singleInt.validate("foo=bar").toEither should be('left) + singleInt.validate("foo=bar").unFix.ask.toEither should be('left) } "reject 'bar=42'" in { - singleInt.validate("bar=42").toEither should be('left) + singleInt.validate("bar=42").unFix.ask.toEither should be('left) } } @@ -33,7 +33,7 @@ class ValidationSpec extends WordSpec with Matchers with EitherValues { "optionalSingleInt" should { "accept empty form" in { - optionalSingleInt.validate("") should be(\/-(ValueNull)) + optionalSingleInt.validate("").unFix.ask should be(\/-(ValueNull)) } } @@ -41,15 +41,12 @@ class ValidationSpec extends WordSpec with Matchers with EitherValues { "singleOptionalInt" should { "be tolerant with invalid values" in { - singleOptionalInt.validate("foo=bar") should be(\/-(ValueObject(Map("foo" -> ValueNull)))) + singleOptionalInt.validate("foo=bar").unFix.ask should be(\/-(ValueObject(Map("foo" -> ValueNull)))) } } val form: Fix[Definition.FormF] = mapping( - "foo" -> optional(int >==> { i => - val ValueNum(n) = i - if (n >= 0) i.right else "must be positive".left - }), + "foo" -> optional(andThen(int, Fix(Min(0)))), "bar" -> mapping( "qux" -> int, "baz" -> optional(int) @@ -58,7 +55,7 @@ class ValidationSpec extends WordSpec with Matchers with EitherValues { "form" should { "accept 'foo=42&bar.qux=12'" in { - val result = form.validate("foo=42&bar.qux=12") + val result = form.validate("foo=42&bar.qux=12").unFix.ask result should be( \/-( ValueObject( @@ -75,12 +72,15 @@ class ValidationSpec extends WordSpec with Matchers with EitherValues { "list" should { "accept 'things.0.foo=42&things.0.bar.qux=12&things.1.foo=83&things.1.bar.baz=1&things.1.bar.qux=0'" in { val result = - list.validate("things.0.foo=42&things.0.bar.qux=12&things.1.foo=83&things.1.bar.baz=1&things.1.bar.qux=0") + list + .validate("things.0.foo=42&things.0.bar.qux=12&things.1.foo=83&things.1.bar.baz=1&things.1.bar.qux=0") + .unFix + .ask result should be( \/-( ValueObject( Map( - "things" -> ValueList(List( + "things" -> ValueList(Vector( ValueObject(Map("foo" -> ValueNum(42), "bar" -> ValueObject(Map( "qux" -> ValueNum(12), @@ -103,7 +103,7 @@ class ValidationSpec extends WordSpec with Matchers with EitherValues { "alt" should { "accept 'choose=form&form.foo=42&form.bar.qux=12'" in { - val result = alt.validate("choose=form&form.foo=42&form.bar.qux=12") + val result = alt.validate("choose=form&form.foo=42&form.bar.qux=12").unFix.ask result should be( \/-( ValueObject( @@ -119,7 +119,7 @@ class ValidationSpec extends WordSpec with Matchers with EitherValues { } "accept 'choose=otherwise&otherwise=24'" in { - val result = alt.validate("choose=otherwise&otherwise=24") + val result = alt.validate("choose=otherwise&otherwise=24").unFix.ask result should be( \/-( ValueObject(