{-# LANGUAGE OverloadedStrings #-} {- | This module defines better resource routing for Scotty. Scotty is defined in terms of "routes", whereas HTTP is defined in terms of "resources". This package adds a "resource" abstraction to the scotty ecosystem. (note: All examples probably require -XOverloadedStrings) Scotty comes out of the box with a way to model "routes". The problem is that "routes" is not the abstraction used by the HTTP standard and it can sometimes be tricky to write a perfectly correct HTTP service using the routes model (where "correct" is judged against rfc-2616). The most blatant, (and who knows, maybe the only) example of this problem is shown by the scotty code: > import Web.Scotty.Trans (get, scottyT, text) > > ... > > scottyT 8080 id $ do > get "/hello" $ do > text "world" If a client requests something like @DELETE /hello@, this scotty application will return @404 Not Found@, which conflicts with section 5.1.1 of rfc-2616. A better response would be @405 Method Not Allowed@, and it would include an automatically generated @Allow@ response header. This library gives users a way to model "resources" which is closer to the abstractions used in the HTTP standard. We can re-write the above example like this: > import Web.Scotty.Trans (scottyT, text) > import Web.Scotty.Resource.Trans (resource, get) > > ... > > scottyT 8080 id $ do > resource "/hello" $ do > get $ do > text "world" Given a request: > DELETE /hello HTTP/1.1 > Host: localhost:8080 The resource-based scotty application will produce something like: > HTTP/1.1 405 Method Not Allowed > Allow: GET Each resource is described by a `WebResource` value, which happens to be a `Monad`. The only reason `WebResource` implements `Monad` to fit in with the do-notation coding style of `ScottyT`. This is an abuse of `Monad`, but, you know, whatever. The `Monoid` typeclass more correctly represents what a `WebResource` really is. The `Monad` and `Monoid` typeclasses are used to compose instances of `WebResource`. Here is another more complex example, with multiple resources. > import Data.Aeson (decode) > import Network.HTTP.Types (notFound404, badRequest400, noContent204) > import Web.Scotty.Resource.Trans (resource, get, post) > import Web.Scotty.Trans (scottyT, text, body, raw, status, param) > > import MyApplication (lookupPerson, storePerson) > > ... > > scottyT 8080 id $ do > > -- an "echo" resource > resource "/echo" $ do > get $ do > text "hello world" > post $ do > -- echo the request body back to the user > raw =<< body > > -- A resource that represents a kind of a RESTful database of people. > -- This resource supports GET and PUT. > resource "/people/:personId" $ do > get $ do > personId <- param "personId" > maybePerson <- lookupPerson personId > case maybePerson of > Nothing -> > status notFound404 > Just person -> > json person > put $ do > personId <- param "personId" > maybePerson <- decode <$> body > case maybePerson of > Nothing -> do > status badRequest400 > text "Invalid person JSON" > Just person -> > storePerson personId person > status noContent204 -} module Web.Scotty.Resource.Trans ( resource, WebResource, options, get, head, post, put, delete, patch, method ) where import Prelude hiding (head) import Control.Monad (liftM, ap) import Control.Monad.IO.Class (MonadIO) import Data.List (intersperse) import Data.Maybe (fromMaybe) import Data.Monoid ((<>)) import Data.Set (fromList, toList) import Data.Text.Encoding (decodeUtf8) import Data.Text.Lazy (fromStrict) import Network.HTTP.Types (Method, methodNotAllowed405) import Network.Wai (requestMethod) import Web.Scotty.Trans (ActionT, RoutePattern, ScottyT, matchAny, request, ScottyError, setHeader, status) {- | Add a resource whose uri matches the route pattern. -} resource :: (MonadIO m, ScottyError e) => RoutePattern -> WebResource e m () -> ScottyT e m () resource uri (W methods ()) = matchAny uri $ getMethod `liftM` request >>= fromMaybe notAllowed where getMethod = (`lookup` methods) . requestMethod notAllowed = do setHeader "Allow" allowed status methodNotAllowed405 {- Build a list of allowed methods. -} allowed = ( fromStrict . decodeUtf8 . mconcat . intersperse ", " . dedupe . fmap fst ) methods {- | An opaque representation of an http resource. Use `get`, `post`, etc. to create one of these. Use the `Monad` or `Monoid` instances to compose them. -} data WebResource e m a = W [(Method, ActionT e m ())] a instance Functor (WebResource e m) where fmap = liftM instance Applicative (WebResource e m) where pure = return (<*>) = ap instance Monad (WebResource e m) where return = W [] W methods a >>= f = let W newMethods b = f a in W (methods <> newMethods) b instance Monoid (WebResource e m a) where mempty = W mempty undefined mappend (W a _) (W b _) = W (a <> b) undefined {- | Create a `WebResource` that handles OPTIONS requests using the given scotty action. -} options :: ActionT e m () -> WebResource e m () options action = W [("OPTIONS", action)] () {- | Create a `WebResource` that handles GET requests using the given scotty action. -} get :: ActionT e m () -> WebResource e m () get action = W [("GET", action)] () {- | Create a `WebResource` that handles HEAD requests using the given scotty action. -} head :: ActionT e m () -> WebResource e m () head action = W [("HEAD", action)] () {- | Create a `WebResource` that handles POST requests using the given scotty action. -} post :: ActionT e m () -> WebResource e m () post action = W [("POST", action)] () {- | Create a `WebResource` that handles PUT requests using the given scotty action. -} put :: ActionT e m () -> WebResource e m () put action = W [("PUT", action)] () {- | Create a `WebResource` that handles DELETE requests using the given scotty action. -} delete :: ActionT e m () -> WebResource e m () delete action = W [("DELETE", action)] () {- | Create a `WebResource` that handles PATCH requests using the given scotty action. -} patch :: ActionT e m () -> WebResource e m () patch action = W [("PATCH", action)] () {- | Create a `WebResource` that handles the specific method using the given scotty action. -} method :: Method -> ActionT e m () -> WebResource e m () method m action = W [(m, action)] () {- | A helper function that removes duplicates from a list. -} dedupe :: (Ord a) => [a] -> [a] dedupe = toList . fromList