All Downloads are FREE. Search and download functionalities are using the official Maven repository.

pipez.dsl.Parser.scala Maven / Gradle / Ivy

The newest version!
package pipez.dsl

import scala.collection.Factory
import scala.collection.immutable.ListMap

/** Conversion of types which allow:
  *   - failure with error messages
  *   - choosing between fail-fast and full attempt to gather all errors
  *   - aggregating errors under path to the field which failed
  */
trait Parser[From, To] {

  def parse(from: From, path: Parser.Path, failFast: Parser.ShouldFailFast): Parser.ParsingResult[To]

  final def parseFast(from: From): Parser.ParsingResult[To] = parse(from, Vector.empty, failFast = true)
  final def parseFull(from: From): Parser.ParsingResult[To] = parse(from, Vector.empty, failFast = false)
}
object Parser
    extends pipez.PipeAutoSupport[Parser]
    with pipez.PipeSemiautoConfiguredSupport[Parser]
    with ParserInstances0 {

  /** Utility to create `Converter` without SAM */
  def instance[From, To](f: From => Either[Vector[String], To]): Parser[From, To] =
    (from: From, path: Path, _: ShouldFailFast) => f(from).left.map(errors => ListMap(path -> errors))

  /** Element of a `Path` to the failed field */
  sealed trait PathSegment extends Product with Serializable
  object PathSegment {

    final case class AtField(name: String) extends PathSegment
    final case class MatchType(name: String) extends PathSegment
    final case class AtIndex(index: Int) extends PathSegment
    final case class AtKey(key: Any) extends PathSegment
    final case class WithKey(key: Any) extends PathSegment
  }

  type Path             = Vector[PathSegment]
  type ShouldFailFast   = Boolean
  type Errors           = ListMap[Path, Vector[String]]
  type ParsingResult[A] = Either[Errors, A]

  implicit val pipeDerivation: pipez.PipeDerivation[Parser] = ParserDerivationDefinition
}

