Be able to parse JSON

This commit is contained in:
Calascibetta Romain 2025-01-31 17:44:50 +01:00
parent a0a0b22f1e
commit 5b07aee781
14 changed files with 593 additions and 190 deletions

View file

@ -28,9 +28,7 @@ let run _quiet roots stdlib main =
in in
go [] go []
in in
match Vif_top.eval cfg main with match Vif_top.eval cfg main with Ok () -> () | Error () -> exit 1
| Ok sstr -> List.iter print_endline sstr
| Error sstr -> List.iter prerr_endline sstr
open Cmdliner open Cmdliner

View file

@ -217,7 +217,7 @@ let eval _cfg ppf ph =
false false
end end
let redirect : fn:(capture:(Buffer.t -> unit) -> 'a) -> 'a = let _redirect : fn:(capture:(Buffer.t -> unit) -> 'a) -> 'a =
fun ~fn -> fun ~fn ->
let filename = Filename.temp_file "vif-" ".stdout" in let filename = Filename.temp_file "vif-" ".stdout" in
Log.debug (fun m -> m "redirect stdout/stderr into %s" filename); Log.debug (fun m -> m "redirect stdout/stderr into %s" filename);
@ -273,7 +273,7 @@ let trim str =
let rec ltrim = function "" :: r -> ltrim r | lst -> lst let rec ltrim = function "" :: r -> ltrim r | lst -> lst
let rtrim lst = List.rev (ltrim (List.rev lst)) let rtrim lst = List.rev (ltrim (List.rev lst))
let trim lst = ltrim (rtrim (List.map trim lst)) let _trim lst = ltrim (rtrim (List.map trim lst))
let rec ends_by_semi_semi = function let rec ends_by_semi_semi = function
| [] -> false | [] -> false
@ -293,23 +293,13 @@ let cut_into_phrases lst =
go [] [] lst go [] [] lst
let eval cfg cmd = let eval cfg cmd =
let buf = Buffer.create 0x7ff in
let ppf = Format.formatter_of_out_channel stderr in let ppf = Format.formatter_of_out_channel stderr in
errors := false; errors := false;
let eval ~capture phrase = let eval phrase =
let lines = ref [] in
let capture () =
capture buf;
match Buffer.contents buf with
| "" -> ()
| str ->
Buffer.clear buf;
lines := str :: !lines
in
let out_phrase = !Oprint.out_phrase in let out_phrase = !Oprint.out_phrase in
let fn_out_phrase ppf = function let fn_out_phrase ppf = function
| Outcometree.Ophr_exception _ as phr -> out_phrase ppf phr | Outcometree.Ophr_exception _ as phr -> out_phrase ppf phr
| phr -> capture (); out_phrase ppf phr; capture () | phr -> out_phrase ppf phr
in in
Oprint.out_phrase := fn_out_phrase; Oprint.out_phrase := fn_out_phrase;
let restore () = Oprint.out_phrase := out_phrase in let restore () = Oprint.out_phrase := out_phrase in
@ -323,25 +313,15 @@ let eval cfg cmd =
restore (); restore ();
Location.report_exception ppf exn Location.report_exception ppf exn
end; end;
Format.pp_print_flush ppf (); Format.pp_print_flush ppf ()
capture ();
trim (List.rev !lines)
in in
let fn ~capture =
capture_compiler_stuff ppf @@ fun () -> capture_compiler_stuff ppf @@ fun () ->
let cmd = let cmd =
match cmd with [] | [ _ ] -> cmd | x :: r -> x :: List.map (( ^ ) " ") r match cmd with [] | [ _ ] -> cmd | x :: r -> x :: List.map (( ^ ) " ") r
in in
let phrases = cut_into_phrases cmd in let phrases = cut_into_phrases cmd in
let phrases = List.iter
List.map
(fun phrase -> (fun phrase ->
match Phrase.parse phrase with match Phrase.parse phrase with Some t -> eval t | None -> ())
| Some t -> eval ~capture t phrases;
| None -> []) if !errors then Error () else Ok ()
phrases
in
let phrases = List.concat phrases in
if !errors then Error phrases else Ok phrases
in
redirect ~fn

View file

@ -1,4 +1,4 @@
type cfg type cfg
val config : stdlib:Fpath.t -> string list -> cfg val config : stdlib:Fpath.t -> string list -> cfg
val eval : cfg -> string list -> (string list, string list) result val eval : cfg -> string list -> (unit, unit) result

View file

@ -3,4 +3,11 @@
(public_name vif) (public_name vif)
(flags (flags
(:standard -linkall)) (:standard -linkall))
(libraries hmap mirage-crypto-rng-miou-unix httpcats tyre)) (libraries
json-data-encoding
multipart_form
jsonm
hmap
mirage-crypto-rng-miou-unix
httpcats
tyre))

