Add coursier.util.ValidationNel

This commit is contained in:
Alexandre Archambault 2018-02-27 22:54:48 +01:00
parent dc87950dc4
commit a83df3e1c8
6 changed files with 145 additions and 87 deletions

View File

@ -4,19 +4,16 @@ import java.net.MalformedURLException
import coursier.core.Authentication import coursier.core.Authentication
import coursier.ivy.IvyRepository import coursier.ivy.IvyRepository
import coursier.util.Parse import coursier.util.{Parse, ValidationNel}
import coursier.util.Traverse.TraverseOps
import scalaz.{Validation, ValidationNel}
import scalaz.Scalaz.vectorInstance
import scalaz.Scalaz.{ToEitherOpsFromEither, ToNelOps, ToTraverseOps, ToValidationOps}
object CacheParse { object CacheParse {
def repository(s: String): Validation[String, Repository] = def repository(s: String): Either[String, Repository] =
if (s == "ivy2local" || s == "ivy2Local") if (s == "ivy2local" || s == "ivy2Local")
Cache.ivy2Local.success Right(Cache.ivy2Local)
else if (s == "ivy2cache" || s == "ivy2Cache") else if (s == "ivy2cache" || s == "ivy2Cache")
Cache.ivy2Cache.success Right(Cache.ivy2Cache)
else { else {
val repo = Parse.repository(s) val repo = Parse.repository(s)
@ -78,34 +75,38 @@ object CacheParse {
Left(s"No password found in user info of URL $url") Left(s"No password found in user info of URL $url")
} }
} }
}.validation }
} }
def repositories(l: Seq[String]): ValidationNel[String, Seq[Repository]] = def repositories(l: Seq[String]): ValidationNel[String, Seq[Repository]] =
l.toVector.traverseU { s => l.toVector.validationNelTraverse { s =>
repository(s).leftMap(_.wrapNel) ValidationNel.fromEither(repository(s))
} }
def cachePolicies(s: String): ValidationNel[String, Seq[CachePolicy]] = def cachePolicies(s: String): ValidationNel[String, Seq[CachePolicy]] =
s.split(',').toVector.traverseM[({ type L[X] = ValidationNel[String, X] })#L, CachePolicy] { s
case "offline" => .split(',')
Vector(CachePolicy.LocalOnly).successNel .toVector
case "update-local-changing" => .validationNelTraverse[String, Seq[CachePolicy]] {
Vector(CachePolicy.LocalUpdateChanging).successNel case "offline" =>
case "update-local" => ValidationNel.success(Seq(CachePolicy.LocalOnly))
Vector(CachePolicy.LocalUpdate).successNel case "update-local-changing" =>
case "update-changing" => ValidationNel.success(Seq(CachePolicy.LocalUpdateChanging))
Vector(CachePolicy.UpdateChanging).successNel case "update-local" =>
case "update" => ValidationNel.success(Seq(CachePolicy.LocalUpdate))
Vector(CachePolicy.Update).successNel case "update-changing" =>
case "missing" => ValidationNel.success(Seq(CachePolicy.UpdateChanging))
Vector(CachePolicy.FetchMissing).successNel case "update" =>
case "force" => ValidationNel.success(Seq(CachePolicy.Update))
Vector(CachePolicy.ForceDownload).successNel case "missing" =>
case "default" => ValidationNel.success(Seq(CachePolicy.FetchMissing))
Vector(CachePolicy.LocalOnly, CachePolicy.FetchMissing).successNel case "force" =>
case other => ValidationNel.success(Seq(CachePolicy.ForceDownload))
s"Unrecognized mode: $other".failureNel case "default" =>
} ValidationNel.success(Seq(CachePolicy.LocalOnly, CachePolicy.FetchMissing))
case other =>
ValidationNel.failure(s"Unrecognized mode: $other")
}
.map(_.flatten)
} }

View File

