zero-bullshit-0.1.0.0: A simple and mildly useful webserver library.

Safe HaskellNone
LanguageHaskell2010

Zero.Server

Contents

Description

A very simple webserver library inspired by express.js, sinatra and the likes.

Use this library to complete the Zero Bullshit Haskell exercises.

If you're unsure on how to proceed, read more on how to setup your Local dev environment to complete the exercises.

Synopsis

Server

startServer :: [Handler] -> IO () Source #

Start the server on port 7879.

As an example, this is a server that exposes /hello and /ping routes.

helloHandler :: Handler
helloHandler
  = simpleHandler GET "/hello" (\req -> stringResponse "hello")

pingHandler :: Handler
pingHandler
  = simpleHandler GET "/ping" (\req -> stringResponse "pong")

main :: IO ()
main
  = startServer [ helloHandler, pingHandler ]
>>> curl localhost:7879/hello
hello
>>> curl localhost:7879/ping
pong

The server will listen on port 7879. If you're following along with the exercises, the integration tests expect to find a server running on that port. In other words, you are good to go!

data Handler Source #

An Handler is something that can handle HTTP requests. You can create handlers with these functions:

data Method Source #

HTTP Method.

Constructors

GET 
POST 
PUT 
PATCH 
DELETE 
Instances
Eq Method Source # 
Instance details

Defined in Zero.Server

Methods

(==) :: Method -> Method -> Bool #

(/=) :: Method -> Method -> Bool #

Show Method Source # 
Instance details

Defined in Zero.Server

simpleHandler :: Method -> String -> (Request -> Response) -> Handler Source #

Most basic HTTP handler.

With a simpleHandler you can turn a Request into a Response, but you're not allowed to use any side effects or maintain any state across requests.

handleRequest :: Request -> Response
handleRequest req
  = stringResponse "hello"

helloHandler :: Handler
helloHandler
  = simpleHandler GET "/hello" handleRequest

Request

data Request Source #

HTTP Request.

Whenever you want to inspect the content of a Request, use requestBody.

Instances
Eq Request Source # 
Instance details

Defined in Zero.Server

Methods

(==) :: Request -> Request -> Bool #

(/=) :: Request -> Request -> Bool #

Show Request Source # 
Instance details

Defined in Zero.Server

requestBody :: Request -> String Source #

Extract the request body as a String. This is the raw request body, no parsing happens at this stage.

decodeJson :: FromJSON a => String -> Either String a Source #

Given a String, either succesfully parse it to a type a or return an error (as a String).

Read the documentation for FromJSON for a practical example.

Response

data Response Source #

HTTP Response.

Note you cannot create values of this type directly. You'll need something like stringResponse, jsonResponse or failureResponse.

isBobHandler :: Request -> Response
isBobHandler req
  = if requestBody req == "Bob"
      then stringResponse "It's definitely Bob."
      else failureResponse "WOAH, not Bob. Be careful."

main :: IO ()
main
  = startServer
      [ simpleHandler POST "/is-bob" isBobHandler ]
>>> curl -XPOST localhost:7879/is-bob -d "Bob"
It's definitely Bob.

stringResponse :: String -> Response Source #

Create a Response with some raw value (just a plain String).

jsonResponse :: ToJSON a => a -> Response Source #

Create a Response with some JSON value. It helps to read this signature as:

If you give me something that can be serialized to JSON,
I'll give you back a response with a JSON serialized body.

As an example, magicNumbers of type [Int] can be serialized to JSON, because both the List type and the Int type can be turned into JSON.

magicNumbers :: [Int]
magicNumbers = [1, 5, 92, 108]

numbersHandler :: Request -> Response
numbersHandler req
  = jsonResponse magicNumbers

failureResponse :: String -> Response Source #

Create a Response with an error and set the status code to 400.

State and side effects

effectfulHandler :: Method -> String -> (Request -> IO Response) -> Handler Source #

An handler that allows side effects (note the IO in IO Response). Unlike a simpleHandler, you can now have IO operations.

For example, you might want to query a database or make an HTTP request to some webservice and use the result in the Response body.

data StatefulHandler state Source #

A data type to describe stateful handlers.

Note that startServer only accepts Handler values, so you'll have to find a way to turn a StatefulHandler into an Handler (read up on handlersWithState as it does exactly that).

statefulHandler :: Method -> String -> (state -> Request -> (state, Response)) -> StatefulHandler state Source #

A StatefulHandler allows you to keep some state around across requests. For example, if you want to implement a counter, you could keep the current tally as state, and increase it everytime a Request comes in.

The tricky bit is understanding this callback (state -> Request -> (state, Response)).

Compare it with the simpler Request -> Response. The difference is that you get the current state as a parameter, and you no longer return just the Response, but an updated version of the state as well.

handlersWithState :: state -> [StatefulHandler state] -> Handler Source #

Once you have some StatefulHandlers that share the same state type (that's important!), you can create a proper Handler to be used in your server definition.

In fact, you cannot use StatefulHandler directly in startServer, as it only accepts values of type Handler.

What's the first parameter state you ask? Well, it's the initial state! The server needs an initial value to pass along the first Request, how else would it be able to come up with some state (especially given that it knows nothing about what state _is_, it could be anything! Yay, polymorphysm).

JSON encoding/decoding

type ToJSON a = ToJSON a Source #

How do I send a JSON response?

Your type needs a ToJSON instance, which you can derive automatically. (That's why you need the Generic thing, but feel free to ignore, it's not important)

import GHC.Generics (Generic)
import qualified Zero.Server as Server

data Person
  = Person { name :: String, age :: Int }
  deriving (Generic, Server.ToJSON)

Then you want to use jsonResponse to produce a Response that contains the JSON representation of your type. Note that encoding to JSON cannot fail, while parsing from JSON could potentially fail if the JSON input is malformed.

myHandler :: Request -> Response
myHandler req
  = jsonResponse p
  where
    p = Person "bob" 69

type FromJSON a = FromJSON a Source #

How do I turn a JSON value into a value of type a?

Your type needs a FromJSON instance, which you can derive automatically.

(That's why you need the Generic thing, but feel free to ignore, it's not important)

import GHC.Generics (Generic)
import qualified Zero.Server as Server

data Person
  = Person { name :: String, age :: Int }
  deriving (Generic, Server.FromJSON)

Then you want to use decodeJson to either get an error (when the JSON is invalid) or a value of type Person.

myHandler :: Request -> Response
myHandler req
  = stringResponse result
  where
    body
      = requestBody req
    result
      = case decodeJson body of
          Left err -> "Failed to decode request body as a Person. It must be something else"
          Right p  -> "Yay! We have a person named: " <> (name p)