diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ce08e1..01899a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,28 +10,33 @@ concurrency: group: ci-${{ github.ref }} cancel-in-progress: true - jobs: build: - runs-on: ubuntu-latest strategy: - fail-fast: true - + fail-fast: false + runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Cache - uses: coursier/cache-action@v6 - - - name: Setup Java - uses: actions/setup-java@v2 - with: - distribution: adopt - java-version: 11 - - - name: Run tests - run: | - ./mill -k --disable-ticker __.resolvedIvyDeps && - ./mill -k --disable-ticker mill.scalalib.scalafmt.ScalafmtModule/checkFormatAll __.sources && - ./mill -j 0 -k --disable-ticker __.test + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "21" + cache: "sbt" + + - name: Setup sbt + uses: sbt/setup-sbt@v1 + + - name: Test + run: sbt ci + + - name: Publish ${{ github.ref }} + # if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main')) + run: sbt ci-release + env: + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + PGP_SECRET: ${{ secrets.PGP_SECRET }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} diff --git a/.mill-version b/.mill-version deleted file mode 100644 index 70016a7..0000000 --- a/.mill-version +++ /dev/null @@ -1 +0,0 @@ -0.10.12 diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..e18f135 --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ +-J-Xmx6g diff --git a/README.md b/README.md index 0cc4755..64acc58 100644 --- a/README.md +++ b/README.md @@ -38,4 +38,4 @@ override def ivyDeps = super.ivyDeps() ++ Agg(ivy"tech.neander::jsonrpclib-fs2:: **/!\ Please be aware that this library is in its early days and offers strictly no guarantee with regards to backward compatibility** -See the examples folder +See the modules/examples folder. diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..2568eb2 --- /dev/null +++ b/build.sbt @@ -0,0 +1,124 @@ +import org.typelevel.sbt.tpolecat.DevMode +import org.typelevel.sbt.tpolecat.OptionsMode +import java.net.URI + +inThisBuild( + List( + organization := "org.polyvariant", + homepage := Some(url("https://github.com/neandertech/jsonrpclib")), + licenses := List(License.Apache2), + developers := List( + Developer("Baccata", "Olivier Mélois", "baccata64@gmail.com", URI.create("https://github.com/baccata").toURL) + ), + sonatypeCredentialHost := "s01.oss.sonatype.org", + sonatypeRepository := "https://s01.oss.sonatype.org/service/local" + ) +) + +val scala213 = "2.13.16" +val scala3 = "3.3.5" +val allScalaVersions = List(scala213, scala3) +val jvmScalaVersions = allScalaVersions +val jsScalaVersions = allScalaVersions +val nativeScalaVersions = allScalaVersions + +ThisBuild / tpolecatOptionsMode := DevMode + +val commonSettings = Seq( + libraryDependencies ++= Seq( + "com.disneystreaming" %%% "weaver-cats" % "0.8.4" % Test + ), + mimaPreviousArtifacts := Set( + "tech.neander" %%% name.value % "0.0.7" + ), + scalacOptions += "-java-output-version:8" +) + +val core = projectMatrix + .in(file("modules") / "core") + .jvmPlatform( + jvmScalaVersions, + Test / unmanagedSourceDirectories ++= Seq( + (projectMatrixBaseDirectory.value / "src" / "test" / "scalajvm-native").getAbsoluteFile + ) + ) + .jsPlatform(jsScalaVersions) + .nativePlatform( + nativeScalaVersions, + Test / unmanagedSourceDirectories ++= Seq( + (projectMatrixBaseDirectory.value / "src" / "test" / "scalajvm-native").getAbsoluteFile + ) + ) + .disablePlugins(AssemblyPlugin) + .settings( + name := "jsonrpclib-core", + commonSettings, + libraryDependencies ++= Seq( + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.30.2" + ) + ) + +val fs2 = projectMatrix + .in(file("modules") / "fs2") + .jvmPlatform(jvmScalaVersions) + .jsPlatform(jsScalaVersions) + .nativePlatform(nativeScalaVersions) + .disablePlugins(AssemblyPlugin) + .dependsOn(core) + .settings( + name := "jsonrpclib-fs2", + commonSettings, + libraryDependencies ++= Seq( + "co.fs2" %%% "fs2-io" % "3.12.0" + ) + ) + +val exampleServer = projectMatrix + .in(file("modules") / "examples/server") + .jvmPlatform(List(scala213)) + .dependsOn(fs2) + .settings( + commonSettings, + publish / skip := true + ) + .disablePlugins(MimaPlugin) + +val exampleClient = projectMatrix + .in(file("modules") / "examples/client") + .jvmPlatform( + List(scala213), + Seq( + fork := true, + envVars += "SERVER_JAR" -> (exampleServer.jvm(scala213) / assembly).value.toString + ) + ) + .disablePlugins(AssemblyPlugin) + .dependsOn(fs2) + .settings( + commonSettings, + publish / skip := true + ) + .disablePlugins(MimaPlugin) + +val root = project + .in(file(".")) + .settings( + publish / skip := true + ) + .disablePlugins(MimaPlugin, AssemblyPlugin) + .aggregate(List(core, fs2, exampleServer, exampleClient).flatMap(_.projectRefs): _*) + +// The core compiles are a workaround for https://github.com/plokhotnyuk/jsoniter-scala/issues/564 +// when we switch to SN 0.5, we can use `makeWithSkipNestedOptionValues` instead: https://github.com/plokhotnyuk/jsoniter-scala/issues/564#issuecomment-2787096068 +val compileCoreModules = { + for { + scalaVersionSuffix <- List("", "3") + platformSuffix <- List("", "JS", "Native") + task <- List("compile", "package") + } yield s"core$platformSuffix$scalaVersionSuffix/$task" +}.mkString(";") + +addCommandAlias( + "ci", + s"$compileCoreModules;test;scalafmtCheckAll;mimaReportBinaryIssues" +) diff --git a/build.sc b/build.sc deleted file mode 100644 index 4b65eea..0000000 --- a/build.sc +++ /dev/null @@ -1,312 +0,0 @@ -import mill.define.Target -import mill.util.Jvm -import $ivy.`com.lihaoyi::mill-contrib-bloop:$MILL_VERSION` -import $ivy.`io.github.davidgregory084::mill-tpolecat::0.3.5` -import $ivy.`io.chris-kipp::mill-ci-release::0.1.9` - -import os.Path -import mill._ -import scalalib._ -import publish._ -import scalajslib._ -import scalanativelib._ -import mill.scalajslib.api._ -import io.github.davidgregory084._ -import io.kipp.mill.ci.release.CiReleaseModule - -object versions { - val scala212Version = "2.12.1" - val scala213Version = "2.13.11" - val scala3Version = "3.3.3" - val scalaJSVersion = "1.14.0" - val scalaNativeVersion = "0.4.17" - val munitVersion = "1.0.0-M9" - val fs2Version = "3.10.0" - val weaverVersion = "0.8.3" - val jsoniterVersion = "2.17.0" - - val scala213 = "2.13" - val scala212 = "2.12" - val scala3 = "3" - - val crossMap = Map( - "2.13" -> scala213Version, - "2.12" -> scala212Version, - "3" -> scala3Version - ) -} -import versions._ - -object core extends RPCCrossPlatformModule { cross => - - def crossPlatformIvyDeps: T[Agg[Dep]] = Agg( - ivy"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros::${jsoniterVersion}" - ) - - object jvm extends mill.Cross[JvmModule](scala213, scala3) - class JvmModule(cv: String) extends cross.JVM(cv) { - object test extends MunitTests - } - - object js extends mill.Cross[JsModule](scala213, scala3) - class JsModule(cv: String) extends cross.JS(cv) { - object test extends MunitTests - } - - object native extends mill.Cross[NativeModule](scala213, scala3) - class NativeModule(cv: String) extends cross.Native(cv) { - object test extends MunitTests - } -} - -object fs2 extends RPCCrossPlatformModule { cross => - - override def crossPlatformModuleDeps = Seq(core) - def crossPlatformIvyDeps: T[Agg[Dep]] = Agg( - ivy"co.fs2::fs2-core::${fs2Version}" - ) - - object jvm extends mill.Cross[JvmModule](scala213, scala3) - class JvmModule(cv: String) extends cross.JVM(cv) { - object test extends WeaverTests - } - - object js extends mill.Cross[JsModule](scala213, scala3) - class JsModule(cv: String) extends cross.JS(cv) { - object test extends WeaverTests - } - - object native extends mill.Cross[NativeModule](scala213, scala3) - class NativeModule(cv: String) extends cross.Native(cv) { - object test extends WeaverTests - } - -} - -object examples extends mill.define.Module { - - object server extends ScalaModule { - def ivyDeps = Agg(ivy"co.fs2::fs2-io:${fs2Version}") - def moduleDeps = Seq(fs2.jvm(versions.scala213)) - def scalaVersion = versions.scala213Version - } - - object client extends ScalaModule { - def ivyDeps = Agg(ivy"co.fs2::fs2-io:$fs2Version") - def moduleDeps = Seq(fs2.jvm(versions.scala213)) - def scalaVersion = versions.scala213Version - def forkEnv: Target[Map[String, String]] = T { - val assembledServer = server.assembly() - super.forkEnv() ++ Map("SERVER_JAR" -> assembledServer.path.toString()) - } - } - -} - -// ############################################################################# -// COMMON SETUP -// ############################################################################# - -trait RPCCrossPlatformModule extends Module { shared => - - def artifactName = s"jsonrpclib-${millModuleSegments.parts.mkString("-")}" - def crossPlatformIvyDeps = T { Agg.empty[Dep] } - def crossPlatformModuleDeps: Seq[Module] = Seq() - def crossPlatformTestModuleDeps: Seq[Module] = Seq() - - class JVM(val crossVersion: String) extends PlatformSpecific { - override def platformLabel: String = "jvm" - - trait WeaverTests extends Tests { - def ivyDeps = super.ivyDeps() ++ Agg(ivy"com.disneystreaming::weaver-cats::$weaverVersion") - def testFramework = "weaver.framework.CatsEffect" - } - - trait MunitTests extends Tests with TestModule.Munit { - def ivyDeps = super.ivyDeps() ++ Agg(ivy"org.scalameta::munit::$munitVersion") - } - - trait Tests extends super.Tests { - override def sources = T.sources(computeSources(this).map(PathRef(_))) - override def moduleDeps = super.moduleDeps ++ shared.crossPlatformTestModuleDeps.flatMap(matchingCross) - } - } - - class JS(val crossVersion: String) extends PlatformSpecific with ScalaJSModule { - override def platformLabel: String = "js" - override def scalaJSVersion = versions.scalaJSVersion - - override def scalacOptions = T { - super.scalacOptions().filterNot(_ == "-Ywarn-unused:params") - } - - override def moduleKind = T(ModuleKind.CommonJSModule) - override def skipIdea = true - - trait WeaverTests extends Tests { - def ivyDeps = super.ivyDeps() ++ Agg(ivy"com.disneystreaming::weaver-cats::$weaverVersion") - def testFramework = "weaver.framework.CatsEffect" - } - - trait MunitTests extends Tests with TestModule.Munit { - def ivyDeps = super.ivyDeps() ++ Agg(ivy"org.scalameta::munit::$munitVersion") - } - - trait Tests extends super.Tests with mill.contrib.Bloop.Module { - override def sources = T.sources(computeSources(this).map(PathRef(_))) - override def skipIdea = true - override def skipBloop = true - override def moduleDeps = super.moduleDeps ++ shared.crossPlatformTestModuleDeps.flatMap(matchingCross).collect { - case m: ScalaJSModule => m - } - } - } - - class Native(val crossVersion: String) extends PlatformSpecific with ScalaNativeModule { - override def platformLabel: String = "native" - override def scalaNativeVersion = versions.scalaNativeVersion - override def scalacOptions = T { - super - .scalacOptions() - .filterNot { opts => - Seq( - "-Ywarn-extra-implicit", - "-Xlint:constant" - ).contains(opts) - } - .filterNot(_.startsWith("-Ywarn-unused")) - } - override def skipIdea = true - override def skipBloop = true - - trait WeaverTests extends Tests { - def ivyDeps = super.ivyDeps() ++ Agg(ivy"com.disneystreaming::weaver-cats::$weaverVersion") - def testFramework = "weaver.framework.CatsEffect" - } - - trait MunitTests extends Tests with TestModule.Munit { - def ivyDeps = super.ivyDeps() ++ Agg(ivy"org.scalameta::munit::$munitVersion") - } - - trait Tests extends super.Tests with mill.contrib.Bloop.Module { - override def nativeLinkStubs = true - override def skipIdea = true - override def skipBloop = true - override def sources = T.sources(computeSources(this).map(PathRef(_))) - override def moduleDeps = super.moduleDeps ++ shared.crossPlatformTestModuleDeps.flatMap(matchingCross).collect { - case m: ScalaNativeModule => m - } - } - } - - trait PlatformSpecific extends JsonRPCModule with mill.contrib.Bloop.Module { self => - def platformLabel: String - def crossVersion: String - override def scalaVersion = versions.crossMap(crossVersion) - - override def millSourcePath = shared.millSourcePath - - override def ivyDeps = super.ivyDeps() ++ shared.crossPlatformIvyDeps() - - def samePlatform(module: Module): Boolean = - self match { - case _: ScalaJSModule => module.isInstanceOf[ScalaJSModule] - case _: ScalaNativeModule => module.isInstanceOf[ScalaNativeModule] - case _ => - !(module.isInstanceOf[ScalaJSModule] || module - .isInstanceOf[ScalaNativeModule]) - } - - def sameScalaVersion(module: Module): Boolean = { - // Don't know why, pattern matching didn't seem to work here - module.isInstanceOf[PlatformSpecific] && (module.asInstanceOf[PlatformSpecific].crossVersion == self.crossVersion) - } - - def sameCross(module: Module) = samePlatform(module) && sameScalaVersion(module) - - def matchingCross(module: Module): Seq[JsonRPCModule] = module match { - case m: RPCCrossPlatformModule => - m.millModuleDirectChildren.collect { - case cross: Cross[_] => - cross.millModuleDirectChildren.collect { - case child: JsonRPCModule if sameCross(child) => child - } - case child: JsonRPCModule if sameCross(child) => Seq(child) - }.flatten - case _ => Seq() - } - - override def moduleDeps: Seq[JsonRPCModule] = - shared.crossPlatformModuleDeps.flatMap(matchingCross) - - override def artifactName = shared.artifactName - - def computeSources(module: mill.define.Module): Seq[os.Path] = { - val modulePath = module.millSourcePath - module match { - case _: ScalaJSModule => - Seq( - modulePath / 'src, - modulePath / s"src-js", - modulePath / s"src-jvm-js", - modulePath / s"src-js-native" - ) - case _: ScalaNativeModule => - Seq( - modulePath / 'src, - modulePath / s"src-native", - modulePath / s"src-jvm-native", - modulePath / s"src-js-native" - ) - case _ => - Seq( - modulePath / 'src, - modulePath / s"src-jvm", - modulePath / s"src-jvm-js", - modulePath / s"src-jvm-native" - ) - } - } - - override def sources = T.sources(computeSources(self).map(PathRef(_))) - - override def skipBloop = { - self match { - case _: ScalaJSModule => true - case _: ScalaNativeModule => true - case _ => false - } - } && { crossVersion != scala213 } - - } -} - -trait JsonRPCModule extends ScalaModule with CiReleaseModule with scalafmt.ScalafmtModule { - def scalafmt() = T.command(reformat()) - def fmt() = T.command(reformat()) - def refreshedEnv = T.input(T.ctx().env) - def publishVersion = T { - if (refreshedEnv().contains("CI")) super.publishVersion() - else "dev" - } - override def scalacOptions = T { - super.scalacOptions() ++ Tpolecat.scalacOptionsFor(scalaVersion()) - } - - override def forkEnv = T { refreshedEnv() } - - def pomSettings = PomSettings( - description = "A Scala jsonrpc library", - organization = "tech.neander", - url = "https://github.com/neandertech/jsonrpclib", - licenses = Seq(License.`Apache-2.0`), - versionControl = VersionControl(Some("https://github.com/neandertech/jsonrpclib")), - developers = Seq( - Developer("Baccata", "Olivier Mélois", "https://github.com/baccata") - ) - ) - - override def sonatypeUri = "https://s01.oss.sonatype.org/service/local" - override def sonatypeSnapshotUri = - "https://s01.oss.sonatype.org/content/repositories/snapshots" -} diff --git a/core/test/src-jvm-native/jsonrpclib/CallIdSpec.scala b/core/test/src-jvm-native/jsonrpclib/CallIdSpec.scala deleted file mode 100644 index 279b161..0000000 --- a/core/test/src-jvm-native/jsonrpclib/CallIdSpec.scala +++ /dev/null @@ -1,20 +0,0 @@ -package jsonrpclib - -import munit._ -import com.github.plokhotnyuk.jsoniter_scala.core._ - -class CallIdSpec() extends FunSuite { - test("json parsing") { - val strJson = """ "25" """.trim - assertEquals(readFromString[CallId](strJson), CallId.StringId("25")) - - val intJson = "25" - assertEquals(readFromString[CallId](intJson), CallId.NumberId(25)) - - val longJson = Long.MaxValue.toString - assertEquals(readFromString[CallId](longJson), CallId.NumberId(Long.MaxValue)) - - val nullJson = "null" - assertEquals(readFromString[CallId](nullJson), CallId.NullId) - } -} diff --git a/examples/client/src/examples/client/ChildProcess.scala b/examples/client/src/examples/client/ChildProcess.scala deleted file mode 100644 index 4daaa33..0000000 --- a/examples/client/src/examples/client/ChildProcess.scala +++ /dev/null @@ -1,68 +0,0 @@ -package examples.client - -import fs2.Stream -import cats.effect._ -import cats.syntax.all._ -import scala.jdk.CollectionConverters._ -import java.io.OutputStream - -trait ChildProcess[F[_]] { - def stdin: fs2.Pipe[F, Byte, Unit] - def stdout: Stream[F, Byte] - def stderr: Stream[F, Byte] -} - -object ChildProcess { - - def spawn[F[_]: Async](command: String*): Stream[F, ChildProcess[F]] = - Stream.bracket(start[F](command))(_._2).map(_._1) - - val readBufferSize = 512 - private def start[F[_]: Async](command: Seq[String]) = Async[F].interruptible { - val p = - new java.lang.ProcessBuilder(command.asJava) - .start() // .directory(new java.io.File(wd)).start() - val done = Async[F].fromCompletableFuture(Sync[F].delay(p.onExit())) - - val terminate: F[Unit] = Sync[F].interruptible(p.destroy()) - - import cats._ - val onGlobal = new (F ~> F) { - def apply[A](fa: F[A]): F[A] = Async[F].evalOn(fa, scala.concurrent.ExecutionContext.global) - } - - val cp = new ChildProcess[F] { - def stdin: fs2.Pipe[F, Byte, Unit] = - writeOutputStreamFlushingChunks[F](Sync[F].interruptible(p.getOutputStream())) - - def stdout: fs2.Stream[F, Byte] = fs2.io - .readInputStream[F](Sync[F].interruptible(p.getInputStream()), chunkSize = readBufferSize) - .translate(onGlobal) - - def stderr: fs2.Stream[F, Byte] = fs2.io - .readInputStream[F](Sync[F].blocking(p.getErrorStream()), chunkSize = readBufferSize) - .translate(onGlobal) - // Avoids broken pipe - we cut off when the program ends. - // Users can decide what to do with the error logs using the exitCode value - .interruptWhen(done.void.attempt) - } - (cp, terminate) - } - - /** Adds a flush after each chunk - */ - def writeOutputStreamFlushingChunks[F[_]]( - fos: F[OutputStream], - closeAfterUse: Boolean = true - )(implicit F: Sync[F]): fs2.Pipe[F, Byte, Nothing] = - s => { - def useOs(os: OutputStream): Stream[F, Nothing] = - s.chunks.foreach(c => F.interruptible(os.write(c.toArray)) >> F.blocking(os.flush())) - - val os = - if (closeAfterUse) Stream.bracket(fos)(os => F.blocking(os.close())) - else Stream.eval(fos) - os.flatMap(os => useOs(os) ++ Stream.exec(F.blocking(os.flush()))) - } - -} diff --git a/mill b/mill deleted file mode 100755 index 4f19f39..0000000 --- a/mill +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env sh - -# This is a wrapper script, that automatically download mill from GitHub release pages -# You can give the required mill version with MILL_VERSION env variable -# If no version is given, it falls back to the value of DEFAULT_MILL_VERSION -DEFAULT_MILL_VERSION=0.10.12 - -set -e - -if [ -z "$MILL_VERSION" ] ; then - if [ -f ".mill-version" ] ; then - MILL_VERSION="$(head -n 1 .mill-version 2> /dev/null)" - elif [ -f "mill" ] && [ "$0" != "mill" ] ; then - MILL_VERSION=$(grep -F "DEFAULT_MILL_VERSION=" "mill" | head -n 1 | cut -d= -f2) - else - MILL_VERSION=$DEFAULT_MILL_VERSION - fi -fi - -if [ "x${XDG_CACHE_HOME}" != "x" ] ; then - MILL_DOWNLOAD_PATH="${XDG_CACHE_HOME}/mill/download" -else - MILL_DOWNLOAD_PATH="${HOME}/.cache/mill/download" -fi -MILL_EXEC_PATH="${MILL_DOWNLOAD_PATH}/${MILL_VERSION}" - -version_remainder="$MILL_VERSION" -MILL_MAJOR_VERSION="${version_remainder%%.*}"; version_remainder="${version_remainder#*.}" -MILL_MINOR_VERSION="${version_remainder%%.*}"; version_remainder="${version_remainder#*.}" - -if [ ! -s "$MILL_EXEC_PATH" ] ; then - mkdir -p "$MILL_DOWNLOAD_PATH" - if [ "$MILL_MAJOR_VERSION" -gt 0 ] || [ "$MILL_MINOR_VERSION" -ge 5 ] ; then - ASSEMBLY="-assembly" - fi - DOWNLOAD_FILE=$MILL_EXEC_PATH-tmp-download - MILL_VERSION_TAG=$(echo $MILL_VERSION | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/') - MILL_DOWNLOAD_URL="https://github.com/lihaoyi/mill/releases/download/${MILL_VERSION_TAG}/$MILL_VERSION${ASSEMBLY}" - curl --fail -L -o "$DOWNLOAD_FILE" "$MILL_DOWNLOAD_URL" - chmod +x "$DOWNLOAD_FILE" - mv "$DOWNLOAD_FILE" "$MILL_EXEC_PATH" - unset DOWNLOAD_FILE - unset MILL_DOWNLOAD_URL -fi - -unset MILL_DOWNLOAD_PATH -unset MILL_VERSION - -exec $MILL_EXEC_PATH "$@" diff --git a/core/src/jsonrpclib/CallId.scala b/modules/core/src/main/scala/jsonrpclib/CallId.scala similarity index 100% rename from core/src/jsonrpclib/CallId.scala rename to modules/core/src/main/scala/jsonrpclib/CallId.scala diff --git a/core/src/jsonrpclib/Channel.scala b/modules/core/src/main/scala/jsonrpclib/Channel.scala similarity index 100% rename from core/src/jsonrpclib/Channel.scala rename to modules/core/src/main/scala/jsonrpclib/Channel.scala diff --git a/core/src/jsonrpclib/Codec.scala b/modules/core/src/main/scala/jsonrpclib/Codec.scala similarity index 97% rename from core/src/jsonrpclib/Codec.scala rename to modules/core/src/main/scala/jsonrpclib/Codec.scala index 3f7ada7..1cc3059 100644 --- a/core/src/jsonrpclib/Codec.scala +++ b/modules/core/src/main/scala/jsonrpclib/Codec.scala @@ -1,7 +1,6 @@ package jsonrpclib import com.github.plokhotnyuk.jsoniter_scala.core._ -import jsonrpclib.Payload trait Codec[A] { diff --git a/core/src/jsonrpclib/ConflictingMethodError.scala b/modules/core/src/main/scala/jsonrpclib/ConflictingMethodError.scala similarity index 100% rename from core/src/jsonrpclib/ConflictingMethodError.scala rename to modules/core/src/main/scala/jsonrpclib/ConflictingMethodError.scala diff --git a/core/src/jsonrpclib/Endpoint.scala b/modules/core/src/main/scala/jsonrpclib/Endpoint.scala similarity index 100% rename from core/src/jsonrpclib/Endpoint.scala rename to modules/core/src/main/scala/jsonrpclib/Endpoint.scala diff --git a/core/src/jsonrpclib/ErrorCodec.scala b/modules/core/src/main/scala/jsonrpclib/ErrorCodec.scala similarity index 100% rename from core/src/jsonrpclib/ErrorCodec.scala rename to modules/core/src/main/scala/jsonrpclib/ErrorCodec.scala diff --git a/core/src/jsonrpclib/ErrorPayload.scala b/modules/core/src/main/scala/jsonrpclib/ErrorPayload.scala similarity index 100% rename from core/src/jsonrpclib/ErrorPayload.scala rename to modules/core/src/main/scala/jsonrpclib/ErrorPayload.scala diff --git a/core/src/jsonrpclib/ErrorReport.scala b/modules/core/src/main/scala/jsonrpclib/ErrorReport.scala similarity index 100% rename from core/src/jsonrpclib/ErrorReport.scala rename to modules/core/src/main/scala/jsonrpclib/ErrorReport.scala diff --git a/core/src/jsonrpclib/Message.scala b/modules/core/src/main/scala/jsonrpclib/Message.scala similarity index 100% rename from core/src/jsonrpclib/Message.scala rename to modules/core/src/main/scala/jsonrpclib/Message.scala diff --git a/core/src/jsonrpclib/Monadic.scala b/modules/core/src/main/scala/jsonrpclib/Monadic.scala similarity index 100% rename from core/src/jsonrpclib/Monadic.scala rename to modules/core/src/main/scala/jsonrpclib/Monadic.scala diff --git a/core/src/jsonrpclib/Payload.scala b/modules/core/src/main/scala/jsonrpclib/Payload.scala similarity index 100% rename from core/src/jsonrpclib/Payload.scala rename to modules/core/src/main/scala/jsonrpclib/Payload.scala diff --git a/core/src/jsonrpclib/ProtocolError.scala b/modules/core/src/main/scala/jsonrpclib/ProtocolError.scala similarity index 100% rename from core/src/jsonrpclib/ProtocolError.scala rename to modules/core/src/main/scala/jsonrpclib/ProtocolError.scala diff --git a/core/src/jsonrpclib/StubTemplate.scala b/modules/core/src/main/scala/jsonrpclib/StubTemplate.scala similarity index 100% rename from core/src/jsonrpclib/StubTemplate.scala rename to modules/core/src/main/scala/jsonrpclib/StubTemplate.scala diff --git a/core/src/jsonrpclib/internals/Constants.scala b/modules/core/src/main/scala/jsonrpclib/internals/Constants.scala similarity index 100% rename from core/src/jsonrpclib/internals/Constants.scala rename to modules/core/src/main/scala/jsonrpclib/internals/Constants.scala diff --git a/core/src/jsonrpclib/internals/FutureBaseChannel.scala b/modules/core/src/main/scala/jsonrpclib/internals/FutureBaseChannel.scala similarity index 100% rename from core/src/jsonrpclib/internals/FutureBaseChannel.scala rename to modules/core/src/main/scala/jsonrpclib/internals/FutureBaseChannel.scala diff --git a/core/src/jsonrpclib/internals/LSPHeaders.scala b/modules/core/src/main/scala/jsonrpclib/internals/LSPHeaders.scala similarity index 98% rename from core/src/jsonrpclib/internals/LSPHeaders.scala rename to modules/core/src/main/scala/jsonrpclib/internals/LSPHeaders.scala index 84699bb..ecc32d3 100644 --- a/core/src/jsonrpclib/internals/LSPHeaders.scala +++ b/modules/core/src/main/scala/jsonrpclib/internals/LSPHeaders.scala @@ -66,6 +66,7 @@ private[jsonrpclib] object LSPHeaders { case s"Content-type: ${mimeType}; charset=${charset}" => headerBuilder.mimeType = mimeType; headerBuilder.charset = charset + case _ => sys.error(s"Invalid header: $line") } object integer { diff --git a/core/src/jsonrpclib/internals/MessageDispatcher.scala b/modules/core/src/main/scala/jsonrpclib/internals/MessageDispatcher.scala similarity index 100% rename from core/src/jsonrpclib/internals/MessageDispatcher.scala rename to modules/core/src/main/scala/jsonrpclib/internals/MessageDispatcher.scala diff --git a/core/src/jsonrpclib/internals/RawMessage.scala b/modules/core/src/main/scala/jsonrpclib/internals/RawMessage.scala similarity index 100% rename from core/src/jsonrpclib/internals/RawMessage.scala rename to modules/core/src/main/scala/jsonrpclib/internals/RawMessage.scala diff --git a/core/src/jsonrpclib/package.scala b/modules/core/src/main/scala/jsonrpclib/package.scala similarity index 100% rename from core/src/jsonrpclib/package.scala rename to modules/core/src/main/scala/jsonrpclib/package.scala diff --git a/modules/core/src/test/scala/jsonrpclib/CallIdSpec.scala b/modules/core/src/test/scala/jsonrpclib/CallIdSpec.scala new file mode 100644 index 0000000..b227173 --- /dev/null +++ b/modules/core/src/test/scala/jsonrpclib/CallIdSpec.scala @@ -0,0 +1,20 @@ +package jsonrpclib + +import weaver._ +import com.github.plokhotnyuk.jsoniter_scala.core._ + +object CallIdSpec extends FunSuite { + test("json parsing") { + val strJson = """ "25" """.trim + + val intJson = "25" + + val longJson = Long.MaxValue.toString + + val nullJson = "null" + assert.same(readFromString[CallId](strJson), CallId.StringId("25")) && + assert.same(readFromString[CallId](intJson), CallId.NumberId(25)) && + assert.same(readFromString[CallId](longJson), CallId.NumberId(Long.MaxValue)) && + assert.same(readFromString[CallId](nullJson), CallId.NullId) + } +} diff --git a/core/test/src-jvm-native/jsonrpclib/RawMessageSpec.scala b/modules/core/src/test/scala/jsonrpclib/RawMessageSpec.scala similarity index 77% rename from core/test/src-jvm-native/jsonrpclib/RawMessageSpec.scala rename to modules/core/src/test/scala/jsonrpclib/RawMessageSpec.scala index 21a0760..be5e41a 100644 --- a/core/test/src-jvm-native/jsonrpclib/RawMessageSpec.scala +++ b/modules/core/src/test/scala/jsonrpclib/RawMessageSpec.scala @@ -1,30 +1,29 @@ package jsonrpclib -import munit._ +import weaver._ +import jsonrpclib.internals._ import com.github.plokhotnyuk.jsoniter_scala.core._ -import internals._ import jsonrpclib.CallId.NumberId import jsonrpclib.OutputMessage.ResponseMessage -class RawMessageSpec() extends FunSuite { +object RawMessageSpec extends FunSuite { test("json parsing with null result") { // This is a perfectly valid response object, as result field has to be present, // but can be null: https://www.jsonrpc.org/specification#response_object val rawMessage = readFromString[RawMessage](""" {"jsonrpc":"2.0","result":null,"id":3} """.trim) - assertEquals( - rawMessage, - RawMessage(jsonrpc = "2.0", result = Some(None), id = Some(NumberId(3))) - ) - - assertEquals(rawMessage.toMessage, Right(ResponseMessage(NumberId(3), Payload.NullPayload))) // This, on the other hand, is an invalid response message, as result field is missing val invalidRawMessage = readFromString[RawMessage](""" {"jsonrpc":"2.0","id":3} """.trim) - assertEquals( + + assert.same( + rawMessage, + RawMessage(jsonrpc = "2.0", result = Some(None), id = Some(NumberId(3))) + ) && + assert.same(rawMessage.toMessage, Right(ResponseMessage(NumberId(3), Payload.NullPayload))) && + assert.same( invalidRawMessage, RawMessage(jsonrpc = "2.0", result = None, id = Some(NumberId(3))) - ) - - assert(invalidRawMessage.toMessage.isLeft, invalidRawMessage.toMessage) + ) && + assert(invalidRawMessage.toMessage.isLeft, invalidRawMessage.toMessage.toString) } } diff --git a/core/test/src-jvm-native/jsonrpclib/internals/HeaderSpec.scala b/modules/core/src/test/scalajvm-native/jsonrpclib/internals/HeaderSpec.scala similarity index 91% rename from core/test/src-jvm-native/jsonrpclib/internals/HeaderSpec.scala rename to modules/core/src/test/scalajvm-native/jsonrpclib/internals/HeaderSpec.scala index 4ae1f6a..e58bd59 100644 --- a/core/test/src-jvm-native/jsonrpclib/internals/HeaderSpec.scala +++ b/modules/core/src/test/scalajvm-native/jsonrpclib/internals/HeaderSpec.scala @@ -1,6 +1,6 @@ package jsonrpclib.internals -import munit.FunSuite +import weaver._ import java.io.ByteArrayInputStream import java.io.BufferedReader import java.io.InputStreamReader @@ -8,8 +8,7 @@ import jsonrpclib.ProtocolError import java.io.IOException import java.io.UncheckedIOException -class HeaderSpec() extends FunSuite { - +object HeaderSpec extends FunSuite { test("headers (all)") { val result = read( "Content-Length: 123\r", @@ -18,7 +17,7 @@ class HeaderSpec() extends FunSuite { "foo..." ) val expected = Result(LSPHeaders(123, "application/vscode-jsonrpc", "utf-8"), "foo...") - assertEquals(result, Right(expected)) + assert.same(result, Right(expected)) } test("headers (only content-)") { @@ -28,7 +27,7 @@ class HeaderSpec() extends FunSuite { "foo..." ) val expected = Result(LSPHeaders(123, "application/json", "UTF-8"), "foo...") - assertEquals(result, Right(expected)) + assert.same(result, Right(expected)) } test("no header)") { @@ -36,7 +35,7 @@ class HeaderSpec() extends FunSuite { "foo" ) val expected = ProtocolError.ParseError("Could not parse LSP headers") - assertEquals(result, Left(expected)) + assert.same(result, Left(expected)) } test("missing content-length") { @@ -46,7 +45,7 @@ class HeaderSpec() extends FunSuite { "foo..." ) val expected = ProtocolError.ParseError("Missing Content-Length header") - assertEquals(result, Left(expected)) + assert.same(result, Left(expected)) } case class Result(header: LSPHeaders, rest: String) diff --git a/examples/client/src/examples/client/ClientMain.scala b/modules/examples/client/src/main/scala/examples/client/ClientMain.scala similarity index 84% rename from examples/client/src/examples/client/ClientMain.scala rename to modules/examples/client/src/main/scala/examples/client/ClientMain.scala index 7c87505..5097f2d 100644 --- a/examples/client/src/examples/client/ClientMain.scala +++ b/modules/examples/client/src/main/scala/examples/client/ClientMain.scala @@ -1,20 +1,14 @@ -package examples.server +package examples.client -import jsonrpclib.CallId -import jsonrpclib.fs2._ import cats.effect._ -import fs2.io._ +import cats.syntax.all._ import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker -import jsonrpclib.Endpoint -import cats.syntax.all._ import fs2.Stream -import jsonrpclib.StubTemplate -import cats.effect.std.Dispatcher -import cats.effect.implicits._ -import java.io.OutputStream -import java.io.InputStream -import examples.client.ChildProcess +import fs2.io._ +import fs2.io.process.Processes +import jsonrpclib.CallId +import jsonrpclib.fs2._ object ClientMain extends IOApp.Simple { @@ -31,13 +25,12 @@ object ClientMain extends IOApp.Simple { def log(str: String): IOStream[Unit] = Stream.eval(IO.consoleForIO.errorln(str)) def run: IO[Unit] = { - import scala.concurrent.duration._ // Using errorln as stdout is used by the RPC channel val run = for { _ <- log("Starting client") serverJar <- sys.env.get("SERVER_JAR").liftTo[IOStream](new Exception("SERVER_JAR env var does not exist")) // Starting the server - rp <- ChildProcess.spawn[IO]("java", "-jar", serverJar) + rp <- fs2.Stream.resource(Processes[IO].spawn(process.ProcessBuilder("java", "-jar", serverJar))) // Creating a channel that will be used to communicate to the server fs2Channel <- FS2Channel[IO](cancelTemplate = cancelEndpoint.some) _ <- Stream(()) diff --git a/examples/server/src/examples/server/ServerMain.scala b/modules/examples/server/src/main/scala/examples/server/ServerMain.scala similarity index 98% rename from examples/server/src/examples/server/ServerMain.scala rename to modules/examples/server/src/main/scala/examples/server/ServerMain.scala index 7fc5d6e..72c9804 100644 --- a/examples/server/src/examples/server/ServerMain.scala +++ b/modules/examples/server/src/main/scala/examples/server/ServerMain.scala @@ -7,7 +7,6 @@ import fs2.io._ import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker import jsonrpclib.Endpoint -import cats.syntax.all._ object ServerMain extends IOApp.Simple { diff --git a/fs2/src/jsonrpclib/fs2/CancelTemplate.scala b/modules/fs2/src/main/scala/jsonrpclib/fs2/CancelTemplate.scala similarity index 100% rename from fs2/src/jsonrpclib/fs2/CancelTemplate.scala rename to modules/fs2/src/main/scala/jsonrpclib/fs2/CancelTemplate.scala diff --git a/fs2/src/jsonrpclib/fs2/FS2Channel.scala b/modules/fs2/src/main/scala/jsonrpclib/fs2/FS2Channel.scala similarity index 100% rename from fs2/src/jsonrpclib/fs2/FS2Channel.scala rename to modules/fs2/src/main/scala/jsonrpclib/fs2/FS2Channel.scala diff --git a/fs2/src/jsonrpclib/fs2/lsp.scala b/modules/fs2/src/main/scala/jsonrpclib/fs2/lsp.scala similarity index 99% rename from fs2/src/jsonrpclib/fs2/lsp.scala rename to modules/fs2/src/main/scala/jsonrpclib/fs2/lsp.scala index eeaff1e..29963a7 100644 --- a/fs2/src/jsonrpclib/fs2/lsp.scala +++ b/modules/fs2/src/main/scala/jsonrpclib/fs2/lsp.scala @@ -1,7 +1,6 @@ package jsonrpclib.fs2 import cats.MonadThrow -import cats.implicits._ import fs2.Chunk import fs2.Stream import fs2.Pipe @@ -14,6 +13,7 @@ import jsonrpclib.Message import jsonrpclib.ProtocolError import jsonrpclib.Payload.Data import jsonrpclib.Payload.NullPayload +import scala.annotation.tailrec object lsp { @@ -106,6 +106,7 @@ object lsp { case object ReadingBody extends Status } + @tailrec private def loop( state: ScanState, acc: Seq[Chunk[Byte]] = Seq.empty diff --git a/fs2/src/jsonrpclib/fs2/package.scala b/modules/fs2/src/main/scala/jsonrpclib/fs2/package.scala similarity index 100% rename from fs2/src/jsonrpclib/fs2/package.scala rename to modules/fs2/src/main/scala/jsonrpclib/fs2/package.scala diff --git a/fs2/test/src/jsonrpclib/fs2/FS2ChannelSpec.scala b/modules/fs2/src/test/scala/jsonrpclib/fs2/FS2ChannelSpec.scala similarity index 99% rename from fs2/test/src/jsonrpclib/fs2/FS2ChannelSpec.scala rename to modules/fs2/src/test/scala/jsonrpclib/fs2/FS2ChannelSpec.scala index 90f2bae..43b7c60 100644 --- a/fs2/test/src/jsonrpclib/fs2/FS2ChannelSpec.scala +++ b/modules/fs2/src/test/scala/jsonrpclib/fs2/FS2ChannelSpec.scala @@ -6,7 +6,6 @@ import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker import fs2.Stream import jsonrpclib._ -import jsonrpclib.fs2.FS2Channel import weaver._ import scala.concurrent.duration._ diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..cc68b53 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.10.11 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..6e9bb88 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,17 @@ +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.11") + +addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.2") + +addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.1") + +addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.10.0") + +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.2") + +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") + +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.3.1") + +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") + +addDependencyTreePlugin