.. post:: 2014-04-04
:tags: OCaml, Opium
:author: Rudi Grinberg
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:
.. code-block:: ocaml
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:
.. code-block:: ocaml
(* 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.
.. code-block:: ocaml
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.
.. code-block:: ocaml
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:
.. code-block:: ocaml
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.
.. code-block:: ocaml
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.