(* copyright (c) 2013, simon cruanes all rights reserved. redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *) (** {1 Expose the State of a Program to the Web} We export some values (and associated functions for converting them to html, and update them) as a FastCGI interface. This module depends on CamlGI. *) (** {2 Some combinators to build HTML documents} *) module HTML = struct type t = | Str of string (* content *) | List of t list | Url of url | Img of image | Concat of t list | H of int * t | Link of link | Tag of string * t | TagWith of string * (string * string) list * t and url = { url_alt : string option; url_url : string; url_descr : string; } and image = { img_alt : string option; img_url : string; } and link = { link_rel : string; link_url : string; } let str s = Str s let bprintf format = let buffer = Buffer.create 64 in let r = ref (str "") in Printf.kbprintf (fun x -> r := str (Buffer.contents buffer)) buffer format; !r let sprintf format = let r = ref (str "") in Printf.ksprintf (fun s -> r := str s) format; !r let list l = List l let url ?alt ~url ~descr = Url { url_alt = alt; url_url = url; url_descr = descr; } let img ?alt url = Img { img_alt = alt; img_url = url; } let append a b = Concat [a; b] let concat l = Concat l let h1 x = H (1, x) let h2 x = H (2, x) let h3 x = H (3, x) let h n x = H (n, x) let p x = Tag ("p", x) let div ?id ?class_ x = match id, class_ with | None, None -> Tag ("div", x) | Some i, None -> TagWith ("div", ["id", i], x) | None, Some c -> TagWith ("div", ["class", c], x) | Some i, Some c -> TagWith ("div", ["id", i; "class", c], x) let span ?id ?class_ x = match id, class_ with | None, None -> Tag ("span", x) | Some i, None -> TagWith ("span", ["id", i], x) | None, Some c -> TagWith ("span", ["class", c], x) | Some i, Some c -> TagWith ("span", ["id", i; "class", c], x) let link ~rel ~url = Link { link_rel = rel; link_url = url; } let head x = Tag ("head", x) let body x = Tag ("body", x) let html x = Tag ("html", x) let _to_hex n = match n with | _ when n >= 0 && n < 10 -> Char.chr (Char.code '0' + n) | 10 -> 'A' | 11 -> 'B' | 12 -> 'C' | 13 -> 'D' | 14 -> 'E' | 15 -> 'F' | _ -> failwith "not an hexadecimal digit" let _encode_char buf c = Buffer.add_string buf "&#x"; let h, l = Char.code c / 16, Char.code c mod 16 in Buffer.add_char buf (_to_hex h); Buffer.add_char buf (_to_hex l) let encode str = let b = Buffer.create (String.length str + 10) in for i = 0 to String.length str - 1 do match str.[i] with | ';' | '/' | '?' | ':' | '@' | '&' | '=' | '+' | '$' | ',' | '<' | '>' | '#' | '%' | '"' | '{' | '}' | '|' | '\\' | '^' | '[' | ']' | '`' -> _encode_char b str.[i] | c when Char.code c < 32 -> _encode_char b str.[i] | c when Char.code c > 127 -> _encode_char b str.[i] | _ -> Buffer.add_char b str.[i] done; Buffer.contents b (* real rendering is always into a buffer (for now) *) let rec to_buf buf x = match x with | Str s -> Buffer.add_string buf (encode s) | List l -> Buffer.add_string buf "" | Url url -> begin match url.url_alt with | None -> Printf.bprintf buf "%s" url.url_url (encode url.url_descr) | Some alt -> Printf.bprintf buf "%s" url.url_url (encode alt) (encode url.url_descr) end | Img i -> failwith "img: not implemented" | Concat l -> List.iteri (fun i y -> if i > 0 then Buffer.add_char buf ' '; to_buf buf y) l | H (n, y) -> Printf.bprintf buf " %a " n to_buf y n | Link _ -> failwith "link: not implemented" | Tag (str, y) -> Printf.bprintf buf "<%s> %a " str to_buf y str | TagWith (str, attrs, y) -> Printf.bprintf buf "<%s " str; List.iter (fun (name,attr) -> Printf.bprintf buf "%s=\"%s\"" name attr) attrs; Printf.bprintf buf "> %a " to_buf y str let render x = let buf = Buffer.create 256 in to_buf buf x; Buffer.contents buf let to_chan oc x = let buf = Buffer.create 256 in to_buf buf x; Buffer.output_buffer oc buf end (** {2 Stateful Object on the Web} *) module State = struct type 'a t = { mutable content : 'a; mutable callbacks : ('a -> unit) list; id : ('a -> string) option; export : 'a -> HTML.t; update : (string * string) list -> 'a -> 'a; } (** A value that can be exposed to the Web. The [export] function is used to print the current state of the object into HTML (when requested). The [update] optional function can be used to update the value, given a query with parameters. *) type wrap = Wrap : 'a t -> wrap (** Hides the type parameter in a GADT. *) let create ?(update=fun _ x -> x) ?id ~export content = { content; export; id; callbacks = []; update; } let on_change st f = st.callbacks <- f :: st.callbacks let handle_request st req = let cgi = new CamlGI.Cgi.cgi req in (* update value? *) try let x = st.content in let params = cgi#params in (* update [x] using the parameters? *) let y = st.update params x in let changed = match st.id with | None -> x != y | Some id -> id x <> id y in (* notify callbacks that we have a new object *) if changed then List.iter (fun f -> f y) st.callbacks; (* now print [y] *) (* TODO: add a head, declaration, etc. *) let html = st.export y in let final_output = HTML.render html in (* render output *) let template = object method output f = f final_output end in cgi#template template with e -> let msg = Printf.sprintf "error: %s" (Printexc.to_string e) in cgi#log msg end (** {2 Routing} *) module Router = struct type t = { mutable default : State.wrap; log : out_channel option; tbl : (string, State.wrap) Hashtbl.t; } let __default = State.Wrap (State.create ~export:HTML.str "") let _log router fmt = match router.log with | None -> Printf.ifprintf stdout fmt | Some oc -> Printf.kfprintf (fun oc -> output_char oc '\n'; flush oc) oc fmt let create ?(default=__default) ?log () = let router = { default; log; tbl = Hashtbl.create 15; } in _log router "new router created"; router let default router default = router.default <- default let unregister router name = Hashtbl.remove router.tbl name let register ?(weak=false) router name state = if Hashtbl.mem router.tbl name then failwith "Router: name already registered" else begin Hashtbl.add router.tbl name state; if weak then match state with | State.Wrap st -> Gc.finalise (fun _ -> unregister router name) st.State.content end let add_list router l = List.iter (fun (name, state) -> register router name state) l let to_list router = Hashtbl.fold (fun name state acc -> (name,state) :: acc) router.tbl [] let random_id () = CamlGI.Cgi.random_sessionid () let handle_request router req = let cgi = new CamlGI.Cgi.cgi req in let url = cgi#url () in let st = try let last_part_i = String.rindex url '/' in let last_part = String.sub url (last_part_i+1) (String.length url -last_part_i-1) in _log router "received request for url /%s" last_part; Hashtbl.find router.tbl last_part with Not_found -> router.default in match st with | State.Wrap st -> State.handle_request st req end (** {2 Main Interface} *) let serve_state ?sockfile ?sockaddr st = match sockfile with | None -> CamlGI.Cgi.register_script ?sockaddr (State.handle_request st) | Some f -> let sockaddr = Unix.ADDR_UNIX f in CamlGI.Cgi.register_script ~sockaddr (State.handle_request st) let serve_router ?sockfile ?sockaddr router = match sockfile with | None -> CamlGI.Cgi.register_script ?sockaddr (Router.handle_request router) | Some f -> let sockaddr = Unix.ADDR_UNIX f in CamlGI.Cgi.register_script ~sockaddr (Router.handle_request router)