@ -1,7 +1,5 @@
package coursier package coursier
import scalaz.{Failure, Success}
sealed abstract class CachePolicy extends Product with Serializable sealed abstract class CachePolicy extends Product with Serializable
object CachePolicy { object CachePolicy {
@ -81,15 +79,15 @@ object CachePolicy {
def fromOption(value: Option[String], description: String): Option[Seq[CachePolicy]] = def fromOption(value: Option[String], description: String): Option[Seq[CachePolicy]] =
value.filter(_.nonEmpty).flatMap { value.filter(_.nonEmpty).flatMap {
str => str =>
CacheParse.cachePolicies(str) match { CacheParse.cachePolicies(str).either match {
case Success(Seq()) => case Right(Seq()) =>
Console.err.println( Console.err.println(
s"Warning: no mode found in $description, ignoring it." s"Warning: no mode found in $description, ignoring it."
) )
None None
case Success(policies) => case Right(policies) =>
Some(policies) Some(policies)
case Failure(errors) => case Left(_) =>
Console.err.println( Console.err.println(
s"Warning: unrecognized mode in $description, ignoring it." s"Warning: unrecognized mode in $description, ignoring it."
) )

View File

@ -17,7 +17,7 @@ import coursier.util.{Parse, Print}
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.concurrent.duration.Duration import scala.concurrent.duration.Duration
import scalaz.concurrent.{Strategy, Task} import scalaz.concurrent.{Strategy, Task}
import scalaz.{Failure, Nondeterminism, Success} import scalaz.Nondeterminism
object Helper { object Helper {
@ -81,11 +81,11 @@ class Helper(
if (common.mode.isEmpty) if (common.mode.isEmpty)
CachePolicy.default CachePolicy.default
else else
CacheParse.cachePolicies(common.mode) match { CacheParse.cachePolicies(common.mode).either match {
case Success(cp) => cp case Right(cp) => cp
case Failure(errors) => case Left(errors) =>
prematureExit( prematureExit(
s"Error parsing modes:\n${errors.list.toList.map(" "+_).mkString("\n")}" s"Error parsing modes:\n${errors.map(" "+_).mkString("\n")}"
) )
} }
@ -116,12 +116,12 @@ class Helper(
repos repos
} }
val standardRepositories = repositoriesValidation match { val standardRepositories = repositoriesValidation.either match {
case Success(repos) => case Right(repos) =>
repos repos
case Failure(errors) => case Left(errors) =>
prematureExit( prematureExit(
s"Error with repositories:\n${errors.list.toList.map(" "+_).mkString("\n")}" s"Error with repositories:\n${errors.map(" "+_).mkString("\n")}"
) )
} }

View File

@ -1,12 +1,11 @@
package coursier.ivy package coursier.ivy
import scala.language.implicitConversions import coursier.util.Traverse.TraverseOps
import coursier.util.ValidationNel
import scalaz.{Failure, Success, ValidationNel}
import scalaz.Scalaz.{ToEitherOpsFromEither, ToFoldableOps, ToTraverseOps, ToValidationOps, vectorInstance}
import fastparse.all._ import fastparse.all._
import scala.language.implicitConversions
final case class PropertiesPattern(chunks: Seq[PropertiesPattern.ChunkOrProperty]) { final case class PropertiesPattern(chunks: Seq[PropertiesPattern.ChunkOrProperty]) {
def string: String = chunks.map(_.string).mkString def string: String = chunks.map(_.string).mkString
@ -15,43 +14,43 @@ final case class PropertiesPattern(chunks: Seq[PropertiesPattern.ChunkOrProperty
def substituteProperties(properties: Map[String, String]): Either[String, Pattern] = { def substituteProperties(properties: Map[String, String]): Either[String, Pattern] = {
val validation = chunks.toVector.traverseM[({ type L[X] = ValidationNel[String, X] })#L, Pattern.Chunk] { val validation = chunks.validationNelTraverse[String, Seq[Pattern.Chunk]] {
case ChunkOrProperty.Prop(name, alternativesOpt) => case ChunkOrProperty.Prop(name, alternativesOpt) =>
properties.get(name) match { properties.get(name) match {
case Some(value) => case Some(value) =>
Vector(Pattern.Chunk.Const(value)).successNel ValidationNel.success(Seq(Pattern.Chunk.Const(value)))
case None => case None =>
alternativesOpt match { alternativesOpt match {
case Some(alt) => case Some(alt) =>
PropertiesPattern(alt) ValidationNel.fromEither(
.substituteProperties(properties) PropertiesPattern(alt)
.right .substituteProperties(properties)
.map(_.chunks.toVector) .right
.validation .map(_.chunks.toVector)
.toValidationNel )
case None => case None =>
name.failureNel ValidationNel.failure(name)
} }
} }
case ChunkOrProperty.Opt(l @ _*) => case ChunkOrProperty.Opt(l @ _*) =>
PropertiesPattern(l) ValidationNel.fromEither(
.substituteProperties(properties) PropertiesPattern(l)
.right .substituteProperties(properties)
.map(l => Vector(Pattern.Chunk.Opt(l.chunks: _*))) .right
.validation .map(l => Seq(Pattern.Chunk.Opt(l.chunks: _*)))
.toValidationNel )
case ChunkOrProperty.Var(name) => case ChunkOrProperty.Var(name) =>
Vector(Pattern.Chunk.Var(name)).successNel ValidationNel.success(Seq(Pattern.Chunk.Var(name)))
case ChunkOrProperty.Const(value) => case ChunkOrProperty.Const(value) =>
Vector(Pattern.Chunk.Const(value)).successNel ValidationNel.success(Seq(Pattern.Chunk.Const(value)))
}.map(Pattern(_)) }.map(c => Pattern(c.flatten))
validation.toEither.left.map { notFoundProps => validation.either.left.map { notFoundProps =>
s"Property(ies) not found: ${notFoundProps.toList.mkString(", ")}" s"Property(ies) not found: ${notFoundProps.mkString(", ")}"
} }
} }
} }
@ -68,30 +67,30 @@ final case class Pattern(chunks: Seq[Pattern.Chunk]) {
def substituteVariables(variables: Map[String, String]): Either[String, String] = { def substituteVariables(variables: Map[String, String]): Either[String, String] = {
def helper(chunks: Seq[Chunk]): ValidationNel[String, Seq[Chunk.Const]] = def helper(chunks: Seq[Chunk]): ValidationNel[String, Seq[Chunk.Const]] =
chunks.toVector.traverseU[ValidationNel[String, Seq[Chunk.Const]]] { chunks.validationNelTraverse[String, Seq[Chunk.Const]] {
case Chunk.Var(name) => case Chunk.Var(name) =>
variables.get(name) match { variables.get(name) match {
case Some(value) => case Some(value) =>
Seq(Chunk.Const(value)).successNel ValidationNel.success(Seq(Chunk.Const(value)))
case None => case None =>
name.failureNel ValidationNel.failure(name)
} }
case Chunk.Opt(l @ _*) => case Chunk.Opt(l @ _*) =>
val res = helper(l) val res = helper(l)
if (res.isSuccess) if (res.isSuccess)
res res
else else
Seq().successNel ValidationNel.success(Seq())
case c: Chunk.Const => case c: Chunk.Const =>
Seq(c).successNel ValidationNel.success(Seq(c))
}.map(_.flatten) }.map(_.flatten)
val validation = helper(chunks) val validation = helper(chunks)
validation match { validation.either match {
case Failure(notFoundVariables) => case Left(notFoundVariables) =>
Left(s"Variables not found: ${notFoundVariables.toList.mkString(", ")}") Left(s"Variables not found: ${notFoundVariables.mkString(", ")}")
case Success(constants) => case Right(constants) =>
val b = new StringBuilder val b = new StringBuilder
constants.foreach(b ++= _.value) constants.foreach(b ++= _.value)
Right(b.result()) Right(b.result())

View File

@ -5,16 +5,49 @@ import scala.collection.mutable.ListBuffer
object Traverse { object Traverse {
implicit class TraverseOps[T](val seq: Seq[T]) { implicit class TraverseOps[T](val seq: Seq[T]) {
def eitherTraverse[L, R](f: T => Either[L, R]): Either[L, Seq[R]] = def eitherTraverse[L, R](f: T => Either[L, R]): Either[L, Seq[R]] =
// Warning: iterates on the whole sequence no matter what, even if the first element is a Left // Warning: iterates on the whole sequence no matter what, even if the first element is a Left
seq.foldLeft[Either[L, ListBuffer[R]]](Right(new ListBuffer)) { seq
case (l @ Left(_), _) => l .foldLeft[Either[L, ListBuffer[R]]](Right(new ListBuffer)) {
case (Right(b), elem) => case (l @ Left(_), _) => l
f(elem) match { case (Right(b), elem) =>
case Left(l) => Left(l) f(elem) match {
case Right(r) => Right(b += r) case Left(l) => Left(l)
case Right(r) => Right(b += r)
}
}
.right
.map(_.result())
def validationNelTraverse[L, R](f: T => ValidationNel[L, R]): ValidationNel[L, Seq[R]] = {
val e = seq
.foldLeft[Either[ListBuffer[L], ListBuffer[R]]](Right(new ListBuffer)) {
case (l @ Left(b), elem) =>
f(elem).either match {
case Left(l0) => Left(b ++= l0)
case Right(_) => l
}
case (Right(b), elem) =>
f(elem).either match {
case Left(l) => Left(new ListBuffer[L] ++= l)
case Right(r) => Right(b += r)
}
}
.left
.map { b =>
b.result() match {
case Nil => sys.error("Can't happen")
case h :: t => ::(h, t)
} }
} }
.right
.map(_.result())
ValidationNel(e)
}
} }
} }

View File

@ -0,0 +1,27 @@
package coursier.util
// not covariant because scala.:: isn't (and is there a point in being covariant in R but not L?)
final case class ValidationNel[L, R](either: Either[::[L], R]) {
def isSuccess: Boolean =
either.isRight
def map[S](f: R => S): ValidationNel[L, S] =
ValidationNel(either.right.map(f))
}
object ValidationNel {
def fromEither[L, R](either: Either[L, R]): ValidationNel[L, R] =
ValidationNel(either.left.map(l => ::(l, Nil)))
def success[L]: SuccessBuilder[L] =
new SuccessBuilder
def failure[R]: FailureBuilder[R] =
new FailureBuilder
final class SuccessBuilder[L] {
def apply[R](r: R): ValidationNel[L, R] =
ValidationNel(Right(r))
}
final class FailureBuilder[R] {
def apply[L](l: L): ValidationNel[L, R] =
ValidationNel(Left(::(l, Nil)))
}
}