private[dsl] trait ParserInstances0 extends ParserInstances1 {

  implicit def parseEither[FromLeft, FromRight, ToLeft, ToRight, FromEither[L, R] <: Either[L, R]](implicit
    left:  Parser[FromLeft, ToLeft],
    right: Parser[FromRight, ToRight]
  ): Parser[FromEither[FromLeft, FromRight], Either[ToLeft, ToRight]] =
    pipez.PipeDerivation
      .derive[Parser, Either[FromLeft, FromRight], Either[ToLeft, ToRight]]
      .asInstanceOf[Parser[FromEither[FromLeft, FromRight], Either[ToLeft, ToRight]]]

  implicit def parseOption[From, To, FromOption[A] <: Option[A]](implicit
    parser: Parser[From, To]
  ): Parser[FromOption[From], Option[To]] =
    pipez.PipeDerivation.derive[Parser, Option[From], Option[To]].asInstanceOf[Parser[FromOption[From], Option[To]]]

  implicit def parseCollection[From, To, FromColl[A] <: Iterable[A], ToColl[_]](implicit
    parser: Parser[From, To],
    ToColl: Factory[To, ToColl[To]]
  ): Parser[FromColl[From], ToColl[To]] = (fromColl, path, failFast) =>
    if (failFast) {
      val toBuilder = ToColl.newBuilder
      var errorsStore: Parser.Errors = null.asInstanceOf[Parser.Errors]
      val iterator = fromColl.iterator.zipWithIndex
      var noError  = true
      while (iterator.hasNext && noError) {
        val (from, index) = iterator.next()
        parser.parse(from, path :+ Parser.PathSegment.AtIndex(index), failFast) match {
          case Left(errors) =>
            errorsStore = errors
            noError = false
          case Right(value) =>
            toBuilder.addOne(value)
        }
      }
      if (noError) Right(toBuilder.result()) else Left(errorsStore)
    } else {
      val toBuilder     = ToColl.newBuilder
      val errorsBuilder = ListMap.newBuilder[Parser.Path, Vector[String]]
      val iterator      = fromColl.iterator.zipWithIndex
      var noError       = true
      while (iterator.hasNext) {
        val (from, index) = iterator.next()
        parser.parse(from, path :+ Parser.PathSegment.AtIndex(index), failFast) match {
          case Left(errors) =>
            errorsBuilder.addAll(errors)
            noError = false
          case Right(value) =>
            if (noError) {
              toBuilder.addOne(value)
            }
        }
      }
      if (noError) Right(toBuilder.result()) else Left(errorsBuilder.result())
    }

  implicit def parseMap[FromKey, FromValue, ToKey, ToValue, FromMap[K, V] <: Map[K, V], ToMap[K, V] <: Map[K, V]](
    implicit
    key:   Parser[FromKey, ToKey],
    value: Parser[FromValue, ToValue],
    ToMap: Factory[(ToKey, ToValue), ToMap[ToKey, ToValue]]
  ): Parser[FromMap[FromKey, FromValue], ToMap[ToKey, ToValue]] = (fromMap, path, failFast) =>
    if (failFast) {
      val toBuilder = ToMap.newBuilder
      var errorsStore: Parser.Errors = null.asInstanceOf[Parser.Errors]
      val iterator = fromMap.iterator
      var noError  = true
      while (iterator.hasNext && noError) {
        val (k, v) = iterator.next()
        (for {
          k <- key.parse(k, path :+ Parser.PathSegment.WithKey(k), failFast)
          v <- value.parse(v, path :+ Parser.PathSegment.AtKey(k), failFast)
        } yield (k, v)) match {
          case Right(pair) =>
            toBuilder.addOne(pair)
          case Left(e) =>
            errorsStore = e
            noError = false
        }
      }
      if (noError) Right(toBuilder.result()) else Left(errorsStore)
    } else {
      val toBuilder     = ToMap.newBuilder
      val errorsBuilder = ListMap.newBuilder[Parser.Path, Vector[String]]
      val iterator      = fromMap.iterator
      var noError       = true
      while (iterator.hasNext) {
        val (k, v) = iterator.next()
        (key.parse(k, path :+ Parser.PathSegment.WithKey(k), failFast),
         value.parse(v, path :+ Parser.PathSegment.AtKey(k), failFast)
        ) match {
          case (Right(k2), Right(v2)) =>
            toBuilder.addOne(k2, v2)
          case (Left(e1), Left(e2)) =>
            errorsBuilder.addAll(e1).addAll(e2)
            noError = false
          case (Left(e), _) =>
            errorsBuilder.addAll(e)
            noError = false
          case (_, Left(e)) =>
            errorsBuilder.addAll(e)
            noError = false
        }
      }
      if (noError) Right(toBuilder.result()) else Left(errorsBuilder.result())
    }
}
private[dsl] trait ParserInstances1 extends ParserInstances2 {

  implicit def parseToOption[From, To](implicit parser: Parser[From, To]): Parser[From, Option[To]] =
    (from, path, failFast) => parser.parse(from, path, failFast).map(Some(_))
}
private[dsl] trait ParserInstances2 {

  implicit def parseFromOption[From, To](implicit parser: Parser[From, To]): Parser[Option[From], To] =
    (from, path, failFast) =>
      Parser.parseOption[From, To, Option].parse(from, path, failFast) match {
        case Right(Some(value)) => Right(value)
        case Right(None)        => Left(ListMap(path -> Vector("Expected non-empty Option")))
        case Left(errors)       => Left(errors)
      }

  implicit def parseToSelf[A, B >: A]: Parser[A, B] = (a, _, _) => Right(a)
}
private[dsl] object ParserDerivationDefinition extends pipez.PipeDerivation[Parser] {
  import Parser.*

  override type Context   = (Path, ShouldFailFast)
  override type Result[A] = ParsingResult[A]

  override def lift[In, Out](f: (In, Context) => Result[Out]): Parser[In, Out] =
    (from: In, path: Path, failFast: ShouldFailFast) => f(from, (path, failFast))
  override def unlift[In, Out](pipe: Parser[In, Out], in: In, ctx: Context): Result[Out] =
    pipe.parse(in, ctx._1, ctx._2)
  override def updateContext(context: Context, path: => pipez.Path): Context = {
    @scala.annotation.tailrec
    def fromPath(in: pipez.Path, out: Path): Path = in match {
      case pipez.Path.Root                => out
      case pipez.Path.Field(from, name)   => fromPath(from, PathSegment.AtField(name) +: out)
      case pipez.Path.Subtype(from, name) => fromPath(from, PathSegment.MatchType(name) +: out)
    }

    val append = fromPath(path, Vector.empty)
    if (append.isEmpty) context else context.copy(context._1 ++ append)
  }
  override def pureResult[A](a: A): Result[A] =
    Right(a)
  override def mergeResults[A, B, C](context: Context, ra: Result[A], rb: => Result[B], f: (A, B) => C): Result[C] =
    if (context._2) {
      // should fail fast
      for {
        a <- ra
        b <- rb
      } yield f(a, b)
    } else {
      // should parse everything
      (ra, rb) match {
        case (Right(a), Right(b)) => Right(f(a, b))
        case (Left(e1), Left(e2)) => Left(e1 ++ e2)
        case (Left(e), _)         => Left(e)
        case (_, Left(e))         => Left(e)
      }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy