diff --git a/cohttp-eio/dune b/cohttp-eio/dune new file mode 100644 index 0000000..ad6e76a --- /dev/null +++ b/cohttp-eio/dune @@ -0,0 +1,8 @@ +(library + (name ppx_deriving_router_cohttp_eio) + (public_name ppx_deriving_router.cohttp_eio) + (virtual_deps cohttp-eio) + (optional) + (libraries ppx_deriving_router) + (ppx_runtime_libraries ppx_deriving_router.cohttp_eio_runtime) + (kind ppx_deriver)) diff --git a/cohttp-eio/runtime/dune b/cohttp-eio/runtime/dune new file mode 100644 index 0000000..3ec4ba7 --- /dev/null +++ b/cohttp-eio/runtime/dune @@ -0,0 +1,11 @@ +(library + (name ppx_deriving_router_runtime_cohttp_eio_runtime) + (public_name ppx_deriving_router.cohttp_eio_runtime) + (virtual_deps cohttp-eio) + (optional) + (wrapped false) + (libraries + uri + cohttp-eio + ppx_deriving_router.runtime_lib + melange-json-native.ppx-runtime)) diff --git a/cohttp-eio/runtime/ppx_deriving_router_runtime.ml b/cohttp-eio/runtime/ppx_deriving_router_runtime.ml new file mode 100644 index 0000000..06294c4 --- /dev/null +++ b/cohttp-eio/runtime/ppx_deriving_router_runtime.ml @@ -0,0 +1,76 @@ +open struct + module IO : Ppx_deriving_router_runtime_lib.IO with type 'a t = 'a = + struct + type 'a t = 'a + + let return = Fun.id + let fail exn = raise exn + let bind x f = f x + let catch f = try Ok (f ()) with exn -> Error exn + end + + module Request : + Ppx_deriving_router_runtime_lib.REQUEST + with type 'a IO.t = 'a IO.t + and type t = Http.Request.t * Eio.Flow.source_ty Eio.Flow.source = + struct + module IO = IO + + type t = Http.Request.t * Eio.Flow.source_ty Eio.Flow.source + + let queries (req, _body) = + let uri = Uri.of_string req.Http.Request.resource in + Uri.query uri + |> List.map (fun (k, vs) -> List.map (fun v -> k, v) vs) + |> List.flatten + + let body ((_req, body) : t) = Eio.Flow.read_all body + + let path (req, _body) = + let uri = Uri.of_string req.Http.Request.resource in + Uri.path uri + + let method_ (req, _body) = + match req.Http.Request.meth with + | `GET -> `GET + | `POST -> `POST + | `PUT -> `PUT + | `DELETE -> `DELETE + | _ -> failwith "Unsupported method" + end + + module Response : + Ppx_deriving_router_runtime_lib.RESPONSE + with type 'a IO.t = 'a IO.t + and type status = Http.Status.t + and type t = Http.Response.t * Cohttp_eio.Body.t = struct + module IO = IO + + type status = Http.Status.t + + let status_ok : status = `OK + let status_bad_request : status = `Bad_request + let status_method_not_allowed : status = `Method_not_allowed + let status_not_found : status = `Not_found + + type t = Http.Response.t * Cohttp_eio.Body.t + + let respond ~status ~headers body = + let headers = Http.Header.of_list headers in + Cohttp_eio.Server.respond_string ~headers ~status ~body () + end + + module Return : + Ppx_deriving_router_runtime_lib.RETURN + with type status = Http.Status.t + and type 'a t = 'a = struct + type status = Http.Status.t + type 'a t = 'a + + let data x = Some x + let status _ = None + let headers _ = [] + end +end + +include Ppx_deriving_router_runtime_lib.Make (Request) (Response) (Return) diff --git a/cohttp-eio/runtime/ppx_deriving_router_runtime.mli b/cohttp-eio/runtime/ppx_deriving_router_runtime.mli new file mode 100644 index 0000000..34bc7b6 --- /dev/null +++ b/cohttp-eio/runtime/ppx_deriving_router_runtime.mli @@ -0,0 +1,8 @@ +include + Ppx_deriving_router_runtime_lib.S + with type Request.t = + Http.Request.t * Eio.Flow.source_ty Eio.Flow.source + and type Response.t = Http.Response.t * Cohttp_eio.Body.t + and type Response.status = Http.Status.t + and type 'a Return.t = 'a + and type 'a IO.t = 'a diff --git a/cohttp-eio/test/dune b/cohttp-eio/test/dune new file mode 100644 index 0000000..d6bb1c8 --- /dev/null +++ b/cohttp-eio/test/dune @@ -0,0 +1,10 @@ +(executable + (name test) + (libraries eio eio.unix eio_main) + (preprocess + (pps ppx_deriving_router.cohttp_eio melange-json-native.ppx))) + +(cram + (deps + ./test.exe + (package ppx_deriving_router))) diff --git a/cohttp-eio/test/routing.ml b/cohttp-eio/test/routing.ml new file mode 100644 index 0000000..e802727 --- /dev/null +++ b/cohttp-eio/test/routing.ml @@ -0,0 +1,97 @@ +type modifier = + | Uppercase + | Lowercase + (** this a custom type which we want to be able to serialize/deserialize + from/to the URL query *) + +let modifier_of_url_query k xs = + match List.assoc_opt k xs with + | Some "uppercase" -> Ok Uppercase + | Some "lowercase" -> Ok Lowercase + | Some _ -> Error "invalid modifier" + | None -> Error "missing modifier" + +let modifier_to_url_query k = function + | Uppercase -> [ k, "uppercase" ] + | Lowercase -> [ k, "lowercase" ] + +module Options = struct + open Ppx_deriving_json_runtime.Primitives + + type t = { a : int option } [@@deriving json, url_query_via_json] +end + +module User_id : sig + type t + + val inject : string -> t + val project : t -> string +end = struct + type t = string + + let inject x = x + let project x = x +end + +module Level = struct + type t = Alert | Warning + + let to_int = function Alert -> 2 | Warning -> 1 + + let of_int = function + | 2 -> Alert + | 1 -> Warning + | _ -> failwith "invalid level" +end + +module Pages = struct + open Ppx_deriving_router_runtime.Primitives + + type user_id = User_id.t + [@@deriving url_query_via_iso, url_path_via_iso] + + type level = Level.t + [@@deriving + url_query_via_iso { t = int; inject = of_int; project = to_int }] + + type t = + | Home [@GET "/"] + | Hello of { + name : string; + modifier : modifier option; + greeting : string option; + } [@GET "/hello/:name"] + | Echo_options of { options : Options.t } + | List_users of { user_ids : user_id list } + | User_info of { user_id : user_id } + | User_info_via_path of { user_id : user_id } [@GET "/user/:user_id"] + | Signal of { level : level } + | Route_with_implicit_path of { param : string option } + | Route_with_implicit_path_post [@POST] + [@@deriving router] +end + +module Api = struct + open Ppx_deriving_router_runtime.Primitives + open Ppx_deriving_json_runtime.Primitives + + type user = { id : int } [@@deriving json] + + type _ t = + | List_users : user list t [@GET "/"] + | Create_user : { id : int [@body] } -> user t [@POST "/"] + | Get_user : { id : int } -> user t [@GET "/:id"] + | Raw_response : Ppx_deriving_router_runtime.response t + [@GET "/raw-response"] + [@@deriving router] +end + +module All = struct + type _ t = + | Pages : Pages.t -> Ppx_deriving_router_runtime.response t + [@prefix "/"] + | Api : 'a Api.t -> 'a t [@prefix "/nested/api"] + | Static : { path : string } -> Ppx_deriving_router_runtime.response t + [@GET "/static/...path"] + [@@deriving router] +end diff --git a/cohttp-eio/test/test.ml b/cohttp-eio/test/test.ml new file mode 100644 index 0000000..49b008a --- /dev/null +++ b/cohttp-eio/test/test.ml @@ -0,0 +1,95 @@ +open Routing +open! Cohttp +open! Cohttp_eio + +let pages_handle route _req = + match route with + | Pages.Home -> Server.respond_string ~status:`OK ~body:"HOME PAGE" () + | Route_with_implicit_path { param } -> + let param = Option.value ~default:"-" param in + Server.respond_string ~status:`OK + ~body:("works as well, param is: " ^ param) + () + | Route_with_implicit_path_post -> + Server.respond_string ~status:`OK ~body:"posted" () + | Echo_options { options } -> + let json = Options.to_json options in + let json = Yojson.Basic.to_string json in + Server.respond_string ~status:`OK ~body:json + ~headers: + (Http.Header.of_list [ "Content-Type", "application/json" ]) + () + | List_users { user_ids } -> + let ids = + match user_ids with + | user_ids -> + Printf.sprintf "[%s]" + (user_ids |> List.map User_id.project |> String.concat ", ") + in + Server.respond_string ~status:`OK + ~body:(Printf.sprintf "User ids = %s" ids) + () + | User_info { user_id } | User_info_via_path { user_id } -> + Server.respond_string ~status:`OK + ~body: + (Printf.sprintf "User info for %S" (User_id.project user_id)) + () + | Signal { level } -> + Server.respond_string ~status:`OK + ~body:(Printf.sprintf "Signal: %d" (Level.to_int level)) + () + | Hello { name; modifier; greeting } -> + let greeting = Option.value greeting ~default:"Hello" in + let name = + match modifier with + | None -> name + | Some Uppercase -> String.uppercase_ascii name + | Some Lowercase -> String.lowercase_ascii name + in + let greeting = Printf.sprintf "%s, %s!" greeting name in + Server.respond_string ~status:`OK ~body:greeting () + +let api_handle : + type a. + a Api.t -> Cohttp.Request.t * Eio.Flow.source_ty Eio.Flow.source -> a + = + fun x _req -> + match x with + | Raw_response -> + Server.respond_string ~status:`OK ~body:"RAW RESPONSE" () + | List_users -> [] + | Create_user { id } -> { Api.id } + | Get_user { id } -> { Api.id } + +let all_handler = + let f : + type a. + a All.t -> + Cohttp.Request.t * Eio.Flow.source_ty Eio.Flow.source -> + a = + fun x req -> + match x with + | Pages p -> pages_handle p req + | Api e -> api_handle e req + | Static { path } -> + Server.respond_string ~status:`OK + ~body:(Printf.sprintf "path=%S" path) + () + in + All.handle { f } + +let log_warning ex = Logs.warn (fun f -> f "%a" Eio.Exn.pp ex) + +let () = + let port = ref 8888 in + Arg.parse + [ "-p", Arg.Set_int port, " Listening port number (8888 by default)" ] + ignore "An HTTP/1.1 server"; + Eio_main.run @@ fun env -> + Eio.Switch.run @@ fun sw -> + let handler _conn req body = all_handler (req, body) in + let socket = + Eio.Net.listen env#net ~sw ~backlog:128 ~reuse_addr:true + (`Tcp (Eio.Net.Ipaddr.V4.loopback, !port)) + and server = Cohttp_eio.Server.make ~callback:handler () in + Cohttp_eio.Server.run socket server ~on_error:log_warning diff --git a/cohttp-eio/test/test.mli b/cohttp-eio/test/test.mli new file mode 100644 index 0000000..e69de29 diff --git a/dream/runtime/ppx_deriving_router_runtime.ml b/dream/runtime/ppx_deriving_router_runtime.ml index 44b529d..d4216d4 100644 --- a/dream/runtime/ppx_deriving_router_runtime.ml +++ b/dream/runtime/ppx_deriving_router_runtime.ml @@ -1,7 +1,20 @@ open struct + module IO : + Ppx_deriving_router_runtime_lib.IO with type 'a t = 'a Lwt.t = struct + type 'a t = 'a Lwt.t + + let return = Lwt.return + let fail = Lwt.fail + let bind = Lwt.bind + let catch = Lwt_result.catch + end + module Request : - Ppx_deriving_router_runtime_lib.REQUEST with type t = Dream.request = - struct + Ppx_deriving_router_runtime_lib.REQUEST + with type 'a IO.t = 'a IO.t + and type t = Dream.request = struct + module IO = IO + type t = Dream.request let queries = Dream.all_queries @@ -19,8 +32,11 @@ open struct module Response : Ppx_deriving_router_runtime_lib.RESPONSE - with type status = Dream.status + with type 'a IO.t = 'a IO.t + and type status = Dream.status and type t = Dream.response = struct + module IO = IO + type status = Dream.status let status_ok : status = `OK diff --git a/dream/runtime/ppx_deriving_router_runtime.mli b/dream/runtime/ppx_deriving_router_runtime.mli index cb90677..7b54080 100644 --- a/dream/runtime/ppx_deriving_router_runtime.mli +++ b/dream/runtime/ppx_deriving_router_runtime.mli @@ -1,6 +1,7 @@ include Ppx_deriving_router_runtime_lib.S - with type Request.t = Dream.request + with type 'a IO.t = 'a Lwt.t + and type Request.t = Dream.request and type Response.t = Dream.response and type Response.status = Dream.status and type 'a Return.t = 'a diff --git a/dune-project b/dune-project index 09a32d5..17380b7 100644 --- a/dune-project +++ b/dune-project @@ -18,7 +18,7 @@ (package (name ppx_deriving_router) (synopsis "Derive typesafe router from type declarations") - (depopts dream) + (depopts dream cohttp-eio) (depends (ocaml (>= 4.14)) diff --git a/native/ppx_deriving_router.ml b/native/ppx_deriving_router.ml index 30a0fce..3bb9324 100644 --- a/native/ppx_deriving_router.ml +++ b/native/ppx_deriving_router.ml @@ -75,14 +75,15 @@ let td_to_ty_handler param td = [%type: [%t td_to_ty (Some param) td] -> Ppx_deriving_router_runtime.request -> - [%t param] Ppx_deriving_router_runtime.return Lwt.t] + [%t param] Ppx_deriving_router_runtime.return + Ppx_deriving_router_runtime.IO.t] | None -> [%type: [%t td_to_ty param td] -> Ppx_deriving_router_runtime.request -> Ppx_deriving_router_runtime.response Ppx_deriving_router_runtime.return - Lwt.t] + Ppx_deriving_router_runtime.IO.t] let td_to_ty_enc param td = let loc = td.ptype_loc in @@ -125,8 +126,10 @@ let derive_mount td m = Stdlib.List.map (fun route -> let f f req = - Lwt.bind (f req) (fun [%p p [%pat? x, _encode]] -> - Lwt.return [%e make_with_encode encode]) + Ppx_deriving_router_runtime.IO.bind (f req) + (fun [%p p [%pat? x, _encode]] -> + Ppx_deriving_router_runtime.IO.return + [%e make_with_encode encode]) in Ppx_deriving_router_runtime.Handle.prefix_route [%e elist ~loc (List.map m.m_prefix ~f:(estring ~loc))] @@ -262,12 +265,14 @@ let derive_path td (exemplar, ctors) = let pbody, ebody = patt_and_expr ~loc "_body" in let expr = match leaf.l_body with - | None -> [%expr Lwt.return [%e make args]] + | None -> + [%expr + Ppx_deriving_router_runtime.IO.return [%e make args]] | Some (name, body) -> let name = { loc; txt = Lident name } in let args = (name, ebody) :: args in [%expr - Lwt.bind + Ppx_deriving_router_runtime.IO.bind (Ppx_deriving_router_runtime.Request.body [%e req]) (fun [%p pbody] -> let [%p pbody] = @@ -286,7 +291,7 @@ let derive_path td (exemplar, ctors) = .Invalid_body msg) in - Lwt.return [%e make args])] + Ppx_deriving_router_runtime.IO.return [%e make args])] in let expr = [%expr @@ -433,7 +438,7 @@ let derive_router_td td = (Some [%pat? p, encode])] req -> - Lwt.bind (f p req) + Ppx_deriving_router_runtime.IO.bind (f p req) (Ppx_deriving_router_runtime.Handle.encode encode))]; [%stri let [%p pvar ~loc (handle_name td)] = diff --git a/native/runtime/ppx_deriving_router_runtime_lib.ml b/native/runtime/ppx_deriving_router_runtime_lib.ml index 0c87ec8..0e72675 100644 --- a/native/runtime/ppx_deriving_router_runtime_lib.ml +++ b/native/runtime/ppx_deriving_router_runtime_lib.ml @@ -2,7 +2,18 @@ type http_method = [ `DELETE | `GET | `POST | `PUT ] module Witness = Ppx_deriving_router_witness +module type IO = sig + type 'a t + + val return : 'a -> 'a t + val fail : exn -> 'a t + val bind : 'a t -> ('a -> 'b t) -> 'b t + val catch : (unit -> 'a t) -> ('a, exn) result t +end + module type REQUEST = sig + module IO : IO + type t val path : t -> string @@ -11,7 +22,7 @@ module type REQUEST = sig val queries : t -> (string * string) list (* request queries component, url decoded *) - val body : t -> string Lwt.t + val body : t -> string IO.t (* request body *) val method_ : t -> http_method @@ -19,6 +30,8 @@ module type REQUEST = sig end module type RESPONSE = sig + module IO : IO + type status val status_ok : status @@ -29,7 +42,7 @@ module type RESPONSE = sig type t val respond : - status:status -> headers:(string * string) list -> string -> t Lwt.t + status:status -> headers:(string * string) list -> string -> t IO.t end module type RETURN = sig @@ -42,13 +55,15 @@ module type RETURN = sig end module type S = sig + module IO : IO + type json = Yojson.Basic.t - module Request : REQUEST + module Request : REQUEST with module IO = IO type request = Request.t - module Response : RESPONSE + module Response : RESPONSE with module IO = IO type response = Response.t @@ -70,7 +85,7 @@ module type S = sig | Encode_raw : response encode | Encode_json : ('a -> json) -> 'a encode - val encode : 'a encode -> 'a return -> response Lwt.t + val encode : 'a encode -> 'a return -> response IO.t type 'v route = | Route : ('a, 'v) Routes.path * 'a * ('v -> 'w) -> 'w route @@ -82,13 +97,13 @@ module type S = sig type 'a router - val make : (request -> 'a Lwt.t) Routes.router -> 'a router + val make : (request -> 'a IO.t) Routes.router -> 'a router val handle : 'a router -> - ('a -> request -> response Lwt.t) -> + ('a -> request -> response IO.t) -> request -> - response Lwt.t + response IO.t (** handle request given a router and a dispatcher *) val dispatch : @@ -99,25 +114,28 @@ module type S = sig | `Method_not_allowed | `Not_found | `Ok of 'a ] - Lwt.t + IO.t end end module Make (Request : REQUEST) - (Response : RESPONSE) + (Response : RESPONSE with module IO = Request.IO) (Return : RETURN with type status = Response.status) : S with type Request.t = Request.t and type Response.t = Response.t and type Response.status = Response.status and type 'a Return.t = 'a Return.t + and type 'a IO.t = 'a Request.IO.t + and type 'a IO.t = 'a Response.IO.t and module Witness = Witness = struct type json = Yojson.Basic.t type request = Request.t type response = Response.t type 'a return = 'a Return.t + module IO = Request.IO module Request = Request module Response = Response module Return = Return @@ -135,7 +153,7 @@ module Make | Encode_raw : response encode | Encode_json : ('a -> json) -> 'a encode - let encode : type a. a encode -> a Return.t -> response Lwt.t = + let encode : type a. a encode -> a Return.t -> response IO.t = fun enc x -> let status = Option.value ~default:Response.status_ok (Return.status x) @@ -145,7 +163,7 @@ module Make | Encode_raw, x -> ( match Return.data x with | None -> Response.respond ~status ~headers "" - | Some x -> Lwt.return x) + | Some x -> IO.return x) | Encode_json to_json, x -> ( match Return.data x with | None -> Response.respond ~status ~headers "" @@ -169,7 +187,7 @@ module Make let to_route (Route (path, a, f)) = Routes.(map f (route path a)) - type 'a router = (Request.t -> 'a Lwt.t) Routes.router + type 'a router = (Request.t -> 'a IO.t) Routes.router let make x = x @@ -177,20 +195,20 @@ module Make let target = Request.path req in match Routes.match' router ~target with | Routes.FullMatch v | Routes.MatchWithTrailingSlash v -> - Lwt.bind - (Lwt_result.catch (fun () -> v req)) + IO.bind + (IO.catch (fun () -> v req)) (function - | Ok v -> Lwt.return (`Ok v) + | Ok v -> IO.return (`Ok v) | Error (Invalid_query_parameter (x, y)) -> - Lwt.return (`Invalid_query_parameter (x, y)) + IO.return (`Invalid_query_parameter (x, y)) | Error (Invalid_body reason) -> - Lwt.return (`Invalid_body reason) - | Error Method_not_allowed -> Lwt.return `Method_not_allowed - | Error exn -> Lwt.fail exn) - | Routes.NoMatch -> Lwt.return `Not_found + IO.return (`Invalid_body reason) + | Error Method_not_allowed -> IO.return `Method_not_allowed + | Error exn -> IO.fail exn) + | Routes.NoMatch -> IO.return `Not_found let handle (router : _ router) f req = - Lwt.bind (dispatch router req) (function + IO.bind (dispatch router req) (function | `Ok v -> f v req | `Invalid_query_parameter (param, msg) -> Response.respond ~status:Response.status_bad_request diff --git a/ppx_deriving_router.opam b/ppx_deriving_router.opam index 9a7d1f8..fae7a53 100644 --- a/ppx_deriving_router.opam +++ b/ppx_deriving_router.opam @@ -20,7 +20,7 @@ depends: [ "uri" "odoc" {with-doc} ] -depopts: ["dream"] +depopts: ["dream" "cohttp-eio"] build: [ ["dune" "subst"] {dev} [