diff --git a/build.sbt b/build.sbt index 6e3f1b4..abd9a3c 100644 --- a/build.sbt +++ b/build.sbt @@ -30,15 +30,18 @@ lazy val root = (project in file(".")) .settings( name := "scala-update", libraryDependencies ++= Seq( - "dev.zio" %% "zio" % zioVersion, - "dev.zio" %% "zio-macros" % zioVersion, - "dev.zio" %% "zio-nio" % zioNioVersion, - "dev.zio" %% "zio-streams" % zioVersion, - "dev.zio" %% "zio-test" % zioVersion % Test, - "dev.zio" %% "zio-test-sbt" % zioVersion % Test, - "io.get-coursier" %% "coursier" % coursierVersion, - "org.scalameta" %% "scalameta" % scalaMetaVersion, - "io.github.kitlangton" %% "zio-tui" % zioTuiVersion + "dev.zio" %% "zio" % zioVersion, + "dev.zio" %% "zio-cli" % "0.2.7", + "dev.zio" %% "zio-macros" % zioVersion, + "dev.zio" %% "zio-nio" % zioNioVersion, + "dev.zio" %% "zio-json" % "0.3.0-RC9", + "dev.zio" %% "zio-streams" % zioVersion, + "dev.zio" %% "zio-test" % zioVersion % Test, + "dev.zio" %% "zio-test-magnolia" % zioVersion % Test, + "dev.zio" %% "zio-test-sbt" % zioVersion % Test, + "io.get-coursier" %% "coursier" % coursierVersion, + "org.scalameta" %% "scalameta" % scalaMetaVersion, + "io.github.kitlangton" %% "zio-tui" % zioTuiVersion ), Compile / mainClass := Some("update.Main"), testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework"), @@ -58,56 +61,56 @@ lazy val root = (project in file(".")) Global / onChangedBuildSource := ReloadOnSourceChanges // I use this so I can run `sbt run` in this project. Very lazy hack. -lazy val example = Seq( - "dev.zio" %% "zio" % "1.0.14", - "dev.zio" %% "zio-macros" % "1.0.14", - "dev.zio" %% "zio-nio" % zioNioVersion, - "dev.zio" %% "zio-streams" % "1.0.14", - "dev.zio" %% "zio-test" % "1.0.14" % Test, - "dev.zio" %% "zio-test-sbt" % "1.0.14" % Test, - "io.get-coursier" %% "coursier" % coursierVersion, - "org.scalameta" %% "scalameta" % "4.5.8", - "io.github.kitlangton" %% "zio-tui" % "0.1.1", - "io.github.neurodyne" %% "zio-aws-s3" % "0.4.12", - "io.d11" %% "zhttp" % "2.0.0-RC8", - "com.coralogix" %% "zio-k8s-client" % "1.4.6", - "com.softwaremill.sttp.client3" %% "async-http-client-backend-zio" % "3.6.1", - "nl.vroste" %% "zio-kinesis" % "0.21.1", - "com.vladkopanev" %% "zio-saga-core" % "0.3.0", - "io.scalac" %% "zio-slick-interop" % "0.3", - "com.typesafe.slick" %% "slick-hikaricp" % "3.3.2", - "info.senia" %% "zio-test-akka-http" % "1.0.14", - "io.getquill" %% "quill-jdbc-zio" % "3.18.0", - "dev.zio" %% "zio-akka-cluster" % "0.3.0", - "dev.zio" %% "zio-cache" % "0.2.0", - "dev.zio" %% "zio-config-magnolia" % "3.0.1", - "dev.zio" %% "zio-config-typesafe" % "3.0.1", - "dev.zio" %% "zio-config-refined" % "3.0.1", - "dev.zio" %% "zio-ftp" % "0.3.6", - "dev.zio" %% "zio-json" % "0.3.0-RC8", - // "dev.zio" %% "zio-kafka" % "2.0.0-RC5", - "dev.zio" %% "zio-logging" % "0.5.14", - "dev.zio" %% "zio-metrics-prometheus" % "1.0.14", - "dev.zio" %% "zio-nio" % "1.0.0-RC11", - "dev.zio" %% "zio-optics" % "0.2.0", - "dev.zio" %% "zio-prelude" % "1.0.0-RC9", - "dev.zio" %% "zio-process" % "0.7.0", - "dev.zio" %% "zio-rocksdb" % "0.3.2", - "dev.zio" %% "zio-s3" % "0.3.7", - "dev.zio" %% "zio-schema" % "0.2.0", - "dev.zio" %% "zio-sqs" % "0.4.3", - "dev.zio" %% "zio-opentracing" % "0.8.3", - "io.laserdisc" %% "tamer-db" % "0.18.1", - "io.jaegertracing" % "jaeger-core" % "1.6.0", - "io.jaegertracing" % "jaeger-client" % "1.6.0", - "io.jaegertracing" % "jaeger-zipkin" % "1.6.0", - "io.zipkin.reporter2" % "zipkin-reporter" % "2.16.3", - "io.zipkin.reporter2" % "zipkin-sender-okhttp3" % "2.16.3", - "dev.zio" %% "zio-interop-cats" % "3.3.0", - "dev.zio" %% "zio-interop-scalaz7x" % "7.3.3.0", - "dev.zio" %% "zio-interop-reactivestreams" % "1.3.12", - "dev.zio" %% "zio-interop-twitter" % "20.10.2", - "dev.zio" %% "zio-zmx" % "0.0.13", - "dev.zio" %% "zio-query" % "0.3.0", - "org.polynote" %% "uzhttp" % "0.2.8" -) +//lazy val example = Seq( +// "dev.zio" %% "zio" % "1.0.14", +// "dev.zio" %% "zio-macros" % "1.0.14", +// "dev.zio" %% "zio-nio" % zioNioVersion, +// "dev.zio" %% "zio-streams" % "1.0.14", +// "dev.zio" %% "zio-test" % "1.0.14" % Test, +// "dev.zio" %% "zio-test-sbt" % "1.0.14" % Test, +// "io.get-coursier" %% "coursier" % coursierVersion, +// "org.scalameta" %% "scalameta" % "4.5.8", +// "io.github.kitlangton" %% "zio-tui" % "0.1.1", +// "io.github.neurodyne" %% "zio-aws-s3" % "0.4.12", +// "io.d11" %% "zhttp" % "2.0.0-RC8", +// "com.coralogix" %% "zio-k8s-client" % "1.4.6", +// "com.softwaremill.sttp.client3" %% "async-http-client-backend-zio" % "3.6.1", +// "nl.vroste" %% "zio-kinesis" % "0.21.1", +// "com.vladkopanev" %% "zio-saga-core" % "0.3.0", +// "io.scalac" %% "zio-slick-interop" % "0.3", +// "com.typesafe.slick" %% "slick-hikaricp" % "3.3.2", +// "info.senia" %% "zio-test-akka-http" % "1.0.14", +// "io.getquill" %% "quill-jdbc-zio" % "3.18.0", +// "dev.zio" %% "zio-akka-cluster" % "0.3.0", +// "dev.zio" %% "zio-cache" % "0.2.0", +// "dev.zio" %% "zio-config-magnolia" % "3.0.1", +// "dev.zio" %% "zio-config-typesafe" % "3.0.1", +// "dev.zio" %% "zio-config-refined" % "3.0.1", +// "dev.zio" %% "zio-ftp" % "0.3.6", +// "dev.zio" %% "zio-json" % "0.3.0-RC8", +// // "dev.zio" %% "zio-kafka" % "2.0.0-RC5", +// "dev.zio" %% "zio-logging" % "0.5.14", +// "dev.zio" %% "zio-metrics-prometheus" % "1.0.14", +// "dev.zio" %% "zio-nio" % "1.0.0-RC11", +// "dev.zio" %% "zio-optics" % "0.2.0", +// "dev.zio" %% "zio-prelude" % "1.0.0-RC9", +// "dev.zio" %% "zio-process" % "0.7.0", +// "dev.zio" %% "zio-rocksdb" % "0.3.2", +// "dev.zio" %% "zio-s3" % "0.3.7", +// "dev.zio" %% "zio-schema" % "0.2.0", +// "dev.zio" %% "zio-sqs" % "0.4.3", +// "dev.zio" %% "zio-opentracing" % "0.8.3", +// "io.laserdisc" %% "tamer-db" % "0.18.1", +// "io.jaegertracing" % "jaeger-core" % "1.6.0", +// "io.jaegertracing" % "jaeger-client" % "1.6.0", +// "io.jaegertracing" % "jaeger-zipkin" % "1.6.0", +// "io.zipkin.reporter2" % "zipkin-reporter" % "2.16.3", +// "io.zipkin.reporter2" % "zipkin-sender-okhttp3" % "2.16.3", +// "dev.zio" %% "zio-interop-cats" % "3.3.0", +// "dev.zio" %% "zio-interop-scalaz7x" % "7.3.3.0", +// "dev.zio" %% "zio-interop-reactivestreams" % "1.3.12", +// "dev.zio" %% "zio-interop-twitter" % "20.10.2", +// "dev.zio" %% "zio-zmx" % "0.0.13", +// "dev.zio" %% "zio-query" % "0.3.0", +// "org.polynote" %% "uzhttp" % "0.2.8" +//) diff --git a/src/main/scala/update/Dependency.scala b/src/main/scala/update/Dependency.scala index e99d0d4..364861c 100644 --- a/src/main/scala/update/Dependency.scala +++ b/src/main/scala/update/Dependency.scala @@ -11,23 +11,23 @@ object PreRelease { implicit val ordering: Ordering[PreRelease] = new Ordering[PreRelease] { private val Re = raw"([A-Za-z]+)(\d+)(\w+)?".r - override def compare(x: PreRelease, y: PreRelease): Int = + override def compare(x: PreRelease, y: PreRelease): Int = x.value match { case Re("RC", n, _) => y.value match { case Re("RC", m, _) => n.toInt compare m.toInt - case _ => 1 + case _ => 1 } case Re("M", n, _) => - y.value match { - case Re("RC", _, _) => -1 - case Re("M", m, _) => n.toInt compare m.toInt - case _ => 1 - } + y.value match { + case Re("RC", _, _) => -1 + case Re("M", m, _) => n.toInt compare m.toInt + case _ => 1 + } case _ => -1 } } - implicit val ordered = Ordered.orderingToOrdered[PreRelease] _ + implicit val ordered = Ordered.orderingToOrdered[PreRelease] _ } // major.minor.patch-prerelease @@ -35,9 +35,9 @@ final case class Version(value: String) { lazy val details: VersionDetails = VersionDetails.fromString(value) - def major: Int = details.major - def minor: Int = details.minor - def patch: Int = details.patch + def major: Int = details.major + def minor: Int = details.minor + def patch: Int = details.patch def preRelease: Option[PreRelease] = details.preRelease def isNewerThan(that: Version): Boolean = @@ -46,6 +46,12 @@ final case class Version(value: String) { (major == that.major && minor == that.minor && patch > that.patch) || (major == that.major && minor == that.minor && patch == that.patch && preRelease.isEmpty && that.preRelease.isDefined) || (major == that.major && minor == that.minor && patch == that.patch && preRelease.isDefined && that.preRelease.isDefined && preRelease.get > that.preRelease.get) + +} + +object Version { + implicit val ordering: Ordering[Version] = + Ordering.by[Version, (Int, Int, Int, Option[PreRelease])](v => (v.major, v.minor, v.patch, v.preRelease)) } // group %% artifact % version diff --git a/src/main/scala/update/UpdateOptions.scala b/src/main/scala/update/UpdateOptions.scala index 7471bec..459a67c 100644 --- a/src/main/scala/update/UpdateOptions.scala +++ b/src/main/scala/update/UpdateOptions.scala @@ -24,7 +24,7 @@ object UpdateOptions { val patch = current.patch val allNewerVersions = - available.filter(_.isNewerThan(current)) + available.filter(_.isNewerThan(current)).sorted val majorVersion = allNewerVersions .filter(v => ((current.preRelease.isDefined && v.major == major) || v.major > major) && v.preRelease.isEmpty) diff --git a/src/main/scala/update/search/Search.scala b/src/main/scala/update/search/Search.scala new file mode 100644 index 0000000..6fc7fcb --- /dev/null +++ b/src/main/scala/update/search/Search.scala @@ -0,0 +1,160 @@ +package update.search + +import tui.TUI +import tui.components.Choose +import update.{Artifact, Group, Version} +import view.View +import zio._ +import zio.json._ + +import java.net.URLEncoder +import java.time.Instant +import scala.annotation.tailrec + +final case class Payload( + response: Response +) + +object Payload { + implicit val codec: JsonCodec[Payload] = + DeriveJsonCodec.gen[Payload] +} + +final case class Response( + numFound: Int, + start: Int, + docs: List[Doc] +) + +object Response { + implicit val codec: JsonCodec[Response] = + DeriveJsonCodec.gen[Response] +} + +final case class Doc( + id: String, + g: String, + a: String, + latestVersion: String, + timestamp: Long +) + +final case class SearchResult( + group: Group, + artifact: Artifact, + scalaVersions: List[String], + latestVersion: Version, + lastUpdated: Instant +) { + def render: View = + View.horizontal( + View.text(s"\"${group.value}\"").blue, + View.text("%%").blue.dim, + View.text(s"\"${artifact.value}\"").blue, + View.text("%").blue.dim, + View.text(s"\"${latestVersion.value}\"").blue + ) +} + +object SearchResult { + def fromDoc(doc: Doc): SearchResult = { + val (artifact, scalaVersion) = + doc.a match { + case s"${artifact}_sjs1_2.11" => + (artifact, "sjs1_2.11") + case s"${artifact}_sjs1_2.12" => + (artifact, "sjs1_2.12") + case s"${artifact}_sjs1_2.13" => + (artifact, "sjs1_2.13") + case s"${artifact}_sjs1_3" => + (artifact, "sjs1_3") + case s"${artifact}_2.11" => + (artifact, "2.11") + case s"${artifact}_2.12" => + (artifact, "2.12") + case s"${artifact}_2.13" => + (artifact, "2.13") + case s"${artifact}_3" => + (artifact, "3") + case other => + (other, "OOPS") + } + + SearchResult( + Group(doc.g), + Artifact(artifact), + List(scalaVersion), + Version(doc.latestVersion), + Instant.ofEpochMilli(doc.timestamp) + ) + } + + // Combine results with same group, artifact and latestVersion but different scala versions + def combineResults(results: List[SearchResult]): List[SearchResult] = { + @tailrec + def loop( + results: List[SearchResult], + acc: List[SearchResult] + ): List[SearchResult] = + (results, acc) match { + case (Nil, acc) => acc.reverse + case (r :: rs, a :: as) + if a.group == r.group && a.artifact == r.artifact && a.latestVersion == r.latestVersion => + loop(rs, a.copy(scalaVersions = a.scalaVersions ++ r.scalaVersions) :: as) + case (r :: rs, acc) => + loop(rs, r :: acc) + } + + loop(results, Nil) + } + +} + +object Doc { + implicit val codec: JsonCodec[Doc] = + DeriveJsonCodec.gen[Doc] +} + +final case class Search() { + + def search(query: String): ZIO[Any, Throwable, List[SearchResult]] = { + val urlEncodedQuery = URLEncoder.encode(query, "UTF-8") + val url = s"https://search.maven.org/solrsearch/select?q=$urlEncodedQuery&start=0&rows=60" + for { + string <- ZIO.attempt { + val source = scala.io.Source.fromURL(url) + val string = source.mkString + source.close() + string + } + payload <- ZIO.from(string.fromJson[Payload]).mapError(new Error(_)) + } yield SearchResult + .combineResults(payload.response.docs.map(SearchResult.fromDoc)) + .distinctBy(sr => (sr.group, sr.artifact, sr.latestVersion)) + } + +} + +object SearchExample extends ZIOAppDefault { + + val run: ZIO[Any with ZIOAppArgs with Scope, Any, Any] = { + for { + _ <- ZIO.unit + search = Search() + results <- search.search("zio-cli") +// _ <- Choose.run(results)(_.render) + _ <- ZIO.debug( + View + .vertical( + Chunk( + View.text("LATEST PACKAGES").blue, + View.text("───────────────").blue.dim + ) ++ + results.map(_.render): _* + ) + .render(100, 10) + ) + } yield results + }.provide(TUI.live(false)) + +} diff --git a/src/main/scala/update/versions/VersionsLive.scala b/src/main/scala/update/versions/VersionsLive.scala index 0f62f59..8daf0a8 100644 --- a/src/main/scala/update/versions/VersionsLive.scala +++ b/src/main/scala/update/versions/VersionsLive.scala @@ -38,7 +38,7 @@ final case class VersionsLive() extends Versions { .map(_.left.map(new Error(_))) .absolve .map { case (versions, _) => - versions.available.map(Version) + versions.available.map(Version(_)) } .catchSome { case e if e.getMessage.contains("not found") => diff --git a/src/test/scala/update/UpdateOptionsSpec.scala b/src/test/scala/update/UpdateOptionsSpec.scala new file mode 100644 index 0000000..750b1bd --- /dev/null +++ b/src/test/scala/update/UpdateOptionsSpec.scala @@ -0,0 +1,83 @@ +package update + +import zio.test._ + +object UpdateOptionsSpec extends ZIOSpecDefault { + def spec = + suite("UpdateOptionsSpec")( + test("correctly categorizes updates") { + val result = UpdateOptions.getOptions( + Version("1.0.0"), + List( + Version("1.0.0"), + Version("1.0.1"), + Version("1.0.2"), + Version("1.1.2"), + Version("1.2.0"), + Version("1.2.1"), + Version("2.2.2"), + Version("2.2.3") + ) + ) + + val expected = + UpdateOptions( + major = Some(Version("2.2.3")), + minor = Some(Version("1.2.1")), + patch = Some(Version("1.0.2")), + preRelease = None + ) + + assertTrue(result == expected) + }, + test("correctly sorts dependencies") { + val result = UpdateOptions.getOptions( + Version("1.9.0"), + List( + Version("1.10.0"), + Version("1.5.1"), + Version("1.7.0"), + Version("1.10.1"), + Version("1.5.0"), + Version("1.6.0"), + Version("1.7.1"), + Version("1.8.0"), + Version("1.9.0") + ) + ) + + val expected = + UpdateOptions( + major = None, + minor = Some(Version("1.10.1")), + patch = None, + preRelease = None + ) + + assertTrue(result == expected) + }, + test("sorts pre-releases") { + + val result = UpdateOptions.getOptions( + Version("1.0.0"), + List( + Version("1.0.1"), + Version("2.0.0-RC2"), + Version("2.0.0-M3"), + Version("2.0.0-RC1") + ) + ) + + val expected = + UpdateOptions( + major = None, + minor = None, + patch = Some(Version("1.0.1")), + preRelease = Some(Version("2.0.0-RC2")) + ) + + assertTrue(result == expected) + + } + ) +} diff --git a/src/test/scala/update/VersionSpec.scala b/src/test/scala/update/VersionSpec.scala new file mode 100644 index 0000000..9b9ae69 --- /dev/null +++ b/src/test/scala/update/VersionSpec.scala @@ -0,0 +1,41 @@ +package update + +import zio.test._ + +object VersionSpec extends ZIOSpecDefault { + def spec = suite("Version")( + test("ordering") { + val versions = List( + Version("1.10.0"), + Version("1.10.1"), + Version("1.5.0"), + Version("1.5.1"), + Version("1.6.0"), + Version("1.7.0"), + Version("1.7.1"), + Version("1.8.0"), + Version("2.0.0"), + Version("1.9.0") + ) + + val expected = + List( + Version("1.5.0"), + Version("1.5.1"), + Version("1.6.0"), + Version("1.7.0"), + Version("1.7.1"), + Version("1.8.0"), + Version("1.9.0"), + Version("1.10.0"), + Version("1.10.1"), + Version("2.0.0") + ) + + val result: List[Version] = + versions.sorted + + assertTrue(result == expected) + } + ) +}