Safe Haskell | None |
---|---|
Language | Haskell2010 |
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
- startServer :: [Handler] -> IO ()
- data Handler
- data Method
- simpleHandler :: Method -> String -> (Request -> Response) -> Handler
- data Request
- requestBody :: Request -> String
- decodeJson :: FromJSON a => String -> Either String a
- data Response
- stringResponse :: String -> Response
- jsonResponse :: ToJSON a => a -> Response
- failureResponse :: String -> Response
- effectfulHandler :: Method -> String -> (Request -> IO Response) -> Handler
- data StatefulHandler state
- statefulHandler :: Method -> String -> (state -> Request -> (state, Response)) -> StatefulHandler state
- handlersWithState :: state -> [StatefulHandler state] -> Handler
- type ToJSON a = ToJSON a
- type FromJSON a = FromJSON a
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!
An Handler
is something that can handle HTTP requests.
You can create handlers with these functions:
HTTP Method.
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
HTTP Request.
Whenever you want to inspect the content of a Request
, use requestBody
.
requestBody :: Request -> String Source #
Extract the request body as a String
.
This is the raw request body, no parsing happens at this stage.
Response
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 #
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 StatefulHandler
s 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)