From 37ef8cd20f0fc649350340fa2273aaf50113edb2 Mon Sep 17 00:00:00 2001 From: Sait Sami Kocatas Date: Mon, 21 Dec 2020 14:01:47 +0100 Subject: [PATCH] implement scala optimized sbt imports, refactor --- build.sbt | 28 +-- .../search}/logging/PresetLogger.scala | 4 +- .../scala/org/slf4j/impl/ExternalLogger.scala | 2 +- project/Dependencies.scala | 167 ++++-------------- readme.md | 22 +-- src/main/resources/reference.conf | 3 +- .../com/deliganli/maven/search/Domain.scala | 30 ++-- .../deliganli/maven/search/Environment.scala | 79 ++++----- .../deliganli/maven/search/Formatter.scala | 28 --- .../deliganli/maven/search/Interpreter.scala | 38 ++++ .../com/deliganli/maven/search/Main.scala | 12 +- .../deliganli/maven/search/MavenClient.scala | 26 --- .../com/deliganli/maven/search/Params.scala | 65 ++++--- .../com/deliganli/maven/search/Program.scala | 96 ---------- .../com/deliganli/maven/search/Query.scala | 23 --- .../deliganli/maven/search/Transformer.scala | 39 ---- .../com/deliganli/maven/search/Visual.scala | 23 --- .../maven/search/commandline/package.scala | 52 ++++++ .../maven/search/{ => dsl}/Clipboard.scala | 2 +- .../com/deliganli/maven/search/dsl/Copy.scala | 35 ++++ .../maven/search/dsl/EventMapper.scala | 38 ++++ .../maven/search/dsl/MavenClient.scala | 48 +++++ .../maven/search/dsl/ModelOperator.scala | 96 ++++++++++ .../com/deliganli/maven/search/dsl/Move.scala | 43 +++++ .../deliganli/maven/search/dsl/Program.scala | 32 ++++ .../deliganli/maven/search/dsl/Prompt.scala | 42 +++++ .../deliganli/maven/search/dsl/Search.scala | 45 +++++ .../maven/search/{ => dsl}/Terminal.scala | 24 ++- .../search/{circe => json}/package.scala | 20 ++- .../deliganli/maven/search/ProgramTest.scala | 45 ++++- 30 files changed, 684 insertions(+), 523 deletions(-) rename logging/src/main/scala/com/deliganli/{core => maven/search}/logging/PresetLogger.scala (78%) delete mode 100644 src/main/scala/com/deliganli/maven/search/Formatter.scala create mode 100644 src/main/scala/com/deliganli/maven/search/Interpreter.scala delete mode 100644 src/main/scala/com/deliganli/maven/search/MavenClient.scala delete mode 100644 src/main/scala/com/deliganli/maven/search/Program.scala delete mode 100644 src/main/scala/com/deliganli/maven/search/Query.scala delete mode 100644 src/main/scala/com/deliganli/maven/search/Transformer.scala delete mode 100644 src/main/scala/com/deliganli/maven/search/Visual.scala create mode 100644 src/main/scala/com/deliganli/maven/search/commandline/package.scala rename src/main/scala/com/deliganli/maven/search/{ => dsl}/Clipboard.scala (93%) create mode 100644 src/main/scala/com/deliganli/maven/search/dsl/Copy.scala create mode 100644 src/main/scala/com/deliganli/maven/search/dsl/EventMapper.scala create mode 100644 src/main/scala/com/deliganli/maven/search/dsl/MavenClient.scala create mode 100644 src/main/scala/com/deliganli/maven/search/dsl/ModelOperator.scala create mode 100644 src/main/scala/com/deliganli/maven/search/dsl/Move.scala create mode 100644 src/main/scala/com/deliganli/maven/search/dsl/Program.scala create mode 100644 src/main/scala/com/deliganli/maven/search/dsl/Prompt.scala create mode 100644 src/main/scala/com/deliganli/maven/search/dsl/Search.scala rename src/main/scala/com/deliganli/maven/search/{ => dsl}/Terminal.scala (61%) rename src/main/scala/com/deliganli/maven/search/{circe => json}/package.scala (60%) diff --git a/build.sbt b/build.sbt index 4d9a577..a61e4c8 100644 --- a/build.sbt +++ b/build.sbt @@ -4,28 +4,12 @@ lazy val `maven-search` = (project in file(".")) .aggregate(logging) .dependsOn(logging) - .enablePlugins(UniversalPlugin, JvmPlugin) + .enablePlugins(JavaAppPackaging) .settings( name := "maven-search", common, - Deployment.assemblySettings, - libraryDependencies ++= Seq( - scopt, - "net.java.dev.jna" % "jna" % "5.6.0" - //"org.fusesource.jansi" % "jansi" % "1.18" - ) ++ Seq( - "org.jline" % "jline-terminal" % Versions.jline, - "org.jline" % "jline-reader" % Versions.jline, - "org.jline" % "jline-console" % Versions.jline, - "org.jline" % "jline-terminal-jna" % Versions.jline - //"org.jline" % "jline-terminal-jansi" % Versions.jline - ) ++ Seq( - "io.circe" %% "circe-core" % Versions.circe, - circeConfig, - http4sCirce - ) - ++ http4sClient - ++ scalatest + deployment, + libraryDependencies ++= Seq(scopt) ++ scalatest ++ jline.jna ++ circe ++ http4sClient ) lazy val logging = (project in file("logging")) @@ -44,3 +28,9 @@ lazy val common = Seq( addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1"), addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.11.0" cross CrossVersion.full) ) + +lazy val deployment = Seq( + maintainer := "Sait Sami Kocatas", + packageSummary := "Command line program searches given query on maven", + executableScriptName := "mvns" +) diff --git a/logging/src/main/scala/com/deliganli/core/logging/PresetLogger.scala b/logging/src/main/scala/com/deliganli/maven/search/logging/PresetLogger.scala similarity index 78% rename from logging/src/main/scala/com/deliganli/core/logging/PresetLogger.scala rename to logging/src/main/scala/com/deliganli/maven/search/logging/PresetLogger.scala index 71753fc..4aa8aec 100644 --- a/logging/src/main/scala/com/deliganli/core/logging/PresetLogger.scala +++ b/logging/src/main/scala/com/deliganli/maven/search/logging/PresetLogger.scala @@ -1,8 +1,8 @@ -package com.deliganli.core.logging +package com.deliganli.maven.search.logging import cats.effect.{Concurrent, ContextShift, Resource, Timer} -import io.odin._ import io.odin.formatter.Formatter +import io.odin.{asyncFileLogger, Level, Logger} object PresetLogger { diff --git a/logging/src/main/scala/org/slf4j/impl/ExternalLogger.scala b/logging/src/main/scala/org/slf4j/impl/ExternalLogger.scala index 20d3124..09a36e0 100644 --- a/logging/src/main/scala/org/slf4j/impl/ExternalLogger.scala +++ b/logging/src/main/scala/org/slf4j/impl/ExternalLogger.scala @@ -1,7 +1,7 @@ package org.slf4j.impl import cats.effect.{Clock, ContextShift, Effect, IO, Timer} -import com.deliganli.core.logging.PresetLogger +import com.deliganli.maven.search.logging.PresetLogger import io.odin._ import io.odin.slf4j.OdinLoggerBinder diff --git a/project/Dependencies.scala b/project/Dependencies.scala index edd33b0..b6c0102 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -3,67 +3,21 @@ import sbt._ object Dependencies { object Versions { - val scalatest = "3.2.0" - val cats = "2.2.0" - val osLib = "0.3.0" - val circeFs2 = "0.13.0" - val circe = "0.13.0" - val circeConfig = "0.7.0" - val fs2 = "2.1.0" - val enumeratum = "1.5.15" - val enumeratumCirce = "1.5.21" - val breeze = "1.0" - val shapeless = "2.3.3" - val mockito = "1.16.0" - val jodaDateTime = "2.10.3" - val http4s = "0.21.7" - val http4sJdk = "0.3.1" - val scopt = "4.0.0-RC2" - val jsoup = "1.13.1" - val doobie = "0.9.0" - val odin = "0.9.1" - val flyway = "6.2.3" - val tsec = "0.2.0" - val awsS3 = "2.13.10" - val monocle = "2.0.0" - val gcVision = "1.99.3" - val gcStorage = "1.107.0" - val pdfbox = "2.0.19" - val jbig2 = "3.0.3" - val itext = "7.1.11" - val jline = "3.16.0" + val scalatest = "3.2.0" + val cats = "2.2.0" + val circe = "0.13.0" + val circeConfig = "0.8.0" + val mockito = "1.16.0" + val http4s = "0.21.7" + val http4sJdk = "0.3.1" + val scopt = "4.0.0-RC2" + val odin = "0.9.1" + val jline = "3.16.0" } val circe = Seq( - "io.circe" %% "circe-core" % Versions.circe, - "io.circe" %% "circe-generic" % Versions.circe, - "io.circe" %% "circe-generic-extras" % Versions.circe, - "io.circe" %% "circe-parser" % Versions.circe, - "io.circe" %% "circe-shapes" % Versions.circe - ) - - val doobie = Seq( - "org.tpolecat" %% "doobie-core" % Versions.doobie, - "org.tpolecat" %% "doobie-hikari" % Versions.doobie, - "org.tpolecat" %% "doobie-quill" % Versions.doobie, - "org.tpolecat" %% "doobie-scalatest" % Versions.doobie % "test" - ) - - val fs2 = Seq( - "co.fs2" %% "fs2-core" % Versions.fs2, - "co.fs2" %% "fs2-io" % Versions.fs2, - "co.fs2" %% "fs2-reactive-streams" % Versions.fs2, - "co.fs2" %% "fs2-experimental" % Versions.fs2 - ) - - val enumeratum = Seq( - "com.beachape" %% "enumeratum" % Versions.enumeratum, - "com.beachape" %% "enumeratum-circe" % Versions.enumeratumCirce - ) - - val breeze = Seq( - "org.scalanlp" %% "breeze" % Versions.breeze - // "org.scalanlp" %% "breeze-natives" % Versions.breeze + "io.circe" %% "circe-core" % Versions.circe, + "io.circe" %% "circe-config" % Versions.circeConfig ) val scalatest = Seq( @@ -81,88 +35,35 @@ object Dependencies { val http4sClient = Seq( "org.http4s" %% "http4s-blaze-client" % Versions.http4s, + "org.http4s" %% "http4s-circe" % Versions.http4s, "org.http4s" %% "http4s-jdk-http-client" % Versions.http4sJdk ) - val jline = Seq( - "org.jline" % "jline-terminal" % Versions.jline, - "org.jline" % "jline-reader" % Versions.jline, - "org.jline" % "jline-console" % Versions.jline, - "org.jline" % "jline-terminal-jna" % Versions.jline, - "org.jline" % "jline-terminal-jansi" % Versions.jline - ) - - val http4sServer = Seq( - "org.http4s" %% "http4s-dsl", - "org.http4s" %% "http4s-blaze-server" - ).map(_ % Versions.http4s) - - val http4sCirce = "org.http4s" %% "http4s-circe" % Versions.http4s - val odin = Seq( "com.github.valskalla" %% "odin-core", - "com.github.valskalla" %% "odin-json", - "com.github.valskalla" %% "odin-extras", "com.github.valskalla" %% "odin-slf4j" ).map(_ % Versions.odin) - val tsec = Seq( - "io.github.jmcardon" %% "tsec-common", - "io.github.jmcardon" %% "tsec-password", - "io.github.jmcardon" %% "tsec-cipher-jca", - "io.github.jmcardon" %% "tsec-cipher-bouncy", - "io.github.jmcardon" %% "tsec-mac", - "io.github.jmcardon" %% "tsec-signatures", - "io.github.jmcardon" %% "tsec-hash-jca", - "io.github.jmcardon" %% "tsec-hash-bouncy", - //"io.github.jmcardon" %% "tsec-libsodium", - "io.github.jmcardon" %% "tsec-jwt-mac", - "io.github.jmcardon" %% "tsec-jwt-sig", - "io.github.jmcardon" %% "tsec-http4s" - ).map(_ % Versions.tsec) - - val pdfbox = Seq( - "org.apache.pdfbox" % "pdfbox" - ).map(_ % Versions.pdfbox) ++ Seq( - "org.apache.pdfbox" % "jbig2-imageio" % Versions.jbig2 - ) - - val itext = Seq( - "com.itextpdf" % "kernel", // always needed - "com.itextpdf" % "io", // always needed - "com.itextpdf" % "layout", // always needed - "com.itextpdf" % "forms", // only needed for forms - "com.itextpdf" % "pdfa", // only needed for PDF/A - "com.itextpdf" % "sign", // only needed for digital signatures - "com.itextpdf" % "barcodes", // only needed for barcodes - "com.itextpdf" % "font-asian", // only needed for Asian fonts - "com.itextpdf" % "hyph" // only needed for hyphenation - ).map(_ % Versions.itext) - - val opencv = Seq( - //"org.openpnp" % "opencv" % "4.3.0-2", - "org.bytedeco" % "javacv-platform" % "1.5.3" - ) - - val monocle = Seq( - "com.github.julien-truffaut" %% "monocle-core" % Versions.monocle, - "com.github.julien-truffaut" %% "monocle-macro" % Versions.monocle, - "com.github.julien-truffaut" %% "monocle-law" % Versions.monocle % "test" - ) + object jline { + + val base = Seq( + "org.jline" % "jline-terminal" % Versions.jline, + "org.jline" % "jline-reader" % Versions.jline, + "org.jline" % "jline-console" % Versions.jline + ) + + def jna = + base ++ Seq( + "org.jline" % "jline-terminal-jna" % Versions.jline, + "net.java.dev.jna" % "jna" % "5.6.0" + ) + + def jansi = + base ++ Seq( + "org.jline" % "jline-terminal-jansi" % Versions.jline, + "org.fusesource.jansi" % "jansi" % "1.18" + ) + } - val sqlite = "org.xerial" % "sqlite-jdbc" % "3.32.3.2" - val postgres = "org.tpolecat" %% "doobie-postgres" % Versions.doobie - val gcVision = "com.google.cloud" % "google-cloud-vision" % Versions.gcVision - val gcStorage = "com.google.cloud" % "google-cloud-storage" % Versions.gcStorage - val awsS3 = "software.amazon.awssdk" % "s3" % Versions.awsS3 - val tesseract = "net.sourceforge.tess4j" % "tess4j" % "4.5.1" - val flyway = "org.flywaydb" % "flyway-core" % Versions.flyway - val circeFs2 = "io.circe" %% "circe-fs2" % Versions.circeFs2 - val circeConfig = "io.circe" %% "circe-config" % Versions.circeConfig - val osLib = "com.lihaoyi" %% "os-lib" % Versions.osLib - val shapeless = "com.chuusai" %% "shapeless" % Versions.shapeless - val jodaTime = "joda-time" % "joda-time" % Versions.jodaDateTime - val scopt = "com.github.scopt" %% "scopt" % Versions.scopt - val jsoup = "org.jsoup" % "jsoup" % Versions.jsoup - val console4cats = "dev.profunktor" %% "console4cats" % "0.8.1" + val scopt = "com.github.scopt" %% "scopt" % Versions.scopt } diff --git a/readme.md b/readme.md index a032afa..5f7210f 100644 --- a/readme.md +++ b/readme.md @@ -1,20 +1,20 @@ Search packages in maven through console, sbt style dependency string can be copied to clipboard ```bash - mvns cats + ./mvns cats ``` ```text -$ java -jar maven-search.jar cats - [1] org.typelevel : cats-tests_2.10 : 0.6.0-M1 - [2] org.typelevel : cats-tests_2.11 : 0.6.0-M1 - [3] org.typelevel : cats-tests_sjs0.6_2.10 : 0.6.0-M1 - [4] org.typelevel : cats-tests_sjs0.6_2.11 : 0.6.0-M1 - [5] org.typelevel : cats-docs_2.10 : 0.6.0-M1 - [6] org.typelevel : cats-docs_2.11 : 0.6.0-M1 - [7] org.typelevel : cats-bench_2.10 : 0.6.0-M1 - [8] org.typelevel : cats-bench_2.11 : 0.6.0-M1 - [9] dev.profunktor : redis4cats-log4cats_2.12 : 0.11.0 +$ ./mvns cats + [1] org.typelevel %% cats-parse % 0.2-41-437af75 + [2] org.typelevel %% cats-core % 2.3.1 + [3] com.flowtick %% graphs-cats % 0.5.0 + [4] org.typelevel %% cats-testkit % 2.3.1 + [5] org.typelevel %% cats-core % 2.3.0-M2 + [6] org.typelevel %%% cats-tests % 0.6.0-M1 + [7] org.typelevel %% cats-tests % 0.6.0-M1 + [8] org.scodec %% scodec-cats % 1.1.0-M4 + [9] io.regadas %% scio-cats % 0.1.3 Page:1 Select a number to copy to clipboard (1 - 9, n:next page): ``` diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index 39c9efc..0e7bacc 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -1,8 +1,9 @@ maven-search { - copyToClipboard = true + query = "" chunkSize = 90 importFormat = "sbt" debug = false mavenUri = "https://search.maven.org/solrsearch/select" itemPerPage = 9 + clearScreen = true } \ No newline at end of file diff --git a/src/main/scala/com/deliganli/maven/search/Domain.scala b/src/main/scala/com/deliganli/maven/search/Domain.scala index 5ebaa49..d681687 100644 --- a/src/main/scala/com/deliganli/maven/search/Domain.scala +++ b/src/main/scala/com/deliganli/maven/search/Domain.scala @@ -1,7 +1,6 @@ package com.deliganli.maven.search import com.deliganli.maven.search.Domain.MavenModel.MavenDoc -import org.http4s.Uri object Domain { @@ -19,16 +18,27 @@ object Domain { sealed trait ImportFormat object ImportFormat { - class Sbt extends ImportFormat - class Maven extends ImportFormat + case object Sbt extends ImportFormat + //case object Maven extends ImportFormat + case object Generic extends ImportFormat } - case class Config( - mavenUri: Uri, - chunkSize: Int, - itemPerPage: Int, - copyToClipboard: Boolean, - importFormat: ImportFormat, - debug: Boolean) + sealed trait ProgramEvent + + object ProgramEvent { + case class Search(page: Int) extends ProgramEvent + case object Prompt extends ProgramEvent + case class Copy(selection: Int) extends ProgramEvent + case class Move(page: Int) extends ProgramEvent + case object Exit extends ProgramEvent + } + + sealed trait UserEvent + + object UserEvent { + case object Next extends UserEvent + case object Prev extends UserEvent + case class Selection(i: Int) extends UserEvent + } } diff --git a/src/main/scala/com/deliganli/maven/search/Environment.scala b/src/main/scala/com/deliganli/maven/search/Environment.scala index 5b5e358..09fd2fc 100644 --- a/src/main/scala/com/deliganli/maven/search/Environment.scala +++ b/src/main/scala/com/deliganli/maven/search/Environment.scala @@ -3,58 +3,49 @@ package com.deliganli.maven.search import java.net.http.HttpClient import java.time.Duration -import cats.effect.{ConcurrentEffect, ContextShift, Resource, Sync, Timer} +import cats.effect.{Concurrent, ConcurrentEffect, ContextShift, Resource, Sync, Timer} import cats.implicits._ -import com.deliganli.core.logging.PresetLogger -import com.deliganli.maven.search.Domain.Config -import com.deliganli.maven.search.Domain.ImportFormat.Sbt -import com.deliganli.maven.search.circe._ +import com.deliganli.maven.search.Params.Config +import com.deliganli.maven.search.dsl._ +import com.deliganli.maven.search.json._ +import com.deliganli.maven.search.logging.PresetLogger import com.typesafe.config.ConfigFactory import io.circe.config.parser import io.odin.Logger -import org.http4s.client.jdkhttpclient.JdkHttpClient +import org.http4s.client.blaze.BlazeClientBuilder import org.http4s.client.{middleware, Client} +import scala.concurrent.ExecutionContext + case class Environment[F[_]]( - params: Params, config: Config, logger: Logger[F], terminal: Terminal[F], clipboard: Clipboard[F], - formatter: Formatter[Sbt], - transformer: Transformer, + eventMapper: EventMapper, maven: MavenClient[F]) object Environment { - def create[F[_]: ConcurrentEffect: ContextShift: Timer](params: Params): Resource[F, Environment[F]] = { + def create[F[_]: ConcurrentEffect: ContextShift: Timer](loader: ClassLoader, params: Params): Resource[F, Environment[F]] = { for { - logger <- PresetLogger.default[F]() - config <- Resource.liftF(loadConfig(params)) - clipboard <- Resource.liftF(Clipboard.system[F]) - mavenClient <- Resource.pure[F, MavenClient[F]](buildMavenClient(logger, config, params)) - terminal <- Terminal.sync[F] - transformer <- Resource.pure[F, Transformer](Transformer.dsl[F](config)) - } yield Environment(params, config, logger, terminal, clipboard, Formatter.sbt, transformer, mavenClient) + config <- Resource.liftF[F, Config](loadConfig(loader, params)) + logger <- buildLogger[F](config) + clipboard <- Resource.liftF[F, Clipboard[F]](Clipboard.system[F]) + terminal <- Terminal.sync[F](config) + mavenClient <- buildMavenClient(logger, config) + eventMapper <- Resource.pure[F, EventMapper](EventMapper.dsl(config)) + } yield Environment(config, logger, terminal, clipboard, eventMapper, mavenClient) + } + + private def buildLogger[F[_]: Concurrent: ContextShift: Timer](config: Config): Resource[F, Logger[F]] = { + if (config.debug) PresetLogger.default[F]() else Resource.pure[F, Logger[F]](Logger.noop[F]) } - private def loadConfig[F[_]: Sync](params: Params): F[Config] = { + def loadConfig[F[_]: Sync](classLoader: ClassLoader, params: Params): F[Config] = { Sync[F] - .fromEither { - val c = ConfigFactory.load(Main.getClass.getClassLoader) - val underlying = c.getConfig("maven-search") - parser.decode[Config](underlying)(config) - } - .map { config => - Config( - mavenUri = config.mavenUri, - chunkSize = config.chunkSize, - itemPerPage = config.itemPerPage, - copyToClipboard = params.copyToClipboard.getOrElse(config.copyToClipboard), - importFormat = params.importFormat.getOrElse(config.importFormat), - debug = params.debug.getOrElse(config.debug) - ) - } + .fromEither(parser.decode[Config](ConfigFactory.load(classLoader).getConfig("maven-search"))(config)) + .map(config => Params.merge(params, config)) } private def defaultHttpClientBuilder[F[_]: ConcurrentEffect: ContextShift] = { @@ -64,25 +55,23 @@ object Environment { .connectTimeout(Duration.ofSeconds(60)) } - private def jdkHttpClient[F[_]: ConcurrentEffect: ContextShift]: Client[F] = { + /*private def jdkHttpClient[F[_]: ConcurrentEffect: ContextShift]: Client[F] = { JdkHttpClient(defaultHttpClientBuilder.build()) - } + }*/ - private def loggingJdkHttpClient[F[_]: ConcurrentEffect: ContextShift](L: Logger[F]): Client[F] = { + private def loggingJdkHttpClient[F[_]: ConcurrentEffect: ContextShift](L: Logger[F], client: Client[F]): Client[F] = { val logger = (s: String) => L.debug(s) - middleware.Logger(logHeaders = true, logBody = true, logAction = logger.some)(jdkHttpClient) + middleware.Logger(logHeaders = true, logBody = true, logAction = logger.some)(client) } - private def buildMavenClient[F[_]: ConcurrentEffect: ContextShift: Timer]( + def buildMavenClient[F[_]: ConcurrentEffect: ContextShift: Timer]( logger: Logger[F], - config: Config, - params: Params - ): MavenClient[F] = { - val client = if (config.debug) loggingJdkHttpClient[F](logger) else jdkHttpClient[F] - val mavenClient = MavenClient.dsl(client, params, config) - - mavenClient + config: Config + ): Resource[F, MavenClient[F]] = { + BlazeClientBuilder[F](ExecutionContext.Implicits.global).resource + .map(client => if (config.debug) loggingJdkHttpClient[F](logger, client) else client) + .map(client => MavenClient.dsl(client, config)) } } diff --git a/src/main/scala/com/deliganli/maven/search/Formatter.scala b/src/main/scala/com/deliganli/maven/search/Formatter.scala deleted file mode 100644 index 782732c..0000000 --- a/src/main/scala/com/deliganli/maven/search/Formatter.scala +++ /dev/null @@ -1,28 +0,0 @@ -package com.deliganli.maven.search - -import com.deliganli.maven.search.Domain.ImportFormat.Sbt -import com.deliganli.maven.search.Domain.MavenModel.MavenDoc - -import scala.util.matching.Regex - -trait Formatter[T] { - def format(doc: MavenDoc): String -} - -object Formatter { - def apply[T](implicit ev: Formatter[T]) = ev - - implicit val sbt: Formatter[Sbt] = new Formatter[Sbt] { - val scalaVersioned: Regex = """(.*)_\d*.\d*""".r - val scalaJsVersioned: Regex = """(.*)_sjs\d*.\d*""".r - - override def format(doc: MavenDoc): String = { - val artifact = None - .orElse(scalaJsVersioned.findFirstMatchIn(doc.a).map(m => s"""%%% "${m.group(1)}"""")) - .orElse(scalaVersioned.findFirstMatchIn(doc.a).map(m => s"""%% "${m.group(1)}"""")) - .getOrElse(s"""% "${doc.a}"""") - - raw""""${doc.g}" ${artifact} % "${doc.v}"""" - } - } -} diff --git a/src/main/scala/com/deliganli/maven/search/Interpreter.scala b/src/main/scala/com/deliganli/maven/search/Interpreter.scala new file mode 100644 index 0000000..8dd07d4 --- /dev/null +++ b/src/main/scala/com/deliganli/maven/search/Interpreter.scala @@ -0,0 +1,38 @@ +package com.deliganli.maven.search + +import cats.effect.concurrent.Ref +import cats.effect.{ConcurrentEffect, ContextShift, Timer} +import cats.implicits._ +import cats.{Applicative, Monad} +import com.deliganli.maven.search.Domain.{ImportFormat, ProgramEvent} +import com.deliganli.maven.search.dsl.ModelOperator.{Generic, ScalaGrouped} +import com.deliganli.maven.search.dsl.{ModelOperator, Program} + +object Interpreter { + case class State[A](page: Int, items: List[A]) + + def task[F[_]: ConcurrentEffect: ContextShift: Timer](env: Environment[F]): F[Unit] = { + env.config.importFormat match { + case ImportFormat.Sbt => + Ref.of(State(0, List.empty[ScalaGrouped])).flatMap { ref => + implicit val P: Program[F, ScalaGrouped] = Program.dsl(ref, ModelOperator.sbt, env) + interpret[F, ScalaGrouped](ProgramEvent.Search(1)) + } + + case _ => + Ref.of(State(0, List.empty[Generic])).flatMap { ref => + implicit val P: Program[F, Generic] = Program.dsl(ref, ModelOperator.generic, env) + interpret[F, Generic](ProgramEvent.Search(1)) + } + } + } + + def interpret[F[_]: Monad: Program[*[_], A], A](event: ProgramEvent): F[Unit] = + event match { + case ProgramEvent.Search(page) => Program[F, A].search(page) *> interpret[F, A](ProgramEvent.Prompt) + case ProgramEvent.Move(page) => Program[F, A].move(page) *> interpret[F, A](ProgramEvent.Prompt) + case ProgramEvent.Prompt => Program[F, A].prompt().flatMap(interpret[F, A]) + case ProgramEvent.Copy(selection) => Program[F, A].copy(selection) + case ProgramEvent.Exit => Applicative[F].unit + } +} diff --git a/src/main/scala/com/deliganli/maven/search/Main.scala b/src/main/scala/com/deliganli/maven/search/Main.scala index fe1f2fb..2530c31 100644 --- a/src/main/scala/com/deliganli/maven/search/Main.scala +++ b/src/main/scala/com/deliganli/maven/search/Main.scala @@ -1,9 +1,8 @@ package com.deliganli.maven.search import cats.Applicative -import cats.effect.{ConcurrentEffect, ContextShift, ExitCode, IO, IOApp, Sync, Timer} +import cats.effect.{ConcurrentEffect, ContextShift, ExitCode, IO, IOApp, Timer} import cats.implicits._ -import com.deliganli.maven.search.Program.{ProgramEvent, State} object Main extends IOApp { @@ -18,13 +17,8 @@ object Main extends IOApp { def task[F[_]: ConcurrentEffect: ContextShift: Timer](params: Params): F[Unit] = { Environment - .create(params) - .use(entrypoint[F]) + .create(getClass.getClassLoader, params) + .use(Interpreter.task[F]) .widen } - - def entrypoint[F[_]: Sync](env: Environment[F]): F[Unit] = { - Program.interpret(env, State(0, Nil))(ProgramEvent.Search(1)) - } - } diff --git a/src/main/scala/com/deliganli/maven/search/MavenClient.scala b/src/main/scala/com/deliganli/maven/search/MavenClient.scala deleted file mode 100644 index b2408f3..0000000 --- a/src/main/scala/com/deliganli/maven/search/MavenClient.scala +++ /dev/null @@ -1,26 +0,0 @@ -package com.deliganli.maven.search - -import cats.effect.Sync -import com.deliganli.maven.search.Domain.{Config, MavenModel} -import com.deliganli.maven.search.circe._ -import org.http4s.circe.CirceEntityCodec.circeEntityDecoder -import org.http4s.client.Client - -trait MavenClient[F[_]] { - def search(page: Int): F[MavenModel] -} - -object MavenClient { - - def dsl[F[_]: Sync](httpClient: Client[F], params: Params, config: Config): MavenClient[F] = { - new MavenClient[F] { - override def search(page: Int): F[MavenModel] = { - val start = (page - 1) * config.itemPerPage - val query = Query.search(config.mavenUri, params.query, start, config.chunkSize) - val result = httpClient.get(query)(_.as[MavenModel]) - - result - } - } - } -} diff --git a/src/main/scala/com/deliganli/maven/search/Params.scala b/src/main/scala/com/deliganli/maven/search/Params.scala index 2ed0a98..4a9bfad 100644 --- a/src/main/scala/com/deliganli/maven/search/Params.scala +++ b/src/main/scala/com/deliganli/maven/search/Params.scala @@ -2,46 +2,43 @@ package com.deliganli.maven.search import cats.effect.Sync import com.deliganli.maven.search.Domain.ImportFormat -import com.deliganli.maven.search.Domain.ImportFormat.{Maven, Sbt} -import scopt.Read +import com.deliganli.maven.search.commandline._ +import org.http4s.Uri +import scopt.OParser case class Params( - query: String = "", - importFormat: Option[ImportFormat] = None, - copyToClipboard: Option[Boolean] = None, - debug: Option[Boolean] = None) + query: String, + mavenUri: Option[Uri], + chunkSize: Option[Int], + itemPerPage: Option[Int], + importFormat: Option[ImportFormat], + debug: Option[Boolean], + clearScreen: Option[Boolean]) object Params { - import scopt.OParser - val builder = OParser.builder[Params] - implicit val read: Read[ImportFormat] = Read.reads { - case "sbt" => new Sbt() - case "maven" => new Maven() - } + private def initializer = Params("", None, None, None, None, None, None) + + def parse[F[_]: Sync](args: List[String]): F[Option[Params]] = Sync[F].delay(OParser.parse(paramParser, args, initializer)) - val paramParser = { - import builder._ - OParser.sequence( - programName("mvns"), - head("maven search", "0.1"), - arg[String]("") - .action((x, p) => p.copy(query = x)) - .text("Query string to be searched in maven central"), - opt[Boolean]("no-copy") - .optional() - .action((x, p) => p.copy(copyToClipboard = Some(x))) - .text("Don't copy to clipboard, disables selection altogether"), - opt[ImportFormat]('f', "import-format") - .optional() - .action((x, p) => p.copy(importFormat = Some(x))) - .text("Format to be copied to clipboard, e.g. sbt"), - opt[Boolean]('d', "debug") - .optional() - .action((x, p) => p.copy(debug = Some(x))) - .text("Debug mode") + case class Config( + query: String, + mavenUri: Uri, + chunkSize: Int, + itemPerPage: Int, + importFormat: ImportFormat, + debug: Boolean, + clearScreen: Boolean) + + def merge(params: Params, config: Config): Config = { + Config( + query = params.query, + mavenUri = params.mavenUri.getOrElse(config.mavenUri), + chunkSize = params.chunkSize.getOrElse(config.chunkSize), + itemPerPage = params.itemPerPage.getOrElse(config.itemPerPage), + importFormat = params.importFormat.getOrElse(config.importFormat), + debug = params.debug.getOrElse(config.debug), + clearScreen = params.clearScreen.getOrElse(config.clearScreen) ) } - - def parse[F[_]: Sync](args: List[String]): F[Option[Params]] = Sync[F].delay(OParser.parse(paramParser, args, Params())) } diff --git a/src/main/scala/com/deliganli/maven/search/Program.scala b/src/main/scala/com/deliganli/maven/search/Program.scala deleted file mode 100644 index 309bbea..0000000 --- a/src/main/scala/com/deliganli/maven/search/Program.scala +++ /dev/null @@ -1,96 +0,0 @@ -package com.deliganli.maven.search - -import cats.implicits._ -import cats.{Applicative, Monad} -import com.deliganli.maven.search.Domain.MavenModel -import com.deliganli.maven.search.Domain.MavenModel.MavenDoc -import com.deliganli.maven.search.Program.ProgramEvent._ - -object Program { - - sealed trait ProgramEvent - - object ProgramEvent { - case class Search(page: Int) extends ProgramEvent - case object Prompt extends ProgramEvent - case class Copy(selection: Int) extends ProgramEvent - case class Move(page: Int) extends ProgramEvent - case object Exit extends ProgramEvent - } - - sealed trait UserEvent - - object UserEvent { - case object Next extends UserEvent - case object Prev extends UserEvent - case class Selection(i: Int) extends UserEvent - } - - case class State(page: Int, docs: List[MavenDoc]) - - def interpret[F[_]: Monad]( - env: Environment[F], - state: State - )( - event: ProgramEvent - ): F[Unit] = { - Applicative[F] - .pure(event) - .flatTap(e => env.logger.debug(e.toString)) - .flatMap { - case Prompt => - def proceed(e: UserEvent): F[ProgramEvent] = - Applicative[F].unit - .flatMap(_ => env.logger.debug(s"size: ${state.docs.size}, page:${state.page}")) - .map(_ => env.transformer.userToProgram(state)(e)) - - def terminate(e: String): F[ProgramEvent] = - env.terminal - .putStrLn(s"$e is invalid") - .as(Exit) - - Applicative[F].unit - .flatMap(_ => env.terminal.readChar.map(env.transformer.stringToUserEvent)) - .flatMap(_.fold(terminate, proceed)) - .flatMap(e => interpret(env, state)(e)) - - case Search(page) => - def updateCache(m: MavenModel): State = { - val cursor = state.page * env.config.itemPerPage - if (cursor < state.docs.size) state.copy(page = page) - else State(page, state.docs ++ m.docs) - } - - Applicative[F].unit - .flatMap(_ => env.maven.search(page)) - .flatTap(m => env.terminal.printTable(m.docs.take(env.config.itemPerPage), page)) - .map(m => updateCache(m)) - .flatMap(updatedState => interpret(env, updatedState)(Prompt)) - - case Move(page) => - def updateCache(): (State, List[MavenDoc]) = { - val updated = state.copy(page = page) - val from = state.page * env.config.itemPerPage - val till = from + env.config.itemPerPage - - (updated, state.docs.slice(from, till)) - } - - Applicative[F] - .pure(updateCache()) - .flatTap { case (_, m) => env.terminal.printTable(m, page) } - .flatMap { case (s, _) => interpret(env, s)(Prompt) } - - case Copy(selection) => - Applicative[F] - .pure(state.docs((state.page - 1) * env.config.itemPerPage + selection)) - .map(doc => env.formatter.format(doc)) - .flatTap(fs => env.clipboard.set(fs)) - .flatTap(fs => env.terminal.putStrLn(s"Copied to clipboard: $fs")) - .void - - case Exit => - Applicative[F].unit - } - } -} diff --git a/src/main/scala/com/deliganli/maven/search/Query.scala b/src/main/scala/com/deliganli/maven/search/Query.scala deleted file mode 100644 index 6168138..0000000 --- a/src/main/scala/com/deliganli/maven/search/Query.scala +++ /dev/null @@ -1,23 +0,0 @@ -package com.deliganli.maven.search - -import org.http4s.Uri - -object Query { - - def search( - uri: Uri, - query: String, - start: Int, - rows: Int - ) = - uri - .withQueryParam("start", start) - .withQueryParam("rows", rows) - .withQueryParam("q", query) - - def versions(uri: Uri, g: String, a: String) = - uri - .withQueryParam("rows", 9) - .withQueryParam("q", s"g:$g AND a:$a") - .withQueryParam("core", "gav") -} diff --git a/src/main/scala/com/deliganli/maven/search/Transformer.scala b/src/main/scala/com/deliganli/maven/search/Transformer.scala deleted file mode 100644 index cece4ec..0000000 --- a/src/main/scala/com/deliganli/maven/search/Transformer.scala +++ /dev/null @@ -1,39 +0,0 @@ -package com.deliganli.maven.search - -import cats.data.Validated -import cats.implicits._ -import com.deliganli.maven.search.Domain.Config -import com.deliganli.maven.search.Program.ProgramEvent.{Copy, Exit, Move, Search} -import com.deliganli.maven.search.Program.{ProgramEvent, State, UserEvent} - -trait Transformer { - def userToProgram(state: State)(e: UserEvent): ProgramEvent - - def stringToUserEvent(s: String): Validated[String, UserEvent] -} - -object Transformer { - - def dsl[F[_]](config: Config): Transformer = { - new Transformer { - def userToProgram(state: State)(e: UserEvent): ProgramEvent = { - def expected: PartialFunction[UserEvent, ProgramEvent] = { - case UserEvent.Next if state.docs.size > state.page * config.itemPerPage => Move(state.page + 1) - case UserEvent.Next if state.docs.size % config.itemPerPage == 0 => Search(state.page + 1) - case UserEvent.Prev if state.page > 1 => Move(state.page - 1) - case UserEvent.Selection(i) => Copy(i) - } - - expected.applyOrElse[UserEvent, ProgramEvent](e, _ => Exit) - } - - def stringToUserEvent(s: String): Validated[String, UserEvent] = { - s match { - case s if s == "n" || s == "N" => UserEvent.Next.valid - case s if s == "p" || s == "P" => UserEvent.Prev.valid - case s => s.toIntOption.map(UserEvent.Selection).toValid(s) - } - } - } - } -} diff --git a/src/main/scala/com/deliganli/maven/search/Visual.scala b/src/main/scala/com/deliganli/maven/search/Visual.scala deleted file mode 100644 index 4009cf4..0000000 --- a/src/main/scala/com/deliganli/maven/search/Visual.scala +++ /dev/null @@ -1,23 +0,0 @@ -package com.deliganli.maven.search - -import com.deliganli.maven.search.Domain.MavenModel.MavenDoc - -object Visual { - - def buildTable(docs: List[MavenDoc]): String = { - val (gSize, aSize, vSize) = docs.foldLeft(0, 0, 0) { - case ((g, a, v), d) => (d.g.length.max(g), d.a.length.max(a), d.v.length.max(v)) - } - - docs.zipWithIndex - .map { case (d, i) => String.format(s"%4s %${gSize}s : %${aSize}s : %${vSize}s", "[" + (i + 1) + "]", d.g, d.a, d.v) } - .mkString("\n") - } - - def message(docs: List[MavenDoc], page: Int): String = { - val prev = if (page > 1) ", p:previous page" else "" - s"""Page:$page - |Select a number to copy to clipboard (1 - ${docs.size}, n:next page${prev}): """.stripMargin - } - -} diff --git a/src/main/scala/com/deliganli/maven/search/commandline/package.scala b/src/main/scala/com/deliganli/maven/search/commandline/package.scala new file mode 100644 index 0000000..85672fb --- /dev/null +++ b/src/main/scala/com/deliganli/maven/search/commandline/package.scala @@ -0,0 +1,52 @@ +package com.deliganli.maven.search + +import cats.implicits._ +import com.deliganli.maven.search.Domain.ImportFormat +import org.http4s.Uri +import scopt.{OParser, Read} + +package object commandline { + val builder = OParser.builder[Params] + + implicit val importFormat: Read[ImportFormat] = Read.reads { + case "sbt" => ImportFormat.Sbt + case "generic" => ImportFormat.Generic + } + + implicit val uri: Read[Uri] = Read.reads(Uri.unsafeFromString) + + val paramParser: OParser[Unit, Params] = { + import builder._ + OParser.sequence( + programName("mvns"), + head("maven search", "0.1"), + arg[String]("") + .action((x, p) => p.copy(query = x)) + .text("Query string to be searched in maven central"), + opt[Uri]("maven-uri") + .optional() + .action((x, p) => p.copy(mavenUri = x.some)) + .text("Use this uri to perform maven style searches"), + opt[Int]("chunk-size") + .optional() + .action((x, p) => p.copy(chunkSize = x.some)) + .text("Amount of the elements that will be requested on each rest call"), + opt[Int]("item-per-page") + .optional() + .action((x, p) => p.copy(itemPerPage = x.some)) + .text("Amount if items to show on each page"), + opt[ImportFormat]('f', "import-format") + .optional() + .action((x, p) => p.copy(importFormat = x.some)) + .text("Format to be copied to clipboard, e.g. sbt"), + opt[Boolean]('d', "debug") + .optional() + .action((x, p) => p.copy(debug = x.some)) + .text("Debug mode"), + opt[Boolean]('c', "clear-screen") + .optional() + .action((x, p) => p.copy(clearScreen = x.some)) + .text("Clear screen on each navigation activity") + ) + } +} diff --git a/src/main/scala/com/deliganli/maven/search/Clipboard.scala b/src/main/scala/com/deliganli/maven/search/dsl/Clipboard.scala similarity index 93% rename from src/main/scala/com/deliganli/maven/search/Clipboard.scala rename to src/main/scala/com/deliganli/maven/search/dsl/Clipboard.scala index 26f5687..8c48b14 100644 --- a/src/main/scala/com/deliganli/maven/search/Clipboard.scala +++ b/src/main/scala/com/deliganli/maven/search/dsl/Clipboard.scala @@ -1,4 +1,4 @@ -package com.deliganli.maven.search +package com.deliganli.maven.search.dsl import java.awt.Toolkit import java.awt.datatransfer.StringSelection diff --git a/src/main/scala/com/deliganli/maven/search/dsl/Copy.scala b/src/main/scala/com/deliganli/maven/search/dsl/Copy.scala new file mode 100644 index 0000000..32ffd59 --- /dev/null +++ b/src/main/scala/com/deliganli/maven/search/dsl/Copy.scala @@ -0,0 +1,35 @@ +package com.deliganli.maven.search.dsl + +import cats.effect.concurrent.Ref +import cats.implicits._ +import cats.{Applicative, Monad} +import com.deliganli.maven.search.Interpreter.State +import com.deliganli.maven.search.Params.Config + +trait Copy[F[_]] { + def copy(selection: Int): F[Unit] +} + +object Copy { + def apply[F[_]](implicit ev: Copy[F]): Copy[F] = ev + + def dsl[F[_]: Monad, A]( + ref: Ref[F, State[A]], + config: Config, + clipboard: Clipboard[F], + terminal: Terminal[F], + pipeline: ModelOperator[A] + ): Copy[F] = + new Copy[F] { + + override def copy(selection: Int): F[Unit] = { + Applicative[F].unit + .flatMap(_ => ref.get) + .map(state => state.items((state.page - 1) * config.itemPerPage + (selection - 1))) + .map(doc => pipeline.format(doc)) + .flatTap(fs => clipboard.set(fs)) + .flatTap(fs => terminal.putStrLn(s"Copied to clipboard: $fs")) + .void + } + } +} diff --git a/src/main/scala/com/deliganli/maven/search/dsl/EventMapper.scala b/src/main/scala/com/deliganli/maven/search/dsl/EventMapper.scala new file mode 100644 index 0000000..2351214 --- /dev/null +++ b/src/main/scala/com/deliganli/maven/search/dsl/EventMapper.scala @@ -0,0 +1,38 @@ +package com.deliganli.maven.search.dsl + +import cats.data.Validated +import cats.implicits._ +import com.deliganli.maven.search.Domain.{ProgramEvent, UserEvent} +import com.deliganli.maven.search.Params.Config + +trait EventMapper { + def userToProgram(size: Int, page: Int)(e: UserEvent): ProgramEvent + + def stringToUserEvent(s: String): Validated[String, UserEvent] +} + +object EventMapper { + + def dsl(config: Config): EventMapper = { + new EventMapper { + def userToProgram(size: Int, page: Int)(e: UserEvent): ProgramEvent = { + def expected: PartialFunction[UserEvent, ProgramEvent] = { + case UserEvent.Next if size > page * config.itemPerPage => ProgramEvent.Move(page + 1) + case UserEvent.Next if size % config.itemPerPage == 0 => ProgramEvent.Search(page + 1) + case UserEvent.Prev if page > 1 => ProgramEvent.Move(page - 1) + case UserEvent.Selection(i) => ProgramEvent.Copy(i) + } + + expected.applyOrElse[UserEvent, ProgramEvent](e, _ => ProgramEvent.Exit) + } + + def stringToUserEvent(s: String): Validated[String, UserEvent] = { + s match { + case s if s == "n" || s == "N" => UserEvent.Next.valid + case s if s == "p" || s == "P" => UserEvent.Prev.valid + case s => s.toIntOption.map(UserEvent.Selection).toValid(s) + } + } + } + } +} diff --git a/src/main/scala/com/deliganli/maven/search/dsl/MavenClient.scala b/src/main/scala/com/deliganli/maven/search/dsl/MavenClient.scala new file mode 100644 index 0000000..be84af3 --- /dev/null +++ b/src/main/scala/com/deliganli/maven/search/dsl/MavenClient.scala @@ -0,0 +1,48 @@ +package com.deliganli.maven.search.dsl + +import cats.effect.Sync +import com.deliganli.maven.search.Domain.MavenModel +import com.deliganli.maven.search.Params.Config +import com.deliganli.maven.search.json._ +import org.http4s.Uri +import org.http4s.circe.CirceEntityCodec.circeEntityDecoder +import org.http4s.client.Client + +trait MavenClient[F[_]] { + def search(page: Int): F[MavenModel] +} + +object MavenClient { + + object Query { + + def search( + uri: Uri, + query: String, + start: Int, + rows: Int + ) = + uri + .withQueryParam("start", start) + .withQueryParam("rows", rows) + .withQueryParam("q", query) + + def versions(uri: Uri, g: String, a: String) = + uri + .withQueryParam("rows", 9) + .withQueryParam("q", s"g:$g AND a:$a") + .withQueryParam("core", "gav") + } + + def dsl[F[_]: Sync](httpClient: Client[F], config: Config): MavenClient[F] = { + new MavenClient[F] { + override def search(page: Int): F[MavenModel] = { + val start = (page - 1) * config.itemPerPage + val query = Query.search(config.mavenUri, config.query, start, config.chunkSize) + val result = httpClient.get(query)(_.as[MavenModel]) + + result + } + } + } +} diff --git a/src/main/scala/com/deliganli/maven/search/dsl/ModelOperator.scala b/src/main/scala/com/deliganli/maven/search/dsl/ModelOperator.scala new file mode 100644 index 0000000..5781e91 --- /dev/null +++ b/src/main/scala/com/deliganli/maven/search/dsl/ModelOperator.scala @@ -0,0 +1,96 @@ +package com.deliganli.maven.search.dsl + +import cats.implicits._ +import com.deliganli.maven.search.Domain.MavenModel.MavenDoc + +trait ModelOperator[A] { + def mavenToModel(docs: List[MavenDoc]): List[A] + def modelToTable(items: List[A]): String + def format(item: A): String +} + +object ModelOperator { + + case class ScalaGrouped( + group: String, + artifact: String, + version: String, + sjs: Boolean, + scala: Boolean) + + val sbt: ModelOperator[ScalaGrouped] = + new ModelOperator[ScalaGrouped] { + val sbtRegex = """((?\d*\.\d*)_)?((?\d*\.\d*)sjs_)?(?.*)""".r + + def refineArtifact(doc: MavenDoc): (String, Option[String], Option[String]) = { + sbtRegex + .findFirstMatchIn(doc.a.reverse) + .map(r => (r.group("artifact").reverse, Option(r.group("scalav")).map(_.reverse), Option(r.group("sjsv")).map(_.reverse))) + .getOrElse(("", None, None)) + } + + override def mavenToModel(docs: List[MavenDoc]): List[ScalaGrouped] = { + docs + .fproduct(refineArtifact) + .groupMap { case (doc, (a, sc, sj)) => (doc.g, a, doc.v) } { case (doc, (a, sc, sj)) => (sc, sj) } + .toList + .flatMap { + case ((g, a, v), vs) => + if (vs.exists(_._2.nonEmpty)) { + List( + ScalaGrouped(g, a, v, vs.exists(_._1.nonEmpty), vs.exists(_._1.nonEmpty)), + ScalaGrouped(g, a, v, sjs = false, scala = vs.exists(_._1.nonEmpty)) + ) + } else { + List(ScalaGrouped(g, a, v, sjs = false, scala = vs.exists(_._1.nonEmpty))) + } + } + } + + override def modelToTable(items: List[ScalaGrouped]): String = { + val (gSize, aSize, vSize) = maxSizes(items.map(x => (x.group, x.artifact, x.version))) + + items.zipWithIndex + .map { + case (d, i) => + val id = "[" + (i + 1) + "]" + val template = + if (d.sjs) s"%4s %${gSize}s %%%%%% %${aSize}s %% %${vSize}s" + else if (d.scala) s"%4s %${gSize}s %%%% %${aSize}s %% %${vSize}s" + else s"%4s %${gSize}s %% %${aSize}s %% %${vSize}s" + + String.format(template, id, d.group, d.artifact, d.version) + } + .mkString("\n") + } + + override def format(item: ScalaGrouped): String = { + val sep = if (item.sjs) "%%%" else if (item.scala) "%%" else "%" + raw""""${item.group}" $sep "${item.artifact}" % "${item.version}"""" + } + } + + case class Generic(group: String, artifact: String, version: String) + + val generic: ModelOperator[Generic] = new ModelOperator[Generic] { + override def mavenToModel(docs: List[MavenDoc]): List[Generic] = docs.map(d => Generic(d.g, d.a, d.v)) + + override def modelToTable(items: List[Generic]): String = { + val (gSize, aSize, vSize) = maxSizes(items.map(x => (x.group, x.artifact, x.version))) + + val template = s"%4s %${gSize}s : %${aSize}s : %${vSize}s" + + items.zipWithIndex + .map { case (d, i) => String.format(template, "[" + (i + 1) + "]", d.group, d.artifact, d.version) } + .mkString("\n") + } + + override def format(item: Generic): String = { + raw""""${item.group}" : "${item.artifact}" : "${item.version}"""" + } + } + + def maxSizes(xs: List[(String, String, String)]) = { + xs.foldLeft(0, 0, 0) { case ((g, a, v), (dg, da, dv)) => (dg.length.max(g), da.length.max(a), dv.length.max(v)) } + } +} diff --git a/src/main/scala/com/deliganli/maven/search/dsl/Move.scala b/src/main/scala/com/deliganli/maven/search/dsl/Move.scala new file mode 100644 index 0000000..5ced49e --- /dev/null +++ b/src/main/scala/com/deliganli/maven/search/dsl/Move.scala @@ -0,0 +1,43 @@ +package com.deliganli.maven.search.dsl + +import cats.effect.concurrent.Ref +import cats.implicits._ +import cats.{Applicative, Monad} +import com.deliganli.maven.search.Interpreter.State +import com.deliganli.maven.search.Params.Config + +trait Move[F[_]] { + def move(page: Int): F[Unit] +} + +object Move { + def apply[F[_]](implicit ev: Move[F]): Move[F] = ev + + def dsl[F[_]: Monad, A]( + ref: Ref[F, State[A]], + config: Config, + terminal: Terminal[F], + pipeline: ModelOperator[A] + ): Move[F] = + new Move[F] { + + override def move(page: Int): F[Unit] = { + def updateCache(): F[List[A]] = + ref.modify { state => + val updated = state.copy(page = page) + val from = (page - 1) * config.itemPerPage + val till = from + config.itemPerPage + + (updated, state.items.slice(from, till)) + } + + Applicative[F].unit + .flatMap(_ => updateCache()) + .flatTap { items => + val table = pipeline.modelToTable(items) + terminal.printTable(table, page, items.size) + } + .void + } + } +} diff --git a/src/main/scala/com/deliganli/maven/search/dsl/Program.scala b/src/main/scala/com/deliganli/maven/search/dsl/Program.scala new file mode 100644 index 0000000..c51cbe3 --- /dev/null +++ b/src/main/scala/com/deliganli/maven/search/dsl/Program.scala @@ -0,0 +1,32 @@ +package com.deliganli.maven.search.dsl + +import cats.Monad +import cats.effect.concurrent.Ref +import com.deliganli.maven.search.Domain.ProgramEvent +import com.deliganli.maven.search.Environment +import com.deliganli.maven.search.Interpreter.State + +trait Program[F[_], A] { + def search(page: Int): F[Unit] + def prompt(): F[ProgramEvent] + def move(page: Int): F[Unit] + def copy(selection: Int): F[Unit] +} + +object Program { + def apply[F[_], A](implicit ev: Program[F, A]): Program[F, A] = ev + + def dsl[F[_]: Monad, A](ref: Ref[F, State[A]], operator: ModelOperator[A], env: Environment[F]): Program[F, A] = { + val S = Search.dsl[F, A](ref, env.config, env.maven, env.terminal, operator) + val P = Prompt.dsl[F, A](ref, env.logger, env.terminal, env.eventMapper) + val M = Move.dsl[F, A](ref, env.config, env.terminal, operator) + val C = Copy.dsl[F, A](ref, env.config, env.clipboard, env.terminal, operator) + + new Program[F, A] { + def search(page: Int): F[Unit] = S.search(page) + def prompt(): F[ProgramEvent] = P.prompt() + def move(page: Int): F[Unit] = M.move(page) + def copy(selection: Int): F[Unit] = C.copy(selection) + } + } +} diff --git a/src/main/scala/com/deliganli/maven/search/dsl/Prompt.scala b/src/main/scala/com/deliganli/maven/search/dsl/Prompt.scala new file mode 100644 index 0000000..33ed2c8 --- /dev/null +++ b/src/main/scala/com/deliganli/maven/search/dsl/Prompt.scala @@ -0,0 +1,42 @@ +package com.deliganli.maven.search.dsl + +import cats.effect.concurrent.Ref +import cats.implicits._ +import cats.{Applicative, Monad} +import com.deliganli.maven.search.Domain.{ProgramEvent, UserEvent} +import com.deliganli.maven.search.Interpreter.State +import io.odin.Logger + +trait Prompt[F[_]] { + def prompt(): F[ProgramEvent] +} + +object Prompt { + def apply[F[_]](implicit ev: Prompt[F]): Prompt[F] = ev + + def dsl[F[_]: Monad, A]( + ref: Ref[F, State[A]], + logger: Logger[F], + terminal: Terminal[F], + eventMapper: EventMapper + ): Prompt[F] = + new Prompt[F] { + + def proceed(e: UserEvent): F[ProgramEvent] = + Applicative[F].unit + .flatMap(_ => ref.get) + .flatTap(state => logger.debug(s"size: ${state.items.size}, page:${state.page}")) + .map(state => eventMapper.userToProgram(state.items.size, state.page)(e)) + + def terminate(e: String): F[ProgramEvent] = + terminal + .putStrLn(s"$e is invalid") + .as(ProgramEvent.Exit) + + override def prompt(): F[ProgramEvent] = { + Applicative[F].unit + .flatMap(_ => terminal.readChar.map(eventMapper.stringToUserEvent)) + .flatMap(_.fold(terminate, proceed)) + } + } +} diff --git a/src/main/scala/com/deliganli/maven/search/dsl/Search.scala b/src/main/scala/com/deliganli/maven/search/dsl/Search.scala new file mode 100644 index 0000000..092817d --- /dev/null +++ b/src/main/scala/com/deliganli/maven/search/dsl/Search.scala @@ -0,0 +1,45 @@ +package com.deliganli.maven.search.dsl + +import cats.effect.concurrent.Ref +import cats.implicits._ +import cats.{Applicative, Monad} +import com.deliganli.maven.search.Interpreter.State +import com.deliganli.maven.search.Params.Config + +trait Search[F[_], A] { + def search(page: Int): F[Unit] +} + +object Search { + def apply[F[_], A](implicit ev: Search[F, A]): Search[F, A] = ev + + def dsl[F[_]: Monad, A]( + ref: Ref[F, State[A]], + config: Config, + maven: MavenClient[F], + terminal: Terminal[F], + pipeline: ModelOperator[A] + ): Search[F, A] = { + new Search[F, A] { + override def search(page: Int): F[Unit] = { + def updateCache(items: List[A]): F[Unit] = + ref.update { state => + val cursor = state.page * config.itemPerPage + if (cursor < state.items.size) state.copy(page = page) + else State(page, state.items ++ items) + } + + Applicative[F].unit + .flatMap(_ => maven.search(page).map(_.docs)) + .map(docs => pipeline.mavenToModel(docs)) + .flatTap { items => + val pageItems = items.take(config.itemPerPage) + val table = pipeline.modelToTable(pageItems) + terminal.printTable(table, page, pageItems.size) + } + .flatTap(items => updateCache(items)) + .void + } + } + } +} diff --git a/src/main/scala/com/deliganli/maven/search/Terminal.scala b/src/main/scala/com/deliganli/maven/search/dsl/Terminal.scala similarity index 61% rename from src/main/scala/com/deliganli/maven/search/Terminal.scala rename to src/main/scala/com/deliganli/maven/search/dsl/Terminal.scala index f606ce2..da13f9f 100644 --- a/src/main/scala/com/deliganli/maven/search/Terminal.scala +++ b/src/main/scala/com/deliganli/maven/search/dsl/Terminal.scala @@ -1,23 +1,23 @@ -package com.deliganli.maven.search +package com.deliganli.maven.search.dsl import cats.effect.{Resource, Sync} import cats.implicits._ -import com.deliganli.maven.search.Domain.MavenModel.MavenDoc -import com.deliganli.maven.search.Visual.{buildTable, message} +import com.deliganli.maven.search.Params.Config import org.jline.terminal import org.jline.terminal.TerminalBuilder +import org.jline.utils.InfoCmp.Capability trait Terminal[F[_]] { def readChar: F[String] def putStrLn(s: String): F[Unit] - def printTable(docs: List[MavenDoc], page: Int): F[Unit] + def printTable(table: String, page: Int, size: Int): F[Unit] } object Terminal { def apply[F[_]](implicit ev: Terminal[F]) = ev - def sync[F[_]: Sync]: Resource[F, Terminal[F]] = { + def sync[F[_]: Sync](config: Config): Resource[F, Terminal[F]] = { underlyingTerminalResource .flatTap(t => Resource.liftF(Sync[F].delay(t.enterRawMode()))) .map { underlying => @@ -34,10 +34,12 @@ object Terminal { } } - def printTable(docs: List[MavenDoc], page: Int): F[Unit] = { + def printTable(table: String, page: Int, size: Int): F[Unit] = { Sync[F].delay { - underlying.writer.println(buildTable(docs)) - underlying.writer.print(message(docs, page)) + if (config.clearScreen) underlying.puts(Capability.clear_screen) + underlying.writer.println(table) + underlying.writer.print(message(size, page)) + if (!config.clearScreen) underlying.writer.println() underlying.writer.flush() } } @@ -58,4 +60,10 @@ object Terminal { .jna(true) .system(true) .dumb(true) + + def message(size: Int, page: Int): String = { + val prev = if (page > 1) ", p:previous page" else "" + s"""Page:$page + |Select a number to copy to clipboard (1 - $size, n:next page$prev): """.stripMargin + } } diff --git a/src/main/scala/com/deliganli/maven/search/circe/package.scala b/src/main/scala/com/deliganli/maven/search/json/package.scala similarity index 60% rename from src/main/scala/com/deliganli/maven/search/circe/package.scala rename to src/main/scala/com/deliganli/maven/search/json/package.scala index 8ea9d26..d5c855f 100644 --- a/src/main/scala/com/deliganli/maven/search/circe/package.scala +++ b/src/main/scala/com/deliganli/maven/search/json/package.scala @@ -1,12 +1,13 @@ package com.deliganli.maven.search -import com.deliganli.maven.search.Domain.ImportFormat.{Maven, Sbt} +import com.deliganli.maven.search.Domain.ImportFormat.{Generic, Sbt} import com.deliganli.maven.search.Domain.MavenModel.MavenDoc -import com.deliganli.maven.search.Domain.{Config, ImportFormat, MavenModel} +import com.deliganli.maven.search.Domain.{ImportFormat, MavenModel} +import com.deliganli.maven.search.Params.Config import io.circe.Decoder import org.http4s.Uri -package object circe { +package object json { implicit val mavenDoc: Decoder[MavenDoc] = Decoder.forProduct4("g", "a", "latestVersion", "p")(MavenDoc.apply) implicit val mavenModel: Decoder[MavenModel] = Decoder.instance { c => @@ -18,17 +19,18 @@ package object circe { implicit val uri: Decoder[Uri] = Decoder.instance(_.as[String].map(Uri.unsafeFromString)) implicit val importFormat: Decoder[ImportFormat] = Decoder.decodeString.emap { - case "sbt" => Right(new Sbt()) - case "maven" => Right(new Maven()) - case v => Left(s"Unknown value: $v - valid values are sbt, maven") + case "sbt" => Right(Sbt) + case "generic" => Right(Generic) + case v => Left(s"Unknown value: $v - valid values are sbt, maven") } - implicit val config: Decoder[Config] = Decoder.forProduct6( + implicit val config: Decoder[Config] = Decoder.forProduct7( + "query", "mavenUri", "chunkSize", "itemPerPage", - "copyToClipboard", "importFormat", - "debug" + "debug", + "clearScreen" )(Config.apply) } diff --git a/src/test/scala/com/deliganli/maven/search/ProgramTest.scala b/src/test/scala/com/deliganli/maven/search/ProgramTest.scala index e558b2c..9b142d7 100644 --- a/src/test/scala/com/deliganli/maven/search/ProgramTest.scala +++ b/src/test/scala/com/deliganli/maven/search/ProgramTest.scala @@ -2,11 +2,11 @@ package com.deliganli.maven.search import cats.effect.IO import cats.implicits._ -import com.deliganli.maven.search.Domain.ImportFormat.Sbt import com.deliganli.maven.search.Domain.MavenModel import com.deliganli.maven.search.Domain.MavenModel.MavenDoc import com.deliganli.maven.search.Tabulate.TabulateSyntax -import com.deliganli.maven.search.circe._ +import com.deliganli.maven.search.json._ +import com.deliganli.maven.search.dsl.ModelOperator import io.circe.parser._ class ProgramTest extends UnitTest { @@ -14,7 +14,13 @@ class ProgramTest extends UnitTest { "Program" should "tabulate" in { UnitTest .resource("applicative.json") - .use(c => parse(c).flatMap(_.as[MavenModel].map(_.docs)).map(Visual.buildTable).pure[IO]) + .use(c => + parse(c) + .flatMap(_.as[MavenModel].map(_.docs)) + .map(ModelOperator.generic.mavenToModel) + .map(ModelOperator.generic.modelToTable) + .pure[IO] + ) .unsafeRunSync() shouldBe Right( t""" [1] xyz.funjava.functional : higherkinded : 0.0.1 | [2] com.propensive : mercator_sjs0.6_2.13 : 0.3.0 @@ -30,7 +36,36 @@ class ProgramTest extends UnitTest { } "Formatter" should "format for sbt" in { - val doc = MavenDoc("com.propensive", "mercator_sjs0.6_2.13", "0.3.0", "jar") - Formatter[Sbt].format(doc) shouldBe raw""""com.propensive" %%% "mercator" % "0.3.0"""" + val doc = MavenDoc("com.propensive", "mercator_sjs0.6_2.13", "0.3.0", "jar") + val P = ModelOperator.sbt + val items = P.mavenToModel(List(doc)) + val formatted = P.format(items.head) + + formatted shouldBe raw""""com.propensive" %%% "mercator" % "0.3.0"""" + } + + "Tabulate" should "group correctly for sbt" in { + val raw = List( + MavenDoc("com.deliganli", "maven-search", "0.1.0", "?"), + MavenDoc("com.deliganli", "maven-search_2.12", "0.1.0", "?"), + MavenDoc("com.deliganli", "maven-search_2.13", "0.1.0", "?"), + MavenDoc("com.deliganli", "maven-search_sjs0.6_2.12", "0.1.0", "?"), + MavenDoc("com.deliganli", "maven-search_sjs0.6_2.13", "0.1.0", "?"), + MavenDoc("com.deliganli", "maven_search", "0.1.0", "?"), + MavenDoc("com.deliganli", "maven_search_2.12", "0.1.0", "?"), + MavenDoc("com.deliganli", "maven_search_sjs1.0_2.13", "0.1.0", "?") + ) + + val P = ModelOperator.sbt + val items = P.mavenToModel(raw) + val table = P.modelToTable(items) + + table shouldBe + t""" [1] com.deliganli % maven-search % 0.1.0 + | [2] com.deliganli %% maven-search % 0.1.0 + | [3] com.deliganli %%% maven-search % 0.1.0 + | [4] com.deliganli % maven_search % 0.1.0 + | [5] com.deliganli %% maven_search % 0.1.0 + | [6] com.deliganli %%% maven_search % 0.1.0""" } }