welshy (empty) → 0.1.0.0
raw patch · 9 files changed
+719/−0 lines, 9 filesdep +aesondep +basedep +blaze-buildersetup-changed
Dependencies added: aeson, base, blaze-builder, bytestring, conduit, http-types, lifted-base, resourcet, text, transformers, unordered-containers, wai, warp
Files
- LICENSE +21/−0
- Setup.hs +2/−0
- Web/Welshy.hs +159/−0
- Web/Welshy/Action.hs +149/−0
- Web/Welshy/FromText.hs +50/−0
- Web/Welshy/Request.hs +126/−0
- Web/Welshy/Response.hs +96/−0
- example.hs +25/−0
- welshy.cabal +91/−0
+ LICENSE view
@@ -0,0 +1,21 @@++The MIT License (MIT)++Copyright (c) 2013 Michael Schröder++Permission is hereby granted, free of charge, to any person obtaining a copy of+this software and associated documentation files (the "Software"), to deal in+the Software without restriction, including without limitation the rights to+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of+the Software, and to permit persons to whom the Software is furnished to do so,+subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ Web/Welshy.hs view
@@ -0,0 +1,159 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module Web.Welshy+ ( welshy, welshyApp++ -- * Middleware & Routing+ , Welshy, middleware, route, RoutePattern+ , get, post, put, patch, delete, head, options++ -- * Actions+ , Action+ , halt, pass+ , catchIO++ -- ** Request+ , request, body+ , capture, captures+ , queryParam, maybeQueryParam, queryParams+ , jsonParam, maybeJsonParam, jsonParams, jsonData+ , bearerAuth++ -- ** Response+ , status, header+ , text, text'+ , html, html'+ , json+ , file, filePart+ , source++ -- * Parameter Parsing+ , Param+ , FromText(..), maybeFromText+ ) where++import Control.Applicative+import Control.Exception+import qualified Control.Exception.Lifted as Lifted+import Control.Monad+import Control.Monad.IO.Class+import Control.Monad.Trans.Writer hiding (pass)+import Data.Monoid+import Data.Text (Text)+import qualified Data.Text as T+import Network.HTTP.Types+import Network.Wai+import Network.Wai.Handler.Warp+import System.IO++import Prelude hiding (head)++import Web.Welshy.Action+import Web.Welshy.FromText+import Web.Welshy.Request+import Web.Welshy.Response++-----------------------------------------------------------------------++-- Note [Exception handling]+-- ~~~~~~~~~~~~~~~~~~~~~~~~~+-- Ideally, all exceptions would be caught by the server and a 500 response+-- sent to the client. Alas, due to lazyness this is not possible: any+-- exceptions occuring inside a 'ResponseBuilder' would need to be caught on+-- a higher level. ('Warp' provides 'settingsOnException', but at that point+-- the connection has already been dropped and we can't send a response to+-- the client anymore.)+--+-- Effectively, this means that if something like the following happens+-- the client will simply get an empty reply:+--+-- > text $ error "wat"+--+-- (The exception will be logged to stderr though.)++-- TODO: clarify this in user-visible documentation.++-----------------------------------------------------------------------++-- | We use this monad to compose WAI 'Middleware', using the 'middleware'+-- and 'route' functions.+newtype Welshy a = Welshy (Writer [Middleware] a)+ deriving (Functor, Applicative, Monad)++-- | Run a Welshy app using the Warp server.+welshy :: Port -> Welshy () -> IO ()+welshy p w = do+ putStr "Aye, Dwi iawn 'n feddw!"+ putStrLn $ " (port " ++ show p ++ ") (ctrl-c to quit)"+ run p (welshyApp w)++-- | Turns a Welshy app into a WAI 'Application'.+welshyApp :: Welshy () -> Application+welshyApp (Welshy w) = foldr id notFound (catchError : execWriter w)+ where+ notFound :: Application+ notFound _ = return $ ResponseBuilder notFound404 [] mempty++ -- see Note [Exception Handling]+ catchError :: Middleware+ catchError app req = Lifted.catch (app req) $ \e -> do+ liftIO $ hPrint stderr (e :: SomeException)+ return $ ResponseBuilder status500 [] mempty++-----------------------------------------------------------------------++-- | Insert middleware into the app. Note that unlike in Scotty,+-- each middleware is run at the point of insertion.+middleware :: Middleware -> Welshy ()+middleware = Welshy . tell . pure++get :: RoutePattern -> Action () -> Welshy ()+get = route GET++post :: RoutePattern -> Action () -> Welshy ()+post = route POST++put :: RoutePattern -> Action () -> Welshy ()+put = route PUT++patch :: RoutePattern -> Action () -> Welshy ()+patch = route PATCH++delete :: RoutePattern -> Action () -> Welshy ()+delete = route DELETE++head :: RoutePattern -> Action () -> Welshy ()+head = route HEAD++options :: RoutePattern -> Action () -> Welshy ()+options = route OPTIONS++-- | Sinatra-style route pattern. Named parameters are prepended with+-- a colon (e.g. @\"\/users\/:id\"@) and can be accessed with 'capture'.+type RoutePattern = Text++-- | Define a route for an HTTP method and URL pattern that runs the given+-- action. Routes are matched in the order they are defined. If no route+-- matches, a 404 response is returned.+route :: StdMethod -> RoutePattern -> Action () -> Welshy ()+route met pat act = middleware $ \nextApp req ->+ case matchRoute met pat req of+ Nothing -> nextApp req+ Just caps -> execAction act caps nextApp req++matchRoute :: StdMethod -> RoutePattern -> Request -> Maybe [Param]+matchRoute met pat req =+ if Right met == parseMethod (requestMethod req)+ then go (filter (/= T.empty) $ T.split (=='/') pat) (pathInfo req) []+ else Nothing+ where+ go [] [] prs = Just prs+ go [] _ _ = Nothing+ go _ [] _ = Nothing+ go (p:ps) (r:rs) prs+ | p == r = go ps rs prs+ | T.null p = Nothing+ | T.head p == ':' = go ps rs $ (T.tail p, r) : prs+ | otherwise = Nothing
+ Web/Welshy/Action.hs view
@@ -0,0 +1,149 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Web.Welshy.Action where++import Control.Applicative+import Control.Arrow+import Control.Exception+import Control.Monad+import Control.Monad.IO.Class+import Control.Monad.Trans.Class+import Control.Monad.Trans.Resource+import qualified Data.Aeson as A+import qualified Data.ByteString.Lazy as BL+import Data.Conduit.Lazy+import Data.Maybe+import Data.Monoid+import Data.Text (Text)+import Network.HTTP.Types+import Network.Wai++-----------------------------------------------------------------------++data Result a = Ok a Response | Halt (Action ()) | Pass++newtype Action a = Action { runAction :: Env -> Response -> IO (Result a) }++instance Functor Action where+ fmap f m = Action $ \r s -> runAction m r s >>= \case+ Ok a s1 -> return $ Ok (f a) s1+ Halt m1 -> return $ Halt m1+ Pass -> return $ Pass++instance Applicative Action where+ pure = return+ (<*>) = ap++-- TODO: this instance should be highlighted in the documentation+instance Alternative Action where+ empty = mzero+ (<|>) = mplus++instance Monad Action where+ return a = Action $ \_ s -> return $ Ok a s+ m >>= k = Action $ \r s -> runAction m r s >>= \case+ Ok a s1 -> runAction (k a) r s1+ Halt s1 -> return $ Halt s1+ Pass -> return $ Pass++ fail msg = halt $ error msg++instance MonadPlus Action where+ mzero = fail "mzero"+ m `mplus` n = Action $ \r s -> runAction m r s >>= \case+ Ok a s1 -> return $ Ok a s1+ Halt __ -> runAction n r s+ Pass -> runAction n r s++instance MonadIO Action where+ liftIO m = Action $ \_ s -> do+ a <- m+ return $ Ok a s++-- | Like `catch` but with the exception handler and result in 'Action'.+catchIO :: Exception e => IO a -> (e -> Action a) -> Action a+catchIO act h = either h return =<< (liftIO $ try $ act)++-----------------------------------------------------------------------++-- | A route, query or form parameter and its value.+type Param = (Text, Text)++data Env = Env { _captures :: [Param]+ , _queryParams :: [Param]+ , _body :: BL.ByteString+ , _jsonParams :: Maybe A.Object+ , _request :: Request }++mkEnv :: [Param] -> Request -> ResourceT IO Env+mkEnv _captures _request = do+ _body <- BL.fromChunks <$> lazyConsume (requestBody _request)+ let _queryParams = queryText _request ++ formParams _body _request+ _jsonParams = either (const Nothing) Just (A.eitherDecode _body)+ return Env {..}++queryText :: Request -> [Param]+queryText = map (second $ fromMaybe "") . queryToQueryText . queryString++formParams :: BL.ByteString -> Request -> [Param]+formParams b req =+ case lookup hContentType (requestHeaders req) of+ Just "application/x-www-form-urlencoded" ->+ map (second $ fromMaybe "") $ queryToQueryText $+ parseQuery $ BL.toStrict $ b+ _ -> []+++execAction :: Action () -> [Param] -> Middleware+execAction act0 caps nextApp req = run act0 =<< mkEnv caps req+ where+ run :: Action () -> Env -> ResourceT IO Response+ run act env = (lift $ runAction act env okRes) >>= \case+ Ok _ res -> return res+ Halt act' -> run act' env+ Pass -> nextApp req++ okRes :: Response+ okRes = ResponseBuilder ok200 [] mempty++-----------------------------------------------------------------------++-- | Stop running the current action and continue with another one.+-- The other action will live in the same request environment and can access+-- the same route captures, but it will start with a fresh default response.+--+-- This is incredibly useful for error handling. For example:+--+-- > patch "/users/:uid" $ do+-- > uid <- capture "uid"+-- > user <- getUserFromDB uid+-- > `catchIO` (\_ -> halt $ status notFound404)+-- > ...+halt :: Action () -> Action a+halt m = Action $ \_ _ -> return $ Halt m++-- | Stop the current action and continue with the next matching route.+pass :: Action a+pass = Action $ \_ _ -> return Pass++-- | Get the raw WAI 'Request'.+request :: Action Request+request = Action $ \r s -> return $ Ok (_request r) s++-- | Get all query parameters.+queryParams :: Action [Param]+queryParams = Action $ \r s -> return $ Ok (_queryParams r) s++-- | Get all route captures.+captures :: Action [Param]+captures = Action $ \r s -> return $ Ok (_captures r) s++-- | Get the request body.+body :: Action BL.ByteString+body = Action $ \r s -> return $ Ok (_body r) s++-- | Modify the raw WAI 'Response'.+modifyResponse :: (Response -> Response) -> Action ()+modifyResponse f = Action $ \_ s -> return $ Ok () (f s)
+ Web/Welshy/FromText.hs view
@@ -0,0 +1,50 @@+module Web.Welshy.FromText where++import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import Data.Int+import Data.Word+import Text.Read (readEither)++-- | A type that can be converted from a strict 'Text' value.+-- Used for parsing route captures, query parameters, header values, etc.+--+-- Minimal complete definition: 'fromText'.+class FromText a where+ fromText :: Text -> Either String a++ -- | Allows a specialized way of parsing lists of values.+ -- The default definition uses 'fromText' to parse comma-delimited lists.+ fromTextList :: Text -> Either String [a]+ fromTextList = mapM fromText . T.split (== ',')++-- | > maybeFromText = either (const Nothing) Just . fromText+maybeFromText :: FromText a => Text -> Maybe a+maybeFromText = either (const Nothing) Just . fromText++instance FromText a => FromText [a] where+ fromText = fromTextList++instance FromText Char where+ fromText t = case T.unpack t of+ [c] -> Right c+ _ -> Left "fromText Char: no parse"+ fromTextList = Right . T.unpack++instance FromText Text where fromText = Right+instance FromText TL.Text where fromText = Right . TL.fromStrict+instance FromText Int where fromText = readEither . T.unpack+instance FromText Int8 where fromText = readEither . T.unpack+instance FromText Int16 where fromText = readEither . T.unpack+instance FromText Int32 where fromText = readEither . T.unpack+instance FromText Int64 where fromText = readEither . T.unpack+instance FromText Integer where fromText = readEither . T.unpack+instance FromText Word where fromText = readEither . T.unpack+instance FromText Word8 where fromText = readEither . T.unpack+instance FromText Word16 where fromText = readEither . T.unpack+instance FromText Word32 where fromText = readEither . T.unpack+instance FromText Word64 where fromText = readEither . T.unpack+instance FromText Bool where fromText = readEither . T.unpack+instance FromText Double where fromText = readEither . T.unpack+instance FromText Float where fromText = readEither . T.unpack
+ Web/Welshy/Request.hs view
@@ -0,0 +1,126 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module Web.Welshy.Request where++import Control.Applicative+import Control.Monad+import Data.Aeson (FromJSON, fromJSON)+import qualified Data.Aeson as A+import qualified Data.ByteString as B+import qualified Data.HashMap.Strict as HashMap+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Network.HTTP.Types+import Network.Wai++import Web.Welshy.Action+import Web.Welshy.FromText+import Web.Welshy.Response++-----------------------------------------------------------------------++-- | Get a parameter captured by the route pattern.+--+-- * If the parameter does not exist, fails with an error.+--+-- * If the parameter was found but could not be parsed, 'pass' is called.+-- This means routes are typed to a degree.+--+capture :: FromText a => Text -> Action a+capture k = (lookup k <$> captures) >>= \case+ Nothing -> fail ("unknown capture: " ++ T.unpack k)+ Just raw -> case fromText raw of+ Left _ -> pass+ Right v -> return v++-- TODO: provide way to customize default response+-- | Get a query parameter.+--+-- * If the parameter does not exist or could not be parsed,+-- the action halts with HTTP status @400 Bad Request@.+--+queryParam :: FromText a => Text -> Action a+queryParam k = (lookup k <$> queryParams) >>= \case+ Nothing -> halt $ status badRequest400+ Just raw -> case fromText raw of+ Left _ -> halt $ status badRequest400+ Right v -> return v++-- | Like 'queryParam', but returns 'Nothing' if the parameter wasn't found.+--+-- * If the parameter could not be parsed,+-- the action halts with HTTP status @400 BadRequest@+--+maybeQueryParam :: FromText a => Text -> Action (Maybe a)+maybeQueryParam k = (lookup k <$> queryParams) >>= \case+ Nothing -> return Nothing+ Just raw -> case fromText raw of+ Left _ -> halt $ status badRequest400+ Right v -> return (Just v)++-- | Get a JSON parameter.+--+-- * If the request body is not a JSON dictionary,+-- or if the parameter does not exist or could not be parsed,+-- the action halts with HTTP status @400 Bad Request@.+--+jsonParam :: FromJSON a => Text -> Action a+jsonParam k = (HashMap.lookup k <$> jsonParams) >>= \case+ Nothing -> halt $ status badRequest400+ Just raw -> case fromJSON raw of+ A.Error _ -> halt $ status badRequest400+ A.Success v -> return v+++-- | Like 'jsonParam', but returns 'Nothing' if the parameter wasn't found.+--+-- * If the request body is not a JSON dictionary,+-- the action halts with HTTP status @400 Bad Request@.+--+maybeJsonParam :: FromJSON a => Text -> Action (Maybe a)+maybeJsonParam k = (HashMap.lookup k <$> jsonParams) >>= \case+ Nothing -> return Nothing+ Just raw -> case fromJSON raw of+ A.Error _ -> halt $ status badRequest400+ A.Success v -> return (Just v)+++-- | Parse the request body as a JSON object.+--+-- * If the body could not be parsed,+-- the action halts with HTTP status @400 Bad Request@.+--+jsonData :: FromJSON a => Action a+jsonData = A.decode <$> body >>= \case+ Nothing -> halt $ status badRequest400+ Just v -> return v++-- | Get all JSON parameters.+--+-- * If the request body is not a JSON dictionary,+-- the action halts with HTTP status @400 Bad Request@.+--+jsonParams :: Action A.Object+jsonParams = Action $ \r s -> do+ case _jsonParams r of+ Nothing -> return $ Halt $ status badRequest400+ Just v -> return $ Ok v s++-----------------------------------------------------------------------++-- | Get the bearer token from an authorization header using the @Bearer@+-- authentication scheme (RFC 6750).+--+-- If the request does not have a (syntactically) valid authorization+-- header for the Bearer scheme, the action halts with HTTP status+-- @401 Unauthorized@.+bearerAuth :: FromText a => Action a+bearerAuth = do+ headers <- requestHeaders <$> request+ maybe (halt $ status unauthorized401) return $ do+ credentials <- lookup hAuthorization headers+ let (scheme, raw) = B.splitAt 7 credentials+ guard (scheme == "Bearer ")+ maybeFromText $ T.decodeUtf8 raw
+ Web/Welshy/Response.hs view
@@ -0,0 +1,96 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module Web.Welshy.Response where++import Blaze.ByteString.Builder+import Data.Aeson (ToJSON)+import qualified Data.Aeson as A+import Data.ByteString (ByteString)+import Data.Conduit+import Data.Text (Text)+import qualified Data.Text.Encoding as T+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Encoding as TL+import Network.HTTP.Types+import Network.Wai++import Web.Welshy.Action++-----------------------------------------------------------------------++-- | Set the HTTP status of the response. The default is 'ok200'.+status :: Status -> Action ()+status s = modifyResponse $ \case+ (ResponseBuilder _ h b) -> ResponseBuilder s h b+ (ResponseFile _ h f fp) -> ResponseFile s h f fp+ (ResponseSource _ h cs) -> ResponseSource s h cs++-- | Add or replace one of the response headers.+header :: HeaderName -> ByteString -> Action ()+header k v = modifyResponse $ \case+ (ResponseBuilder s h b) -> ResponseBuilder s (update h) b+ (ResponseFile s h f fp) -> ResponseFile s (update h) f fp+ (ResponseSource s h cs) -> ResponseSource s (update h) cs+ where update h = (k,v) : filter ((/= k) . fst) h++-- | Set the response body to the given lazy 'TL.Text'+-- and the content-type to @text/plain@.+text :: TL.Text -> Action ()+text t = do+ header hContentType "text/plain"+ _builder $ fromLazyByteString $ TL.encodeUtf8 t++-- | Like 'text' but with a strict 'Text' value.+text' :: Text -> Action ()+text' t = do+ header hContentType "text/plain"+ _builder $ fromByteString $ T.encodeUtf8 t++-- | Set the response body to the given lazy 'TL.Text'+-- and the content-type to @text/html@.+html :: TL.Text -> Action ()+html t = do+ header hContentType "text/html"+ _builder $ fromLazyByteString $ TL.encodeUtf8 t++-- | Like 'html' but with a strict 'Text' value.+html' :: Text -> Action ()+html' t = do+ header hContentType "text/html"+ _builder $ fromByteString $ T.encodeUtf8 t++-- | Set the response body to the JSON encoding of the given value+-- and the content-type to @application/json@.+json :: ToJSON a => a -> Action ()+json v = do+ header hContentType "application/json"+ _builder $ fromLazyByteString $ A.encode v++-- | Sends the given file as the response.+file :: FilePath -> Action ()+file = flip _file Nothing++-- | 'filePart' @f offset byteCount@ sends @byteCount@ bytes of the file @f@,+-- beginning at @offset@, as the response.+filePart :: FilePath -> Integer -> Integer -> Action ()+filePart f offset byteCount = _file f (Just $ FilePart offset byteCount)++_file :: FilePath -> Maybe FilePart -> Action ()+_file f part = modifyResponse $ \case+ (ResponseBuilder s h _) -> ResponseFile s h f part+ (ResponseFile s h _ _) -> ResponseFile s h f part+ (ResponseSource s h _) -> ResponseFile s h f part++_builder :: Builder -> Action ()+_builder b = modifyResponse $ \case+ (ResponseBuilder s h _) -> ResponseBuilder s h b+ (ResponseFile s h _ _) -> ResponseBuilder s h b+ (ResponseSource s h _) -> ResponseBuilder s h b++-- | Set the response body to the given 'Source'.+source :: Source (ResourceT IO) (Flush Builder) -> Action ()+source src = modifyResponse $ \case+ (ResponseBuilder s h _) -> ResponseSource s h src+ (ResponseFile s h _ _) -> ResponseSource s h src+ (ResponseSource s h _) -> ResponseSource s h src
+ example.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE OverloadedStrings #-}++import Control.Applicative+import Control.Monad+import qualified Data.Text.Lazy as T+import Network.HTTP.Types+import Web.Welshy++fibs :: [Int]+fibs = 0 : 1 : zipWith (+) fibs (tail fibs)++main :: IO ()+main = welshy 3000 $ do+ get "/fibs" $ do+ offset <- queryParam "offset" <|> return 0+ length <- queryParam "length"++ when (offset < 0 || length < 0)+ (halt $ status badRequest400)++ when (offset + length > 1000)+ (halt $ status requestedRangeNotSatisfiable416)++ let result = take length $ drop offset fibs+ text $ T.pack $ show result
+ welshy.cabal view
@@ -0,0 +1,91 @@+name: welshy+version: 0.1.0.0+synopsis: Haskell web framework (because Scotty had trouble yodeling)+description:+ A Haskell web framework heavily influenced by the excellent Scotty, + which was in turn influenced by Ruby's Sinatra.+ .+ Welshy strives to make it easier to do error handling without overly+ complicating the control flow. An example:+ .+ @{-# LANGUAGE OverloadedStrings #-}@+ .+ > import Control.Applicative+ > import Control.Monad+ > import qualified Data.Text.Lazy as T+ > import Network.HTTP.Types+ > import Web.Welshy+ > + > fibs :: [Int]+ > fibs = 0 : 1 : zipWith (+) fibs (tail fibs)+ >+ > main :: IO ()+ > main = welshy 3000 $ do+ > get "/fibs" $ do+ > offset <- queryParam "offset" <|> return 0+ > length <- queryParam "length"+ > + > when (offset < 0 || length < 0)+ > (halt $ status badRequest400)+ > + > when (offset + length > 1000)+ > (halt $ status requestedRangeNotSatisfiable416)+ > + > let result = take length $ drop offset fibs+ > text $ T.pack $ show result+ .+ Some of the features demonstrated here:+ .+ * You can 'halt' the current action at any point and continue+ with a different one.+ .+ * Functions like 'queryParam' and 'jsonParam' have built-in error handling.+ .+ * Welshy's 'Action' monad is an instance of 'Alternative'.++++license: MIT+license-file: LICENSE+author: Michael Schröder+maintainer: mcschroeder@gmail.com+homepage: https://github.com/mcschroeder/welshy+bug-reports: https://github.com/mcschroeder/welshy/issues+copyright: (c) 2013 Michael Schröder+category: Web+build-type: Simple+cabal-version: >=1.8++extra-source-files:+ example.hs++library+ exposed-modules: Web.Welshy+ other-modules: Web.Welshy.Action,+ Web.Welshy.FromText,+ Web.Welshy.Request,+ Web.Welshy.Response+ build-depends:+ base ==4.6.*+ , aeson ==0.6.*+ , blaze-builder ==0.3.*+ , bytestring ==0.10.*+ , conduit ==1.0.*+ , http-types ==0.8.*+ , lifted-base ==0.2.*+ , resourcet ==0.4.*+ , text ==0.11.*+ , transformers ==0.3.*+ , unordered-containers ==0.2.*+ , wai ==1.4.*+ , warp ==1.3.* + + extensions:+ GeneralizedNewtypeDeriving+ -- LambdaCase+ OverloadedStrings+ RecordWildCards++source-repository head+ type: git+ location: https://github.com/mcschroeder/welshy.git