Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

md2mld is an unnecessary build dep of rpc #153

Merged
merged 10 commits into from
Mar 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions docs/dune
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
(rule
(targets index.mld)
(deps ../README.md)
(action
(with-stdout-to
%{targets}
(run %{bin:md2mld} -min-header 3 %{deps})
)
)
)
; (rule
; (alias runtest)
; (package rpc)
; (deps ../README.md)
; (action
; (progn
; (with-stdout-to
; index.mld.new
; (run %{bin:md2mld} -min-header 3 %{deps}))
; (diff? index.mld index.mld.new))))

(documentation
(package rpc)
)
(package rpc))
327 changes: 327 additions & 0 deletions docs/index.mld
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
{3 OCaml-RPC -- remote procedure calls \(RPC\) library}

[ocaml-rpc] is a library that provides remote procedure calls \(RPC\)
using XML or JSON as transport encodings. The transport mechanism itself
is outside the scope of this library as all conversions are from and to
strings. The [odoc] generated documentation is available at
{{: http://mirage.github.io/ocaml-rpc/rpclib/index.html} mirage.github.io/ocaml-rpc/rpclib}.

{{: https://travis-ci.org/mirage/ocaml-rpc} {%html: <img src="https://travis-ci.org/mirage/ocaml-rpc.svg?branch=master" alt="Build Status"></img>%}}

{4 RPC types}

An RPC value is defined as follow:

{[
type t =
Int of int64
| Int32 of int32
| Bool of bool
| Float of float
| String of string
| DateTime of string
| Enum of t list
| Dict of (string * t) list
| Null
]}

{4 Generating code}

The idea behind [ocaml-rpc] is to generate type definitions that can be used to
convert values of a given type to and from their RPC representations.

In order to do so, it is sufficient to add [[@@deriving rpcty]] to the
corresponding type definition. Hence :

{[
type t = ... [@@deriving rpcty]
]}

This will give a value [typ_of_t] of type [Rpc.Types.typ], which can be used in
conjunction with the [Rpcmarshal] module to:

{ul
{- Convert values of type [t] to values of type [Rpc.t]:

{[
let rpc_of_t t = Rpcmarshal.marshal typ_of_t t

]}
}
{- Convert values of type [Rpc.t] to values of type [t] :

{[
let t_of_rpc rpc = Rpcmarshal.unmarshal typ_of_t rpc

]}
}
}

Optionally, it is possible to have different field name in the OCaml
type \(if it is a record\) and in the dictionary argument \(the first
elements of [Dict]\):

{[
type t = { foo: int [@key "type"]; bar: int [@key "let"]; } [@@deriving rpcty]
]}

This will replace "foo" by "type" and "bar" by "let" in the RPC
representation. This is particularly useful when you want to integrate
with an existing API and the field names are not valid OCaml identifiers.

The library also provides the [[@@deriving rpc]] ppx, which is similar to
[rpcty], but directly generates the conversion functions.

{[
type t = ... [@@deriving rpc]
]}

will give two functions:

{ul
{- A function to convert values of type [t] to values of type [Rpc.t] :
[val rpc_of_t : t -> Rpc.t]

}
{- A function to convert values of type [Rpc.t] to values of type [t] :
[val t_of_rpc : Rpc.t -> (t,string) Result.result]

}
}

It also supports the [@key] annotations for having different field names:

{[
type t = { foo: int [@key "type"]; bar: int [@key "let"]; } [@@deriving rpc]
]}

{4 Conversion functions}

[ocaml-rpc] currently support two protocols: XMLRPC and JSON\(RPC\). Function
signatures are:

{[
val Xmlrpc.to_string : Rpc.t -> string
val Xmlrpc.of_string : string -> Rpc.t
val Jsonrpc.to_string : Rpc.t -> string
val Jsonrpc.of_string : string -> Rpc.t
]}

So if you want to marshal a value x of type t to JSON, you can use the
following function:

{[
Jsonrpc.to_string (rpc_of_t x)
]}

{4 IDL generator}

The [Idl] module makes it possible to define an abstract interface in OCaml
using the following pattern:

{[
module CalcInterface(R : Idl.RPC) = struct
open R

let int_p = Idl.Param.mk Rpc.Types.int

let add = R.declare "add"
["Add two numbers"]
(int_p @-> int_p @-> returning int_p Idl.DefaultError.err)

let mul = R.declare "mul"
["Multiply two numbers"]
(int_p @-> int_p @-> returning int_p Idl.DefaultError.err)

let implementation = implement
{ Idl.Interface.name = "Calc"; namespace = Some "Calc"; description = ["Calculator supporting addition and multiplication"]; version = (1,0,0) }
end
]}

Then we can generate various "bindings" from it by passing a module
implementing the [RPC] signature to this functor:

{ul
{- OCaml bindings for clients or servers can be generated using one of the
[GenClient*] or [GenServer*] functors, respectively.}
}

For example one can generate an RPC client this way:

{[
module CalcClient :
sig
val add :
(Rpc.call -> Rpc.response) ->
int -> int -> (int, Idl.DefaultError.t) result
val mul :
(Rpc.call -> Rpc.response) ->
int -> int -> (int, Idl.DefaultError.t) result
end = CalcInterface(Idl.GenClient ())

]}
The functions in the resulting [CalcClient] module can be used to call their
corresponding RPC methods.
[CalcClient] does not implement the transport mechanism itself, that should
be provided by passing an a rpc function of type [Rpc.call -> Rpc.response].

[CalcClient.add rpc 4 5] will marshal the parameters [4] and [5] into their
RPC representations, construct an [Rpc.call], pass that call to the given [rpc]
function, and return either an [Ok] containing the unmarshalled result or an
[Error] with the error description depending on the response returned by
[rpc].

There are variations of the [GenClient] module:

[GenClientExn] raises an exception in case the response indicates a failure,
instead of returning a [result]:

{[
module CalcClient :
sig
val add : (Rpc.call -> Rpc.response) -> int -> int -> int
val mul : (Rpc.call -> Rpc.response) -> int -> int -> int
end = CalcInterface(Idl.GenClientExn ())

]}

and [GenClientExnRpc] allows one to specify the rpc function once when
constructing the client module:

{[
module CalcClient :
sig
val add : int -> int -> int
val mul : int -> int -> int
end = CalcInterface(Idl.GenClientExnRpc (struct let rpc = rpc end))

]}

Bindings for a server can be generated in a similar way:

{[
module CalcServer :
sig
val add : (int -> int -> (int, Idl.DefaultError.t) result) -> unit
val mul : (int -> int -> (int, Idl.DefaultError.t) result) -> unit
val implementation : Idl.server_implementation
end = CalcInterface(Idl.GenServer ())

]}
The implementations of each RPC method should be specified by passing it to
the corresponding function in [CalcServer]:

{[
CalcServer.add (fun a b -> Ok (a + b));
CalcServer.mul (fun a b -> Ok (a * b));

]}
Then we can generate our server from the [implementation] \(in case of
[GenClient], [implementation] is unused\):

{[
let rpc : (Rpc.call -> Rpc.response) = Idl.server CalcServer.implementation

]}
Again, the transport mechanism is not implemented by [CalcServer]. We just
get an rpc function that, given an [Rpc.call], calls the implementation of
that RPC method and performs the marshalling and unmarshalling.
It is up to the user of this library to connect this [rpc] function to a real
server that responds to client requests.

Here we also have a [GenServerExn] functor, for server implementations that
raise exceptions instead of returning a [result].

The [rpclib-lwt] and [rpclib-async] packages provide similar client and
server generators that use [Lwt] and [Async], respectively.

The [Xmlrpc] and [Jsonrpc] modules can be helpful when implementing the [rpc]
function for an XML-RPC or JSON-RPC client/server: they provide functions for
converting rpc requests and responses to/from their respective wire formats.

{ul
{- Commandline interfaces can be generated using [Cmdlinergen]:

{[
module CalcCli :
sig
val implementation :
unit ->
((Rpc.call -> Rpc.response) ->
(unit -> unit) Cmdliner.Term.t * Cmdliner.Term.info)
list
end = CalcInterface(Cmdlinergen.Gen ())

]}
We can use the [implementation] to construct the CLI.
Again, we need to pass an [rpc] function that knows how to make RPC calls.

{[
let () =
let cmds = (List.map (fun t -> t rpc) (CalcCli.implementation ())) in
let open Cmdliner in
Term.(exit @@ eval_choice default_cmd cmds)

]}
}
{- Some generators use the output of [Codegen]. This functor generates a structure
that contains information about the methods, their parameters, return types,
etc. Currently these generators that use the output of [Codegen] require the
method parameters to be named.

}
}

{[
module CalcInterface(R : Idl.RPC) = struct
open R

let int_p_1 = Idl.Param.mk ~name:"int1" Rpc.Types.int
let int_p_2 = Idl.Param.mk ~name:"int2" Rpc.Types.int
let int_p = Idl.Param.mk Rpc.Types.int

let add = R.declare "add"
["Add two numbers"]
(int_p_1 @-> int_p_2 @-> returning int_p Idl.DefaultError.err)

let implementation = implement
{ Idl.Interface.name = "Calc"; namespace = Some "Calc"; description = ["Calculator supporting addition and multiplication"]; version = (1,0,0) }
end

module CalcCode :
sig
val implementation : unit -> Codegen.Interface.t
end = CalcInterface(Codegen.Gen ())

let interfaces = Codegen.Interfaces.create
~name:"calc"
~title:"Calculator"
~description:["Interface for a Calculator"]
~interfaces:[CalcCode.implementation ()]

]}

{ul
{- [Markdowngen] can generate a markdown file documenting these interfaces:
{[
let md = Markdowngen.to_string interfaces

]}
}
{- [Pythongen] can generate Python code that contains various classes
wrapping a Python implementation, providing typechecking &amp; method
dispatch, and a CLI.
{[
let code = Pythongen.of_interfaces interfaces |> Pythongen.string_of_ts

]}
}
}

The possibilities are not limited to the above generators provided by
[ocaml-rpc]. Any third-party module implementing the [RPC] signature can be
used to generate something from an interface defined following the above
pattern. For example, it is possible to write an [RPC] implementation that
generates a GUI for a given interface.


2 changes: 1 addition & 1 deletion dune-project
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
(lang dune 1.5)
(lang dune 2.0)
(name rpclib)
Loading