Introducing Opium¶
One itch that I usually need to scratch is setting up quick and dirty REST API’s - preferably in OCaml. OCaml does have a few options for web development but I found them to be not so great fits for my needs. To summarize:
Ocsigen - Is an innovative web framework but it’s too heavyweight for my common and simple use case; all of the client side stuff is immediately useless when all I want is just to throw some json over a POST request. Plus, I also prefer Async to Lwt and Ocsigen is much too big to port.
Ocamlnet - Mature and stable and from the sounds of it, pretty fast as well. However, by the looks of it, it’s too low level. Also it does not use cooperative threads for concurrency (Async or Lwt), and that’s a deal breaker for me.
To OCaml programmers coming from other languages, I’ll re-iterate all of the above very briefly: What I want is a Sinatra clone written in OCaml.
Without further ado, I’d like to introduce Opium, my own stab at this problem and I’m ready to declare it at a state where it’s not embarassing to show around. The project’s readme already contains high level documentation and a few examples. Instead of repeating that here I’ll quickly describe the project and do a little tutorial that’s a little more beginner friendly.
Getting Started¶
Opium has been available on OPAM for a while now and can be installed with:
$ opam install opium
From the dependencies that opium we can immediately see that opium is written on top of the async backend of cohttp, a pure OCaml http library for Lwt + Async. Cohttp is a great library but it’s a little too low level for the kind of code I’d like to write.
If you’ve installed everything correctly, the following simple example:
open Core.Std
open Async.Std
open Opium.Std
let app =
App.empty |> (get "/" begin fun req ->
`String "Hello World" |> respond'
end)
let () =
app
|> App.command
|> Command.run
Should compile after:
$ corebuild -pkg opium hello_opium.native
What do we get out of this? Run ./hello_opium.native -h
and see.
Opium generates a convenient executable for you with a few common
options. For example to run a specific port and print debug information
to stdout we can:
$ ./hello_opium.native -p 9000 -d
Now we can test our little server with:
$ curl 127.0.0.1:9000
Basics¶
Routing¶
The most basic functionality that opium provides is a simple interface for binding http requests to functions. It uses a simple glob like routing for url paths and normally binds one function to an http method. Parameters can also be specified. Here’s a couple examples:
(* Named parameters, only get request *)
let e1 = get "/hello/:name" (fun req ->
let name = param req "name" in
`String ("hello " ^ name) |> respond')
(* Splat paramteres *)
let e2 = get "/splat/*/anything" (fun req -> (`String "*") |> respond')
(* Multiple http methods *)
let f _ = `String "testing" |> respond'
let both = Fn.compose (get f) (put f)
Some sort of type safety would of course be ideal but I’m still in the process of figuring out some of the approaches to this problem.
Response helpers¶
Opium provides a few conveniences for generating common responses such as Json, Html, etc. and sets the response headers for you appropriately.
let e3 = get "/xxx/:x/:y" begin fun req ->
let x = "x" |> param req |> Int.of_string in
let y = "y" |> param req |> Int.of_string in
let sum = Float.of_int (x + y) in
`Json (Cow.Json.Float sum) |> respond'
end
By the way, respond'
is simply respond
wrapped with
Deferred.return
.
Debugging¶
Try hitting the following endpoint after you run your application in
debug mode (-d
flag). Make sure to compile with debugging as well.
let throws = get "/throw" (fun req ->
Log.Global.info "Crashing...";
failwith "expected failure!")
You get a nice stack trace and the requested that caused it:
((request
((headers
((accept
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
(accept-encoding gzip,deflate,sdch)
(accept-language "en-GB,en;q=0.8,en-US;q=0.6,ru;q=0.4")
(connection keep-alive)
(host localhost:3000)
(user-agent
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36")))
(meth GET)
(uri
((scheme http) (host localhost) (port 3000) (path (/ yyy)) (query ())))
(version HTTP_1_1) (encoding Unknown)))
(env ()))
(lib/monitor.ml.Error_
((exn (Failure "expected failure!"))
(backtrace
("Raised at file \"pervasives.ml\", line 20, characters 22-33"
"Called from file \"opium/cookie.ml\", line 60, characters 4-15"
"Called from file \"lib/monitor.ml\", line 169, characters 25-32"
"Called from file \"lib/jobs.ml\", line 214, characters 10-13" ""))
(monitor
(((name try_with) (here ()) (id 220) (has_seen_error true)
(someone_is_listening true) (kill_index 0))))))
OK I admit, nice might be pushing it.
Going deeper¶
Opium is an extremely simple toolkit (I’m careful not to call it a framework on purpose). At its heart there are only 4 basic types:
{Request,Response} - Wrappers around cohttp’s {request,response}
Handler -
Request.t -> Response.t Deferred.t
Middleware -
Handler.t -> Handler.t
A handler is a full blown opium on its own (even though we usually have
multiple handlers we dispatch to with routing). For example if we expand
out the type signature for the familiar get
function we get:
val get : string -> Handler.t -> builder
We see that the function parameter we pass to get is nothing more than a simple handler.
Middleware on the ojther hand is the main building block of reusable components. In fact all of opium is built in terms of such middleware, Router, Debugging, Static pages, etc. The low level layer that knows what to do with them is called Rock (Kind of like Rack in ruby, or WSGI in python). For something that’s pretty flexible, middleware is extremely simple, all it does is transform handlers. To give you a small taste, here’s a simple middleware that will randomly reject based on their user agent. Also available in the readme.
open Core.Std
open Async.Std
open Opium.Std
let is_substring ~substring s = Pcre.pmatch ~pat:(".*" ^ substring ^ ".*") s
let reject_ua ~f =
let filter handler req =
match Cohttp.Header.get (Request.headers req) "user-agent" with
| Some ua when f ua ->
Log.Global.info "Rejecting %s" ua;
`String ("Please upgrade your browser") |> respond'
| _ -> handler req in
Rock.Middleware.create ~filter ~name:(Info.of_string "reject_ua")
let app = App.empty
|> get "/" (fun req -> `String ("Hello World") |> respond')
|> middleware @@ reject_ua ~f:(is_substring ~substring:"MSIE")
let _ =
Command.run (App.command ~summary:"Reject UA" app)
$ corebuild -pkg opium,pcre middleware_ua.native
Actually I’ve lied a little bit as you can tell from the example above.
A middleware is not just a Handler.t -> Handler.t
. That is only it’s
filter component. Middleware is also named, it is mainly useful for
debugging.
The Future¶
Until 1.0.0 is mostly suitable for me and brave beta testers. This means that opium still has some potentially embarrassing bugs, and interface breakges are to be expected But I still invite all users and potential contributors to help me improve Opium.
At this moment I’m most interested in bug reports and suggestions to the interface. More features are of course to be expected, such as support for sessions, whether cookie based or in memory. One big feature that will probably not make it into 1.0 is Lwt support. I’d love to have it but 2 backends would be a little much for me to maintain on my own.
Finally, stick around because I have more posts about Opium planned.