diff --git a/dependency/shared/src/main/scala/dependency/DependencyLike.scala b/dependency/shared/src/main/scala/dependency/DependencyLike.scala index dde5357..2ef3793 100644 --- a/dependency/shared/src/main/scala/dependency/DependencyLike.scala +++ b/dependency/shared/src/main/scala/dependency/DependencyLike.scala @@ -40,8 +40,16 @@ final case class DependencyLike[+A <: NameAttributes, +E <: NameAttributes]( .mkString private def paramsString: String = excludeString ++ userParamsString - def render: String = - s"${module.render}:$version$paramsString" + def render: String = { + val b = new StringBuilder + b ++= module.render + if (version.nonEmpty) { + b += ':' + b ++= version + } + b ++= paramsString + b.result() + } override def toString: String = render } diff --git a/dependency/shared/src/main/scala/dependency/parser/DependencyParser.scala b/dependency/shared/src/main/scala/dependency/parser/DependencyParser.scala index cb15f3d..98c379f 100644 --- a/dependency/shared/src/main/scala/dependency/parser/DependencyParser.scala +++ b/dependency/shared/src/main/scala/dependency/parser/DependencyParser.scala @@ -25,6 +25,9 @@ object DependencyParser { case (module, remaining) => val (version, configOpt, params) = splitRemainingPart(remaining, acceptInlineConfiguration) assert(acceptInlineConfiguration || configOpt.isEmpty) + val version0 = + if (acceptInlineConfiguration && version == " ") "" + else version val (excludeParams, remainingParams) = params.partition(_.startsWith("exclude=")) val maybeExclusions = excludeParams @@ -38,21 +41,18 @@ object DependencyParser { } for { - _ <- ModuleParser.validateValue(version, "version") + _ <- ModuleParser.validateValue(version0, "version") exclusions <- maybeExclusions } yield { val userParams = remainingParams.iterator.map(parseParam).toSeq ++ configOpt.toSeq.map("$inlineConfiguration" -> Some(_)) - DependencyLike(module, version, exclusions, userParams) + DependencyLike(module, version0, exclusions, userParams) } } - private def attrSeparator = "," - private def argSeparator = ":" - private def splitRemainingPart(input: String, acceptInlineConfiguration: Boolean): (String, Option[String], Seq[String]) = { def simpleSplit(s: String): (String, Seq[String]) = - s.split(attrSeparator) match { + s.split(ModuleParser.paramSeparator) match { case Array(coordsEnd, attrs @ _*) => (coordsEnd, attrs) } @@ -72,7 +72,7 @@ object DependencyParser { } if (acceptInlineConfiguration) - versionPart.split(argSeparator, 2) match { + versionPart.split(ModuleParser.argSeparator, 2) match { case Array(ver, config) => (ver, Some(config), attrs0) case Array(ver) => (ver, None, attrs0) } diff --git a/dependency/shared/src/main/scala/dependency/parser/ModuleParser.scala b/dependency/shared/src/main/scala/dependency/parser/ModuleParser.scala index 17c9428..ae830e7 100644 --- a/dependency/shared/src/main/scala/dependency/parser/ModuleParser.scala +++ b/dependency/shared/src/main/scala/dependency/parser/ModuleParser.scala @@ -3,6 +3,9 @@ package parser object ModuleParser { + private[parser] def paramSeparator = "," + private[parser] def argSeparator = ":" + private implicit class EitherWithFilter[L, R](private val e: Either[L, R]) extends AnyVal { def withFilter(f: R => Boolean): Either[L, R] = if (e.forall(f)) e else throw new MatchError(e) @@ -19,7 +22,7 @@ object ModuleParser { */ def parse(input: String): Either[String, AnyModule] = { - val parts = input.split(":", -1).map(Some(_).filter(_.nonEmpty)) + val parts = input.split(argSeparator, -1).map(Some(_).filter(_.nonEmpty)) val values = parts match { case Array(Some(org), Some(name)) => Right((org, name, NoAttributes)) @@ -40,7 +43,11 @@ object ModuleParser { def parsePrefix(input: String): Either[String, (AnyModule, String)] = { - val parts = input.split(":", -1).map(Some(_).filter(_.nonEmpty)) + val (input0, paramsOpt) = input.split(paramSeparator, 2) match { + case Array(input0) => (input0, None) + case Array(input0, params) => (input0, Some(params)) + } + val parts = input0.split(argSeparator, -1).map(Some(_).filter(_.nonEmpty)) val values = parts match { case Array(Some(org), Some(name), rest @ _*) => Right((org, name, NoAttributes, rest)) @@ -56,7 +63,7 @@ object ModuleParser { _ <- validateValue(org, "organization") (name, attributes) <- parseNamePart(name0) _ <- validateValue(name, "module name") - } yield (ModuleLike(org, name, nameAttributes, attributes), rest.map(_.getOrElse("")).mkString(":")) + } yield (ModuleLike(org, name, nameAttributes, attributes), rest.map(_.getOrElse("")).mkString(argSeparator) + paramsOpt.fold("")(paramSeparator + _)) } private def parseNamePart(input: String): Either[String, (String, Map[String, String])] = { diff --git a/dependency/shared/src/test/scala/dependency/parser/DependencyParserNoVersionTests.scala b/dependency/shared/src/test/scala/dependency/parser/DependencyParserNoVersionTests.scala new file mode 100644 index 0000000..1b151a2 --- /dev/null +++ b/dependency/shared/src/test/scala/dependency/parser/DependencyParserNoVersionTests.scala @@ -0,0 +1,150 @@ +package dependency +package parser + +import com.eed3si9n.expecty.Expecty.expect + +class DependencyParserNoVersionTests extends munit.FunSuite { + + test("simple") { + val res = DependencyParser.parse("org:name") + val expected = Right(Dependency("org", "name", "")) + expect(res == expected) + } + + test("simple with colon") { + val res = DependencyParser.parse("org:name:") + val expected = Right(Dependency("org", "name", "")) + expect(res == expected) + } + + test("scala") { + val res = DependencyParser.parse("org::name") + val expected = Right(ScalaDependency("org", "name", "")) + expect(res == expected) + } + + test("scala full cross-version") { + val res = DependencyParser.parse("org:::name") + val expected = Right(ScalaDependency(ScalaModule("org", "name", fullCrossVersion = true), "")) + expect(res == expected) + } + + test("scala platform") { + val res = DependencyParser.parse("org::name::") + val expected = Right(ScalaDependency(ScalaModule("org", "name", fullCrossVersion = false, platform = true), "")) + expect(res == expected) + } + + test("scala with attributes") { + val res = DependencyParser.parse("org::name;scala=2.12;sbt=1.0") + val expected = Right(ScalaDependency(ScalaModule("org", "name").copy(attributes = Map("scala" -> "2.12", "sbt" -> "1.0")), "")) + expect(res == expected) + } + + test("attributes") { + val res = DependencyParser.parse("org:name;scala=2.12;sbt=1.0") + val expected = Right(Dependency(Module("org", "name").copy(attributes = Map("scala" -> "2.12", "sbt" -> "1.0")), "")) + expect(res == expected) + } + + test("exclude") { + val res = DependencyParser.parse("org:name,exclude=fu%ba") + val expected = Right(Dependency("org", "name", "").copy(exclude = CovariantSet(Module("fu", "ba")))) + expect(res == expected) + } + + test("scala exclude") { + val res = DependencyParser.parse("org:name,exclude=fu%%ba") + val expected = Right(Dependency("org", "name", "").copy(exclude = CovariantSet(ScalaModule("fu", "ba")))) + expect(res == expected) + } + + test("several exclude") { + val res = DependencyParser.parse("org:name,exclude=fu%ba,exclude=aa%%aa-1") + val expected = Right(Dependency("org", "name", "").copy(exclude = CovariantSet(Module("fu", "ba"), ScalaModule("aa", "aa-1")))) + expect(res == expected) + } + + test("param") { + val res = DependencyParser.parse("org:name,something=ba") + val expected = Right(Dependency("org", "name", "").copy(userParams = Seq("something" -> Some("ba")))) + expect(res == expected) + } + + test("no-value param") { + val res = DependencyParser.parse("org:name,something") + val expected = Right(Dependency("org", "name", "").copy(userParams = Seq("something" -> None))) + expect(res == expected) + } + + test("multiple same key params") { + val res = DependencyParser.parse("org:name,something=a,something,something=b") + val expected = Right(Dependency("org", "name", "").copy( + userParams = Seq( + "something" -> Some("a"), + "something" -> None, + "something" -> Some("b") + ) + )) + expect(res == expected) + } + + test("scala + params + exclusions") { + val res = DependencyParser.parse("org:::name::,intransitive,exclude=foo%*,exclude=comp%%*,url=aaaa") + val expected = Right( + ScalaDependency(ScalaModule("org", "name", fullCrossVersion = true, platform = true), "") + .copy( + exclude = CovariantSet( + Module("foo", "*"), + ScalaModule("comp", "*") + ), + userParams = Seq( + "intransitive" -> None, + "url" -> Some("aaaa") + ) + ) + ) + expect(res == expected) + } + + test("inline config") { + val res = DependencyParser.parse("org:name: :runtime") + val expected = Right( + Dependency("org", "name", "").copy( + userParams = Seq( + "$inlineConfiguration" -> Some("runtime") + ) + ) + ) + expect(res == expected) + } + test("inline config with param") { + val res = DependencyParser.parse("org:name: :runtime,something=ba") + val expected = Right( + Dependency("org", "name", "").copy( + userParams = Seq( + "something" -> Some("ba"), + "$inlineConfiguration" -> Some("runtime") + ) + ) + ) + expect(res == expected) + } + + test("reject slash in org") { + val res = DependencyParser.parse("o/rg::name") + expect(res.isLeft) + } + test("reject backslash in org") { + val res = DependencyParser.parse("o\\rg::name") + expect(res.isLeft) + } + test("reject slash in name") { + val res = DependencyParser.parse("org::/name") + expect(res.isLeft) + } + test("reject backslash in name") { + val res = DependencyParser.parse("org::\\name") + expect(res.isLeft) + } +}