112
lib/vif/json.ml Normal file
View file

@ -0,0 +1,112 @@
type value = [ `Null | `Bool of bool | `String of string | `Float of float ]
type t = [ value | `A of t list | `O of (string * t) list ]
type 'a or_error = ('a, [ `Msg of string ]) result
type await = [ `Await ]
type error = [ `Error of Jsonm.error ]
type eoi = [ `End ]
let error_msgf fmt = Fmt.kstr (fun msg -> Error (`Msg msg)) fmt
let _max_young_size = Sys.word_size / 8 * 256
let rec pp ppf = function
| `Null -> Fmt.const Fmt.string "()" ppf ()
| `Bool v -> Fmt.bool ppf v
| `Float v -> Fmt.float ppf v
| `Name v -> Fmt.string ppf v
| `String v -> Fmt.string ppf v
| `O lst -> Fmt.Dump.list (Fmt.Dump.pair Fmt.string pp) ppf lst
| `A lst -> Fmt.Dump.list pp ppf lst
let decode ~input k =
let decoder = Jsonm.decoder `Manual in
let rec await k `Await =
match input () with
| Some str ->
let buf = Bytes.unsafe_of_string str in
Jsonm.Manual.src decoder buf 0 (Bytes.length buf);
k ()
| None ->
Jsonm.Manual.src decoder Bytes.empty 0 0;
k ()
and error (`Error err) =
error_msgf "Invalid JSON input: %a" Jsonm.pp_error err
and end_of_input `End = error_msgf "Unexpected end of input"
and arr acc k =
match Jsonm.decode decoder with
| #await as v -> await (fun () -> arr acc k) v
| #error as v -> error v
| #eoi as v -> end_of_input v
| `Lexeme `Ae -> k (`A (List.rev acc))
| `Lexeme v -> core (fun v -> arr (v :: acc) k) v
and name n k =
match Jsonm.decode decoder with
| #await as v -> await (fun () -> name n k) v
| #error as v -> error v
| #eoi as v -> end_of_input v
| `Lexeme v -> core (fun v -> k (n, v)) v
and obj acc k =
match Jsonm.decode decoder with
| #await as v -> await (fun () -> obj acc k) v
| #error as v -> error v
| #eoi as v -> end_of_input v
| `Lexeme `Oe -> k (`O (List.rev acc))
| `Lexeme (`Name n) -> name n (fun v -> obj (v :: acc) k)
| `Lexeme v ->
error_msgf "Unexpected lexeme: %a (expected key)" Jsonm.pp_lexeme v
and core k = function
| #value as v -> k v
| `Os -> obj [] k
| `As -> arr [] k
| `Ae | `Oe -> error_msgf "Retrieve invalid end of JSON array/object"
| `Name _ -> error_msgf "Retrieve invalid JSON key value"
and top () =
match Jsonm.decode decoder with
| #await as v -> await top v
| #error as v -> error v
| #eoi -> k `Null
| `Lexeme (#Jsonm.lexeme as lexeme) -> core k lexeme
in
top ()
module Stack = struct
type stack =
| In_array of t list * stack
| In_object of (string * t) list * stack
| Empty
end
let encode ?minify ?(size_chunk = _max_young_size) ~output t =
let encoder = Jsonm.encoder ?minify `Manual in
let buf = Bytes.create size_chunk in
let rec encode k stack value =
match Jsonm.encode encoder value with
| `Ok -> k stack
| `Partial ->
let len = Bytes.length buf - Jsonm.Manual.dst_rem encoder in
output (Bytes.sub_string buf 0 len);
Jsonm.Manual.dst encoder buf 0 (Bytes.length buf);
encode k stack `Await
and value k v stack =
match v with
| #value as v -> encode (continue k) stack (`Lexeme v)
| `O ms -> encode (obj k ms) stack (`Lexeme `Os)
| `A vs -> encode (arr k vs) stack (`Lexeme `As)
and obj k ms stack =
match ms with
| (n, v) :: ms ->
let stack = Stack.In_object (ms, stack) in
encode (value k v) stack (`Lexeme (`Name n))
| [] -> encode (continue k) stack (`Lexeme `Oe)
and arr k vs stack =
match vs with
| v :: vs ->
let stack = Stack.In_array (vs, stack) in
value k v stack
| [] -> encode (continue k) stack (`Lexeme `Ae)
and continue k = function
| Stack.In_array (vs, stack) -> arr k vs stack
| Stack.In_object (ms, stack) -> obj k ms stack
| Stack.Empty as stack -> encode k stack `End
in
Jsonm.Manual.dst encoder buf 0 (Bytes.length buf);
value (Fun.const ()) t Stack.Empty

102
lib/vif/main.ml Normal file
View file

@ -0,0 +1,102 @@
#require "miou.unix" ;;
#require "mirage-crypto-rng-miou-unix" ;;
#require "vif" ;;
#require "digestif.c" ;;
#require "base64" ;;
type cfg = Config
let index _req server Config =
Vif.Response.with_string server `OK "Hello from an OCaml script!"
;;
let hex _req arg server Config =
Vif.Response.with_string server `OK (Fmt.str "%02x\n%!" arg)
;;
let digest req server Config =
let ic = Vif.Request.to_stream req in
let rec go ctx =
match Vif.Stream.get ic with
| Some str -> go (Digestif.SHA1.feed_string ctx str)
| None -> Digestif.SHA1.get ctx
in
let hash = go Digestif.SHA1.empty in
let hash = Digestif.SHA1.to_hex hash in
Vif.Response.with_string server `OK hash
;;
let random req len server Config =
let buf = Bytes.create 0x7ff in
Vif.Response.with_stream server `OK @@ fun oc ->
let rec go rem =
if rem > 0 then begin
let len = Int.min rem (Bytes.length buf) in
Mirage_crypto_rng.generate_into buf len;
let str = Bytes.sub_string buf 0 len in
let str = Base64.encode_exn str in
Vif.Stream.put oc str;
go (rem - len)
end
in
go len
;;
type foo =
{ username : string
; password : string
; age : int option }
;;
let foo =
let open Json_encoding in
let username = req "username" string in
let password = req "password" string in
let age = opt "age" int in
let foo = obj3 username password age in
let inj (username, password, age) = { username; password; age } in
let prj { username; password; age } = (username, password, age) in
conv prj inj foo
;;
let login req server Config =
match Vif.Request.to_json req with
| Ok (foo : foo) ->
Logs.debug (fun m -> m "username: %s" foo.username);
Logs.debug (fun m -> m "password: %s" foo.password);
Vif.Response.with_string server `OK "Foo"
| Error (`Msg err) ->
Logs.err (fun m -> m "Invalid JSON: %s" err);
Vif.Response.with_string server `Not_acceptable err
let routes =
let open Vif.U in
let open Vif.R in
let open Vif.Content_type in
[
get (rel /?? nil) --> index
; get (rel / "random" /% Tyre.int /?? nil) --> random
; get (rel / "hex" /% Tyre.int /?? nil) --> hex
; post any (rel / "digest" /?? nil) --> digest
; post (json_encoding foo) (rel / "json" /?? nil) --> login
]
;;
let default req target server Config =
Logs.debug (fun m -> m "We are into the default case");
Vif.Response.with_string server `Not_found (Fmt.str "%S not found\n%!" target)
;;
let rng =
let open Mirage_crypto_rng_miou_unix in
let finally = kill in
Vif.D.device ~name:"rng" ~finally [] @@ fun Config ->
initialize (module Pfortuna)
;;
let () =
Miou_unix.run @@ fun () ->
let sockaddr = Unix.(ADDR_INET (inet_addr_loopback, 8080)) in
let cfg = Vif.config sockaddr in
Vif.run ~cfg ~default ~devices:Vif.Ds.[ rng ] routes Config
;;

View file

@ -1,5 +1,22 @@
let src = Logs.Src.create "vif"
module Log = (val Logs.src_log src : Logs.LOG)
module Json = Json
module U = Vif_u module U = Vif_u
module R = Vif_r
module R = struct
include Vif_r
open Vif_content_type
type ('fu, 'return) t =
| Handler : ('f, 'x) body * ('x, 'r) Vif_u.t -> ('f, 'r) t
let get t = Handler (Body Null, t)
let post body t = Handler (Body body, t)
let route (Handler (body, t)) f = Route (body, t, f)
let ( --> ) = route
end
module C = Vif_c module C = Vif_c
module D = Vif_d module D = Vif_d
module S = Vif_s module S = Vif_s
@ -27,6 +44,7 @@ module Ds = struct
Vif_d.Hmap.iter fn m Vif_d.Hmap.iter fn m
end end
module Content_type = Vif_content_type
module Stream = Stream module Stream = Stream
module Method = Vif_method module Method = Vif_method
module Status = Vif_status module Status = Vif_status
@ -60,11 +78,75 @@ let config ?http ?tls ?(backlog = 64) ?stop sockaddr =
let stop = Httpcats.Server.stop let stop = Httpcats.Server.stop
let is_application_json { Multipart_form.Content_type.ty; subty; _ } =
match (ty, subty) with `Application, `Iana_token "json" -> true | _ -> false
let request server =
let extract : type c a.
(c, a) Vif_content_type.t -> (c, a) Vif_request.t option = function
| Vif_content_type.Any as encoding ->
Some (Vif_request.of_reqd ~encoding server.S.reqd)
| Null as encoding -> Some (Vif_request.of_reqd ~encoding server.S.reqd)
| Json_encoding _ as encoding ->
let headers =
match server.S.reqd with
| `V1 reqd ->
let request = H1.Reqd.request reqd in
H1.Headers.to_list request.H1.Request.headers
| `V2 reqd ->
let request = H2.Reqd.request reqd in
H2.Headers.to_list request.H2.Request.headers
in
let c = List.assoc_opt "content-type" headers in
let c = Option.map (fun c -> c ^ "\r\n") c in
let c = Option.to_result ~none:`Not_found c in
let c = Result.bind c Multipart_form.Content_type.of_string in
begin
match c with
| Ok c when is_application_json c ->
Some (Vif_request.of_reqd ~encoding server.S.reqd)
| _ -> None
end
| Json as encoding ->
let headers =
match server.S.reqd with
| `V1 reqd ->
let request = H1.Reqd.request reqd in
H1.Headers.to_list request.H1.Request.headers
| `V2 reqd ->
let request = H2.Reqd.request reqd in
H2.Headers.to_list request.H2.Request.headers
in
let c = List.assoc_opt "content-type" headers in
let c = Option.map (fun c -> c ^ "\r\n") c in
let c = Option.to_result ~none:`Not_found c in
let c = Result.bind c Multipart_form.Content_type.of_string in
Log.debug (fun m ->
m "content-type: %a"
Fmt.(
Dump.result ~ok:Multipart_form.Content_type.pp
~error:(any "#errored"))
c);
begin
match c with
| Ok c when is_application_json c ->
Some (Vif_request.of_reqd ~encoding server.S.reqd)
| _ -> None
end
in
{ Vif_r.extract }
let handler ~default routes devices user's_value socket reqd = let handler ~default routes devices user's_value socket reqd =
let request = Request.of_reqd reqd in let target =
let target = Request.target request in match reqd with
| `V1 reqd -> (H1.Reqd.request reqd).H1.Request.target
| `V2 reqd -> (H2.Reqd.request reqd).H2.Request.target
in
let server = { Vif_s.reqd; socket; devices } in let server = { Vif_s.reqd; socket; devices } in
R.dispatch ~default routes ~target server request user's_value let request = request server in
Log.debug (fun m -> m "Handle a new request to %s" target);
let fn = R.dispatch ~default routes ~request ~target in
match fn server user's_value with Vif_response.Response -> ()
let run ~cfg ~devices ~default routes user's_value = let run ~cfg ~devices ~default routes user's_value =
let domains = Miou.Domain.available () in let domains = Miou.Domain.available () in

View file

@ -22,12 +22,75 @@ module U : sig
val eval : ('f, string) t -> 'f val eval : ('f, string) t -> 'f
end end
module Stream : sig
type 'a t
val create : int -> 'a t
val put : 'a t -> 'a -> unit
val get : 'a t -> 'a option
val close : 'a t -> unit
end
module Headers : sig
type t = (string * string) list
end
module Method : sig
type t =
[ `CONNECT
| `DELETE
| `GET
| `HEAD
| `OPTIONS
| `POST
| `PUT
| `TRACE
| `Other of string ]
end
module Json = Json
module Content_type : sig
type null
type json
type ('c, 'a) t
val null : (null, unit) t
val json : (json, Json.t) t
val json_encoding : 'a Json_encoding.encoding -> (json, 'a) t
val any : ('c, string) t
end
module Request : sig
type ('c, 'a) t
val target : ('c, 'a) t -> string
val meth : ('c, 'a) t -> Method.t
val version : ('c, 'a) t -> int
val headers : ('c, 'a) t -> Headers.t
val to_string : ('c, 'a) t -> string
val to_stream : ('c, 'a) t -> string Stream.t
val to_json : (Content_type.json, 'a) t -> ('a, [> `Msg of string ]) result
end
module R : sig module R : sig
type 'r route type 'r route
type ('fu, 'return) t
type request
val route : ('f, 'r) U.t -> 'f -> 'r route val get : ('x, 'r) U.t -> ((Content_type.null, unit) Request.t -> 'x, 'r) t
val ( --> ) : ('f, 'r) U.t -> 'f -> 'r route
val dispatch : default:(string -> 'r) -> 'r route list -> target:string -> 'r val post :
('c, 'a) Content_type.t -> ('x, 'r) U.t -> (('c, 'a) Request.t -> 'x, 'r) t
val ( --> ) : ('f, 'r) t -> 'f -> 'r route
val dispatch :
default:(('c, string) Request.t -> string -> 'r)
-> 'r route list
-> request:request
-> target:string
-> 'r
end end
module C : sig module C : sig
@ -112,28 +175,6 @@ module S : sig
val device : ('value, 'a) D.device -> t -> 'a val device : ('value, 'a) D.device -> t -> 'a
end end
module Stream : sig
type 'a t
val create : int -> 'a t
val put : 'a t -> 'a -> unit
val get : 'a t -> 'a option
val close : 'a t -> unit
end
module Method : sig
type t =
[ `CONNECT
| `DELETE
| `GET
| `HEAD
| `OPTIONS
| `POST
| `PUT
| `TRACE
| `Other of string ]
end
module Status : sig module Status : sig
type t = type t =
[ `Accepted [ `Accepted
@ -187,28 +228,13 @@ module Status : sig
| `Use_proxy ] | `Use_proxy ]
end end
module Headers : sig
type t = (string * string) list
end
module Request : sig
type t
val target : t -> string
val meth : t -> Method.t
val version : t -> int
val headers : t -> Headers.t
val to_string : t -> string
val to_stream : t -> string Stream.t
end
module Response : sig module Response : sig
type t type t
val with_stream : val with_stream :
S.t -> ?headers:Headers.t -> Status.t -> (string Stream.t -> unit) -> unit S.t -> ?headers:Headers.t -> Status.t -> (string Stream.t -> unit) -> t
val with_string : S.t -> ?headers:Headers.t -> Status.t -> string -> unit val with_string : S.t -> ?headers:Headers.t -> Status.t -> string -> t
end end
type config type config
@ -230,7 +256,7 @@ val stop : unit -> stop
val run : val run :
cfg:config cfg:config
-> devices:'value Ds.t -> devices:'value Ds.t
-> default:(string -> S.t -> Request.t -> 'value -> unit) -> default:(('c, string) Request.t -> string -> S.t -> 'value -> Response.t)
-> (S.t -> Request.t -> 'value -> unit) R.route list -> (S.t -> 'value -> Response.t) R.route list
-> 'value -> 'value
-> unit -> unit

View file

@ -0,0 +1,13 @@
type null = Null
and json = Json
type ('c, 'a) t =
| Null : (null, unit) t
| Json_encoding : 'a Json_encoding.encoding -> (json, 'a) t
| Json : (json, Json.t) t
| Any : ('c, string) t
let null = Null
let json_encoding e = Json_encoding e
let json = Json
let any = Any

0
lib/vif/vif_m.ml Normal file
View file

View file

@ -1,3 +1,7 @@
let src = Logs.Src.create "vif.r"
module Log = (val Logs.src_log src : Logs.LOG)
type 'a atom = 'a Tyre.Internal.wit type 'a atom = 'a Tyre.Internal.wit
let atom re = Tyre.Internal.build re let atom re = Tyre.Internal.build re
@ -177,15 +181,15 @@ let rec extract_path : type f x r.
let k f = k (f v) in let k f = k (f v) in
extract_path ~original rep subs k extract_path ~original rep subs k
let rec extract_query : type x r. let rec extract_query : type f x r.
original:string -> (x, r) query -> Re.Group.t -> x -> r = original:string -> (f, r) query -> Re.Group.t -> f -> r =
fun ~original wq subs f -> fun ~original wq subs k ->
match wq with match wq with
| Nil -> f | Nil -> k
| Any -> f | Any -> k
| Cons (rea, req) -> | Cons (rea, req) ->
let v = extract ~original rea subs in let v = extract ~original rea subs in
extract_query ~original req subs (f v) extract_query ~original req subs (k v)
let extract : type r f. original:string -> (f, r) t -> Re.Group.t -> f -> r = let extract : type r f. original:string -> (f, r) t -> Re.Group.t -> f -> r =
fun ~original (Url (wp, wq)) subs f -> fun ~original (Url (wp, wq)) subs f ->
@ -202,37 +206,61 @@ let extract t =
extract ~original:target url subs f extract ~original:target url subs f
*) *)
type 'r route = Route : ('f, 'r) Vif_u.t * 'f -> 'r route type ('fu, 'return) body =
| Body :
('c, 'a) Vif_content_type.t
-> (('c, 'a) Vif_request.t -> 'r, 'r) body
let route t f = Route (t, f) type 'r route = Route : ('f, 'x) body * ('x, 'r) Vif_u.t * 'f -> 'r route
let ( --> ) = route
type 'r re = Re : 'f * Re.Mark.t * ('f, 'r) t -> 'r re let route body t f = Route (body, t, f)
type 'r re = Re : ('f, 'x) body * 'f * Re.Mark.t * ('x, 'r) t -> 'r re
let rec build_info_list idx = function let rec build_info_list idx = function
| [] -> ([], []) | [] -> ([], [])
| Route (t, f) :: l -> | Route (b, t, f) :: l ->
let idx, ret, re = url idx t in let idx, ret, re = url idx t in
let rel, wl = build_info_list idx l in let rel, wl = build_info_list idx l in
let id, re = Re.mark re in let id, re = Re.mark re in
(re :: rel, Re (f, id, ret) :: wl) (re :: rel, Re (b, f, id, ret) :: wl)
let rec find_and_trigger : type r. type request = {
original:string -> Re.Group.t -> r re list -> r = extract: 'c 'a. ('c, 'a) Vif_content_type.t -> ('c, 'a) Vif_request.t option
fun ~original subs -> function }
| [] -> assert false
| Re (f, id, ret) :: l ->
if Re.Mark.test subs id then extract ~original ret subs f
else find_and_trigger ~original subs l
let dispatch : type r. let rec find_and_trigger : type f r a.
default:(string -> r) -> r route list -> target:string -> r = original:string -> request:request -> Re.Group.t -> r re list -> r =
fun ~original ~request subs -> function
| [] -> raise Not_found
| Re (Body body, f, id, ret) :: l ->
if Re.Mark.test subs id then
match request.extract body with
| Some request -> extract ~original ret subs (f request)
| None -> find_and_trigger ~original ~request subs l
else find_and_trigger ~original ~request subs l
let dispatch : type f r c a.
default:((c, string) Vif_request.t -> string -> r)
-> r route list
-> request:request
-> target:string
-> r =
fun ~default l -> fun ~default l ->
let rel, wl = build_info_list 1 l in let rel, wl = build_info_list 1 l in
let re = Re.(compile (whole_string (alt rel))) in let re = Re.(compile (whole_string (alt rel))) in
fun ~target -> fun ~request ~target ->
match Re.exec_opt re target with match Re.exec_opt re target with
| None -> default target | None ->
| Some subs -> ( Log.debug (fun m -> m "Fallback to the default route");
try find_and_trigger ~original:target subs wl let[@warning "-8"] (Some request) = request.extract Any in
with Not_found -> assert false) default request target
| Some subs -> begin
try find_and_trigger ~original:target ~request subs wl
with exn ->
Log.debug (fun m ->
m "Fallback to the default route (exn: %s)"
(Printexc.to_string exn));
let[@warning "-8"] (Some request) = request.extract Any in
default request target
end

