aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSébastien Dailly <sebastien@dailly.me>2025-06-01 17:12:06 +0200
committerSébastien Dailly <sebastien@dailly.me>2025-06-14 10:58:01 +0200
commit2f18b8a33cabd0ea666781ba048d0174b4dc5031 (patch)
treeaacda421b89e8133e3c73942e9ede61283a5005c
Initial commit
-rw-r--r--bin/client.ml23
-rw-r--r--bin/dune22
-rw-r--r--bin/js_assets/dune3
-rw-r--r--bin/main.ml58
-rw-r--r--dream_service.opam32
-rw-r--r--dune-project27
-rwxr-xr-xjs/application.ml31
-rw-r--r--js/application.mli63
-rw-r--r--js/content.ml122
-rw-r--r--js/dune23
-rw-r--r--lib/brr_handler/dune11
-rw-r--r--lib/brr_handler/js_handler.ml53
-rw-r--r--lib/brr_handler/js_handler.mli8
-rw-r--r--lib/cohttp_handler/cohttp_handler.ml83
-rw-r--r--lib/cohttp_handler/dune15
-rw-r--r--lib/dream_handler/dream_handler.ml165
-rw-r--r--lib/dream_handler/dream_handler.mli47
-rw-r--r--lib/dream_handler/dune13
-rw-r--r--lib/dune2
-rw-r--r--lib/operators/dune4
-rw-r--r--lib/operators/operators.ml94
-rw-r--r--lib/path/dune6
-rw-r--r--lib/path/path.ml116
-rw-r--r--lib/path/path.mli18
-rw-r--r--lib/services/dune10
-rw-r--r--lib/services/services.ml49
-rw-r--r--lib/virtuals/_readme.rst5
-rw-r--r--lib/virtuals/v_string/brr_string/dune4
-rw-r--r--lib/virtuals/v_string/brr_string/v_string.ml10
-rw-r--r--lib/virtuals/v_string/dream_string/dune4
-rw-r--r--lib/virtuals/v_string/dream_string/v_string.ml6
-rw-r--r--lib/virtuals/v_string/dune4
-rw-r--r--lib/virtuals/v_string/v_string.mli14
-rw-r--r--readme.rst95
-rw-r--r--services/dune10
-rw-r--r--services/nb_car.ml15
36 files changed, 1265 insertions, 0 deletions
diff --git a/bin/client.ml b/bin/client.ml
new file mode 100644
index 0000000..17284e1
--- /dev/null
+++ b/bin/client.ml
@@ -0,0 +1,23 @@
+open Lwt.Syntax
+
+let root = "http://[::1]:8080"
+
+let request =
+ let* result =
+ Cohttp_handler.request ~root
+ (module Services_impl.Nb_car)
+ () { value = "foobar" }
+ in
+ match result with
+ | Error code ->
+ prerr_endline ("Got code " ^ code);
+ Lwt.return_unit
+ | Ok { value; nbcar } ->
+ print_endline
+ (String.concat " "
+ [
+ "The number of characters for"; value; "is"; Int64.to_string nbcar;
+ ]);
+ Lwt.return_unit
+
+let _ = Lwt_main.run request
diff --git a/bin/dune b/bin/dune
new file mode 100644
index 0000000..7044b82
--- /dev/null
+++ b/bin/dune
@@ -0,0 +1,22 @@
+(executables
+ (public_names dream_service client)
+ (names main client)
+ (libraries
+ lwt
+ lwt.unix
+ dream
+ dream_handler
+ cohttp_handler
+ uri
+ services
+ services_impl
+ )
+ )
+
+
+(rule
+ (target jsAssets.ml)
+ (deps (glob_files js_assets/**))
+ (action (with-stdout-to %{null}
+ (run ocaml-crunch -m plain js_assets/ -o %{target}))))
+
diff --git a/bin/js_assets/dune b/bin/js_assets/dune
new file mode 100644
index 0000000..155a383
--- /dev/null
+++ b/bin/js_assets/dune
@@ -0,0 +1,3 @@
+; Copy the files generated with js_of_ocaml into this directory.
+; We have to explicitaly name the targets we want to include here.
+(copy_files ../../js/content.js)
diff --git a/bin/main.ml b/bin/main.ml
new file mode 100644
index 0000000..f1f0cfd
--- /dev/null
+++ b/bin/main.ml
@@ -0,0 +1,58 @@
+(** Create the handler for the service *)
+let handler =
+ Dream_handler.handle
+ (module Services_impl.Nb_car)
+ (fun (() : Services_impl.Nb_car.placeholders) body ->
+ Lwt.return_ok
+ Services_impl.Nb_car.
+ {
+ value = body.value;
+ nbcar = Int64.of_int (String.length body.value);
+ })
+
+(* The handler and the route are not created at the same time because we may
+ want create a specific handler, for example one checking CRSF in the query
+ and can’t infer this from the service signature only *)
+
+(** And create the route. *)
+let route = Dream_handler.register (module Services_impl.Nb_car) handler
+
+(** Generate a default static page *)
+let hello : Dream.handler =
+ fun _ ->
+ Dream.html
+ {|<html>
+ <body>
+ <h1>Hello!</h1>
+ <div>
+ <noscript>Sorry, you need to enable JavaScript to see this page.</noscript>
+ <script id="lib" type="text/javascript" defer="defer" src="js/content.js"></script>
+ <script>
+ var script = document.getElementById('lib');
+ lib.addEventListener('load', function() {
+ client.start()
+ })
+ </script>
+ </div>
+ <div id="content" />
+ </body>
+</html>|}
+
+let js_assets _root path _request =
+ (* This module is automatically generated — see the dune file to see the rule *)
+ match JsAssets.read path with
+ | None -> Dream.empty `Not_Found
+ | Some asset -> Dream.respond asset
+
+let () =
+ Dream.run @@ Dream.logger
+ @@ Dream.router
+ [
+ Dream.get "/js/**" (Dream.static ~loader:js_assets "");
+ Dream.get "/" hello;
+ route;
+ ]
+
+(* Now test the application by connecting to
+ http://localhost:8080/
+ *)
diff --git a/dream_service.opam b/dream_service.opam
new file mode 100644
index 0000000..55a27b2
--- /dev/null
+++ b/dream_service.opam
@@ -0,0 +1,32 @@
+# This file is generated by dune, edit dune-project instead
+opam-version: "2.0"
+synopsis: "A short synopsis"
+description: "A longer description"
+maintainer: ["Maintainer Name <maintainer@example.com>"]
+authors: ["Author Name <author@example.com>"]
+license: "LICENSE"
+tags: ["add topics" "to describe" "your" "project"]
+homepage: "https://github.com/username/reponame"
+doc: "https://url/to/documentation"
+bug-reports: "https://github.com/username/reponame/issues"
+depends: [
+ "dune" {>= "3.19"}
+ "ocaml"
+ "odoc" {with-doc}
+]
+build: [
+ ["dune" "subst"] {dev}
+ [
+ "dune"
+ "build"
+ "-p"
+ name
+ "-j"
+ jobs
+ "@install"
+ "@runtest" {with-test}
+ "@doc" {with-doc}
+ ]
+]
+dev-repo: "git+https://github.com/username/reponame.git"
+x-maintenance-intent: ["(latest)"]
diff --git a/dune-project b/dune-project
new file mode 100644
index 0000000..d14c1f0
--- /dev/null
+++ b/dune-project
@@ -0,0 +1,27 @@
+(lang dune 3.19)
+
+(name dream_service)
+
+(generate_opam_files true)
+(implicit_transitive_deps false)
+
+(source
+ (github username/reponame))
+
+(authors "Author Name <author@example.com>")
+
+(maintainers "Maintainer Name <maintainer@example.com>")
+
+(license LICENSE)
+
+(documentation https://url/to/documentation)
+
+(package
+ (name dream_service)
+ (synopsis "A short synopsis")
+ (description "A longer description")
+ (depends ocaml)
+ (tags
+ ("add topics" "to describe" your project)))
+
+; See the complete stanza docs at https://dune.readthedocs.io/en/stable/reference/dune-project/index.html
diff --git a/js/application.ml b/js/application.ml
new file mode 100755
index 0000000..fcc3fb4
--- /dev/null
+++ b/js/application.ml
@@ -0,0 +1,31 @@
+module Make (S : sig
+ type t
+end) =
+struct
+ module State = S
+
+ module type Processor = sig
+ type t
+
+ val process : t -> S.t -> S.t
+ end
+
+ module ID : Processor with type t = unit = struct
+ type t = unit
+
+ let process () state = state
+ end
+
+ type event = E : 'a * (module Processor with type t = 'a) -> event
+
+ (** Simple helper for the main event loop *)
+ let run : ?eq:(S.t -> S.t -> bool) -> S.t -> event Note.E.t -> S.t Note.S.t =
+ fun ?eq init event ->
+ let action =
+ Note.E.map (fun (E (t, (module P))) st -> P.process t st) event
+ in
+ Note.S.accum ?eq init action
+
+ let dispatch : (module Processor with type t = 's) -> 's -> event =
+ fun (type s) (module P : Processor with type t = s) v -> E (v, (module P))
+end
diff --git a/js/application.mli b/js/application.mli
new file mode 100644
index 0000000..2e813f5
--- /dev/null
+++ b/js/application.mli
@@ -0,0 +1,63 @@
+(** The Make module build the main application loop.contents
+
+ The function [run] update the state on each event, and return a new state.
+ Each event must follow the [event] type, which is composed from the type
+ [t], and a module with a fonction [update].
+
+ This example create an application with the state containing a simple
+ counter. An even which increment this counter is created and can be used to
+ update the state.
+
+ {[
+ type state = { value : int }
+
+ (** Increment the state. *)
+ module Incr = struct
+ type t = unit
+
+ let process () state = { value = state.value + 1 }
+ end
+
+ (** Decrement the state. *)
+ module Incr = struct
+ type t = unit
+
+ let process () state = { value = state.value - 1 }
+ end
+
+ module App = Make(struct type t = state end)
+
+ (* Create the event processor *)
+ let incr_event = App.dispatch (module Incr) ()
+ and decr_event = App.dispatch (module Decr) () in
+
+ let init = { value = 0 } in
+
+ (* Run the main loop *)
+ let state = App.run
+ init
+ (E.select
+ [ incr_event
+ ; decr_event ] ) in …
+ ]} *)
+module Make (S : sig
+ type t
+end) : sig
+ module type Processor = sig
+ type t
+
+ val process : t -> S.t -> S.t
+ end
+
+ module ID : Processor with type t = unit
+
+ type event
+
+ val dispatch : (module Processor with type t = 's) -> 's -> event
+ (** [dispatch (module P) v] will create an event holding a value [v] and
+ associated with the processor [P] *)
+
+ val run : ?eq:(S.t -> S.t -> bool) -> S.t -> event Note.E.t -> S.t Note.S.t
+ (** The function [run state ev] will create a signal continually updated each
+ time the event [ev] occur *)
+end
diff --git a/js/content.ml b/js/content.ml
new file mode 100644
index 0000000..f41bc46
--- /dev/null
+++ b/js/content.ml
@@ -0,0 +1,122 @@
+module OptionInfix = Operators.Binding (Option)
+
+let add_field :
+ label:Jstr.t ->
+ id':Jstr.t ->
+ value':Jstr.t ->
+ Brr.El.t list ->
+ Brr.El.t list =
+ fun ~label ~id' ~value' elt ->
+ Brr.El.label ~at:Brr.At.[ for' id' ] [ Brr.El.txt label ]
+ :: Brr.El.input ~at:Brr.At.[ type' (Jstr.v "text"); id id'; value value' ] ()
+ :: elt
+
+module State = struct
+ type t = { word : string; len : int; counter : int }
+
+ let repr_html : t -> Brr.El.t list =
+ fun { word; len; counter } ->
+ [
+ Brr.El.form
+ @@ add_field ~id':(Jstr.v "text_state") ~label:(Jstr.v "Word received")
+ ~value':(Jstr.v word)
+ @@ add_field ~id':(Jstr.v "nbcar_state") ~label:(Jstr.v "Nb of car")
+ ~value':(Jstr.of_int len)
+ @@ add_field ~id':(Jstr.v "counter_state") ~label:(Jstr.v "Request sent")
+ ~value':(Jstr.of_int counter) [];
+ ]
+end
+
+(** Service transforming the response from the request into the state *)
+module WordCount = struct
+ type t = Services_impl.Nb_car.response
+
+ let process response state =
+ Brr.Console.log
+ [
+ Jstr.v response.Services_impl.Nb_car.value;
+ Int64.to_int response.Services_impl.Nb_car.nbcar;
+ ];
+ State.
+ {
+ counter = state.counter + 1;
+ word = response.Services_impl.Nb_car.value;
+ len = Int64.to_int response.Services_impl.Nb_car.nbcar;
+ }
+end
+
+module App = Application.Make (State)
+
+let main () =
+ let open OptionInfix in
+ let- content_div =
+ Brr.Document.find_el_by_id Brr.G.document (Jstr.v "content")
+ in
+
+ let form =
+ Brr.El.form
+ [
+ Brr.El.label
+ ~at:Brr.At.[ for' (Jstr.v "text") ]
+ [ Brr.El.txt (Jstr.v "Text") ];
+ Brr.El.input
+ ~at:
+ Brr.At.
+ [
+ type' (Jstr.v "text"); id (Jstr.v "text"); name (Jstr.v "text");
+ ]
+ ();
+ Brr.El.input
+ ~at:Brr.At.[ type' (Jstr.v "submit"); value (Jstr.v "Count") ]
+ ();
+ ]
+ in
+
+ (* Listen the submit event on the form. This is an example of event of event :
+
+ First we listen for the click event, and then for the request response
+ event. *)
+ let post_event : Brr.El.t -> App.event Note.event =
+ fun form ->
+ Note.E.join
+ @@ Note_brr.Evr.on_el Brr_io.Form.Ev.submit
+ (fun ev ->
+ (* Do not send the query, we use it with javascript *)
+ Brr.Ev.prevent_default ev;
+
+ (* Extract the data from the form *)
+ let data = Brr_io.Form.(Data.of_form (of_el form)) in
+ let text_value = Brr_io.Form.Data.find data (Jstr.v "text") in
+ let value =
+ match text_value with
+ | Some (`String s) -> Jstr.to_string s
+ | _ -> ""
+ in
+
+ (* Send the request *)
+ Js_handler.send (module Services_impl.Nb_car) () { value }
+ |> Note_brr.Futr.to_event
+ |> Note.E.map (function
+ | Error _ -> App.dispatch (module App.ID) ()
+ | Ok response -> App.dispatch (module WordCount) response))
+ form
+ in
+ let bottom = Brr.El.div [] in
+ Brr.El.append_children content_div [ form; Brr.El.hr (); bottom ];
+
+ let state =
+ App.run
+ { word = ""; len = 0; counter = 0 }
+ (Note.E.select [ post_event form ])
+ in
+
+ Note_brr.Elr.def_children bottom (Note.S.map State.repr_html state);
+
+ let log state = ignore state in
+ Note.Logr.hold (Note.S.log state log)
+
+let () =
+ Brr.Console.(debug [ Jstr.v "Js started" ]);
+ let open Jv in
+ let post = obj [| ("start", repr main) |] in
+ set global "client" post
diff --git a/js/dune b/js/dune
new file mode 100644
index 0000000..b9477f4
--- /dev/null
+++ b/js/dune
@@ -0,0 +1,23 @@
+(executable
+ (name content)
+ (libraries
+ brr
+ note
+ note.brr
+
+ operators
+ services_impl
+ js_handler
+ brr_string
+ )
+ (modes js)
+ (preprocess (pps js_of_ocaml-ppx))
+ (link_flags (:standard -no-check-prims))
+ (js_of_ocaml (flags :standard --opt 3 --target-env browser --disable genprim --disable debugger))
+ )
+
+(rule
+ (targets content.js)
+ (deps content.bc.js)
+ (action (copy %{deps} %{targets})))
+
diff --git a/lib/brr_handler/dune b/lib/brr_handler/dune
new file mode 100644
index 0000000..7d096c9
--- /dev/null
+++ b/lib/brr_handler/dune
@@ -0,0 +1,11 @@
+(library
+ (name js_handler)
+ (libraries
+ brr
+ yojson
+ v_string
+ path
+ services
+ )
+ (preprocess (pps ppx_yojson_conv lwt_ppx))
+ )
diff --git a/lib/brr_handler/js_handler.ml b/lib/brr_handler/js_handler.ml
new file mode 100644
index 0000000..71e1ce6
--- /dev/null
+++ b/lib/brr_handler/js_handler.ml
@@ -0,0 +1,53 @@
+let get_method : type a b. (a, b) Services.method_ -> Jstr.t = function
+ | GET -> Jstr.v "GET"
+ | POST -> Jstr.v "POST"
+ | PUT -> Jstr.v "PUT"
+ | DELETE -> Jstr.v "DELETE"
+ | HEAD -> Jstr.v "HEAD"
+ | CONNECT -> Jstr.v "CONNECT"
+ | OPTIONS -> Jstr.v "OPTIONS"
+ | TRACE -> Jstr.v "TRACE"
+ | PATCH -> Jstr.v "PATCH"
+
+let send :
+ (module Services.JsonClientHandler
+ with type request = 'request
+ and type response = 'response
+ and type placeholders = 'placeholders) ->
+ 'placeholders ->
+ 'request ->
+ ('response, Jv.Error.t) Fut.result =
+ fun (type request response placeholders)
+ (module S : Services.JsonClientHandler
+ with type request = request
+ and type response = response
+ and type placeholders = placeholders) parameters request ->
+ let json_repr =
+ S.yojson_of_request request |> Ppx_yojson_conv_lib.Yojson.Safe.to_string
+ in
+ let body =
+ match S.method_ with
+ | GET | HEAD -> None
+ | _ -> Some (Brr_io.Fetch.Body.of_jstr (Jstr.of_string json_repr))
+ in
+ let init =
+ Brr_io.Fetch.Request.init ?body ~method':(get_method S.method_) ()
+ in
+
+ (* There is no way to retreive the type of the string from the virtual module.
+ I know the type match, but I have to tell this to the compiler…
+ *)
+ let url_of_string : V_string.t -> Jstr.t = Obj.magic in
+ let url' : Jstr.t = url_of_string @@ Path.build parameters S.path in
+ let response = Brr_io.Fetch.(request @@ Request.v ~init url') in
+
+ (* Now handle the response *)
+ let open Fut.Result_syntax in
+ let* content = response in
+ let body = Brr_io.Fetch.Response.as_body content in
+ let* str_body = Brr_io.Fetch.Body.text body in
+ let str = Jstr.to_string str_body in
+ try
+ let json = Ppx_yojson_conv_lib.Yojson.Safe.from_string str in
+ Result.Ok (S.response_of_yojson json) |> Fut.return
+ with Yojson.Json_error err -> Fut.error (Jv.Error.v (Jstr.v err))
diff --git a/lib/brr_handler/js_handler.mli b/lib/brr_handler/js_handler.mli
new file mode 100644
index 0000000..c8611f7
--- /dev/null
+++ b/lib/brr_handler/js_handler.mli
@@ -0,0 +1,8 @@
+val send :
+ (module Services.JsonClientHandler
+ with type request = 'request
+ and type response = 'response
+ and type placeholders = 'placeholders) ->
+ 'placeholders ->
+ 'request ->
+ ('response, Jv.Error.t) Fut.result
diff --git a/lib/cohttp_handler/cohttp_handler.ml b/lib/cohttp_handler/cohttp_handler.ml
new file mode 100644
index 0000000..3842e7c
--- /dev/null
+++ b/lib/cohttp_handler/cohttp_handler.ml
@@ -0,0 +1,83 @@
+open Cohttp_lwt_unix
+open Lwt.Syntax
+
+let url_of_string : V_string.t -> string = Obj.magic
+
+let code_of_response : Response.t -> (int, string) Result.t =
+ fun resp ->
+ let code = resp |> Response.status |> Cohttp.Code.code_of_status in
+ match Cohttp.Code.is_success code with
+ | true -> Ok 200
+ | false -> Error (string_of_int code)
+
+let get_method : type a b. (a, b) Services.method_ -> Http.Method.t = function
+ | POST -> `POST
+ | GET -> `GET
+ | PUT -> `PUT
+ | DELETE -> `DELETE
+ | PATCH -> `PATCH
+ | HEAD -> `HEAD
+ | CONNECT -> `CONNECT
+ | OPTIONS -> `OPTIONS
+ | TRACE -> `TRACE
+
+(** Encodde the response given by cohttp for the service *)
+let map_response :
+ (module Services.JsonClientHandler
+ with type request = 'request
+ and type response = 'response
+ and type placeholders = 'placeholders) ->
+ Cohttp_lwt.Body.t ->
+ 'response Lwt.t =
+ fun (type request response placeholders)
+ (module S : Services.JsonClientHandler
+ with type request = request
+ and type response = response
+ and type placeholders = placeholders) body ->
+ match S.method_ with
+ | GET | POST | PUT | DELETE | PATCH | CONNECT | OPTIONS | TRACE ->
+ let* body_content = Cohttp_lwt.Body.to_string body in
+ let json = Ppx_yojson_conv_lib.Yojson.Safe.from_string body_content in
+ Lwt.return (S.response_of_yojson json)
+ | HEAD -> Lwt.return_unit
+
+let request :
+ root:string ->
+ (module Services.JsonClientHandler
+ with type request = 'request
+ and type response = 'response
+ and type placeholders = 'placeholders) ->
+ 'placeholders ->
+ 'request ->
+ ('response, string) result Lwt.t =
+ fun (type request response placeholders) ~root
+ (module S : Services.JsonClientHandler
+ with type request = request
+ and type response = response
+ and type placeholders = placeholders) parameters request ->
+ let uri =
+ V_string.concat ~sep:(V_string.v "/")
+ [ V_string.v root; Path.build parameters S.path ]
+ in
+
+ (* There is no body for GET or HEAD method *)
+ let request_body =
+ match S.method_ with
+ | Services.GET | Services.HEAD -> None
+ | _ ->
+ Some
+ (request |> S.yojson_of_request
+ |> Ppx_yojson_conv_lib.Yojson.Safe.to_string
+ |> Cohttp_lwt.Body.of_string)
+ in
+
+ let uri = Uri.of_string (url_of_string uri) in
+ let* response, body =
+ Client.call ?headers:None ?body:request_body (get_method S.method_) uri
+ in
+
+ match code_of_response response with
+ | Error _ as e -> Lwt.return e
+ | Ok _ ->
+ let* response_body = map_response (module S) body in
+ Lwt.return_ok response_body
diff --git a/lib/cohttp_handler/dune b/lib/cohttp_handler/dune
new file mode 100644
index 0000000..3aabbd0
--- /dev/null
+++ b/lib/cohttp_handler/dune
@@ -0,0 +1,15 @@
+(library
+ (name cohttp_handler)
+ (libraries
+ uri
+ cohttp
+ cohttp-lwt
+ cohttp-lwt-unix
+
+ operators
+ v_string
+ path
+ services
+ )
+ (preprocess (pps ppx_yojson_conv lwt_ppx))
+ )
diff --git a/lib/dream_handler/dream_handler.ml b/lib/dream_handler/dream_handler.ml
new file mode 100644
index 0000000..0454b32
--- /dev/null
+++ b/lib/dream_handler/dream_handler.ml
@@ -0,0 +1,165 @@
+open Lwt_result.Syntax
+
+(** Extract the content from the body request.
+
+ The module given in argument is the definition of the service. *)
+let read_body :
+ (module Services.JsonServerHandler with type request = 'request) ->
+ Dream.request ->
+ ('request, Dream.response) result Lwt.t =
+ fun (type request)
+ (module S : Services.JsonServerHandler with type request = request)
+ request ->
+ let%lwt json =
+ match S.method_ with
+ | GET | HEAD ->
+ (* GET and HEAD method doesn’t have any body. We assume here the body
+ is typed as Unit *)
+ Lwt.return `Null
+ | _ ->
+ let%lwt body = Dream.body request in
+ Yojson.Safe.from_string body |> Lwt.return
+ in
+ let json_content = Lwt.return @@ S.request_of_yojson json in
+ Lwt_result.ok json_content
+
+let create_response :
+ (module Services.JsonServerHandler with type response = 'response) ->
+ ('response, Dream.response) result Lwt.t ->
+ Dream.response Dream.promise =
+ fun (type response)
+ (module S : Services.JsonServerHandler with type response = response)
+ response_content ->
+ let response =
+ let* response_content = response_content in
+ let yojson_content = S.yojson_of_response response_content in
+ Yojson.Safe.to_string yojson_content |> Lwt_result.return
+ in
+ match%lwt response with Ok json -> Dream.json json | Error e -> Lwt.return e
+
+(** Simple handler which read the content and apply the transformations to the
+ response. *)
+let handle :
+ (module Services.JsonServerHandler
+ with type placeholders = 'placeholders
+ and type request = 'request
+ and type response = 'response) ->
+ ('placeholders -> 'request -> ('response, string) Lwt_result.t) ->
+ 'placeholders ->
+ Dream.handler =
+ fun (type placeholders request response)
+ (module S : Services.JsonServerHandler
+ with type placeholders = placeholders
+ and type response = response
+ and type request = request) f args request ->
+ let response =
+ let* body = read_body (module S) request in
+ Lwt_result.map_error
+ (fun e -> Dream.response ~status:`Internal_Server_Error e)
+ (f args body)
+ in
+ create_response (module S) response
+
+module MakeChecked (S : Services.JsonServerHandler) = struct
+ exception Invalid_method
+
+ (** Derive the handler from the standard one by adding a new field [token] in
+ the request *)
+ module Service = struct
+ include S
+ open Ppx_yojson_conv_lib.Yojson_conv.Primitives
+
+ type ('a, 'b) result = ('a, 'b) Result.t = Ok of 'a | Error of 'b
+ [@@deriving yojson]
+
+ type request = { content : S.request; token : string }
+ [@@deriving of_yojson]
+ (** This type add the validation token in the body message *)
+
+ let method_ : (request, response) Services.method_ =
+ match S.method_ with
+ (* We can’t add the crsf token with thoses methods because they do not
+ have body *)
+ | GET | HEAD -> raise Invalid_method
+ | POST -> POST
+ | PUT -> PUT
+ | DELETE -> DELETE
+ | CONNECT -> CONNECT
+ | OPTIONS -> OPTIONS
+ | TRACE -> TRACE
+ | PATCH -> PATCH
+ end
+
+ let check_token :
+ Dream.request -> string -> (unit, Dream.response) Lwt_result.t =
+ fun request token ->
+ match%lwt Dream.verify_csrf_token request token with
+ | `Ok -> Lwt.return_ok ()
+ | _ -> Lwt_result.fail (Dream.response ~status:`Unauthorized "")
+
+ (** Override the handle function by checking the token validity *)
+ let handle :
+ (S.placeholders -> S.request -> (S.response, string) Lwt_result.t) ->
+ S.placeholders ->
+ Dream.handler =
+ fun f args request ->
+ let response =
+ let* content = read_body (module Service) request in
+
+ (* Extract the token from the body and check the validity *)
+ let* () = check_token request content.token in
+
+ Lwt_result.map_error
+ (fun e -> Dream.response ~status:`Internal_Server_Error e)
+ (f args content.content)
+ in
+ create_response (module Service) response
+end
+
+let extract_param request name =
+ Dream.param request name |> Dream.from_percent_encoded
+
+let method' : type a b.
+ (a, b) Services.method_ -> string -> Dream.handler -> Dream.route = function
+ | GET -> Dream.get
+ | PUT -> Dream.put
+ | POST -> Dream.post
+ | DELETE -> Dream.delete
+ | HEAD -> Dream.head
+ | CONNECT -> Dream.connect
+ | OPTIONS -> Dream.options
+ | TRACE -> Dream.trace
+ | PATCH -> Dream.patch
+
+(** Handle the given URL encoded in the application.
+
+ Use the type system to ensure that the path for the route will use the same
+ arguments name as in the extraction, and that this url will match the
+ signature for the handler
+
+ [handle] method ?path url handler
+
+ will call the [handler] with the arguments extracted from [url]. If [path]
+ is given, the route will be created against [path] instead, which allow to
+ use differents url inside a scope. *)
+let register :
+ ?path:'placeholders Path.t ->
+ (module Services.JsonServerHandler with type placeholders = 'placeholders) ->
+ ('a -> Dream.handler) ->
+ Dream.route =
+ fun (type placeholders) ?path
+ (module S : Services.JsonServerHandler
+ with type placeholders = placeholders) f ->
+ let partial_handler =
+ (* There is no unification possible for the type p' and p when both are
+ available.
+ That’s why need to evaluate it now in order to remove any abstraction as
+ soon as possible *)
+ match path with
+ | None -> method' S.method_ Path.(repr' S.path)
+ | Some p' -> method' S.method_ Path.(repr' p')
+ in
+
+ partial_handler (fun request ->
+ let placeholders = Path.unzip S.path (extract_param request) in
+ f placeholders request)
diff --git a/lib/dream_handler/dream_handler.mli b/lib/dream_handler/dream_handler.mli
new file mode 100644
index 0000000..636ba64
--- /dev/null
+++ b/lib/dream_handler/dream_handler.mli
@@ -0,0 +1,47 @@
+val handle :
+ (module Services.JsonServerHandler
+ with type placeholders = 'placeholders
+ and type request = 'request
+ and type response = 'response) ->
+ ('placeholders -> 'request -> ('response, string) Lwt_result.t) ->
+ 'placeholders ->
+ Dream.handler
+(** [handle (module S) f] create a handler for the requests.
+
+ @arg f is the function receiving the variable parts of the url, the body
+ (matching the type S.request), the request (in order to fetch the
+ parameters) and returning a content of type S.response.
+
+ The function does not read any parameters from the URI, as the body is
+ supposed having all the required informations.
+
+ *)
+
+module MakeChecked (S : Services.JsonServerHandler) : sig
+ exception Invalid_method
+ (** Exception raised if the method does not allow body content *)
+
+ val handle :
+ (S.placeholders -> S.request -> (S.response, string) Lwt_result.t) ->
+ S.placeholders ->
+ Dream.handler
+end
+
+val register :
+ ?path:'placeholders Path.t ->
+ (module Services.JsonServerHandler with type placeholders = 'placeholders) ->
+ ('placeholders -> Dream.handler) ->
+ Dream.route
+(** Register a handler as a route. The module gives all the required information
+ (path, methods…) we need, and the handler can be created using the function
+ `handle` just above.
+
+ {[
+ let handler =
+ Dream_handler.handle
+ (module Service)
+ (fun (_args : Service.parameters) (_body : Services.request) ->
+ Lwt.return_ok _)
+
+ let route = Route_builder.register (module Service) handler
+ ]}*)
diff --git a/lib/dream_handler/dune b/lib/dream_handler/dune
new file mode 100644
index 0000000..3e3ff18
--- /dev/null
+++ b/lib/dream_handler/dune
@@ -0,0 +1,13 @@
+(library
+ (name dream_handler)
+
+ (libraries
+ lwt
+ dream
+ result
+ yojson
+ path
+ services)
+ (preprocess (pps ppx_yojson_conv lwt_ppx))
+ )
+
diff --git a/lib/dune b/lib/dune
new file mode 100644
index 0000000..75fa3fa
--- /dev/null
+++ b/lib/dune
@@ -0,0 +1,2 @@
+(library
+ (name dream_service))
diff --git a/lib/operators/dune b/lib/operators/dune
new file mode 100644
index 0000000..c3eb766
--- /dev/null
+++ b/lib/operators/dune
@@ -0,0 +1,4 @@
+(library
+ (name operators)
+
+ (libraries lwt))
diff --git a/lib/operators/operators.ml b/lib/operators/operators.ml
new file mode 100644
index 0000000..88d8563
--- /dev/null
+++ b/lib/operators/operators.ml
@@ -0,0 +1,94 @@
+module type T = sig
+ type 'a t
+
+ val iter : ('a -> unit) -> 'a t -> unit
+ val map : ('a -> 'b) -> 'a t -> 'b t
+ val bind : 'a t -> ('a -> 'b t) -> 'b t
+end
+
+module DefaultIter (T : sig
+ type 'a t
+
+ val map : ('a -> 'b) -> 'a t -> 'b t
+end) =
+struct
+ let iter f v = ignore (T.map f v)
+end
+
+module type MONAD = sig
+ type 'a t
+
+ val map : ('a -> 'b) -> 'a t -> 'b t
+ val return : 'a -> 'a t
+ val bind : 'a t -> ('a -> 'b t) -> 'b t
+end
+
+module Binding (T : T) = struct
+ (* Create the let binding operators for a module.
+
+ - let* is the binding operator we can find in a monad.
+ - let/ is the map operator
+ - let- is the operator which reduce to unit. (I wanted to use . as symbol
+ like in caqti, but this is not allowed )
+
+ The list of the symbols used is describe here :
+ https://v2.ocaml.org/manual/bindingops.html#start-section
+ *)
+
+ let ( let* ) : 'a T.t -> ('a -> 'b T.t) -> 'b T.t = T.bind
+ let ( let+ ) : 'a T.t -> ('a -> 'b) -> 'b T.t = fun t f -> T.map f t
+ let ( let- ) : 'a T.t -> ('a -> unit) -> unit = fun t f -> T.iter f t
+end
+
+module type Traversable = sig
+ type 'a t
+
+ (**
+
+ Build the traversable module.
+
+ [>] means that the parameter is not wrapped in the MONAD
+ [**] means that the function is returning both a MONAD and the type
+ [*] means that the function binding into a new MONAD
+
+ The name is choosen in order to make sense in the successive binding. You
+ should have the first binding using [let>…] form, then [let_], and finally
+ just [let]
+
+ *)
+ module Make (T : MONAD) : sig
+ val ( let>** ) : 'a t -> ('a -> 'b t T.t) -> 'b t T.t
+ val ( let>* ) : 'a t -> ('a -> 'b T.t) -> 'b t T.t
+ val ( let** ) : 'a t T.t -> ('a -> 'b t T.t) -> 'b t T.t
+ val ( let* ) : 'a t T.t -> ('a -> 'b T.t) -> 'b t T.t
+ end
+end
+
+module TraversableResult : Traversable with type 'a t = ('a, string) result =
+struct
+ type 'a t = ('a, string) result
+
+ module Make (T : MONAD) = struct
+ let traverse : 'a t -> ('a -> 'b T.t) -> 'b t T.t =
+ fun v f ->
+ match v with
+ | Ok x -> T.map (fun x -> Ok x) (f x)
+ | Error e -> T.return (Error e)
+
+ let ( let>* ) : 'a t -> ('a -> 'b T.t) -> 'b t T.t = traverse
+
+ let ( let>** ) : 'a t -> ('a -> 'b t T.t) -> 'b t T.t =
+ fun v f ->
+ let result = traverse v (fun v -> f v) in
+ T.map Result.join result
+
+ let ( let* ) : 'a t T.t -> ('a -> 'b T.t) -> 'b t T.t =
+ fun v f -> T.bind v (fun v -> traverse v f)
+
+ let ( let** ) : 'a t T.t -> ('a -> 'b t T.t) -> 'b t T.t =
+ fun v f ->
+ T.bind v (fun v ->
+ let result = traverse v (fun v -> f v) in
+ T.map Result.join result)
+ end
+end
diff --git a/lib/path/dune b/lib/path/dune
new file mode 100644
index 0000000..83e17c2
--- /dev/null
+++ b/lib/path/dune
@@ -0,0 +1,6 @@
+(library
+ (name path)
+
+ (libraries v_string)
+ )
+
diff --git a/lib/path/path.ml b/lib/path/path.ml
new file mode 100644
index 0000000..7caba87
--- /dev/null
+++ b/lib/path/path.ml
@@ -0,0 +1,116 @@
+(** Describe the components in the path *)
+type _ typ =
+ | Int : int64 typ
+ | String : string typ
+ | Fixed : V_string.t -> unit typ
+
+(** [get_param f] extract the value from the poositional argument.
+
+ The function [f] call the server engine for the value *)
+let get_param : type a. int -> (string -> string) -> a typ -> a =
+ fun idx f -> function
+ | Fixed _ -> ()
+ | String -> f (string_of_int idx)
+ | Int -> Int64.of_string (f (string_of_int idx))
+
+let repr_param : type a. int -> a typ -> V_string.t =
+ fun idx -> function
+ | String | Int -> V_string.v (":" ^ string_of_int idx)
+ | Fixed v -> v
+
+let encode_param : type a. a -> a typ -> V_string.t =
+ fun value -> function
+ | String -> V_string.v value
+ | Int -> V_string.v (Int64.to_string value)
+ | Fixed v -> v
+
+type _ t =
+ | [] : unit t
+ | ( :: ) : 'x t * 'y t -> ('x * 'y) t
+ | T1 : 'a typ -> 'a t
+ | T2 : 'a typ * 'b typ -> ('a * 'b) t
+ | T3 : 'a typ * 'b typ * 'c typ -> ('a * 'b * 'c) t
+ | T4 : 'a typ * 'b typ * 'c typ * 'd typ -> ('a * 'b * 'c * 'd) t
+
+(** Count the number of arguments. *)
+let rec count : type a. a t -> int = function
+ | [] -> 0
+ | T1 _ -> 1
+ | T2 _ -> 2
+ | T3 _ -> 3
+ | T4 _ -> 4
+ | p1 :: p2 -> count p1 + count p2
+
+(** Extract the arguments from the path template *)
+let unzip : 'a t -> (string -> string) -> 'a =
+ fun t f ->
+ let rec _unzip : type a. int -> a t -> a =
+ fun idx path ->
+ let extract idx p = get_param idx f p in
+ match path with
+ | [] -> ()
+ | p1 :: tl -> (_unzip idx p1, _unzip (idx + count p1) tl)
+ | T1 p1 -> extract idx p1
+ | T2 (p1, p2) -> (extract idx p1, extract (idx + 1) p2)
+ | T3 (p1, p2, p3) ->
+ (extract idx p1, extract (idx + 1) p2, extract (idx + 2) p3)
+ | T4 (p1, p2, p3, p4) ->
+ ( extract idx p1,
+ extract (idx + 1) p2,
+ extract (idx + 2) p3,
+ extract (idx + 3) p4 )
+ in
+ _unzip 0 t
+
+let repr : 'a t -> V_string.t =
+ fun t ->
+ let rec _repr : type a. int -> V_string.t list -> a t -> V_string.t list =
+ fun idx acc t ->
+ match t with
+ | [] -> acc
+ | T1 t -> repr_param idx t :: acc
+ | T2 (t1, t2) -> repr_param idx t1 :: repr_param (idx + 1) t2 :: acc
+ | T3 (t1, t2, t3) ->
+ repr_param idx t1
+ :: repr_param (idx + 2) t2
+ :: repr_param (idx + 3) t3
+ :: acc
+ | T4 (t1, t2, t3, t4) ->
+ repr_param idx t1
+ :: repr_param (idx + 1) t2
+ :: repr_param (idx + 2) t3
+ :: repr_param (idx + 3) t4
+ :: acc
+ | tx :: ty ->
+ let idx' = count tx in
+ let acc' = _repr idx' acc ty in
+ _repr idx acc' tx
+ in
+ let args = _repr 0 [] t in
+ V_string.concat ~sep:(V_string.v "/") args
+
+let repr' a = repr a |> V_string.s
+
+let build : 'a -> 'a t -> V_string.t =
+ fun parameters uri ->
+ ignore (parameters, uri);
+ let rec _build : type a. V_string.t list -> a -> a t -> V_string.t list =
+ fun acc value path ->
+ match path with
+ | [] -> acc
+ | T1 t -> encode_param value t :: acc
+ | T2 (t1, t2) ->
+ let v1, v2 = value in
+ encode_param v1 t1 :: encode_param v2 t2 :: acc
+ | T3 (t1, t2, t3) ->
+ let v1, v2, v3 = value in
+ encode_param v1 t1 :: encode_param v2 t2 :: encode_param v3 t3 :: acc
+ | T4 (t1, t2, t3, t4) ->
+ let v1, v2, v3, v4 = value in
+ encode_param v1 t1 :: encode_param v2 t2 :: encode_param v3 t3
+ :: encode_param v4 t4 :: acc
+ | tx :: ty ->
+ let vx, vy = value in
+ _build (_build acc vy ty) vx tx
+ in
+ V_string.concat ~sep:(V_string.v "/") (_build [] parameters uri)
diff --git a/lib/path/path.mli b/lib/path/path.mli
new file mode 100644
index 0000000..e5b5029
--- /dev/null
+++ b/lib/path/path.mli
@@ -0,0 +1,18 @@
+(** Describe the components in the path *)
+type _ typ =
+ | Int : int64 typ
+ | String : string typ
+ | Fixed : V_string.t -> unit typ
+
+type _ t =
+ | [] : unit t
+ | ( :: ) : 'x t * 'y t -> ('x * 'y) t
+ | T1 : 'a typ -> 'a t
+ | T2 : 'a typ * 'b typ -> ('a * 'b) t
+ | T3 : 'a typ * 'b typ * 'c typ -> ('a * 'b * 'c) t
+ | T4 : 'a typ * 'b typ * 'c typ * 'd typ -> ('a * 'b * 'c * 'd) t
+
+val unzip : 'a t -> (string -> string) -> 'a
+val repr : 'a t -> V_string.t
+val repr' : 'a t -> string
+val build : 'a -> 'a t -> V_string.t
diff --git a/lib/services/dune b/lib/services/dune
new file mode 100644
index 0000000..be8de92
--- /dev/null
+++ b/lib/services/dune
@@ -0,0 +1,10 @@
+(library
+ (name services)
+
+ (libraries
+ lwt
+ yojson
+ path)
+ (preprocess (pps ppx_yojson_conv))
+ )
+
diff --git a/lib/services/services.ml b/lib/services/services.ml
new file mode 100644
index 0000000..c0895fe
--- /dev/null
+++ b/lib/services/services.ml
@@ -0,0 +1,49 @@
+type ('a, 'b) method_ =
+ | GET : (unit, 'b) method_
+ | POST : ('a, 'b) method_
+ | PUT : ('a, 'b) method_
+ | DELETE : ('a, 'b) method_
+ | HEAD : (unit, unit) method_
+ | CONNECT : ('a, 'b) method_
+ | OPTIONS : ('a, 'b) method_
+ | TRACE : ('a, 'b) method_
+ | PATCH : ('a, 'b) method_
+
+(** A simple service, with an input and and answer *)
+module type Handler = sig
+ type request
+ (** The body of the request *)
+
+ type response
+ (** The body of the response *)
+
+ val method_ : (request, response) method_
+ (** The method used in the service. *)
+
+ type placeholders
+ (** Parameters given in the url path. This type will match variable parts in
+ the path to the service *)
+
+ val path : placeholders Path.t
+ (** Path to the service *)
+end
+
+(** The service implemented in the server:
+
+ We need to be able to decode the content and encode the response. *)
+module type JsonServerHandler = sig
+ include Handler
+
+ val request_of_yojson : Yojson.Safe.t -> request
+ (** Extract the request elements from the json *)
+
+ val yojson_of_response : response -> Yojson.Safe.t
+ (** Produce a json from the response given by the service *)
+end
+
+module type JsonClientHandler = sig
+ include Handler
+
+ val yojson_of_request : request -> Yojson.Safe.t
+ val response_of_yojson : Yojson.Safe.t -> response
+end
diff --git a/lib/virtuals/_readme.rst b/lib/virtuals/_readme.rst
new file mode 100644
index 0000000..4c31089
--- /dev/null
+++ b/lib/virtuals/_readme.rst
@@ -0,0 +1,5 @@
+This directory contains hold the `virtual libraries` used in the application.
+They allow to share common code between the javascript and server side with
+differents implementations (for exemple in the string representation).
+
+.. _`virtual libraries`: https://dune.readthedocs.io/en/stable/variants.html#virtual-library
diff --git a/lib/virtuals/v_string/brr_string/dune b/lib/virtuals/v_string/brr_string/dune
new file mode 100644
index 0000000..0e3f942
--- /dev/null
+++ b/lib/virtuals/v_string/brr_string/dune
@@ -0,0 +1,4 @@
+(library
+ (name brr_string)
+ (libraries brr)
+ (implements v_string))
diff --git a/lib/virtuals/v_string/brr_string/v_string.ml b/lib/virtuals/v_string/brr_string/v_string.ml
new file mode 100644
index 0000000..9aeb01f
--- /dev/null
+++ b/lib/virtuals/v_string/brr_string/v_string.ml
@@ -0,0 +1,10 @@
+type t = Jstr.t
+
+let v : string -> t = Jstr.v
+let s = Jstr.to_string
+let concat : ?sep:t -> t list -> t = Jstr.concat
+
+let encode v =
+ match Brr.Uri.encode_component v with
+ | Ok s -> s
+ | Error _ -> Jstr.empty
diff --git a/lib/virtuals/v_string/dream_string/dune b/lib/virtuals/v_string/dream_string/dune
new file mode 100644
index 0000000..ff6c9b2
--- /dev/null
+++ b/lib/virtuals/v_string/dream_string/dune
@@ -0,0 +1,4 @@
+(library
+ (name dream_string)
+ (libraries dream)
+ (implements v_string))
diff --git a/lib/virtuals/v_string/dream_string/v_string.ml b/lib/virtuals/v_string/dream_string/v_string.ml
new file mode 100644
index 0000000..14a4e33
--- /dev/null
+++ b/lib/virtuals/v_string/dream_string/v_string.ml
@@ -0,0 +1,6 @@
+type t = string
+
+let v : string -> t = Fun.id
+let s : t -> string = Fun.id
+let concat : ?sep:t -> t list -> t = fun ?(sep = "") -> StringLabels.concat ~sep
+let encode = Dream.to_percent_encoded ?international:None
diff --git a/lib/virtuals/v_string/dune b/lib/virtuals/v_string/dune
new file mode 100644
index 0000000..badb7d3
--- /dev/null
+++ b/lib/virtuals/v_string/dune
@@ -0,0 +1,4 @@
+(library
+ (name v_string)
+ (virtual_modules v_string)
+ (default_implementation dream_string))
diff --git a/lib/virtuals/v_string/v_string.mli b/lib/virtuals/v_string/v_string.mli
new file mode 100644
index 0000000..267dbf2
--- /dev/null
+++ b/lib/virtuals/v_string/v_string.mli
@@ -0,0 +1,14 @@
+type t
+(**
+ This module provide a common signature between the server and javascript
+ side in order to create URL in the same way.
+*)
+
+val v : string -> t
+val s : t -> string
+val concat : ?sep:t -> t list -> t
+
+val encode : t -> t
+(** Encode an element of the url by percent-encoding. The function is
+ expected to encode "/" as well.
+ *)
diff --git a/readme.rst b/readme.rst
new file mode 100644
index 0000000..adbc3ba
--- /dev/null
+++ b/readme.rst
@@ -0,0 +1,95 @@
+.. -*- mode: rst -*-
+.. -*- coding: utf-8 -*-
+
+This is a sample project showing how to process http requests in a safe way in
+OCaml.
+
+For example:
+
+ - A service using the GET method cannot have any body resquest
+ - A service using the HEAD method cannot have any body response
+
+Interface
+=========
+
+The interface for a service is given in an OCaml module, and is declaring all
+the possibles values for a service.
+
+Each service shall describe:
+
+- an URL to the server
+- the body of the request
+- the body of the response
+
+.. code:: ocaml
+
+ type request = { value : string } [@@deriving yojson]
+
+ type response = { value : string; nbcar : int64 } [@@deriving yojson]
+
+ (** The method used in the service *)
+ let method_ = Services.POST
+
+ type placeholders = unit
+ (** No placeholder here in the request url *)
+
+ (** The path to the service, matching the type parameters *)
+ let path = Path.(T1 (Fixed (V_string.v "api/counter")))
+
+Here, we are not declaring any implementation, only the types.
+
+Implementation
+==============
+
+The server part
+---------------
+
+
+There is no implementation yet, this is done in the server, for example using
+`Dream_handler`:
+
+.. code:: ocaml
+
+ let handler =
+ Dream_handler.handle
+ (module Services_impl.Nb_car)
+ (fun (() : Services_impl.Nb_car.placeholders) body ->
+ Lwt.return_ok
+ Services_impl.Nb_car.
+ {
+ value = body.value;
+ nbcar = Int64.of_int (String.length body.value);
+ })
+
+As the url is also given in the service interface, we can also create the route:
+
+.. code:: ocaml
+
+ let route = Dream_handler.register (module Services_impl.Nb_car) handler
+
+ let () =
+ Dream.run @@ Dream.logger
+ @@ Dream.router
+ [
+ route;
+ ]
+
+
+The client part
+---------------
+
+This example use the library brr to make a request to this service:
+
+.. code:: ocaml
+
+ let futr = Js_handler.send (module Services_impl.Nb_car) () { value }
+
+Another example using cohttp :
+
+.. code:: ocaml
+
+ let root = "http://[::1]:8080"
+ let* result =
+ Cohttp_handler.request ~root
+ (module Services_impl.Nb_car)
+ () { value = "foobar" }
diff --git a/services/dune b/services/dune
new file mode 100644
index 0000000..fe10406
--- /dev/null
+++ b/services/dune
@@ -0,0 +1,10 @@
+(library
+ (name services_impl)
+ (libraries
+ path
+ services
+ v_string
+ )
+ (preprocess (pps ppx_yojson_conv))
+ )
+
diff --git a/services/nb_car.ml b/services/nb_car.ml
new file mode 100644
index 0000000..3d8f329
--- /dev/null
+++ b/services/nb_car.ml
@@ -0,0 +1,15 @@
+(** Service counting the characters in a word *)
+
+open Ppx_yojson_conv_lib.Yojson_conv.Primitives
+
+type request = { value : string } [@@deriving yojson]
+type response = { value : string; nbcar : int64 } [@@deriving yojson]
+
+(** The method used in the service *)
+let method_ = Services.POST
+
+type placeholders = unit
+(** No placeholder here in the request url *)
+
+(** The path to the service, matching the type parameters *)
+let path = Path.(T1 (Fixed (V_string.v "api/counter")))