On Code etc.

12 notes

Shopping Carts with Ocsigen.

A pretty common thing for an ecommerce website to have is some sort of shopping cart. There are ways around it - primarily by purchasing one item at a time (or even worse - filling out an order form with the item identifiers!) - but the shopping cart reigns supreme.

To implement a shopping cart, a few things usually need to happen. When someone visits the website, you need to stick a cookie on their browser, and record that on the server, referencing the data (the cart contents). Then this new data store is your cart for the request. Of course you actually first need to check if they already have a cookie - then check if the reference is good, and if so update the expiration time on the cookie and your local data, and then use the cart selected for the request. And this looking for cookies, verifying that the data exists, etc, needs to happen on every request.

Of course, a shopping cart is only one reason why you might want to do something like this - there are many reasons why you might want to store data on a per user basis (but not per login, or not even on a site where logging in is necessary) - recently viewed pages, ‘favorites’, etc.

Ocsigen (well, actually Eliom, but since eliom is distributed with ocsigen, and the former has more name recognition, I’ll use it) abstracts this all out by making it possible (and easy) to create “session data”. You simply name a place to store the data (any data type you want) and then in requests you can get the data, modify it, save it. There is no need to mess with cookies, or even worry about where the data is being stored.

It is interesting to consider this design decision - to not try to avoid the paradigm of the web entirely, as requests still are independent and accessible via sane urls (some continuations based web frameworks will only allow you to enter the site at one point, and then will store the state in a token stored in the url - completely abusing the entire idea of urls and the web). But certain low-level aspects that are tedious to handle and often necessary - like this issue of per-client data - are abstracted out. In this way Ocsigen can be seen not as yet another library to handle dealing with web programming, nor a completely different programming model that is compiled down to something that speaks the language of the web but rather a high-level language in which to write web applications.

But enough digression - here is a shopping cart mechanism from a recent project I was working on, a store to sell MP3s:

type cart = { songs : song list }
let session_data = create_volatile_table ()

let get_cart sp = 
  let sess_data = get_volatile_session_data  ~table:session_data ~sp () in
    match sess_data with
      |Data c -> c.songs
      |Data_session_expired -> []
      |No_data -> []

let mod_cart sp fn =
  let sess_data = get_volatile_session_data  ~table:session_data ~sp () in
  let new_sess = match sess_data with
    |Data c -> {songs = fn (c.songs)}
    |Data_session_expired
    |No_data -> {songs = fn []} in
    set_volatile_session_data  ~table:session_data ~sp new_sess

And that’s it. The action to add to the cart? It’s one line, once you get past the action boilerplate:

let add_to_cart =
  Eliom_predefmod.Action.register_new_post_coservice'
    ~post_params:(user_type song_of_string string_of_song "song")
    (fun sp () song ->
       mod_cart sp (fun c -> if not (List.mem song c) then c@[song] else c);
       return ())

Delete is as simple:

let del_from_cart =
  Eliom_predefmod.Action.register_new_post_coservice'
    ~post_params:(user_type song_of_string string_of_song "song")
    (fun sp () song ->
       mod_cart sp (fun c -> BatList.remove c song);
      return ())

And the last part, to complete the cart mechanism, are the forms to post to the add and del actions. These use one of the more clever features that Ocsigen has - provided you can give it functions to turn native types to and from strings, you can put native types in html forms (as selects, or hidden inputs), you can preapply services with them, and as you saw above, you can use them as parameters to services. One of the most convenient uses of this in my application is a form that is just a button, that has one hidden element - a song. This is written as:

let mk_song_form txt s =
  (fun song_n ->
     [div [user_type_input ~input_type:`Hidden ~name:song_n ~value:s string_of_song ();
       button ~button_type:`Submit [pcdata txt]]])

So then anywhere where I want to put a button to add a song to the cart, where the song is ‘s’, and sp is the conventional name for the server params argument to all services (see the above actions for where it appears), the code is as simple as:

post_form add_to_cart sp (mk_song_form "Add to cart" s) ()

Or to delete:

post_form del_from_cart sp (mk_song_form "Del from cart" s) ()

Pretty simple, eh?

Filed under ocsigen ocaml web programming

  1. dbpatterson posted this