Hacking together GHCJS support for Servant

Source Demo

So tonight I wanted to hack something together in 3 hours and I did!

I wanted servant to work in the browser.

Servant allows you to define routes at the typelevel and it automagically creates a whole freakin rest api from that. Yay for haskell.

Anyhow. Having type-safe communication between server and browser just sounded awesome. So I had to hack some prototype together.

Hacking it

Luckily the library maintainers did a lot of work for me already. They split up the servant package in servant and servant-server on my request. With servant-server containing all the server specific stuff.

servant compiled perfectly under GHCJS. Neat.

Okay so now we need not only a way to create serverside code. We also need clientside functions. servant-client to the rescue! Lets try compile that.

Darnit. the http-client dependency can't be compiled by ghcjs. Probably due to it being dependent on network. Okay so lets add a conditional to the Cabalfile that http-client should only be loaded when ghcjs is not used at the compiler.

if !impl(ghcjs)
    build-depends:http-client

Ok nice. that works. Now we got a bunch of errors because of HTTP.Client not being in scope. Kinda makes sense. Okay so lets
just use the C Preprocessor to check if the GHCJS compiler is present. and if so, dont import that module anywhere its used. Do the same for any code that uses the module.

so now only one function is complaining it lacks an accompanying binding. Neat! One function should be doable to implement.

instead of using http-client we use the JavaScriptFFI to use the XMLHTTPRequest API to make HTTP calls. A little bit of hacking and marshalling later I discover I can't marshal Lazy bytestrings from and to javascript.... No time left, I need to go to bed! Okay lets just add another CPP #ifdef and just import the strict version if we use GHCJS... That seems to work! Except that some external function expects a Lazy bytestring. Okay lets just convert the strict bytestring to a lazy bytestring for that specific function call. Super hack.

Okay so now everything compiles. It's super hacky. but it compiles...

So I set up a little test environment ... and.... IT WORKS! WOOHOO :)

I've only tested the GET HTTP method. But I dont see why others wouldn't work. Also I haven't done any form of exception handling but that's something for later as I need to go sleep now. I'm glad this works!

Check out the source and the demo:
Source Demo

And the test setup!

Common.hs

data Book = Book { title :: String
                 , author :: String
                 } deriving (Generic,Show)
instance FromJSON Book
instance ToJSON Book
type MyApi =  "books" :> Get [Book] :<|> "static" :> Raw
data Book = Book { title :: String
                 , author :: String
                 } deriving (Generic,Show)
   instance FromJSON Book
instance ToJSON Book
type MyApi =  "books" :> Get [Book] :<|> "static" :> Raw

Server.hs

getBooks :: EitherT (Int, String) IO [Book]
getBooks  = return [Book "yo" "yo"]
server = getBooks :<|> serveDirectory "static"
main = Network.Wai.Handler.Warp.run 3000 (serve bookApi $ server)

Client.hs

getAllBooks :: BaseUrl -> EitherT String IO [Book]
(getAllBooks :<|> raw) = client myApi



main = runEitherT $ do
  case parseBaseUrl "http://test.arianvp.me" of
    Left s -> liftIO $ print s
    Right u -> do
      books <- getAllBooks u
      liftIO . appendToBody . fromString . show $ books