Skip to content

Commit ca3b26c

Browse files
committed
Adds installation instructions and examples
1 parent 58f5d5b commit ca3b26c

File tree

8 files changed

+185
-18
lines changed

8 files changed

+185
-18
lines changed

.scalafmt.conf

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
11
version = "3.5.3"
22
runner.dialect = scala213
33
maxColumn = 120
4+
fileOverride {
5+
"glob:**/fs2/src/**" {
6+
runner.dialect = scala213source3
7+
}
8+
"glob:**/fs2/test/src/**" {
9+
runner.dialect = scala213source3
10+
}
11+
"glob:**/core/test/src-jvm-native/**" {
12+
runner.dialect = scala213source3
13+
}
14+
"glob:**/core/src/**" {
15+
runner.dialect = scala213source3
16+
}
17+
}

README.md

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,41 @@
11
[![CI](https://github.com/neandertech/jsonrpclib/actions/workflows/ci.yml/badge.svg)](https://github.com/neandertech/jsonrpclib/actions/workflows/ci.yml)
22

3+
[![jsonrpclib-fs2 Scala version support](https://index.scala-lang.org/neandertech/jsonrpclib/jsonrpclib-fs2/latest-by-scala-version.svg?platform=jvm](https://index.scala-lang.org/neandertech/jsonrpclib/jsonrpclib-fs2)
4+
5+
[![jsonrpclib-fs2 Scala version support](https://index.scala-lang.org/neandertech/jsonrpclib/jsonrpclib-fs2/latest-by-scala-version.svg?platform=sjs1](https://index.scala-lang.org/neandertech/jsonrpclib/jsonrpclib-fs2)
6+
7+
38
# jsonrpclib
49

5-
This is a cross-platform, cross-scala-version [jsonrpc](https://www.jsonrpc.org/) library that provides construct for bidirectional communication using
6-
the jsonrpc protocol.
10+
This is a cross-platform, cross-scala-version library that provides construct for bidirectional communication using the [jsonrpc](https://www.jsonrpc.org/) protocol. It is built on top of [fs2](https://fs2.io/#/) and [jsoniter-scala](https://github.com/plokhotnyuk/jsoniter-scala)
11+
12+
This library does not enforce any transport, and can work on top of stdin/stdout or other channels.
13+
14+
## Installation
15+
16+
The dependencies below are following [cross-platform semantics](http://youforgotapercentagesignoracolon.com/).
17+
Adapt according to your needs
18+
19+
### SBT
720

8-
This library does not enforce any transport, and works as long as you can provide input/output byte streams.
21+
```scala
22+
libraryDependencies += "tech.neander" %%% "jsonrpclib-fs2" % version
23+
```
924

25+
### Mill
1026

11-
## Dev Notes
27+
```scala
28+
override def ivyDeps = super.ivyDeps() ++ Agg(ivy"tech.neander::jsonrpclib-fs2::$version")
29+
```
1230

13-
### Scala-native
31+
### Scala-cli
1432

15-
See
16-
* https://github.com/scala-native/scala-native/blob/63d07093f6d0a6e9de28cd8f9fb6bc1d6596c6ec/test-interface/src/main/scala/scala/scalanative/testinterface/NativeRPC.scala
33+
```scala
34+
//> using lib "tech.neander::jsonrpclib-fs2:<VERSION>"
35+
```
1736

37+
## Usage
1838

19-
### Scala-js
39+
**/!\ Please be aware that this library is in its early days and offers strictly no guarantee with regards to backward compatibility**
2040

21-
See
22-
* https://github.com/scala-js/scala-js-js-envs/blob/main/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala#L245
23-
* https://github.com/scala-js/scala-js/blob/0708917912938714d52be1426364f78a3d1fd269/test-bridge/src/main/scala/org/scalajs/testing/bridge/JSRPC.scala
41+
See the examples folder

build.sc

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import mill.define.Target
2+
import mill.util.Jvm
13
import $ivy.`com.lihaoyi::mill-contrib-bloop:$MILL_VERSION`
24
import $ivy.`io.github.davidgregory084::mill-tpolecat::0.3.1`
35
import $ivy.`de.tototec::de.tobiasroeser.mill.vcs.version::0.1.4`
@@ -20,6 +22,7 @@ object versions {
2022
val scalaNativeVersion = "0.4.4"
2123
val munitVersion = "0.7.29"
2224
val munitNativeVersion = "1.0.0-M6"
25+
val fs2 = "3.2.11"
2326

2427
val scala213 = "2.13"
2528
val scala212 = "2.12"
@@ -59,7 +62,7 @@ object fs2 extends RPCCrossPlatformModule { cross =>
5962

6063
override def crossPlatformModuleDeps = Seq(core)
6164
def crossPlatformIvyDeps: T[Agg[Dep]] = Agg(
62-
ivy"co.fs2::fs2-core::3.2.8"
65+
ivy"co.fs2::fs2-core::${versions.fs2}"
6366
)
6467

6568
object jvm extends mill.Cross[JvmModule](scala213, scala3)
@@ -74,6 +77,26 @@ object fs2 extends RPCCrossPlatformModule { cross =>
7477

7578
}
7679

80+
object examples extends mill.define.Module {
81+
82+
object server extends ScalaModule {
83+
def ivyDeps = Agg(ivy"co.fs2::fs2-io:${versions.fs2}")
84+
def moduleDeps = Seq(fs2.jvm(versions.scala213))
85+
def scalaVersion = versions.scala213Version
86+
}
87+
88+
object client extends ScalaModule {
89+
def ivyDeps = Agg(ivy"co.fs2::fs2-io:${versions.fs2}", ivy"eu.monniot::fs2-process:0.4.4")
90+
def moduleDeps = Seq(fs2.jvm(versions.scala213))
91+
def scalaVersion = versions.scala213Version
92+
def forkEnv: Target[Map[String, String]] = T {
93+
val assembledServer = server.assembly()
94+
super.forkEnv() ++ Map("SERVER_JAR" -> assembledServer.path.toString())
95+
}
96+
}
97+
98+
}
99+
77100
// #############################################################################
78101
// COMMON SETUP
79102
// #############################################################################

devnotes.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Dev Notes
2+
3+
In case somebody wants to implement future-based channels, here are some source of inspiration :
4+
5+
### Scala-native
6+
7+
See
8+
* https://github.com/scala-native/scala-native/blob/63d07093f6d0a6e9de28cd8f9fb6bc1d6596c6ec/test-interface/src/main/scala/scala/scalanative/testinterface/NativeRPC.scala
9+
10+
11+
### Scala-js
12+
13+
See
14+
* https://github.com/scala-js/scala-js-js-envs/blob/main/nodejs-env/src/main/scala/org/scalajs/jsenv/nodejs/ComSupport.scala#L245
15+
* https://github.com/scala-js/scala-js/blob/0708917912938714d52be1426364f78a3d1fd269/test-bridge/src/main/scala/org/scalajs/testing/bridge/JSRPC.scala
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package examples.server
2+
3+
import jsonrpclib.CallId
4+
import jsonrpclib.fs2._
5+
import cats.effect._
6+
import fs2.io._
7+
import eu.monniot.process.Process
8+
import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec
9+
import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker
10+
import jsonrpclib.Endpoint
11+
import cats.syntax.all._
12+
import fs2.Stream
13+
import jsonrpclib.StubTemplate
14+
15+
object ClientMain extends IOApp.Simple {
16+
17+
// Reserving a method for cancelation.
18+
val cancelTemplate = CancelTemplate.make[CallId]("$/cancel", identity, identity)
19+
20+
// Creating a datatype that'll serve as a request (and response) of an endpoint
21+
case class IntWrapper(value: Int)
22+
object IntWrapper {
23+
implicit val jcodec: JsonValueCodec[IntWrapper] = JsonCodecMaker.make
24+
}
25+
26+
type IOStream[A] = fs2.Stream[IO, A]
27+
def log(str: String): IOStream[Unit] = Stream.eval(IO.consoleForIO.errorln(str))
28+
29+
def run: IO[Unit] = {
30+
// Using errorln as stdout is used by the RPC channel
31+
val run = for {
32+
_ <- log("Starting client")
33+
serverJar <- sys.env.get("SERVER_JAR").liftTo[IOStream](new Exception("SERVER_JAR env var does not exist"))
34+
serverProcess <- Stream.resource(Process.spawn[IO]("java", "-jar", serverJar))
35+
fs2Channel <- FS2Channel.lspCompliant[IO](serverProcess.stdout, serverProcess.stdin)
36+
// Opening the stream to be able to send and receive data
37+
_ <- fs2Channel.openStream
38+
// Creating a `IntWrapper => IO[IntWrapper]` stub that can call the server
39+
increment = fs2Channel.simpleStub[IntWrapper, IntWrapper]("increment")
40+
result <- Stream.eval(IO.pure(IntWrapper(0)).flatMap(increment))
41+
_ <- log(s"Client received $result")
42+
_ <- log("Terminating client")
43+
} yield ()
44+
run.compile.drain
45+
}
46+
47+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package examples.server
2+
3+
import jsonrpclib.CallId
4+
import jsonrpclib.fs2._
5+
import cats.effect._
6+
import fs2.io._
7+
import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec
8+
import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker
9+
import jsonrpclib.Endpoint
10+
11+
object ServerMain extends IOApp.Simple {
12+
13+
// Reserving a method for cancelation.
14+
val cancelTemplate = CancelTemplate.make[CallId]("$/cancel", identity, identity)
15+
16+
// Creating a datatype that'll serve as a request (and response) of an endpoint
17+
case class IntWrapper(value: Int)
18+
object IntWrapper {
19+
implicit val jcodec: JsonValueCodec[IntWrapper] = JsonCodecMaker.make
20+
}
21+
22+
// Implementing an incrementation endpoint
23+
val increment = Endpoint[IO]("increment").simple { in: IntWrapper =>
24+
IO.consoleForIO.errorln(s"Server received $in") >>
25+
IO.pure(in.copy(value = in.value + 1))
26+
}
27+
28+
def run: IO[Unit] = {
29+
// Using errorln as stdout is used by the RPC channel
30+
IO.consoleForIO.errorln("Starting server") >>
31+
FS2Channel
32+
.lspCompliant[IO](fs2.io.stdin[IO](bufSize = 512), fs2.io.stdout[IO])
33+
.flatMap(_.withEndpointStream(increment)) // mounting an endpoint onto the channel
34+
.flatMap(_.openStreamForever) // starts the communication
35+
.compile
36+
.drain
37+
.guarantee(IO.consoleForIO.errorln("Terminating server"))
38+
}
39+
40+
}

fs2/src/jsonrpclib/fs2/FS2Channel.scala

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,30 @@ import scala.util.Try
1919
import _root_.fs2.concurrent.SignallingRef
2020

2121
trait FS2Channel[F[_]] extends Channel[F] {
22-
def withEndpoint(endpoint: Endpoint[F])(implicit F: Functor[F]): Resource[F, Unit] =
23-
Resource.make(mountEndpoint(endpoint))(_ => unmountEndpoint(endpoint.method))
22+
def withEndpoint(endpoint: Endpoint[F])(implicit F: Functor[F]): Resource[F, FS2Channel[F]] =
23+
Resource.make(mountEndpoint(endpoint))(_ => unmountEndpoint(endpoint.method)).map(_ => this)
2424

25-
def withEndpoints(endpoint: Endpoint[F], rest: Endpoint[F]*)(implicit F: Monad[F]): Resource[F, Unit] =
26-
(endpoint :: rest.toList).traverse_(withEndpoint)
25+
def withEndpointStream(endpoint: Endpoint[F])(implicit F: MonadCancelThrow[F]): Stream[F, FS2Channel[F]] =
26+
Stream.resource(withEndpoint(endpoint))
27+
28+
def withEndpoints(endpoint: Endpoint[F], rest: Endpoint[F]*)(implicit F: Monad[F]): Resource[F, FS2Channel[F]] =
29+
(endpoint :: rest.toList).traverse_(withEndpoint).as(this)
30+
31+
def withEndpointStream(endpoint: Endpoint[F], rest: Endpoint[F]*)(implicit
32+
F: MonadCancelThrow[F]
33+
): Stream[F, FS2Channel[F]] =
34+
Stream.resource(withEndpoints(endpoint, rest: _*))
2735

2836
def open: Resource[F, Unit]
2937
def openStream: Stream[F, Unit]
38+
def openStreamForever: Stream[F, Nothing]
3039
}
3140

3241
object FS2Channel {
3342

3443
def lspCompliant[F[_]: Concurrent](
3544
byteStream: Stream[F, Byte],
36-
byteSink: Pipe[F, Byte, Nothing],
45+
byteSink: Pipe[F, Byte, Unit],
3746
bufferSize: Int = 512,
3847
maybeCancelTemplate: Option[CancelTemplate] = None
3948
): Stream[F, FS2Channel[F]] = internals.LSP.writeSink(byteSink, bufferSize).flatMap { sink =>
@@ -120,6 +129,7 @@ object FS2Channel {
120129

121130
def open: Resource[F, Unit] = Resource.make[F, Unit](isOpen.set(true))(_ => isOpen.set(false))
122131
def openStream: Stream[F, Unit] = Stream.resource(open)
132+
def openStreamForever: Stream[F, Nothing] = openStream.evalMap(_ => F.never)
123133

124134
protected[fs2] def cancel(callId: CallId): F[Unit] = state.get.map(_.runningCalls.get(callId)).flatMap {
125135
case None => F.unit

fs2/src/jsonrpclib/fs2/internals/LSP.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import java.nio.charset.StandardCharsets
1414
object LSP {
1515

1616
def writeSink[F[_]: Concurrent](
17-
writePipe: fs2.Pipe[F, Byte, Nothing],
17+
writePipe: fs2.Pipe[F, Byte, Unit],
1818
bufferSize: Int
1919
): Stream[F, Payload => F[Unit]] =
2020
Stream.eval(Queue.bounded[F, Payload](bufferSize)).flatMap { queue =>

0 commit comments

Comments
 (0)