Better handling of Ivy patterns

This commit is contained in:
Alexandre Archambault 2016-07-03 17:21:17 +02:00
parent bdbc8e6dd9
commit 1c34362b6f
No known key found for this signature in database
GPG Key ID: 14640A6839C263A9
10 changed files with 485 additions and 175 deletions

View File

@ -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"),

View File

@ -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(

View 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
}

View File

@ -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")
}
}

View File

@ -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")
)
)
}

View File

@ -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
}

View File

@ -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))

View File

@ -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 {

View File

@ -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

View File

@ -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)
}
}
}
}