View file

@ -1,17 +1,24 @@
type t = { let src = Logs.Src.create "vif.request"
module Log = (val Logs.src_log src : Logs.LOG)
type ('c, 'a) t = {
request: [ `V1 of H1.Request.t | `V2 of H2.Request.t ] request: [ `V1 of H1.Request.t | `V2 of H2.Request.t ]
; body: [ `V1 of H1.Body.Reader.t | `V2 of H2.Body.Reader.t ] ; body: [ `V1 of H1.Body.Reader.t | `V2 of H2.Body.Reader.t ]
; encoding: ('c, 'a) Vif_content_type.t
} }
let of_reqd = function let of_reqd : type c a.
encoding:(c, a) Vif_content_type.t -> Httpcats.Server.reqd -> (c, a) t =
fun ~encoding -> function
| `V1 reqd -> | `V1 reqd ->
let request = `V1 (H1.Reqd.request reqd) in let request = `V1 (H1.Reqd.request reqd) in
let body = `V1 (H1.Reqd.request_body reqd) in let body = `V1 (H1.Reqd.request_body reqd) in
{ request; body } { request; body; encoding }
| `V2 reqd -> | `V2 reqd ->
let request = `V2 (H2.Reqd.request reqd) in let request = `V2 (H2.Reqd.request reqd) in
let body = `V2 (H2.Reqd.request_body reqd) in let body = `V2 (H2.Reqd.request_body reqd) in
{ request; body } { request; body; encoding }
let target { request; _ } = let target { request; _ } =
match request with match request with
@ -70,3 +77,31 @@ let to_stream { body; _ } =
| `V2 body -> | `V2 body ->
to_stream ~schedule:H2.Body.Reader.schedule_read to_stream ~schedule:H2.Body.Reader.schedule_read
~close:H2.Body.Reader.close body ~close:H2.Body.Reader.close body
let destruct : type a. a Json_encoding.encoding -> Json.t -> a =
Json_encoding.destruct
let error_msgf fmt = Format.kasprintf (fun msg -> Error (`Msg msg)) fmt
let to_json : type a.
(Vif_content_type.json, a) t -> (a, [> `Msg of string ]) result = function
| { encoding= Any; _ } as req -> Ok (to_string req)
| { encoding= Json; _ } as req ->
Log.debug (fun m -> m "Parse the body as a JSON data");
let stream = to_stream req in
Log.debug (fun m -> m "Get the stream");
let input () =
let str = Stream.get stream in
Log.debug (fun m -> m "json(%a)" Fmt.(option (fmt "%S")) str);
str
in
Json.decode ~input Result.ok
| { encoding= Json_encoding encoding; _ } as req -> (
let stream = to_stream req in
let input () = Stream.get stream in
match Json.decode ~input Result.ok with
| Error (`Msg _) as err -> err
| Ok (json : Json.t) -> (
try Ok (destruct encoding json)
with Json_encoding.Cannot_destruct (_, _) ->
error_msgf "Invalid JSON value"))

View file

@ -1,70 +1,67 @@
type t = { response: Httpcats.response; compress: [ `GZip | `DEFLATE ] option } type t = Response
let strf fmt = Format.asprintf fmt let strf fmt = Format.asprintf fmt
let with_string server ?(headers = []) status str = module Hdrs = Vif_headers
match Vif_s.reqd server with
| `V1 reqd ->
let headers =
Vif_headers.add_unless_exists headers "content-length"
(strf "%d" (String.length str))
in
let headers = H1.Headers.of_list headers in
let status =
match status with
| #H1.Status.t as status -> status
| _ -> invalid_arg "Response.with_string: invalid status"
in
let resp = H1.Response.create ~headers status in
H1.Reqd.respond_with_string reqd resp str
| `V2 reqd ->
let headers =
Vif_headers.add_unless_exists headers "content-length"
(strf "%d" (String.length str))
in
let headers = H2.Headers.of_list headers in
let resp = H2.Response.create ~headers status in
H2.Reqd.respond_with_string reqd resp str
let with_stream server ?(headers = []) status fn = let compress_string ~headers str =
match List.assoc_opt "content-encoding" headers with
| Some "gzip" -> assert false
| _ -> str
let with_string server ?headers:(hdrs = []) status str =
match Vif_s.reqd server with match Vif_s.reqd server with
| `V1 reqd -> | `V1 reqd ->
let headers = let length = strf "%d" (String.length str) in
Vif_headers.add_unless_exists headers "transfer-encoding" "chunked" let hdrs = Hdrs.add_unless_exists hdrs "content-length" length in
in let str = compress_string ~headers:hdrs str in
let headers = H1.Headers.of_list headers in let hdrs = H1.Headers.of_list hdrs in
let status = let status =
match status with match status with
| #H1.Status.t as status -> status | #H1.Status.t as status -> status
| _ -> invalid_arg "Response.with_string: invalid status" | _ -> invalid_arg "Response.with_string: invalid status"
in in
let resp = H1.Response.create ~headers status in let resp = H1.Response.create ~headers:hdrs status in
H1.Reqd.respond_with_string reqd resp str;
Response
| `V2 reqd ->
let length = strf "%d" (String.length str) in
let hdrs = Hdrs.add_unless_exists hdrs "content-length" length in
let str = compress_string ~headers:hdrs str in
let hdrs = H2.Headers.of_list hdrs in
let resp = H2.Response.create ~headers:hdrs status in
H2.Reqd.respond_with_string reqd resp str;
Response
let with_stream server ?headers:(hdrs = []) status fn =
match Vif_s.reqd server with
| `V1 reqd ->
let hdrs = Hdrs.add_unless_exists hdrs "transfer-encoding" "chunked" in
let hdrs = H1.Headers.of_list hdrs in
let status =
match status with
| #H1.Status.t as status -> status
| _ -> invalid_arg "Response.with_string: invalid status"
in
let resp = H1.Response.create ~headers:hdrs status in
let stream = Stream.create 0x7ff in let stream = Stream.create 0x7ff in
let body = H1.Reqd.respond_with_streaming reqd resp in let body = H1.Reqd.respond_with_streaming reqd resp in
let res0 = let res0 = Miou.Ownership.create ~finally:H1.Body.Writer.close body in
let finally = H1.Body.Writer.close in let res1 = Miou.Ownership.create ~finally:Stream.close stream in
Miou.Ownership.create ~finally body let rec send stream body res =
in
let res1 =
let finally = Stream.close in
Miou.Ownership.create ~finally stream
in
let prm0 =
Miou.async ~give:[ res0 ] @@ fun () ->
let rec go () =
match Stream.get stream with match Stream.get stream with
| Some str -> | Some str ->
let () = H1.Body.Writer.write_string body str in H1.Body.Writer.write_string body str;
go () send stream body res
| None -> H1.Body.Writer.close body; Miou.Ownership.disown res0 | None -> H1.Body.Writer.close body; Miou.Ownership.disown res
in
go ()
in in
let prm0 = Miou.async ~give:[ res0 ] @@ fun () -> send stream body res0 in
let prm1 = let prm1 =
Miou.async ~give:[ res1 ] @@ fun () -> Miou.async ~give:[ res1 ] @@ fun () ->
let () = fn stream in let () = fn stream in
Stream.close stream; Miou.Ownership.disown res1 Stream.close stream; Miou.Ownership.disown res1
in in
Miou.await_all [ prm0; prm1 ] Miou.await_all [ prm0; prm1 ]
|> List.iter (function Ok () -> () | Error exn -> raise exn) |> List.iter (function Ok () -> () | Error exn -> raise exn);
Response
| `V2 _ -> assert false | `V2 _ -> assert false

71
main.ml
View file

@ -1,18 +1,22 @@
#require "miou.unix" ;; #require "miou.unix"
#require "mirage-crypto-rng-miou-unix" ;;
#require "vif" ;;
#require "digestif.c" ;;
#require "base64" ;;
let index server _req () = #require "mirage-crypto-rng-miou-unix"
#require "vif"
#require "digestif.c"
#require "base64"
type cfg = Config
let index _req server Config =
Vif.Response.with_string server `OK "Hello from an OCaml script!" Vif.Response.with_string server `OK "Hello from an OCaml script!"
;;
let test arg server _req () = let hex _req arg server Config =
Vif.Response.with_string server `OK (Fmt.str "%02x\n%!" arg) Vif.Response.with_string server `OK (Fmt.str "%02x\n%!" arg)
;;
let digest server req () = let digest req server Config =
let ic = Vif.Request.to_stream req in let ic = Vif.Request.to_stream req in
let rec go ctx = let rec go ctx =
match Vif.Stream.get ic with match Vif.Stream.get ic with
@ -22,9 +26,8 @@ let digest server req () =
let hash = go Digestif.SHA1.empty in let hash = go Digestif.SHA1.empty in
let hash = Digestif.SHA1.to_hex hash in let hash = Digestif.SHA1.to_hex hash in
Vif.Response.with_string server `OK hash Vif.Response.with_string server `OK hash
;;
let random len server req () = let random req len server Config =
let buf = Bytes.create 0x7ff in let buf = Bytes.create 0x7ff in
Vif.Response.with_stream server `OK @@ fun oc -> Vif.Response.with_stream server `OK @@ fun oc ->
let rec go rem = let rec go rem =
@ -38,33 +41,53 @@ let random len server req () =
end end
in in
go len go len
;;
type foo = { username: string; password: string; age: int option }
let foo =
let open Json_encoding in
let username = req "username" string in
let password = req "password" string in
let age = opt "age" int in
let foo = obj3 username password age in
let inj (username, password, age) = { username; password; age } in
let prj { username; password; age } = (username, password, age) in
conv prj inj foo
let login req server Config =
match Vif.Request.to_json req with
| Ok (foo : foo) ->
Logs.debug (fun m -> m "username: %s" foo.username);
Logs.debug (fun m -> m "password: %s" foo.password);
Vif.Response.with_string server `OK "Foo"
| Error (`Msg err) ->
Logs.err (fun m -> m "Invalid JSON: %s" err);
Vif.Response.with_string server `Not_acceptable err
let routes = let routes =
let open Vif.U in let open Vif.U in
let open Vif.R in let open Vif.R in
let open Vif.Content_type in
[ [
(rel /?? nil) --> index get (rel /?? nil) --> index
; (rel / "random" /% Tyre.int /?? nil) --> random ; get (rel / "random" /% Tyre.int /?? nil) --> random
; (rel / "test" /% Tyre.int /?? nil) --> test ; get (rel / "hex" /% Tyre.int /?? nil) --> hex
; (rel / "digest" /?? nil) --> digest ; post any (rel / "digest" /?? nil) --> digest
; post (json_encoding foo) (rel / "json" /?? nil) --> login
] ]
;;
let default target server req () = let default req target server Config =
Logs.debug (fun m -> m "We are into the default case");
Vif.Response.with_string server `Not_found (Fmt.str "%S not found\n%!" target) Vif.Response.with_string server `Not_found (Fmt.str "%S not found\n%!" target)
;;
let rng = let rng =
let open Mirage_crypto_rng_miou_unix in let open Mirage_crypto_rng_miou_unix in
let finally = kill in let finally = kill in
Vif.D.device ~name:"rng" ~finally [] @@ fun _cfg -> Vif.D.device ~name:"rng" ~finally [] @@ fun Config ->
initialize (module Pfortuna) initialize (module Pfortuna)
;;
let () = let () =
Miou_unix.run @@ fun () -> Miou_unix.run @@ fun () ->
let sockaddr = Unix.(ADDR_INET (inet_addr_loopback, 8080)) in let sockaddr = Unix.(ADDR_INET (inet_addr_loopback, 8080)) in
let cfg = Vif.config sockaddr in let cfg = Vif.config sockaddr in
Vif.run ~cfg ~default ~devices:Vif.Ds.[ rng; ] routes () Vif.run ~cfg ~default ~devices:Vif.Ds.[ rng ] routes Config
;;