inhabitedtype / ocaml-webmachine

A REST toolkit for OCaml
Other
221 stars 31 forks source link

Illustrate how to prepare a simple class serving static content #41

Open foretspaisibles opened 9 years ago

foretspaisibles commented 9 years ago

It would be interesting to show how to prepare a simple class serving static content. I will take a look at this.

seliopou commented 9 years ago

Thanks for the suggestions. I have a few resources I use internally related to serving static files as well as directories. I've been meaning to add them here but since they depend on a filesystem interface may need special treatment for both async and lwt.

foretspaisibles commented 9 years ago

I wanted to try crunch available with opam, that prepares a OCaml module containing a file-system hierarchy. For things like CSS, JS-Frameworks and the like, it might be appropriated.

foretspaisibles commented 8 years ago

I worked on this and built up a working example, serving pages built with bootstrap.

There is a point that still needs to be clarified, since I am not sure what is the best way to set the content-type header on the response.

My content_types_provided looked like this:

    method content_types_provided rd =
      self#log_headers rd;
      Wm.continue [
        ("*/*"       , self#to_data);
      ] rd

and the #to_data method ended with

Wm.continue (`String data) (self#set_mime_type rd)

where #set_mime_type uses Rd.with_resp_headers to set the "content-type" value according to the path being retrieved (trust the path suffix). This does not work as expected, examining the issue with curl shows that the header is set two times:

< HTTP/1.1 200 OK
< content-length: 23409
< content-type: text/css
< content-type: */*

I therefore assume that the consumer of the result of content_types_provided method sets the content-type header itself.

What would be the best way to serve static resources without building a-priori knowledge of the type of static resources being served in the class? My goal would be to write a generic class parametrised by an association list mapping paths to content which could be used to serve static content. A possibility would be to generate the answer of the #content_types_provided method using that association list and a dictionary mapping globbing patterns to mime-types, but there might be abetter way.

foretspaisibles commented 8 years ago

BTW are you interested in seeing such a class becoming a part of the webmachine (e.g. as lib/webmachine_Static.ml) or would you prefer seeing this in a separate project?

seliopou commented 8 years ago

Here's the resource implementation I use for serving a single static file. It necessarily relies on core and async since it does filesystem I/O. It also uses the magic-mime library to determine content types based on file extensions. Ideally, an implementation for lwt and async would exist in corresponding subpackages, which would also export the applied Webmachine functor for each of the libraries.

open Core.Std
open Async.Std

let serve_file filename rd =
  Reader.open_file filename
  >>| Reader.pipe
  >>= fun pipe ->
    continue (`Pipe pipe) rd

class file filename = object(self)
  inherit [Body.t] resource

  val content_type = Magic_mime.lookup filename
  val stat_struct  = stat_file filename
  val methods = [`GET; `HEAD; `OPTIONS]

  method private to_content rd =
    serve_file filename rd

  method resource_exists rd =
    match stat_struct with
    | `Stat _ -> continue true rd
    | _       -> continue false rd

  method forbidden rd =
    match stat_struct with
    | `Forbidden -> continue true rd
    | _          -> continue false rd

  method options rd =
    continue ["content-type", content_type] rd

  method allowed_methods rd =
    continue methods rd

  method content_types_accepted rd =
    continue [] rd

  method content_types_provided rd =
    continue [content_type, self#to_content] rd
end
artemkin commented 8 years ago

@seliopou offtopic and so opinion based, but why do you use classes? I've been writing some OCaml using async, lwt and cohttp libraries, but classes and objects are still so foreign to me. I always considered them as abandoned features of OCaml, and they seem to be uncommon now.

foretspaisibles commented 8 years ago

A good rule of thumb to decide wether to use functions or objects to solve a problem is that functions shine when there is a few types and a lot of treatments while objects are used at their best when there are few treatments and a lot of different types or objects on which to use these treatments.

Here we are in the case where we have few treatments and a lot of possible types or objects, so, at this level of the discussion, it is sound to use objects for this. Besides this, I understand this library is a port of an Erlang project, using classes could have made the port easier. @seliopou Am I right?

seliopou commented 8 years ago

There are two uses of the object system that should be distinguished. The first is its use in the implementation of the decision diagram. This use is not directly exposed to the user, and was indeed a matter of expediency while porting. Specifically, open recursion allowed me to write the node handlers in the logic code in whichever order came to mind. This use can be eliminated by topologically sorting the handlers according to their call graph and moving all the state within the logic class to a separate data type. This use may have some performance impact, but shouldn't affect the user much at all.

The second is the use of a virtual class for defining resources. This was an intentional choice. Using a virtual class neatly accomplishes the following goals:

To elaborate on the fourth point, using classes allows one to reuse code via inheritance. Now I'm no OOP zealot, but this method of code reuse is a great fit when dealing with what are essentially handlers attached to hierarchical paths. Usually the entities down the hierarchy utilize parts of the handler logic without modification, while sometimes parts of the handler logic are augmented with additional checks or processing. I leverage this all over the place internally, to great effect.

Now in theory, you could do this with modules, signatures, and functors. The cost you pay for that is a minor proliferation of signatures and functors in order to handle default-only and complete modules.

And to comment briefly on the third point, one way that this port of webmachine differs from the Erlang implementation is that only request/response information is threaded through the decision diagram in a purely functional way. Any application state that may exist has to be managed within the resource class. This was also an intentional decision in order to minimize the number of type variables that users had to deal with—'body Rd.t would become ('a, 'body) Rd.t and that would then cascade into practically every type.

In terms of any perception of classes or objects being abandoned features, my understanding of feature evolution in OCaml is that once it's in the language, it's in the language. Whenever I've heard of people working on new features and tooling for the language, they are always sure to address objects. Perhaps begrudgingly, but they still do it. So while their use may not be widespread, they're definitely not going to be removed from the language any time soon.

Hope this helps. Maybe I should put this in a wiki.