package pidgio
Install
dune-project
Dependency
Authors
Maintainers
Sources
sha256=1956664485b9073d26246d0655ddbd219e83c662c9617e30cd8d9215fed408c6
sha512=db5896e80e8f1822d2a8357727371d10940bda771fd3b0b9477fd93f3427ba095a7b7864616aa6192dcdc8e53dd77c627bec55361b88f180a933140f3a2724e8
Description
A tool for describing JSONRPC servers on processes that uses Pidgin as its format
Added to opam-repository:
README
pidgio
Every JSONRPC entry-point is parametrized by a Highway path (instead of just a method) and the handler of an entrypoint use Primavera for abstracting over dependencies/effects.
Example
Our goal is to have a generic and portable implementation; therefore, the library makes no assumptions whatsoever about how to handle JSON and provides an abstraction layer on top of it. pidgio-yojson is a direct implementation based on the Yojson.Basic representation. We will use it for this example.
For the same reason, the library does not handle I/O; instead, two libraries are provided: pidgio-unix (the basic version, which is based on the Unix module) and pidgio-eio (which is based on Eio). For this example, we will use Unix.
Building the Server Module
First, we're going to get a module on Unix and Yojson (using pidgio-unix and `pidgio-yojson):
module Server = Pidgio_unix.Make (Pidgio_yojson)The server module is compact and exposes the API that we will primarily use. You can view its API in the lib/sigs.mli file, which exposes the complete API. Essentially, there are several things to note:
Server.Effwhich is a Primavera module for abstracting effects/dependencies- What is needed to describe routes (including their paths and parameters)
- What is nedded to describe services
- Everything we need to run our server
The logic behind route declaration and routing is described in general terms in the Highway project.
Building our first service
We're going to create two services. The first one responds with pong when you send it ping.
let ping =
let open Server in
straight
~to_pidgin:Pidgin.Repr.string
(route [ s "ping" ] ignore_param)
(fun [] () _request -> return "pong")This first example already gives us a lot of clues. First, a route takes 3 required arguments:
to_pidgin: which allows you to serialize the result into a Pidgin expression so you don't have to worry about JSON. Here, since our response is a simple string, we don't need to go through a lot of steps. (We relay a lot onPidginfor describing things, this is why the library is calledpidgio)routethe server route which is apath+prism. A path, a way to extract information from a JSON-RPC entry point method, and a Pidgin prism that describes the entry point's parameters. (A Prism includes aconvand acheck, which also allow you to generate requests from a route)handler: the service controller. It performs the actual action of the service. The form of the function is as follow:fun list_of_path_fragment param_value request -> ...and returns a value wrapped from the moduleEff.
There are other parameters, such as precondition and postcondition, but they follow the same conventions as those described in Highway. It is also possible to use failable, which returns a value of type (('a, 'b) result, 'handler) eff and takes an additional argument, to_error, which must convert the error value into a JSONRPC error so that the focus remains solely on the happy path. And notify, a service that has no request ID and does not return a result. Notifications can share the same path as a service (if it appears later in the list) because requests with an ID ignore notifications in the routing phase.
If we specify the ping type as '_weak39 Server.service', it's essentially because a service's type parameter is a handler that will be unified with the other services. Without further ado, let's create a slightly more complicated service.
A more complicated service
Now that we've looked at a very simple service that doesn't involve any effects, we can build an echo service that will take the following JSON:
{
"message": "A string for the message",
"loudly": true
}Where message is obviously the message that we want to echo, and loudly is an optional boolean, if it is set to true, the response will be uppercased.
First, let's define our parameter:
type param =
{ message : string (* The message to echo *)
; loudly : bool (* If the flag is set to true, the echo is UPPERCASED *)
}We can lift it as a parameter using Server.param and using Pidgin API:
let echo_param =
let conv { message; loudly } =
let open Pidgin.Repr in
record [ "message", string message; "loudly", bool loudly ]
and check =
let open Pidgin.Check in
record (fun fields ->
let+ message = req fields "message" string
and+ loudly = opt fields "loudly" bool in
{ message; loudly = Option.value ~default:false loudly })
in
Server.param ~conv ~checkNow, we will define our response:
type response =
{ message : string
; loudly : bool
; time : float (* this field is here just to use effects ... *)
}Let's write a to_pidgin function for our response:
let response_to_pidgin { message; loudly; time } =
let open Pidgin.Repr in
record
[ "message", string message
; "loudly", bool loudly
; "time", float time ]Now, let's define our service:
let echo =
let open Server in
straight
~to_pidgin:response_to_pidgin
(route [ s "echo" ] echo_param)
(fun [] { message; loudly } _req ->
let+ time = Eff.perform (fun h -> h#get_time) in
let message =
if loudly then String.uppercase_ascii message else message
in
{ message; loudly; time })Now, if we inspect the type of echo : (< get_time : float; .. > as '_weak6) Server.service we keep the variable weakly generalized but we see that we need, at least, to handle the get_time method (thanks to Primavera).
Running the server
Now we can start our server by providing it with a dependency manager—which is an object (allowing handlers to be composed modularly through inheritance) and a list of services.
let () =
let handler =
object
method get_time = Unix.time ()
end
in
Server.run ~handler [ ping; echo ]And voila !
Trivia
Pidgio is a broadly generic implementation of the experiment described in Kohai for controlling software from within Emacs (and potentially other editors). Its goal is not to be very strict in how it handles JSON-RPC, but to make it easy to take advantage of a code editor’s toolkit to get a user interface “for free” when building software. The general idea is described in the following presentation.
Dev Dependencies (7)
-
odoc
with-doc -
ocaml-lsp-server
with-dev-setup -
merlin
with-dev-setup -
ocp-indent
with-dev-setup -
ocamlformat
with-dev-setup -
utop
with-dev-setup -
alcotest
with-test & >= "1.9.1"
Used by (3)
Conflicts
None