mirror of https://github.com/sbt/sbt.git
Better handling of Ivy patterns
This commit is contained in:
parent
bdbc8e6dd9
commit
1c34362b6f
26
build.sbt
26
build.sbt
|
|
@ -119,6 +119,7 @@ lazy val core = crossProject
|
|||
.settings(mimaDefaultSettings: _*)
|
||||
.settings(
|
||||
name := "coursier",
|
||||
libraryDependencies += "com.lihaoyi" %%% "fastparse" % "0.3.7",
|
||||
resourceGenerators.in(Compile) += {
|
||||
(target, version).map { (dir, ver) =>
|
||||
import sys.process._
|
||||
|
|
@ -152,6 +153,31 @@ lazy val core = crossProject
|
|||
ProblemFilters.exclude[MissingMethodProblem]("coursier.core.Project.apply"),
|
||||
ProblemFilters.exclude[MissingMethodProblem]("coursier.core.Project.copy"),
|
||||
ProblemFilters.exclude[MissingMethodProblem]("coursier.core.Project.this"),
|
||||
// Reworked Ivy pattern handling
|
||||
ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.pattern"),
|
||||
ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.copy"),
|
||||
ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.properties"),
|
||||
ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.parts"),
|
||||
ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.substitute"),
|
||||
ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.this"),
|
||||
ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.substituteProperties"),
|
||||
ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.propertyRegex"),
|
||||
ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.apply"),
|
||||
ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.variableRegex"),
|
||||
ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.Pattern.optionalPartRegex"),
|
||||
ProblemFilters.exclude[MissingClassProblem]("coursier.ivy.Pattern$PatternPart$Literal$"),
|
||||
ProblemFilters.exclude[MissingClassProblem]("coursier.ivy.Pattern$PatternPart"),
|
||||
ProblemFilters.exclude[MissingClassProblem]("coursier.ivy.Pattern$PatternPart$"),
|
||||
ProblemFilters.exclude[IncompatibleMethTypeProblem]("coursier.ivy.IvyRepository.apply"),
|
||||
ProblemFilters.exclude[MissingClassProblem]("coursier.ivy.Pattern$PatternPart$Optional$"),
|
||||
ProblemFilters.exclude[MissingClassProblem]("coursier.ivy.Pattern$PatternPart$Literal"),
|
||||
ProblemFilters.exclude[MissingClassProblem]("coursier.ivy.Pattern$PatternPart$Optional"),
|
||||
ProblemFilters.exclude[IncompatibleResultTypeProblem]("coursier.ivy.IvyRepository.pattern"),
|
||||
ProblemFilters.exclude[IncompatibleMethTypeProblem]("coursier.ivy.IvyRepository.copy"),
|
||||
ProblemFilters.exclude[DirectMissingMethodProblem]("coursier.ivy.IvyRepository.properties"),
|
||||
ProblemFilters.exclude[IncompatibleResultTypeProblem]("coursier.ivy.IvyRepository.metadataPattern"),
|
||||
ProblemFilters.exclude[IncompatibleMethTypeProblem]("coursier.ivy.IvyRepository.this"),
|
||||
ProblemFilters.exclude[IncompatibleResultTypeProblem]("coursier.util.Parse.repository"),
|
||||
// Since 1.0.0-M12
|
||||
// Extra `authentication` field
|
||||
ProblemFilters.exclude[MissingMethodProblem]("coursier.core.Artifact.apply"),
|
||||
|
|
|
|||
|
|
@ -864,12 +864,12 @@ object Cache {
|
|||
str + "/"
|
||||
}
|
||||
|
||||
lazy val ivy2Local = IvyRepository(
|
||||
ivy2HomeUri + "local/" + coursier.ivy.Pattern.default,
|
||||
lazy val ivy2Local = IvyRepository.fromPattern(
|
||||
(ivy2HomeUri + "local/") +: coursier.ivy.Pattern.default,
|
||||
dropInfoAttributes = true
|
||||
)
|
||||
|
||||
lazy val ivy2Cache = IvyRepository(
|
||||
lazy val ivy2Cache = IvyRepository.parse(
|
||||
ivy2HomeUri + "cache/" +
|
||||
"(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[organisation]/[module]/[type]s/[artifact]-[revision](-[classifier]).[ext]",
|
||||
metadataPatternOpt = Some(
|
||||
|
|
@ -879,6 +879,8 @@ object Cache {
|
|||
withChecksums = false,
|
||||
withSignatures = false,
|
||||
dropInfoAttributes = true
|
||||
).getOrElse(
|
||||
throw new Exception("Cannot happen")
|
||||
)
|
||||
|
||||
lazy val default = new File(
|
||||
|
|
|
|||
|
|
@ -18,17 +18,21 @@ object CacheParse {
|
|||
else {
|
||||
val repo = Parse.repository(s)
|
||||
|
||||
val url = repo match {
|
||||
val url = repo.map {
|
||||
case m: MavenRepository =>
|
||||
m.root
|
||||
case i: IvyRepository =>
|
||||
i.pattern
|
||||
// FIXME We're not handling metadataPattern here
|
||||
i.pattern.chunks.takeWhile {
|
||||
case _: coursier.ivy.Pattern.Chunk.Const => true
|
||||
case _ => false
|
||||
}.map(_.string).mkString
|
||||
case r =>
|
||||
sys.error(s"Unrecognized repository: $r")
|
||||
}
|
||||
|
||||
val validatedUrl = try {
|
||||
Cache.url(url).success
|
||||
url.map(Cache.url).validation
|
||||
} catch {
|
||||
case e: MalformedURLException =>
|
||||
("Error parsing URL " + url + Option(e.getMessage).fold("")(" (" + _ + ")")).failure
|
||||
|
|
@ -37,7 +41,7 @@ object CacheParse {
|
|||
validatedUrl.flatMap { url =>
|
||||
Option(url.getUserInfo) match {
|
||||
case None =>
|
||||
repo.success
|
||||
repo.validation
|
||||
case Some(userInfo) =>
|
||||
userInfo.split(":", 2) match {
|
||||
case Array(user, password) =>
|
||||
|
|
@ -48,7 +52,7 @@ object CacheParse {
|
|||
url.getFile
|
||||
).toString
|
||||
|
||||
val repo0 = repo match {
|
||||
repo.validation.map {
|
||||
case m: MavenRepository =>
|
||||
m.copy(
|
||||
root = baseUrl,
|
||||
|
|
@ -56,15 +60,18 @@ object CacheParse {
|
|||
)
|
||||
case i: IvyRepository =>
|
||||
i.copy(
|
||||
pattern = baseUrl,
|
||||
pattern = coursier.ivy.Pattern(
|
||||
coursier.ivy.Pattern.Chunk.Const(baseUrl) +: i.pattern.chunks.dropWhile {
|
||||
case _: coursier.ivy.Pattern.Chunk.Const => true
|
||||
case _ => false
|
||||
}
|
||||
),
|
||||
authentication = Some(Authentication(user, password))
|
||||
)
|
||||
case r =>
|
||||
sys.error(s"Unrecognized repository: $r")
|
||||
}
|
||||
|
||||
repo0.success
|
||||
|
||||
case _ =>
|
||||
s"No password found in user info of URL $url".failure
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,40 +4,35 @@ import coursier.Fetch
|
|||
import coursier.core._
|
||||
|
||||
import scalaz._
|
||||
import scalaz.Scalaz.ToEitherOps
|
||||
import scalaz.Scalaz._
|
||||
|
||||
case class IvyRepository(
|
||||
pattern: String,
|
||||
metadataPatternOpt: Option[String] = None,
|
||||
changing: Option[Boolean] = None,
|
||||
properties: Map[String, String] = Map.empty,
|
||||
withChecksums: Boolean = true,
|
||||
withSignatures: Boolean = true,
|
||||
withArtifacts: Boolean = true,
|
||||
pattern: Pattern,
|
||||
metadataPatternOpt: Option[Pattern],
|
||||
changing: Option[Boolean],
|
||||
withChecksums: Boolean,
|
||||
withSignatures: Boolean,
|
||||
withArtifacts: Boolean,
|
||||
// hack for SBT putting infos in properties
|
||||
dropInfoAttributes: Boolean = false,
|
||||
authentication: Option[Authentication] = None
|
||||
dropInfoAttributes: Boolean,
|
||||
authentication: Option[Authentication]
|
||||
) extends Repository {
|
||||
|
||||
def metadataPattern: String = metadataPatternOpt.getOrElse(pattern)
|
||||
def metadataPattern: Pattern = metadataPatternOpt.getOrElse(pattern)
|
||||
|
||||
import Repository._
|
||||
lazy val revisionListingPatternOpt: Option[Pattern] = {
|
||||
val idx = metadataPattern.chunks.indexWhere { chunk =>
|
||||
chunk == Pattern.Chunk.Var("revision")
|
||||
}
|
||||
|
||||
private val pattern0 = Pattern(pattern, properties)
|
||||
private val metadataPattern0 = Pattern(metadataPattern, properties)
|
||||
|
||||
private val revisionListingPatternOpt = {
|
||||
val idx = metadataPattern.indexOf("[revision]/")
|
||||
if (idx < 0)
|
||||
None
|
||||
else
|
||||
// FIXME A bit too permissive... we should check that [revision] indeed begins
|
||||
// a path component (that is, has a '/' before it no matter what)
|
||||
// This is trickier than simply checking for a '/' character before it in metadataPattern,
|
||||
// because of optional parts in it.
|
||||
Some(Pattern(metadataPattern.take(idx), properties))
|
||||
Some(Pattern(metadataPattern.chunks.take(idx)))
|
||||
}
|
||||
|
||||
import Repository._
|
||||
|
||||
// See http://ant.apache.org/ivy/history/latest-milestone/concept.html for a
|
||||
// list of variables that should be supported.
|
||||
// Some are missing (branch, conf, originalName).
|
||||
|
|
@ -92,14 +87,14 @@ case class IvyRepository(
|
|||
}
|
||||
|
||||
val retainedWithUrl = retained.flatMap { p =>
|
||||
pattern0.substitute(variables(
|
||||
pattern.substituteVariables(variables(
|
||||
dependency.module,
|
||||
Some(project.actualVersion),
|
||||
p.`type`,
|
||||
p.name,
|
||||
p.ext,
|
||||
Some(p.classifier).filter(_.nonEmpty)
|
||||
)).toList.map(p -> _)
|
||||
)).toList.map(p -> _) // FIXME Validation errors are ignored
|
||||
}
|
||||
|
||||
retainedWithUrl.map { case (p, url) =>
|
||||
|
|
@ -143,13 +138,13 @@ case class IvyRepository(
|
|||
case None =>
|
||||
findNoInverval(module, version, fetch)
|
||||
case Some(itv) =>
|
||||
val listingUrl = revisionListingPattern.substitute(
|
||||
val listingUrl = revisionListingPattern.substituteVariables(
|
||||
variables(module, None, "ivy", "ivy", "xml", None)
|
||||
).flatMap { s =>
|
||||
if (s.endsWith("/"))
|
||||
s.right
|
||||
else
|
||||
s"Don't know how to list revisions of $metadataPattern".left
|
||||
s"Don't know how to list revisions of ${metadataPattern.string}".left
|
||||
}
|
||||
|
||||
def fromWebPage(s: String) = {
|
||||
|
|
@ -194,7 +189,7 @@ case class IvyRepository(
|
|||
|
||||
val eitherArtifact: String \/ Artifact =
|
||||
for {
|
||||
url <- metadataPattern0.substitute(
|
||||
url <- metadataPattern.substituteVariables(
|
||||
variables(module, Some(version), "ivy", "ivy", "xml", None)
|
||||
)
|
||||
} yield {
|
||||
|
|
@ -257,3 +252,91 @@ case class IvyRepository(
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
object IvyRepository {
|
||||
def parse(
|
||||
pattern: String,
|
||||
metadataPatternOpt: Option[String] = None,
|
||||
changing: Option[Boolean] = None,
|
||||
properties: Map[String, String] = Map.empty,
|
||||
withChecksums: Boolean = true,
|
||||
withSignatures: Boolean = true,
|
||||
withArtifacts: Boolean = true,
|
||||
// hack for SBT putting infos in properties
|
||||
dropInfoAttributes: Boolean = false,
|
||||
authentication: Option[Authentication] = None
|
||||
): String \/ IvyRepository =
|
||||
|
||||
for {
|
||||
propertiesPattern <- PropertiesPattern.parse(pattern)
|
||||
metadataPropertiesPatternOpt <- metadataPatternOpt.fold(Option.empty[PropertiesPattern].right[String])(PropertiesPattern.parse(_).map(Some(_)))
|
||||
|
||||
pattern <- propertiesPattern.substituteProperties(properties)
|
||||
metadataPatternOpt <- metadataPropertiesPatternOpt.fold(Option.empty[Pattern].right[String])(_.substituteProperties(properties).map(Some(_)))
|
||||
|
||||
} yield
|
||||
IvyRepository(
|
||||
pattern,
|
||||
metadataPatternOpt,
|
||||
changing,
|
||||
withChecksums,
|
||||
withSignatures,
|
||||
withArtifacts,
|
||||
dropInfoAttributes,
|
||||
authentication
|
||||
)
|
||||
|
||||
// because of the compatibility apply method below, we can't give default values
|
||||
// to the default constructor of IvyPattern
|
||||
// this method accepts the same arguments as this constructor, with default values when possible
|
||||
def fromPattern(
|
||||
pattern: Pattern,
|
||||
metadataPatternOpt: Option[Pattern] = None,
|
||||
changing: Option[Boolean] = None,
|
||||
withChecksums: Boolean = true,
|
||||
withSignatures: Boolean = true,
|
||||
withArtifacts: Boolean = true,
|
||||
// hack for SBT putting infos in properties
|
||||
dropInfoAttributes: Boolean = false,
|
||||
authentication: Option[Authentication] = None
|
||||
): IvyRepository =
|
||||
IvyRepository(
|
||||
pattern,
|
||||
metadataPatternOpt,
|
||||
changing,
|
||||
withChecksums,
|
||||
withSignatures,
|
||||
withArtifacts,
|
||||
dropInfoAttributes,
|
||||
authentication
|
||||
)
|
||||
|
||||
@deprecated("Can now raise exceptions - use parse instead", "1.0.0-M13")
|
||||
def apply(
|
||||
pattern: String,
|
||||
metadataPatternOpt: Option[String] = None,
|
||||
changing: Option[Boolean] = None,
|
||||
properties: Map[String, String] = Map.empty,
|
||||
withChecksums: Boolean = true,
|
||||
withSignatures: Boolean = true,
|
||||
withArtifacts: Boolean = true,
|
||||
// hack for SBT putting infos in properties
|
||||
dropInfoAttributes: Boolean = false,
|
||||
authentication: Option[Authentication] = None
|
||||
): IvyRepository =
|
||||
parse(
|
||||
pattern,
|
||||
metadataPatternOpt,
|
||||
changing,
|
||||
properties,
|
||||
withChecksums,
|
||||
withSignatures,
|
||||
withArtifacts,
|
||||
dropInfoAttributes,
|
||||
authentication
|
||||
) match {
|
||||
case \/-(repo) => repo
|
||||
case -\/(msg) =>
|
||||
throw new IllegalArgumentException(s"Error while parsing Ivy patterns: $msg")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,146 +1,192 @@
|
|||
package coursier.ivy
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scalaz._, Scalaz._
|
||||
|
||||
import scalaz._
|
||||
import fastparse.all._
|
||||
|
||||
import scala.util.matching.Regex
|
||||
import java.util.regex.Pattern.quote
|
||||
case class PropertiesPattern(chunks: Seq[PropertiesPattern.ChunkOrProperty]) {
|
||||
|
||||
object Pattern {
|
||||
def string: String = chunks.map(_.string).mkString
|
||||
|
||||
val default =
|
||||
"[organisation]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[revision]/[type]s/" +
|
||||
"[artifact](-[classifier]).[ext]"
|
||||
import PropertiesPattern.ChunkOrProperty
|
||||
|
||||
val propertyRegex = (quote("${") + "[^" + quote("{[()]}") + "]*" + quote("}")).r
|
||||
val optionalPartRegex = (quote("(") + "[^" + quote("{()}") + "]*" + quote(")")).r
|
||||
val variableRegex = (quote("[") + "[^" + quote("{[()]}") + "]*" + quote("]")).r
|
||||
def substituteProperties(properties: Map[String, String]): String \/ Pattern = {
|
||||
|
||||
sealed abstract class PatternPart(val effectiveStart: Int, val effectiveEnd: Int) extends Product with Serializable {
|
||||
require(effectiveStart <= effectiveEnd)
|
||||
def start = effectiveStart
|
||||
def end = effectiveEnd
|
||||
|
||||
// FIXME Some kind of validation should be used here, to report all the missing variables,
|
||||
// not only the first one missing.
|
||||
def apply(content: String): Map[String, String] => String \/ String
|
||||
}
|
||||
object PatternPart {
|
||||
final case class Literal(override val effectiveStart: Int, override val effectiveEnd: Int) extends PatternPart(effectiveStart, effectiveEnd) {
|
||||
def apply(content: String): Map[String, String] => String \/ String = {
|
||||
assert(content.length == effectiveEnd - effectiveStart)
|
||||
val matches = variableRegex.findAllMatchIn(content).toList
|
||||
|
||||
variables =>
|
||||
@tailrec
|
||||
def helper(idx: Int, matches: List[Regex.Match], b: StringBuilder): String \/ String =
|
||||
if (idx >= content.length)
|
||||
\/-(b.result())
|
||||
else {
|
||||
assert(matches.headOption.forall(_.start >= idx))
|
||||
matches.headOption.filter(_.start == idx) match {
|
||||
case Some(m) =>
|
||||
val variableName = content.substring(m.start + 1, m.end - 1)
|
||||
variables.get(variableName) match {
|
||||
case None => -\/(s"Variable not found: $variableName")
|
||||
case Some(value) =>
|
||||
b ++= value
|
||||
helper(m.end, matches.tail, b)
|
||||
}
|
||||
case None =>
|
||||
val nextIdx = matches.headOption.fold(content.length)(_.start)
|
||||
b ++= content.substring(idx, nextIdx)
|
||||
helper(nextIdx, matches, b)
|
||||
}
|
||||
}
|
||||
|
||||
helper(0, matches, new StringBuilder)
|
||||
}
|
||||
}
|
||||
final case class Optional(start0: Int, end0: Int) extends PatternPart(start0 + 1, end0 - 1) {
|
||||
override def start = start0
|
||||
override def end = end0
|
||||
|
||||
def apply(content: String): Map[String, String] => String \/ String = {
|
||||
assert(content.length == effectiveEnd - effectiveStart)
|
||||
val inner = Literal(effectiveStart, effectiveEnd).apply(content)
|
||||
|
||||
variables =>
|
||||
\/-(inner(variables).fold(_ => "", x => x))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def substituteProperties(s: String, properties: Map[String, String]): String =
|
||||
propertyRegex.findAllMatchIn(s).toVector.foldRight(s) { case (m, s0) =>
|
||||
val key = s0.substring(m.start + "${".length, m.end - "}".length)
|
||||
val value = properties.getOrElse(key, "")
|
||||
s0.take(m.start) + value + s0.drop(m.end)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final case class Pattern(
|
||||
pattern: String,
|
||||
properties: Map[String, String]
|
||||
) {
|
||||
|
||||
import Pattern._
|
||||
|
||||
private val pattern0 = substituteProperties(pattern, properties)
|
||||
|
||||
val parts = {
|
||||
val optionalParts = optionalPartRegex.findAllMatchIn(pattern0).toList.map { m =>
|
||||
PatternPart.Optional(m.start, m.end)
|
||||
}
|
||||
|
||||
val len = pattern0.length
|
||||
|
||||
@tailrec
|
||||
def helper(
|
||||
idx: Int,
|
||||
opt: List[PatternPart.Optional],
|
||||
acc: List[PatternPart]
|
||||
): Vector[PatternPart] =
|
||||
if (idx >= len)
|
||||
acc.toVector.reverse
|
||||
else
|
||||
opt match {
|
||||
case Nil =>
|
||||
helper(len, Nil, PatternPart.Literal(idx, len) :: acc)
|
||||
case (opt0 @ PatternPart.Optional(start0, end0)) :: rem =>
|
||||
if (idx < start0)
|
||||
helper(start0, opt, PatternPart.Literal(idx, start0) :: acc)
|
||||
else {
|
||||
assert(idx == start0, s"idx: $idx, start0: $start0")
|
||||
helper(end0, rem, opt0 :: acc)
|
||||
val validation = chunks.toVector.traverseU {
|
||||
case ChunkOrProperty.Prop(name, alternativesOpt) =>
|
||||
properties.get(name) match {
|
||||
case Some(value) =>
|
||||
Seq(Pattern.Chunk.Const(value)).successNel
|
||||
case None =>
|
||||
alternativesOpt match {
|
||||
case Some(alt) =>
|
||||
PropertiesPattern(alt)
|
||||
.substituteProperties(properties)
|
||||
.map(_.chunks)
|
||||
.validation
|
||||
.toValidationNel
|
||||
case None =>
|
||||
name.failureNel
|
||||
}
|
||||
}
|
||||
|
||||
helper(0, optionalParts, Nil)
|
||||
case ChunkOrProperty.Opt(l @ _*) =>
|
||||
PropertiesPattern(l)
|
||||
.substituteProperties(properties)
|
||||
.map(l => Seq(Pattern.Chunk.Opt(l.chunks: _*)))
|
||||
.validation
|
||||
.toValidationNel
|
||||
|
||||
case ChunkOrProperty.Var(name) =>
|
||||
Seq(Pattern.Chunk.Var(name)).successNel
|
||||
|
||||
case ChunkOrProperty.Const(value) =>
|
||||
Seq(Pattern.Chunk.Const(value)).successNel
|
||||
|
||||
}.map(_.flatten).map(Pattern(_))
|
||||
|
||||
validation.disjunction.leftMap { notFoundProps =>
|
||||
s"Property(ies) not found: ${notFoundProps.toList.mkString(", ")}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case class Pattern(chunks: Seq[Pattern.Chunk]) {
|
||||
|
||||
def +:(chunk: Pattern.Chunk): Pattern =
|
||||
Pattern(chunk +: chunks)
|
||||
|
||||
import Pattern.Chunk
|
||||
|
||||
def string: String = chunks.map(_.string).mkString
|
||||
|
||||
def substituteVariables(variables: Map[String, String]): String \/ String = {
|
||||
|
||||
def helper(chunks: Seq[Chunk]): ValidationNel[String, Seq[Chunk.Const]] =
|
||||
chunks.toVector.traverseU[ValidationNel[String, Seq[Chunk.Const]]] {
|
||||
case Chunk.Var(name) =>
|
||||
variables.get(name) match {
|
||||
case Some(value) =>
|
||||
Seq(Chunk.Const(value)).successNel
|
||||
case None =>
|
||||
name.failureNel
|
||||
}
|
||||
case Chunk.Opt(l @ _*) =>
|
||||
val res = helper(l)
|
||||
if (res.isSuccess)
|
||||
res
|
||||
else
|
||||
Seq().successNel
|
||||
case c: Chunk.Const =>
|
||||
Seq(c).successNel
|
||||
}.map(_.flatten)
|
||||
|
||||
val validation = helper(chunks)
|
||||
|
||||
validation match {
|
||||
case Failure(notFoundVariables) =>
|
||||
s"Variables not found: ${notFoundVariables.toList.mkString(", ")}".left
|
||||
case Success(constants) =>
|
||||
val b = new StringBuilder
|
||||
constants.foreach(b ++= _.value)
|
||||
b.result().right
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object PropertiesPattern {
|
||||
|
||||
sealed abstract class ChunkOrProperty extends Product with Serializable {
|
||||
def string: String
|
||||
}
|
||||
|
||||
assert(pattern0.isEmpty == parts.isEmpty)
|
||||
if (pattern0.nonEmpty) {
|
||||
for ((a, b) <- parts.zip(parts.tail))
|
||||
assert(a.end == b.start)
|
||||
assert(parts.head.start == 0)
|
||||
assert(parts.last.end == pattern0.length)
|
||||
object ChunkOrProperty {
|
||||
case class Prop(name: String, alternative: Option[Seq[ChunkOrProperty]]) extends ChunkOrProperty {
|
||||
def string: String =
|
||||
s"$${" + name + alternative.fold("")(alt => "-" + alt.map(_.string).mkString) + "}"
|
||||
}
|
||||
case class Var(name: String) extends ChunkOrProperty {
|
||||
def string: String = "[" + name + "]"
|
||||
}
|
||||
case class Opt(content: ChunkOrProperty*) extends ChunkOrProperty {
|
||||
def string: String = "(" + content.map(_.string).mkString + ")"
|
||||
}
|
||||
case class Const(value: String) extends ChunkOrProperty {
|
||||
def string: String = value
|
||||
}
|
||||
|
||||
implicit def fromString(s: String): ChunkOrProperty = Const(s)
|
||||
}
|
||||
|
||||
private val substituteHelpers = parts.map { part =>
|
||||
part(pattern0.substring(part.effectiveStart, part.effectiveEnd))
|
||||
private object Parser {
|
||||
|
||||
private val notIn = s"[]{}()$$".toSet
|
||||
private val chars = P(CharsWhile(c => !notIn(c)).!)
|
||||
private val noHyphenChars = P(CharsWhile(c => !notIn(c) && c != '-').!)
|
||||
|
||||
private val constant = P(chars).map(ChunkOrProperty.Const)
|
||||
|
||||
private lazy val property: Parser[ChunkOrProperty.Prop] =
|
||||
P(s"$${" ~ noHyphenChars ~ ("-" ~ chunks).? ~ "}")
|
||||
.map { case (name, altOpt) => ChunkOrProperty.Prop(name, altOpt) }
|
||||
|
||||
private lazy val variable: Parser[ChunkOrProperty.Var] = P("[" ~ chars ~ "]").map(ChunkOrProperty.Var)
|
||||
|
||||
private lazy val optional: Parser[ChunkOrProperty.Opt] = P("(" ~ chunks ~ ")")
|
||||
.map(l => ChunkOrProperty.Opt(l: _*))
|
||||
|
||||
lazy val chunks: Parser[Seq[ChunkOrProperty]] = P((constant | property | variable | optional).rep)
|
||||
.map(_.toVector) // "Vector" is more readable than "ArrayBuffer"
|
||||
}
|
||||
|
||||
def substitute(variables: Map[String, String]): String \/ String =
|
||||
substituteHelpers.foldLeft[String \/ String](\/-("")) {
|
||||
case (acc0, helper) =>
|
||||
for {
|
||||
acc <- acc0
|
||||
s <- helper(variables)
|
||||
} yield acc + s
|
||||
def parser: Parser[Seq[ChunkOrProperty]] = Parser.chunks
|
||||
|
||||
|
||||
def parse(pattern: String): String \/ PropertiesPattern =
|
||||
parser.parse(pattern) match {
|
||||
case f: Parsed.Failure =>
|
||||
f.msg.left
|
||||
case Parsed.Success(v, _) =>
|
||||
PropertiesPattern(v).right
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Pattern {
|
||||
|
||||
sealed abstract class Chunk extends Product with Serializable {
|
||||
def string: String
|
||||
}
|
||||
|
||||
object Chunk {
|
||||
case class Var(name: String) extends Chunk {
|
||||
def string: String = "[" + name + "]"
|
||||
}
|
||||
case class Opt(content: Chunk*) extends Chunk {
|
||||
def string: String = "(" + content.map(_.string).mkString + ")"
|
||||
}
|
||||
case class Const(value: String) extends Chunk {
|
||||
def string: String = value
|
||||
}
|
||||
|
||||
implicit def fromString(s: String): Chunk = Const(s)
|
||||
}
|
||||
|
||||
import Chunk.{ Var, Opt }
|
||||
|
||||
// Corresponds to
|
||||
// [organisation]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[revision]/[type]s/[artifact](-[classifier]).[ext]
|
||||
|
||||
val default = Pattern(
|
||||
Seq(
|
||||
Var("organisation"), "/",
|
||||
Var("module"), "/",
|
||||
Opt("scala_", Var("scalaVersion"), "/"),
|
||||
Opt("sbt_", Var("sbtVersion"), "/"),
|
||||
Var("revision"), "/",
|
||||
Var("type"), "s/",
|
||||
Var("artifact"), Opt("-", Var("classifier")), ".", Var("ext")
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import coursier.maven.MavenRepository
|
|||
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
|
||||
import scalaz.\/
|
||||
import scalaz.Scalaz.ToEitherOps
|
||||
|
||||
object Parse {
|
||||
|
||||
/**
|
||||
|
|
@ -127,21 +130,21 @@ object Parse {
|
|||
def moduleVersionConfigs(l: Seq[String]): (Seq[String], Seq[(Module, String, Option[String])]) =
|
||||
valuesAndErrors(moduleVersionConfig, l)
|
||||
|
||||
def repository(s: String): Repository =
|
||||
def repository(s: String): String \/ Repository =
|
||||
if (s == "central")
|
||||
MavenRepository("https://repo1.maven.org/maven2")
|
||||
MavenRepository("https://repo1.maven.org/maven2").right
|
||||
else if (s.startsWith("sonatype:"))
|
||||
MavenRepository(s"https://oss.sonatype.org/content/repositories/${s.stripPrefix("sonatype:")}")
|
||||
MavenRepository(s"https://oss.sonatype.org/content/repositories/${s.stripPrefix("sonatype:")}").right
|
||||
else if (s.startsWith("bintray:"))
|
||||
MavenRepository(s"https://dl.bintray.com/${s.stripPrefix("bintray:")}/maven")
|
||||
MavenRepository(s"https://dl.bintray.com/${s.stripPrefix("bintray:")}/maven").right
|
||||
else if (s.startsWith("typesafe:ivy-"))
|
||||
IvyRepository(
|
||||
s"https://repo.typesafe.com/typesafe/ivy-" + s.stripPrefix("typesafe:ivy-") + "/" +
|
||||
IvyRepository.fromPattern(
|
||||
(s"https://repo.typesafe.com/typesafe/ivy-" + s.stripPrefix("typesafe:ivy-") + "/") +:
|
||||
coursier.ivy.Pattern.default
|
||||
)
|
||||
).right
|
||||
else if (s.startsWith("ivy:"))
|
||||
IvyRepository(s.stripPrefix("ivy:"))
|
||||
IvyRepository.parse(s.stripPrefix("ivy:"))
|
||||
else
|
||||
MavenRepository(s)
|
||||
MavenRepository(s).right
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -436,7 +436,10 @@ object Tasks {
|
|||
m
|
||||
case i: IvyRepository =>
|
||||
if (i.authentication.isEmpty) {
|
||||
val base = i.pattern.takeWhile(c => c != '[' && c != '(' && c != '$')
|
||||
val base = i.pattern.chunks.takeWhile {
|
||||
case _: coursier.ivy.Pattern.Chunk.Const => true
|
||||
case _ => false
|
||||
}.map(_.string).mkString
|
||||
|
||||
httpHost(base).flatMap(credentials.get).fold(i) { auth =>
|
||||
i.copy(authentication = Some(auth))
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ object IvyTests extends TestSuite {
|
|||
|
||||
// only tested on the JVM for lack of support of XML attributes in the platform-dependent XML stubs
|
||||
|
||||
val sbtRepo = IvyRepository(
|
||||
val sbtRepo = IvyRepository.parse(
|
||||
"https://repo.scala-sbt.org/scalasbt/sbt-plugin-releases/" +
|
||||
"[organisation]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)" +
|
||||
"[revision]/[type]s/[artifact](-[classifier]).[ext]",
|
||||
dropInfoAttributes = true
|
||||
).getOrElse(
|
||||
throw new Exception("Cannot happen")
|
||||
)
|
||||
|
||||
val tests = TestSuite {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
com.lihaoyi:fastparse-utils_2.11:0.3.7:default
|
||||
com.lihaoyi:fastparse_2.11:0.3.7:default
|
||||
com.lihaoyi:sourcecode_2.11:0.1.1:default
|
||||
io.get-coursier:coursier_2.11:1.0.0-SNAPSHOT:compile
|
||||
org.jsoup:jsoup:1.9.2:default
|
||||
org.scala-lang:scala-library:2.11.8:default
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
package coursier.test
|
||||
|
||||
import coursier.ivy.PropertiesPattern
|
||||
import coursier.ivy.PropertiesPattern.ChunkOrProperty
|
||||
import coursier.ivy.PropertiesPattern.ChunkOrProperty._
|
||||
|
||||
import utest._
|
||||
|
||||
import scalaz.Scalaz.ToEitherOps
|
||||
|
||||
object IvyPatternParserTests extends TestSuite {
|
||||
|
||||
val tests = TestSuite {
|
||||
|
||||
'plugin - {
|
||||
val strPattern = "[organization]/[module](/scala_[scalaVersion])(/sbt_[sbtVersion])/[revision]/resolved.xml.[ext]"
|
||||
val expectedChunks = Seq[ChunkOrProperty](
|
||||
Var("organization"),
|
||||
"/", Var("module"),
|
||||
Opt("/scala_", Var("scalaVersion")),
|
||||
Opt("/sbt_", Var("sbtVersion")),
|
||||
"/", Var("revision"),
|
||||
"/resolved.xml.", Var("ext")
|
||||
)
|
||||
|
||||
assert(PropertiesPattern.parse(strPattern).map(_.chunks) == expectedChunks.right)
|
||||
}
|
||||
|
||||
'activatorLaunchLocal - {
|
||||
val strPattern =
|
||||
"file://${activator.local.repository-${activator.home-${user.home}/.activator}/repository}" +
|
||||
"/[organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)" +
|
||||
"[revision]/[type]s/[artifact](-[classifier]).[ext]"
|
||||
val expectedChunks = Seq[ChunkOrProperty](
|
||||
"file://",
|
||||
Prop("activator.local.repository", Some(Seq(
|
||||
Prop("activator.home", Some(Seq(
|
||||
Prop("user.home", None),
|
||||
"/.activator"
|
||||
))),
|
||||
"/repository"
|
||||
))), "/",
|
||||
Var("organization"), "/",
|
||||
Var("module"), "/",
|
||||
Opt("scala_", Var("scalaVersion"), "/"),
|
||||
Opt("sbt_", Var("sbtVersion"), "/"),
|
||||
Var("revision"), "/",
|
||||
Var("type"), "s/",
|
||||
Var("artifact"), Opt("-", Var("classifier")), ".", Var("ext")
|
||||
)
|
||||
|
||||
val pattern0 = PropertiesPattern.parse(strPattern)
|
||||
assert(pattern0.map(_.chunks) == expectedChunks.right)
|
||||
|
||||
val pattern = pattern0.toOption.get
|
||||
|
||||
* - {
|
||||
val varPattern = pattern.substituteProperties(Map(
|
||||
"activator.local.repository" -> "xyz"
|
||||
)).map(_.string)
|
||||
|
||||
val expectedVarPattern =
|
||||
"file://xyz" +
|
||||
"/[organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)" +
|
||||
"[revision]/[type]s/[artifact](-[classifier]).[ext]"
|
||||
|
||||
assert(varPattern == expectedVarPattern.right)
|
||||
}
|
||||
|
||||
* - {
|
||||
val varPattern = pattern.substituteProperties(Map(
|
||||
"activator.local.repository" -> "xyz",
|
||||
"activator.home" -> "aaaa"
|
||||
)).map(_.string)
|
||||
|
||||
val expectedVarPattern =
|
||||
"file://xyz" +
|
||||
"/[organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)" +
|
||||
"[revision]/[type]s/[artifact](-[classifier]).[ext]"
|
||||
|
||||
assert(varPattern == expectedVarPattern.right)
|
||||
}
|
||||
|
||||
* - {
|
||||
val varPattern = pattern.substituteProperties(Map(
|
||||
"activator.home" -> "aaaa"
|
||||
)).map(_.string)
|
||||
|
||||
val expectedVarPattern =
|
||||
"file://aaaa/repository" +
|
||||
"/[organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)" +
|
||||
"[revision]/[type]s/[artifact](-[classifier]).[ext]"
|
||||
|
||||
assert(varPattern == expectedVarPattern.right)
|
||||
}
|
||||
|
||||
* - {
|
||||
val varPattern0 = pattern.substituteProperties(Map(
|
||||
"user.home" -> "homez"
|
||||
))
|
||||
|
||||
val expectedVarPattern =
|
||||
"file://homez/.activator/repository" +
|
||||
"/[organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)" +
|
||||
"[revision]/[type]s/[artifact](-[classifier]).[ext]"
|
||||
|
||||
assert(varPattern0.map(_.string) == expectedVarPattern.right)
|
||||
|
||||
val varPattern = varPattern0.toOption.get
|
||||
|
||||
* - {
|
||||
val res = varPattern.substituteVariables(Map(
|
||||
"organization" -> "org",
|
||||
"module" -> "mod",
|
||||
"revision" -> "1.1.x",
|
||||
"type" -> "jarr",
|
||||
"artifact" -> "art",
|
||||
"classifier" -> "docc",
|
||||
"ext" -> "jrr"
|
||||
)).map(_.string)
|
||||
val expectedRes = "file://homez/.activator/repository/org/mod/1.1.x/jarrs/art-docc.jrr"
|
||||
|
||||
assert(res == expectedRes.right)
|
||||
}
|
||||
}
|
||||
|
||||
* - {
|
||||
val varPattern = pattern.substituteProperties(Map())
|
||||
assert(varPattern.isLeft)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue