scotty-0.12/0000755000000000000000000000000007346545000011152 5ustar0000000000000000scotty-0.12/LICENSE0000644000000000000000000000276707346545000012173 0ustar0000000000000000Copyright (c) 2012-2017 Andrew Farmer All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Andrew Farmer nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. scotty-0.12/README.md0000755000000000000000000000237707346545000012445 0ustar0000000000000000# Scotty [![Build Status](https://travis-ci.org/scotty-web/scotty.svg)](https://travis-ci.org/scotty-web/scotty) A Haskell web framework inspired by Ruby's Sinatra, using WAI and Warp. ```haskell {-# LANGUAGE OverloadedStrings #-} import Web.Scotty import Data.Monoid (mconcat) main = scotty 3000 $ get "/:word" $ do beam <- param "word" html $ mconcat ["

Scotty, ", beam, " me up!

"] ``` Scotty is the cheap and cheerful way to write RESTful, declarative web applications. * A page is as simple as defining the verb, URL pattern, and Text content. * It is template-language agnostic. Anything that returns a Text value will do. * Conforms to the [web application interface (WAI)](https://github.com/yesodweb/wai/). * Uses the very fast Warp webserver by default. See examples/basic.hs to see Scotty in action. (basic.hs needs the wai-extra package) ```bash > runghc examples/basic.hs Setting phasers to stun... (port 3000) (ctrl-c to quit) (visit localhost:3000/somepath) ``` As for the name: Sinatra + Warp = Scotty. ### More Information Tutorials and related projects can be found in the [Scotty wiki](https://github.com/scotty-web/scotty/wiki). ### Development & Support Open an issue on GitHub. Copyright (c) 2012-2019 Andrew Farmer scotty-0.12/Setup.hs0000644000000000000000000000005607346545000012607 0ustar0000000000000000import Distribution.Simple main = defaultMain scotty-0.12/Web/0000755000000000000000000000000007346545000011667 5ustar0000000000000000scotty-0.12/Web/Scotty.hs0000644000000000000000000002724507346545000013522 0ustar0000000000000000{-# LANGUAGE OverloadedStrings, RankNTypes #-} -- | It should be noted that most of the code snippets below depend on the -- OverloadedStrings language pragma. -- -- Scotty is set up by default for development mode. For production servers, -- you will likely want to modify 'Trans.settings' and the 'defaultHandler'. See -- the comments on each of these functions for more information. module Web.Scotty ( -- * scotty-to-WAI scotty, scottyApp, scottyOpts, scottySocket, Options(..) -- * Defining Middleware and Routes -- -- | 'Middleware' and routes are run in the order in which they -- are defined. All middleware is run first, followed by the first -- route that matches. If no route matches, a 404 response is given. , middleware, get, post, put, delete, patch, options, addroute, matchAny, notFound -- ** Route Patterns , capture, regex, function, literal -- ** Accessing the Request, Captures, and Query Parameters , request, header, headers, body, bodyReader, param, params, jsonData, files -- ** Modifying the Response and Redirecting , status, addHeader, setHeader, redirect -- ** Setting Response Body -- -- | Note: only one of these should be present in any given route -- definition, as they completely replace the current 'Response' body. , text, html, file, json, stream, raw -- ** Exceptions , raise, raiseStatus, rescue, next, finish, defaultHandler, liftAndCatchIO -- * Parsing Parameters , Param, Trans.Parsable(..), Trans.readEither -- * Types , ScottyM, ActionM, RoutePattern, File ) where -- With the exception of this, everything else better just import types. import qualified Web.Scotty.Trans as Trans import Data.Aeson (FromJSON, ToJSON) import qualified Data.ByteString as BS import Data.ByteString.Lazy.Char8 (ByteString) import Data.Text.Lazy (Text) import Network.HTTP.Types (Status, StdMethod) import Network.Socket (Socket) import Network.Wai (Application, Middleware, Request, StreamingBody) import Network.Wai.Handler.Warp (Port) import Web.Scotty.Internal.Types (ScottyT, ActionT, Param, RoutePattern, Options, File) type ScottyM = ScottyT Text IO type ActionM = ActionT Text IO -- | Run a scotty application using the warp server. scotty :: Port -> ScottyM () -> IO () scotty p = Trans.scottyT p id -- | Run a scotty application using the warp server, passing extra options. scottyOpts :: Options -> ScottyM () -> IO () scottyOpts opts = Trans.scottyOptsT opts id -- | Run a scotty application using the warp server, passing extra options, -- and listening on the provided socket. This allows the user to provide, for -- example, a Unix named socket, which can be used when reverse HTTP proxying -- into your application. scottySocket :: Options -> Socket -> ScottyM () -> IO () scottySocket opts sock = Trans.scottySocketT opts sock id -- | Turn a scotty application into a WAI 'Application', which can be -- run with any WAI handler. scottyApp :: ScottyM () -> IO Application scottyApp = Trans.scottyAppT id -- | Global handler for uncaught exceptions. -- -- Uncaught exceptions normally become 500 responses. -- You can use this to selectively override that behavior. -- -- Note: IO exceptions are lifted into Scotty exceptions by default. -- This has security implications, so you probably want to provide your -- own defaultHandler in production which does not send out the error -- strings as 500 responses. defaultHandler :: (Text -> ActionM ()) -> ScottyM () defaultHandler = Trans.defaultHandler -- | Use given middleware. Middleware is nested such that the first declared -- is the outermost middleware (it has first dibs on the request and last action -- on the response). Every middleware is run on each request. middleware :: Middleware -> ScottyM () middleware = Trans.middleware -- | Throw an exception, which can be caught with 'rescue'. Uncaught exceptions -- turn into HTTP 500 responses. raise :: Text -> ActionM a raise = Trans.raise -- | Throw an exception, which can be caught with 'rescue'. Uncaught exceptions turn into HTTP responses corresponding to the given status. raiseStatus :: Status -> Text -> ActionM a raiseStatus = Trans.raiseStatus -- | Abort execution of this action and continue pattern matching routes. -- Like an exception, any code after 'next' is not executed. -- -- As an example, these two routes overlap. The only way the second one will -- ever run is if the first one calls 'next'. -- -- > get "/foo/:bar" $ do -- > w :: Text <- param "bar" -- > unless (w == "special") next -- > text "You made a request to /foo/special" -- > -- > get "/foo/:baz" $ do -- > w <- param "baz" -- > text $ "You made a request to: " <> w next :: ActionM a next = Trans.next -- | Abort execution of this action. Like an exception, any code after 'finish' -- is not executed. -- -- As an example only requests to @\/foo\/special@ will include in the response -- content the text message. -- -- > get "/foo/:bar" $ do -- > w :: Text <- param "bar" -- > unless (w == "special") finish -- > text "You made a request to /foo/special" -- -- /Since: 0.10.3/ finish :: ActionM a finish = Trans.finish -- | Catch an exception thrown by 'raise'. -- -- > raise "just kidding" `rescue` (\msg -> text msg) rescue :: ActionM a -> (Text -> ActionM a) -> ActionM a rescue = Trans.rescue -- | Like 'liftIO', but catch any IO exceptions and turn them into Scotty exceptions. liftAndCatchIO :: IO a -> ActionM a liftAndCatchIO = Trans.liftAndCatchIO -- | Redirect to given URL. Like throwing an uncatchable exception. Any code after the call to redirect -- will not be run. -- -- > redirect "http://www.google.com" -- -- OR -- -- > redirect "/foo/bar" redirect :: Text -> ActionM a redirect = Trans.redirect -- | Get the 'Request' object. request :: ActionM Request request = Trans.request -- | Get list of uploaded files. files :: ActionM [File] files = Trans.files -- | Get a request header. Header name is case-insensitive. header :: Text -> ActionM (Maybe Text) header = Trans.header -- | Get all the request headers. Header names are case-insensitive. headers :: ActionM [(Text, Text)] headers = Trans.headers -- | Get the request body. body :: ActionM ByteString body = Trans.body -- | Get an IO action that reads body chunks -- -- * This is incompatible with 'body' since 'body' consumes all chunks. bodyReader :: ActionM (IO BS.ByteString) bodyReader = Trans.bodyReader -- | Parse the request body as a JSON object and return it. Raises an exception if parse is unsuccessful. jsonData :: FromJSON a => ActionM a jsonData = Trans.jsonData -- | Get a parameter. First looks in captures, then form data, then query parameters. -- -- * Raises an exception which can be caught by 'rescue' if parameter is not found. -- -- * If parameter is found, but 'read' fails to parse to the correct type, 'next' is called. -- This means captures are somewhat typed, in that a route won't match if a correctly typed -- capture cannot be parsed. param :: Trans.Parsable a => Text -> ActionM a param = Trans.param -- | Get all parameters from capture, form and query (in that order). params :: ActionM [Param] params = Trans.params -- | Set the HTTP response status. Default is 200. status :: Status -> ActionM () status = Trans.status -- | Add to the response headers. Header names are case-insensitive. addHeader :: Text -> Text -> ActionM () addHeader = Trans.addHeader -- | Set one of the response headers. Will override any previously set value for that header. -- Header names are case-insensitive. setHeader :: Text -> Text -> ActionM () setHeader = Trans.setHeader -- | Set the body of the response to the given 'Text' value. Also sets \"Content-Type\" -- header to \"text/plain; charset=utf-8\" if it has not already been set. text :: Text -> ActionM () text = Trans.text -- | Set the body of the response to the given 'Text' value. Also sets \"Content-Type\" -- header to \"text/html; charset=utf-8\" if it has not already been set. html :: Text -> ActionM () html = Trans.html -- | Send a file as the response. Doesn't set the \"Content-Type\" header, so you probably -- want to do that on your own with 'setHeader'. file :: FilePath -> ActionM () file = Trans.file -- | Set the body of the response to the JSON encoding of the given value. Also sets \"Content-Type\" -- header to \"application/json; charset=utf-8\" if it has not already been set. json :: ToJSON a => a -> ActionM () json = Trans.json -- | Set the body of the response to a StreamingBody. Doesn't set the -- \"Content-Type\" header, so you probably want to do that on your -- own with 'setHeader'. stream :: StreamingBody -> ActionM () stream = Trans.stream -- | Set the body of the response to the given 'BL.ByteString' value. Doesn't set the -- \"Content-Type\" header, so you probably want to do that on your own with 'setHeader'. raw :: ByteString -> ActionM () raw = Trans.raw -- | get = 'addroute' 'GET' get :: RoutePattern -> ActionM () -> ScottyM () get = Trans.get -- | post = 'addroute' 'POST' post :: RoutePattern -> ActionM () -> ScottyM () post = Trans.post -- | put = 'addroute' 'PUT' put :: RoutePattern -> ActionM () -> ScottyM () put = Trans.put -- | delete = 'addroute' 'DELETE' delete :: RoutePattern -> ActionM () -> ScottyM () delete = Trans.delete -- | patch = 'addroute' 'PATCH' patch :: RoutePattern -> ActionM () -> ScottyM () patch = Trans.patch -- | options = 'addroute' 'OPTIONS' options :: RoutePattern -> ActionM () -> ScottyM () options = Trans.options -- | Add a route that matches regardless of the HTTP verb. matchAny :: RoutePattern -> ActionM () -> ScottyM () matchAny = Trans.matchAny -- | Specify an action to take if nothing else is found. Note: this _always_ matches, -- so should generally be the last route specified. notFound :: ActionM () -> ScottyM () notFound = Trans.notFound -- | Define a route with a 'StdMethod', 'Text' value representing the path spec, -- and a body ('Action') which modifies the response. -- -- > addroute GET "/" $ text "beam me up!" -- -- The path spec can include values starting with a colon, which are interpreted -- as /captures/. These are named wildcards that can be looked up with 'param'. -- -- > addroute GET "/foo/:bar" $ do -- > v <- param "bar" -- > text v -- -- >>> curl http://localhost:3000/foo/something -- something addroute :: StdMethod -> RoutePattern -> ActionM () -> ScottyM () addroute = Trans.addroute -- | Match requests using a regular expression. -- Named captures are not yet supported. -- -- > get (regex "^/f(.*)r$") $ do -- > path <- param "0" -- > cap <- param "1" -- > text $ mconcat ["Path: ", path, "\nCapture: ", cap] -- -- >>> curl http://localhost:3000/foo/bar -- Path: /foo/bar -- Capture: oo/ba -- regex :: String -> RoutePattern regex = Trans.regex -- | Standard Sinatra-style route. Named captures are prepended with colons. -- This is the default route type generated by OverloadedString routes. i.e. -- -- > get (capture "/foo/:bar") $ ... -- -- and -- -- > {-# LANGUAGE OverloadedStrings #-} -- > ... -- > get "/foo/:bar" $ ... -- -- are equivalent. capture :: String -> RoutePattern capture = Trans.capture -- | Build a route based on a function which can match using the entire 'Request' object. -- 'Nothing' indicates the route does not match. A 'Just' value indicates -- a successful match, optionally returning a list of key-value pairs accessible -- by 'param'. -- -- > get (function $ \req -> Just [("version", pack $ show $ httpVersion req)]) $ do -- > v <- param "version" -- > text v -- -- >>> curl http://localhost:3000/ -- HTTP/1.1 -- function :: (Request -> Maybe [Param]) -> RoutePattern function = Trans.function -- | Build a route that requires the requested path match exactly, without captures. literal :: String -> RoutePattern literal = Trans.literal scotty-0.12/Web/Scotty/0000755000000000000000000000000007346545000013154 5ustar0000000000000000scotty-0.12/Web/Scotty/Action.hs0000644000000000000000000003341507346545000014733 0ustar0000000000000000{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} module Web.Scotty.Action ( addHeader , body , bodyReader , file , files , finish , header , headers , html , liftAndCatchIO , json , jsonData , next , param , params , raise , raiseStatus , raw , readEither , redirect , request , rescue , setHeader , status , stream , text , Param , Parsable(..) -- private to Scotty , runAction ) where import Blaze.ByteString.Builder (fromLazyByteString) import qualified Control.Exception as E import Control.Monad.Error.Class import Control.Monad.Reader hiding (mapM) import qualified Control.Monad.State.Strict as MS import Control.Monad.Trans.Except import qualified Data.Aeson as A import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as BL import qualified Data.CaseInsensitive as CI import Data.Default.Class (def) import Data.Int import qualified Data.Text as ST import qualified Data.Text.Encoding as STE import qualified Data.Text.Lazy as T import Data.Text.Lazy.Encoding (encodeUtf8) import Data.Word import Network.HTTP.Types -- not re-exported until version 0.11 #if !MIN_VERSION_http_types(0,11,0) import Network.HTTP.Types.Status #endif import Network.Wai import Numeric.Natural import Prelude () import Prelude.Compat import Web.Scotty.Internal.Types import Web.Scotty.Util -- Nothing indicates route failed (due to Next) and pattern matching should continue. -- Just indicates a successful response. runAction :: (ScottyError e, Monad m) => ErrorHandler e m -> ActionEnv -> ActionT e m () -> m (Maybe Response) runAction h env action = do (e,r) <- flip MS.runStateT def $ flip runReaderT env $ runExceptT $ runAM $ action `catchError` (defH h) return $ either (const Nothing) (const $ Just $ mkResponse r) e -- | Default error handler for all actions. defH :: (ScottyError e, Monad m) => ErrorHandler e m -> ActionError e -> ActionT e m () defH _ (Redirect url) = do status status302 setHeader "Location" url defH Nothing (ActionError s e) = do status s let code = T.pack $ show $ statusCode s let msg = T.fromStrict $ STE.decodeUtf8 $ statusMessage s html $ mconcat ["

", code, " ", msg, "

", showError e] defH h@(Just f) (ActionError _ e) = f e `catchError` (defH h) -- so handlers can throw exceptions themselves defH _ Next = next defH _ Finish = return () -- | Throw an exception, which can be caught with 'rescue'. Uncaught exceptions -- turn into HTTP 500 responses. raise :: (ScottyError e, Monad m) => e -> ActionT e m a raise = raiseStatus status500 -- | Throw an exception, which can be caught with 'rescue'. Uncaught exceptions turn into HTTP responses corresponding to the given status. raiseStatus :: (ScottyError e, Monad m) => Status -> e -> ActionT e m a raiseStatus s = throwError . ActionError s -- | Abort execution of this action and continue pattern matching routes. -- Like an exception, any code after 'next' is not executed. -- -- As an example, these two routes overlap. The only way the second one will -- ever run is if the first one calls 'next'. -- -- > get "/foo/:bar" $ do -- > w :: Text <- param "bar" -- > unless (w == "special") next -- > text "You made a request to /foo/special" -- > -- > get "/foo/:baz" $ do -- > w <- param "baz" -- > text $ "You made a request to: " <> w next :: (ScottyError e, Monad m) => ActionT e m a next = throwError Next -- | Catch an exception thrown by 'raise'. -- -- > raise "just kidding" `rescue` (\msg -> text msg) rescue :: (ScottyError e, Monad m) => ActionT e m a -> (e -> ActionT e m a) -> ActionT e m a rescue action h = catchError action $ \e -> case e of ActionError _ err -> h err -- handle errors other -> throwError other -- rethrow internal error types -- | Like 'liftIO', but catch any IO exceptions and turn them into 'ScottyError's. liftAndCatchIO :: (ScottyError e, MonadIO m) => IO a -> ActionT e m a liftAndCatchIO io = ActionT $ do r <- liftIO $ liftM Right io `E.catch` (\ e -> return $ Left $ stringError $ show (e :: E.SomeException)) either throwError return r -- | Redirect to given URL. Like throwing an uncatchable exception. Any code after the call to redirect -- will not be run. -- -- > redirect "http://www.google.com" -- -- OR -- -- > redirect "/foo/bar" redirect :: (ScottyError e, Monad m) => T.Text -> ActionT e m a redirect = throwError . Redirect -- | Finish the execution of the current action. Like throwing an uncatchable -- exception. Any code after the call to finish will not be run. -- -- /Since: 0.10.3/ finish :: (ScottyError e, Monad m) => ActionT e m a finish = throwError Finish -- | Get the 'Request' object. request :: Monad m => ActionT e m Request request = ActionT $ liftM getReq ask -- | Get list of uploaded files. files :: Monad m => ActionT e m [File] files = ActionT $ liftM getFiles ask -- | Get a request header. Header name is case-insensitive. header :: (ScottyError e, Monad m) => T.Text -> ActionT e m (Maybe T.Text) header k = do hs <- liftM requestHeaders request return $ fmap strictByteStringToLazyText $ lookup (CI.mk (lazyTextToStrictByteString k)) hs -- | Get all the request headers. Header names are case-insensitive. headers :: (ScottyError e, Monad m) => ActionT e m [(T.Text, T.Text)] headers = do hs <- liftM requestHeaders request return [ ( strictByteStringToLazyText (CI.original k) , strictByteStringToLazyText v) | (k,v) <- hs ] -- | Get the request body. body :: (ScottyError e, MonadIO m) => ActionT e m BL.ByteString body = ActionT ask >>= (liftIO . getBody) -- | Get an IO action that reads body chunks -- -- * This is incompatible with 'body' since 'body' consumes all chunks. bodyReader :: Monad m => ActionT e m (IO B.ByteString) bodyReader = ActionT $ getBodyChunk `liftM` ask -- | Parse the request body as a JSON object and return it. -- -- If the JSON object is malformed, this sets the status to -- 400 Bad Request, and throws an exception. -- -- If the JSON fails to parse, this sets the status to -- 422 Unprocessable Entity. -- -- These status codes are as per https://www.restapitutorial.com/httpstatuscodes.html. jsonData :: (A.FromJSON a, ScottyError e, MonadIO m) => ActionT e m a jsonData = do b <- body when (b == "") $ do let htmlError = "jsonData - No data was provided." raiseStatus status400 $ stringError htmlError case A.eitherDecode b of Left err -> do let htmlError = "jsonData - malformed." `mappend` " Data was: " `mappend` BL.unpack b `mappend` " Error was: " `mappend` err raiseStatus status400 $ stringError htmlError Right value -> case A.fromJSON value of A.Error err -> do let htmlError = "jsonData - failed parse." `mappend` " Data was: " `mappend` BL.unpack b `mappend` "." `mappend` " Error was: " `mappend` err raiseStatus status422 $ stringError htmlError A.Success a -> do return a -- | Get a parameter. First looks in captures, then form data, then query parameters. -- -- * Raises an exception which can be caught by 'rescue' if parameter is not found. -- -- * If parameter is found, but 'read' fails to parse to the correct type, 'next' is called. -- This means captures are somewhat typed, in that a route won't match if a correctly typed -- capture cannot be parsed. param :: (Parsable a, ScottyError e, Monad m) => T.Text -> ActionT e m a param k = do val <- ActionT $ liftM (lookup k . getParams) ask case val of Nothing -> raise $ stringError $ "Param: " ++ T.unpack k ++ " not found!" Just v -> either (const next) return $ parseParam v -- | Get all parameters from capture, form and query (in that order). params :: Monad m => ActionT e m [Param] params = ActionT $ liftM getParams ask -- | Minimum implemention: 'parseParam' class Parsable a where -- | Take a 'T.Text' value and parse it as 'a', or fail with a message. parseParam :: T.Text -> Either T.Text a -- | Default implementation parses comma-delimited lists. -- -- > parseParamList t = mapM parseParam (T.split (== ',') t) parseParamList :: T.Text -> Either T.Text [a] parseParamList t = mapM parseParam (T.split (== ',') t) -- No point using 'read' for Text, ByteString, Char, and String. instance Parsable T.Text where parseParam = Right instance Parsable ST.Text where parseParam = Right . T.toStrict instance Parsable B.ByteString where parseParam = Right . lazyTextToStrictByteString instance Parsable BL.ByteString where parseParam = Right . encodeUtf8 -- | Overrides default 'parseParamList' to parse String. instance Parsable Char where parseParam t = case T.unpack t of [c] -> Right c _ -> Left "parseParam Char: no parse" parseParamList = Right . T.unpack -- String -- | Checks if parameter is present and is null-valued, not a literal '()'. -- If the URI requested is: '/foo?bar=()&baz' then 'baz' will parse as (), where 'bar' will not. instance Parsable () where parseParam t = if T.null t then Right () else Left "parseParam Unit: no parse" instance (Parsable a) => Parsable [a] where parseParam = parseParamList instance Parsable Bool where parseParam t = if t' == T.toCaseFold "true" then Right True else if t' == T.toCaseFold "false" then Right False else Left "parseParam Bool: no parse" where t' = T.toCaseFold t instance Parsable Double where parseParam = readEither instance Parsable Float where parseParam = readEither instance Parsable Int where parseParam = readEither instance Parsable Int8 where parseParam = readEither instance Parsable Int16 where parseParam = readEither instance Parsable Int32 where parseParam = readEither instance Parsable Int64 where parseParam = readEither instance Parsable Integer where parseParam = readEither instance Parsable Word where parseParam = readEither instance Parsable Word8 where parseParam = readEither instance Parsable Word16 where parseParam = readEither instance Parsable Word32 where parseParam = readEither instance Parsable Word64 where parseParam = readEither instance Parsable Natural where parseParam = readEither -- | Useful for creating 'Parsable' instances for things that already implement 'Read'. Ex: -- -- > instance Parsable Int where parseParam = readEither readEither :: Read a => T.Text -> Either T.Text a readEither t = case [ x | (x,"") <- reads (T.unpack t) ] of [x] -> Right x [] -> Left "readEither: no parse" _ -> Left "readEither: ambiguous parse" -- | Set the HTTP response status. Default is 200. status :: Monad m => Status -> ActionT e m () status = ActionT . MS.modify . setStatus -- Not exported, but useful in the functions below. changeHeader :: Monad m => (CI.CI B.ByteString -> B.ByteString -> [(HeaderName, B.ByteString)] -> [(HeaderName, B.ByteString)]) -> T.Text -> T.Text -> ActionT e m () changeHeader f k = ActionT . MS.modify . setHeaderWith . f (CI.mk $ lazyTextToStrictByteString k) . lazyTextToStrictByteString -- | Add to the response headers. Header names are case-insensitive. addHeader :: Monad m => T.Text -> T.Text -> ActionT e m () addHeader = changeHeader add -- | Set one of the response headers. Will override any previously set value for that header. -- Header names are case-insensitive. setHeader :: Monad m => T.Text -> T.Text -> ActionT e m () setHeader = changeHeader replace -- | Set the body of the response to the given 'T.Text' value. Also sets \"Content-Type\" -- header to \"text/plain; charset=utf-8\" if it has not already been set. text :: (ScottyError e, Monad m) => T.Text -> ActionT e m () text t = do changeHeader addIfNotPresent "Content-Type" "text/plain; charset=utf-8" raw $ encodeUtf8 t -- | Set the body of the response to the given 'T.Text' value. Also sets \"Content-Type\" -- header to \"text/html; charset=utf-8\" if it has not already been set. html :: (ScottyError e, Monad m) => T.Text -> ActionT e m () html t = do changeHeader addIfNotPresent "Content-Type" "text/html; charset=utf-8" raw $ encodeUtf8 t -- | Send a file as the response. Doesn't set the \"Content-Type\" header, so you probably -- want to do that on your own with 'setHeader'. file :: Monad m => FilePath -> ActionT e m () file = ActionT . MS.modify . setContent . ContentFile -- | Set the body of the response to the JSON encoding of the given value. Also sets \"Content-Type\" -- header to \"application/json; charset=utf-8\" if it has not already been set. json :: (A.ToJSON a, ScottyError e, Monad m) => a -> ActionT e m () json v = do changeHeader addIfNotPresent "Content-Type" "application/json; charset=utf-8" raw $ A.encode v -- | Set the body of the response to a Source. Doesn't set the -- \"Content-Type\" header, so you probably want to do that on your -- own with 'setHeader'. stream :: Monad m => StreamingBody -> ActionT e m () stream = ActionT . MS.modify . setContent . ContentStream -- | Set the body of the response to the given 'BL.ByteString' value. Doesn't set the -- \"Content-Type\" header, so you probably want to do that on your -- own with 'setHeader'. raw :: Monad m => BL.ByteString -> ActionT e m () raw = ActionT . MS.modify . setContent . ContentBuilder . fromLazyByteString scotty-0.12/Web/Scotty/Internal/0000755000000000000000000000000007346545000014730 5ustar0000000000000000scotty-0.12/Web/Scotty/Internal/Types.hs0000644000000000000000000002116007346545000016370 0ustar0000000000000000{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} module Web.Scotty.Internal.Types where import Blaze.ByteString.Builder (Builder) import Control.Applicative import qualified Control.Exception as E import Control.Monad.Base (MonadBase, liftBase, liftBaseDefault) import Control.Monad.Catch (MonadCatch, catch, MonadThrow, throwM) import Control.Monad.Error.Class import qualified Control.Monad.Fail as Fail import Control.Monad.Reader import Control.Monad.State.Strict import Control.Monad.Trans.Control (MonadBaseControl, StM, liftBaseWith, restoreM, ComposeSt, defaultLiftBaseWith, defaultRestoreM, MonadTransControl, StT, liftWith, restoreT) import Control.Monad.Trans.Except import qualified Data.ByteString as BS import Data.ByteString.Lazy.Char8 (ByteString) import Data.Default.Class (Default, def) import Data.String (IsString(..)) import Data.Text.Lazy (Text, pack) import Data.Typeable (Typeable) import Network.HTTP.Types import Network.Wai hiding (Middleware, Application) import qualified Network.Wai as Wai import Network.Wai.Handler.Warp (Settings, defaultSettings) import Network.Wai.Parse (FileInfo) import Prelude () import Prelude.Compat --------------------- Options ----------------------- data Options = Options { verbose :: Int -- ^ 0 = silent, 1(def) = startup banner , settings :: Settings -- ^ Warp 'Settings' -- Note: to work around an issue in warp, -- the default FD cache duration is set to 0 -- so changes to static files are always picked -- up. This likely has performance implications, -- so you may want to modify this for production -- servers using `setFdCacheDuration`. } instance Default Options where def = Options 1 defaultSettings ----- Transformer Aware Applications/Middleware ----- type Middleware m = Application m -> Application m type Application m = Request -> m Response --------------- Scotty Applications ----------------- data ScottyState e m = ScottyState { middlewares :: [Wai.Middleware] , routes :: [Middleware m] , handler :: ErrorHandler e m } instance Default (ScottyState e m) where def = ScottyState [] [] Nothing addMiddleware :: Wai.Middleware -> ScottyState e m -> ScottyState e m addMiddleware m s@(ScottyState {middlewares = ms}) = s { middlewares = m:ms } addRoute :: Middleware m -> ScottyState e m -> ScottyState e m addRoute r s@(ScottyState {routes = rs}) = s { routes = r:rs } addHandler :: ErrorHandler e m -> ScottyState e m -> ScottyState e m addHandler h s = s { handler = h } newtype ScottyT e m a = ScottyT { runS :: State (ScottyState e m) a } deriving ( Functor, Applicative, Monad ) ------------------ Scotty Errors -------------------- data ActionError e = Redirect Text | Next | Finish | ActionError Status e -- | In order to use a custom exception type (aside from 'Text'), you must -- define an instance of 'ScottyError' for that type. class ScottyError e where stringError :: String -> e showError :: e -> Text instance ScottyError Text where stringError = pack showError = id instance ScottyError e => ScottyError (ActionError e) where stringError = ActionError status500 . stringError showError (Redirect url) = url showError Next = pack "Next" showError Finish = pack "Finish" showError (ActionError _ e) = showError e type ErrorHandler e m = Maybe (e -> ActionT e m ()) ------------------ Scotty Actions ------------------- type Param = (Text, Text) type File = (Text, FileInfo ByteString) data ActionEnv = Env { getReq :: Request , getParams :: [Param] , getBody :: IO ByteString , getBodyChunk :: IO BS.ByteString , getFiles :: [File] } data RequestBodyState = BodyUntouched | BodyCached ByteString [BS.ByteString] -- whole body, chunks left to stream | BodyCorrupted data BodyPartiallyStreamed = BodyPartiallyStreamed deriving (Show, Typeable) instance E.Exception BodyPartiallyStreamed data Content = ContentBuilder Builder | ContentFile FilePath | ContentStream StreamingBody data ScottyResponse = SR { srStatus :: Status , srHeaders :: ResponseHeaders , srContent :: Content } instance Default ScottyResponse where def = SR status200 [] (ContentBuilder mempty) newtype ActionT e m a = ActionT { runAM :: ExceptT (ActionError e) (ReaderT ActionEnv (StateT ScottyResponse m)) a } deriving ( Functor, Applicative, MonadIO ) instance (Monad m, ScottyError e) => Monad (ActionT e m) where return = ActionT . return ActionT m >>= k = ActionT (m >>= runAM . k) #if !(MIN_VERSION_base(4,13,0)) fail = Fail.fail #endif instance (Monad m, ScottyError e) => Fail.MonadFail (ActionT e m) where fail = ActionT . throwError . stringError instance ( Monad m, ScottyError e #if !(MIN_VERSION_base(4,8,0)) , Functor m #endif ) => Alternative (ActionT e m) where empty = mzero (<|>) = mplus instance (Monad m, ScottyError e) => MonadPlus (ActionT e m) where mzero = ActionT . ExceptT . return $ Left Next ActionT m `mplus` ActionT n = ActionT . ExceptT $ do a <- runExceptT m case a of Left _ -> runExceptT n Right r -> return $ Right r instance MonadTrans (ActionT e) where lift = ActionT . lift . lift . lift instance (ScottyError e, Monad m) => MonadError (ActionError e) (ActionT e m) where throwError = ActionT . throwError catchError (ActionT m) f = ActionT (catchError m (runAM . f)) instance (MonadBase b m, ScottyError e) => MonadBase b (ActionT e m) where liftBase = liftBaseDefault instance (MonadThrow m, ScottyError e) => MonadThrow (ActionT e m) where throwM = ActionT . throwM instance (MonadCatch m, ScottyError e) => MonadCatch (ActionT e m) where catch (ActionT m) f = ActionT (m `catch` (runAM . f)) instance MonadTransControl (ActionT e) where type StT (ActionT e) a = StT (StateT ScottyResponse) (StT (ReaderT ActionEnv) (StT (ExceptT (ActionError e)) a)) liftWith = \f -> ActionT $ liftWith $ \run -> liftWith $ \run' -> liftWith $ \run'' -> f $ run'' . run' . run . runAM restoreT = ActionT . restoreT . restoreT . restoreT instance (ScottyError e, MonadBaseControl b m) => MonadBaseControl b (ActionT e m) where type StM (ActionT e m) a = ComposeSt (ActionT e) m a liftBaseWith = defaultLiftBaseWith restoreM = defaultRestoreM instance (MonadReader r m, ScottyError e) => MonadReader r (ActionT e m) where {-# INLINE ask #-} ask = lift ask {-# INLINE local #-} local f = ActionT . mapExceptT (mapReaderT (mapStateT $ local f)) . runAM instance (MonadState s m, ScottyError e) => MonadState s (ActionT e m) where {-# INLINE get #-} get = lift get {-# INLINE put #-} put = lift . put instance (Semigroup a) => Semigroup (ScottyT e m a) where x <> y = (<>) <$> x <*> y instance ( Monoid a #if !(MIN_VERSION_base(4,11,0)) , Semigroup a #endif #if !(MIN_VERSION_base(4,8,0)) , Functor m #endif ) => Monoid (ScottyT e m a) where mempty = return mempty #if !(MIN_VERSION_base(4,11,0)) mappend = (<>) #endif instance ( Monad m #if !(MIN_VERSION_base(4,8,0)) , Functor m #endif , Semigroup a ) => Semigroup (ActionT e m a) where x <> y = (<>) <$> x <*> y instance ( Monad m, ScottyError e, Monoid a #if !(MIN_VERSION_base(4,11,0)) , Semigroup a #endif #if !(MIN_VERSION_base(4,8,0)) , Functor m #endif ) => Monoid (ActionT e m a) where mempty = return mempty #if !(MIN_VERSION_base(4,11,0)) mappend = (<>) #endif ------------------ Scotty Routes -------------------- data RoutePattern = Capture Text | Literal Text | Function (Request -> Maybe [Param]) instance IsString RoutePattern where fromString = Capture . pack scotty-0.12/Web/Scotty/Route.hs0000644000000000000000000002447107346545000014616 0ustar0000000000000000{-# LANGUAGE CPP, FlexibleContexts, FlexibleInstances, OverloadedStrings, RankNTypes, ScopedTypeVariables #-} module Web.Scotty.Route ( get, post, put, delete, patch, options, addroute, matchAny, notFound, capture, regex, function, literal ) where import Control.Arrow ((***)) import Control.Concurrent.MVar import Control.Exception (throw) import Control.Monad.IO.Class import qualified Control.Monad.State as MS import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as BL import Data.Maybe (fromMaybe, isJust) import Data.String (fromString) import qualified Data.Text.Lazy as T import qualified Data.Text as TS import Network.HTTP.Types import Network.Wai (Request(..)) #if MIN_VERSION_wai(3,2,2) import Network.Wai.Internal (getRequestBodyChunk) #endif import qualified Network.Wai.Parse as Parse hiding (parseRequestBody) import Prelude () import Prelude.Compat import qualified Text.Regex as Regex import Web.Scotty.Action import Web.Scotty.Internal.Types import Web.Scotty.Util -- | get = 'addroute' 'GET' get :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () get = addroute GET -- | post = 'addroute' 'POST' post :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () post = addroute POST -- | put = 'addroute' 'PUT' put :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () put = addroute PUT -- | delete = 'addroute' 'DELETE' delete :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () delete = addroute DELETE -- | patch = 'addroute' 'PATCH' patch :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () patch = addroute PATCH -- | options = 'addroute' 'OPTIONS' options :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () options = addroute OPTIONS -- | Add a route that matches regardless of the HTTP verb. matchAny :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () matchAny pattern action = ScottyT $ MS.modify $ \s -> addRoute (route (handler s) Nothing pattern action) s -- | Specify an action to take if nothing else is found. Note: this _always_ matches, -- so should generally be the last route specified. notFound :: (ScottyError e, MonadIO m) => ActionT e m () -> ScottyT e m () notFound action = matchAny (Function (\req -> Just [("path", path req)])) (status status404 >> action) -- | Define a route with a 'StdMethod', 'T.Text' value representing the path spec, -- and a body ('Action') which modifies the response. -- -- > addroute GET "/" $ text "beam me up!" -- -- The path spec can include values starting with a colon, which are interpreted -- as /captures/. These are named wildcards that can be looked up with 'param'. -- -- > addroute GET "/foo/:bar" $ do -- > v <- param "bar" -- > text v -- -- >>> curl http://localhost:3000/foo/something -- something addroute :: (ScottyError e, MonadIO m) => StdMethod -> RoutePattern -> ActionT e m () -> ScottyT e m () addroute method pat action = ScottyT $ MS.modify $ \s -> addRoute (route (handler s) (Just method) pat action) s route :: (ScottyError e, MonadIO m) => ErrorHandler e m -> Maybe StdMethod -> RoutePattern -> ActionT e m () -> Middleware m route h method pat action app req = let tryNext = app req {- | We match all methods in the case where 'method' is 'Nothing'. See https://github.com/scotty-web/scotty/issues/196 -} methodMatches :: Bool methodMatches = case method of Nothing -> True Just m -> Right m == parseMethod (requestMethod req) in if methodMatches then case matchRoute pat req of Just captures -> do env <- mkEnv req captures res <- runAction h env action maybe tryNext return res Nothing -> tryNext else tryNext matchRoute :: RoutePattern -> Request -> Maybe [Param] matchRoute (Literal pat) req | pat == path req = Just [] | otherwise = Nothing matchRoute (Function fun) req = fun req matchRoute (Capture pat) req = go (T.split (=='/') pat) (compress $ T.split (=='/') $ path req) [] where go [] [] prs = Just prs -- request string and pattern match! go [] r prs | T.null (mconcat r) = Just prs -- in case request has trailing slashes | otherwise = Nothing -- request string is longer than pattern go p [] prs | T.null (mconcat p) = Just prs -- in case pattern has trailing slashes | otherwise = Nothing -- request string is not long enough go (p:ps) (r:rs) prs | p == r = go ps rs prs -- equal literals, keeping checking | T.null p = Nothing -- p is null, but r is not, fail | T.head p == ':' = go ps rs $ (T.tail p, r) : prs -- p is a capture, add to params | otherwise = Nothing -- both literals, but unequal, fail compress ("":rest@("":_)) = compress rest compress (x:xs) = x : compress xs compress [] = [] -- Pretend we are at the top level. path :: Request -> T.Text path = T.fromStrict . TS.cons '/' . TS.intercalate "/" . pathInfo -- Stolen from wai-extra's Network.Wai.Parse, modified to accept body as list of Bytestrings. -- Reason: WAI's getRequestBodyChunk is an IO action that returns the body as chunks. -- Once read, they can't be read again. We read them into a lazy Bytestring, so Scotty -- user can get the raw body, even if they also want to call wai-extra's parsing routines. parseRequestBody :: MonadIO m => [B.ByteString] -> Parse.BackEnd y -> Request -> m ([Parse.Param], [Parse.File y]) parseRequestBody bl s r = case Parse.getRequestBodyType r of Nothing -> return ([], []) Just rbt -> do mvar <- liftIO $ newMVar bl -- MVar is a bit of a hack so we don't have to inline -- large portions of Network.Wai.Parse let provider = modifyMVar mvar $ \bsold -> case bsold of [] -> return ([], B.empty) (b:bs) -> return (bs, b) liftIO $ Parse.sinkRequestBody s rbt provider mkEnv :: forall m. MonadIO m => Request -> [Param] -> m ActionEnv mkEnv req captures = do bodyState <- liftIO $ newMVar BodyUntouched let rbody = getRequestBodyChunk req takeAll :: ([B.ByteString] -> IO [B.ByteString]) -> IO [B.ByteString] takeAll prefix = rbody >>= \b -> if B.null b then prefix [] else takeAll (prefix . (b:)) safeBodyReader :: IO B.ByteString safeBodyReader = do state <- takeMVar bodyState let direct = putMVar bodyState BodyCorrupted >> rbody case state of s@(BodyCached _ []) -> do putMVar bodyState s return B.empty BodyCached b (chunk:rest) -> do putMVar bodyState $ BodyCached b rest return chunk BodyUntouched -> direct BodyCorrupted -> direct bs :: IO BL.ByteString bs = do state <- takeMVar bodyState case state of s@(BodyCached b _) -> do putMVar bodyState s return b BodyCorrupted -> throw BodyPartiallyStreamed BodyUntouched -> do chunks <- takeAll return let b = BL.fromChunks chunks putMVar bodyState $ BodyCached b chunks return b shouldParseBody = isJust $ Parse.getRequestBodyType req (formparams, fs) <- if shouldParseBody then liftIO $ do wholeBody <- BL.toChunks `fmap` bs parseRequestBody wholeBody Parse.lbsBackEnd req else return ([], []) let convert (k, v) = (strictByteStringToLazyText k, strictByteStringToLazyText v) parameters = captures ++ map convert formparams ++ queryparams queryparams = parseEncodedParams $ rawQueryString req return $ Env req parameters bs safeBodyReader [ (strictByteStringToLazyText k, fi) | (k,fi) <- fs ] parseEncodedParams :: B.ByteString -> [Param] parseEncodedParams bs = [ (T.fromStrict k, T.fromStrict $ fromMaybe "" v) | (k,v) <- parseQueryText bs ] -- | Match requests using a regular expression. -- Named captures are not yet supported. -- -- > get (regex "^/f(.*)r$") $ do -- > path <- param "0" -- > cap <- param "1" -- > text $ mconcat ["Path: ", path, "\nCapture: ", cap] -- -- >>> curl http://localhost:3000/foo/bar -- Path: /foo/bar -- Capture: oo/ba -- regex :: String -> RoutePattern regex pattern = Function $ \ req -> fmap (map (T.pack . show *** T.pack) . zip [0 :: Int ..] . strip) (Regex.matchRegexAll rgx $ T.unpack $ path req) where rgx = Regex.mkRegex pattern strip (_, match, _, subs) = match : subs -- | Standard Sinatra-style route. Named captures are prepended with colons. -- This is the default route type generated by OverloadedString routes. i.e. -- -- > get (capture "/foo/:bar") $ ... -- -- and -- -- > {-# LANGUAGE OverloadedStrings #-} -- > ... -- > get "/foo/:bar" $ ... -- -- are equivalent. capture :: String -> RoutePattern capture = fromString -- | Build a route based on a function which can match using the entire 'Request' object. -- 'Nothing' indicates the route does not match. A 'Just' value indicates -- a successful match, optionally returning a list of key-value pairs accessible -- by 'param'. -- -- > get (function $ \req -> Just [("version", T.pack $ show $ httpVersion req)]) $ do -- > v <- param "version" -- > text v -- -- >>> curl http://localhost:3000/ -- HTTP/1.1 -- function :: (Request -> Maybe [Param]) -> RoutePattern function = Function -- | Build a route that requires the requested path match exactly, without captures. literal :: String -> RoutePattern literal = Literal . T.pack #if !(MIN_VERSION_wai(3,2,2)) getRequestBodyChunk :: Request -> IO B.ByteString getRequestBodyChunk = requestBody #endif scotty-0.12/Web/Scotty/Trans.hs0000644000000000000000000001306207346545000014601 0ustar0000000000000000{-# LANGUAGE OverloadedStrings, RankNTypes #-} -- | It should be noted that most of the code snippets below depend on the -- OverloadedStrings language pragma. -- -- The functions in this module allow an arbitrary monad to be embedded -- in Scotty's monad transformer stack in order that Scotty be combined -- with other DSLs. -- -- Scotty is set up by default for development mode. For production servers, -- you will likely want to modify 'settings' and the 'defaultHandler'. See -- the comments on each of these functions for more information. module Web.Scotty.Trans ( -- * scotty-to-WAI scottyT, scottyAppT, scottyOptsT, scottySocketT, Options(..) -- * Defining Middleware and Routes -- -- | 'Middleware' and routes are run in the order in which they -- are defined. All middleware is run first, followed by the first -- route that matches. If no route matches, a 404 response is given. , middleware, get, post, put, delete, patch, options, addroute, matchAny, notFound -- ** Route Patterns , capture, regex, function, literal -- ** Accessing the Request, Captures, and Query Parameters , request, header, headers, body, bodyReader, param, params, jsonData, files -- ** Modifying the Response and Redirecting , status, addHeader, setHeader, redirect -- ** Setting Response Body -- -- | Note: only one of these should be present in any given route -- definition, as they completely replace the current 'Response' body. , text, html, file, json, stream, raw -- ** Exceptions , raise, raiseStatus, rescue, next, finish, defaultHandler, ScottyError(..), liftAndCatchIO -- * Parsing Parameters , Param, Parsable(..), readEither -- * Types , RoutePattern, File -- * Monad Transformers , ScottyT, ActionT ) where import Blaze.ByteString.Builder (fromByteString) import Control.Monad (when) import Control.Monad.State.Strict (execState, modify) import Control.Monad.IO.Class import Data.Default.Class (def) import Network.HTTP.Types (status404, status500) import Network.Socket (Socket) import Network.Wai import Network.Wai.Handler.Warp (Port, runSettings, runSettingsSocket, setPort, getPort) import Web.Scotty.Action import Web.Scotty.Route import Web.Scotty.Internal.Types hiding (Application, Middleware) import Web.Scotty.Util (socketDescription) import qualified Web.Scotty.Internal.Types as Scotty -- | Run a scotty application using the warp server. -- NB: scotty p === scottyT p id scottyT :: (Monad m, MonadIO n) => Port -> (m Response -> IO Response) -- ^ Run monad 'm' into 'IO', called at each action. -> ScottyT e m () -> n () scottyT p = scottyOptsT $ def { settings = setPort p (settings def) } -- | Run a scotty application using the warp server, passing extra options. -- NB: scottyOpts opts === scottyOptsT opts id scottyOptsT :: (Monad m, MonadIO n) => Options -> (m Response -> IO Response) -- ^ Run monad 'm' into 'IO', called at each action. -> ScottyT e m () -> n () scottyOptsT opts runActionToIO s = do when (verbose opts > 0) $ liftIO $ putStrLn $ "Setting phasers to stun... (port " ++ show (getPort (settings opts)) ++ ") (ctrl-c to quit)" liftIO . runSettings (settings opts) =<< scottyAppT runActionToIO s -- | Run a scotty application using the warp server, passing extra options, and -- listening on the provided socket. -- NB: scottySocket opts sock === scottySocketT opts sock id scottySocketT :: (Monad m, MonadIO n) => Options -> Socket -> (m Response -> IO Response) -> ScottyT e m () -> n () scottySocketT opts sock runActionToIO s = do when (verbose opts > 0) $ do d <- liftIO $ socketDescription sock liftIO $ putStrLn $ "Setting phasers to stun... (" ++ d ++ ") (ctrl-c to quit)" liftIO . runSettingsSocket (settings opts) sock =<< scottyAppT runActionToIO s -- | Turn a scotty application into a WAI 'Application', which can be -- run with any WAI handler. -- NB: scottyApp === scottyAppT id scottyAppT :: (Monad m, Monad n) => (m Response -> IO Response) -- ^ Run monad 'm' into 'IO', called at each action. -> ScottyT e m () -> n Application scottyAppT runActionToIO defs = do let s = execState (runS defs) def let rapp req callback = runActionToIO (foldl (flip ($)) notFoundApp (routes s) req) >>= callback return $ foldl (flip ($)) rapp (middlewares s) notFoundApp :: Monad m => Scotty.Application m notFoundApp _ = return $ responseBuilder status404 [("Content-Type","text/html")] $ fromByteString "

404: File Not Found!

" -- | Global handler for uncaught exceptions. -- -- Uncaught exceptions normally become 500 responses. -- You can use this to selectively override that behavior. -- -- Note: IO exceptions are lifted into 'ScottyError's by 'stringError'. -- This has security implications, so you probably want to provide your -- own defaultHandler in production which does not send out the error -- strings as 500 responses. defaultHandler :: (ScottyError e, Monad m) => (e -> ActionT e m ()) -> ScottyT e m () defaultHandler f = ScottyT $ modify $ addHandler $ Just (\e -> status status500 >> f e) -- | Use given middleware. Middleware is nested such that the first declared -- is the outermost middleware (it has first dibs on the request and last action -- on the response). Every middleware is run on each request. middleware :: Middleware -> ScottyT e m () middleware = ScottyT . modify . addMiddleware scotty-0.12/Web/Scotty/Util.hs0000644000000000000000000000460207346545000014427 0ustar0000000000000000module Web.Scotty.Util ( lazyTextToStrictByteString , strictByteStringToLazyText , setContent , setHeaderWith , setStatus , mkResponse , replace , add , addIfNotPresent , socketDescription ) where import Network.Socket (SockAddr(..), Socket, getSocketName, socketPort) import Network.Wai import Network.HTTP.Types import qualified Data.ByteString as B import qualified Data.Text.Lazy as T import qualified Data.Text.Encoding as ES import qualified Data.Text.Encoding.Error as ES import Web.Scotty.Internal.Types lazyTextToStrictByteString :: T.Text -> B.ByteString lazyTextToStrictByteString = ES.encodeUtf8 . T.toStrict strictByteStringToLazyText :: B.ByteString -> T.Text strictByteStringToLazyText = T.fromStrict . ES.decodeUtf8With ES.lenientDecode setContent :: Content -> ScottyResponse -> ScottyResponse setContent c sr = sr { srContent = c } setHeaderWith :: ([(HeaderName, B.ByteString)] -> [(HeaderName, B.ByteString)]) -> ScottyResponse -> ScottyResponse setHeaderWith f sr = sr { srHeaders = f (srHeaders sr) } setStatus :: Status -> ScottyResponse -> ScottyResponse setStatus s sr = sr { srStatus = s } -- Note: we currently don't support responseRaw, which may be useful -- for websockets. However, we always read the request body, which -- is incompatible with responseRaw responses. mkResponse :: ScottyResponse -> Response mkResponse sr = case srContent sr of ContentBuilder b -> responseBuilder s h b ContentFile f -> responseFile s h f Nothing ContentStream str -> responseStream s h str where s = srStatus sr h = srHeaders sr -- Note: we assume headers are not sensitive to order here (RFC 2616 specifies they are not) replace :: Eq a => a -> b -> [(a,b)] -> [(a,b)] replace k v = add k v . filter ((/= k) . fst) add :: a -> b -> [(a,b)] -> [(a,b)] add k v m = (k,v):m addIfNotPresent :: Eq a => a -> b -> [(a,b)] -> [(a,b)] addIfNotPresent k v = go where go [] = [(k,v)] go l@((x,y):r) | x == k = l | otherwise = (x,y) : go r -- Assemble a description from the Socket's PortID. socketDescription :: Socket -> IO String socketDescription sock = do sockName <- getSocketName sock case sockName of SockAddrUnix u -> return $ "unix socket " ++ u _ -> fmap (\port -> "port " ++ show port) $ socketPort sock scotty-0.12/bench/0000755000000000000000000000000007346545000012231 5ustar0000000000000000scotty-0.12/bench/Main.hs0000644000000000000000000000253107346545000013452 0ustar0000000000000000{-# language OverloadedStrings , GeneralizedNewtypeDeriving #-} module Main (main) where import Control.Monad import Data.Default.Class (def) import Data.Functor.Identity import Data.Text (Text) import Lucid.Base import Lucid.Html5 import Web.Scotty import Web.Scotty.Internal.Types import qualified Control.Monad.State.Lazy as SL import qualified Control.Monad.State.Strict as SS import qualified Data.ByteString.Lazy as BL import Weigh main :: IO () main = do mainWith $ do setColumns [Case,Allocated,GCs,Live,Check,Max,MaxOS] setFormat Markdown io "ScottyM Strict" BL.putStr (SS.evalState (runS $ renderBST htmlScotty) def) io "ScottyM Lazy" BL.putStr (SL.evalState (runScottyLazy $ renderBST htmlScottyLazy) def) io "Identity" BL.putStr (runIdentity $ renderBST htmlIdentity) htmlTest :: Monad m => HtmlT m () htmlTest = replicateM_ 2 $ div_ $ do replicateM_ 1000 $ div_ $ do replicateM_ 10000 $ div_ "test" htmlIdentity :: HtmlT Identity () htmlIdentity = htmlTest {-# noinline htmlIdentity #-} htmlScotty :: HtmlT ScottyM () htmlScotty = htmlTest {-# noinline htmlScotty #-} htmlScottyLazy :: HtmlT ScottyLazy () htmlScottyLazy = htmlTest {-# noinline htmlScottyLazy #-} newtype ScottyLazy a = ScottyLazy { runScottyLazy:: SL.State (ScottyState Text IO) a } deriving (Functor,Applicative,Monad) scotty-0.12/changelog.md0000755000000000000000000001465707346545000013443 0ustar0000000000000000## 0.12 [2020.05.16] * Provide `MonadReader` and `MonadState` instances for `ActionT`. * Add HTTP Status code as a field to `ActionError`, and add a sister function to `raise`, `raiseStatus`. This makes throwing a specific error code and exiting much cleaner, and avoids the strange defaulting to HTTP 500. This will make internal functions easier to implement with the right status codes 'thrown', such as `jsonData`. * Correct http statuses returned by `jsonData` (#228). * Better error message when no data is provided to `jsonData` (#226). * Add `Semigroup` and `Monoid` instances for `ActionT` and `ScottyT` * ScottyT: Use strict StateT instead of lazy * Handle adjacent slashes in the request path as one (thanks @SkyWriter) ## 0.11.5 [2019.09.07] * Allow building the test suite with `hspec-wai-0.10`. ## 0.11.4 [2019.05.02] * Allow building with `base-4.13` (GHC 8.8). ## 0.11.3 [2019.01.08] * Drop the test suite's dependency on `hpc-coveralls`, which is unmaintained and does not build with GHC 8.4 or later. ## 0.11.2 [2018.07.02] * Migrate from `Network` to `Network.Socket` to avoid deprecation warnings. ## 0.11.1 [2018.04.07] * Add `MonadThrow` and `MonadCatch` instances for `ActionT` [abhinav] * Fix `matchAny` so that all methods are matched, not just standard ones [taphu] ## 0.11.0 * IO exceptions are no longer automatically turned into ScottyErrors by `liftIO`. Use `liftAndCatchIO` to get that behavior. * New `finish` function. * Text values are now leniently decoded from ByteStrings. * Added `MonadFail` instance for `ScottyT` * Lots of bound bumps on dependencies. ## 0.10.2 * Removed debug statement from routes ## 0.10.1 * `Parsable` instances for `Word`, `Word8`, `Word16`, `Word32`, `Word64` [adamflott] * `Parsable` instances for `Int8`, `Int16`, `Int32`, `Int64`, and `Natural` * Removed redundant `Monad` constraint on `middleware` ## 0.10.0 * The monad parameters to `ScottyT` have been decoupled, causing the type of the `ScottyT` constructor to change. As a result, `ScottyT` is no longer a `MonadTrans` instance, and the type signatures of`scottyT`, `scottyAppT`, and `scottyOptsT` have been simplified. [ehamberg] * `socketDescription` no longer uses the deprecated `PortNum` constructor. Instead, it uses the `Show` instance for `PortNumber`. This changes the bytes from host to network order, so the output of `socketDescription` could change. [ehamberg] * `Alternative`, `MonadPlus` instances for `ActionT` * `scotty` now depends on `transformers-compat`. As a result, `ActionT` now uses `ExceptT`, regardless of which version of `transformers` is used. As a result, several functions in `Web.Scotty.Trans` no longer require a `ScottyError` constraint, since `ExceptT` does not require an `Error` constraint (unlike `ErrorT`). * Added support for OPTIONS routes via the `options` function [alvare] * Add `scottySocket` and `scottySocketT`, exposing Warp Unix socket support [hakujin] * `Parsable` instance for lazy `ByteString` [tattsun] * Added streaming uploads via the `bodyReader` function, which retrieves chunks of the request body. [edofic] - `ActionEnv` had a `getBodyChunk` field added (in `Web.Scotty.Internal.Types`) - `RequestBodyState` and `BodyPartiallyStreamed` added to `Web.Scotty.Internal.Types` * `jsonData` uses `aeson`'s `eitherDecode` instead of just `decode` [k-bx] ## 0.9.1 * text/html/json only set Content-Type header when not already set ## 0.9.0 * Add `charset=utf-8` to `Content-Type` for `text`, `html` and `json` * Assume HTTP status 500 for `defaultHandler` * Remove deprecated `source` method. * No longer depend on conduit. ## 0.8.2 * Bump `aeson` upper bound * Fix `mtl` related deprecation warnings ## 0.8.1 * Export internal types * Added `MonadBase`, `MonadTransControl` and `MonadBaseControl` instances for `ActionT` ## 0.8.0 * Upgrade to wai/wai-extra/warp 3.0 * No longer depend on conduit-extra. * The `source` response method has been deprecated in favor of a new `stream` response, matching changes in WAI 3.0. * Removed the deprecated `reqHeader` function. ## 0.7.3 * Bump upper bound for case-insensitive, mtl and transformers. ## 0.7.2 * Bump lower bound on conduit, add conduit-extra to cabal build depends. ## 0.7.1 * Default warp settings now use `setFdCacheDuration 0` to work around a warp issue where file changes are not getting picked up. ## 0.7.0 * Renamed `reqHeader` to `header`. Added `headers` function to get all headers. * Changed `MonadIO` instance for `ActionT` such that IO exceptions are lifted into `ScottyError`s via `stringError`. * Make `Bool` parsing case-insensitive. Goal: support both Haskell's True/False and Javascript's true/false. Thanks to Ben Gamari for suggesting this. * Bump `aeson`/`text` upper bounds. * Bump `wai`/`wai-extra`/`warp` bounds, including new lower bound for `warp`, which fixes a security issue related to Slowloris protection. ## 0.6.2 * Bump upper bound for `text`. ## 0.6.1 * Match changes in `wai-extra`. ## 0.6.0 * The Scotty transformers (`ScottyT` and `ActionT`) are now parameterized over a custom exception type, allowing one to extend Scotty's `ErrorT` layer with something richer than `Text` errors. See the `exceptions` example for use. `ScottyM` and `ActionM` remain specialized to `Text` exceptions for simplicity. * Both monads are now instances of `Functor` and `Applicative`. * There is a new `cookies` example. * Internals brought up-to-date with WAI 2.0 and related packages. ## 0.5.0 * The Scotty monads (`ScottyM` and `ActionM`) are now monad transformers, allowing Scotty applications to be embedded in arbitrary `MonadIO`s. The old API continues to be exported from `Web.Scotty` where: type ScottyM = ScottyT IO type ActionM = ActionT IO The new transformers are found in `Web.Scotty.Trans`. See the `globalstate` example for use. Special thanks to Dan Frumin (co-dan) for much of the legwork here. * Added support for HTTP PATCH method. * Removed lambda action syntax. This will return when we have a better story for typesafe routes. * `reqHeader :: Text -> ActionM Text` ==> `reqHeader :: Text -> ActionM (Maybe Text)` * New `raw` method to set body to a raw `ByteString` * Parse error thrown by `jsonData` now includes the body it couldn't parse. * `header` split into `setHeader` and `addHeader`. The former replaces a response header (original behavior). The latter adds a header (useful for multiple `Set-Cookie`s, for instance). scotty-0.12/examples/0000755000000000000000000000000007346545000012770 5ustar0000000000000000scotty-0.12/examples/404.html0000755000000000000000000000003507346545000014166 0ustar0000000000000000

This is a 404 page!

scotty-0.12/examples/LICENSE0000755000000000000000000000276707346545000014014 0ustar0000000000000000Copyright (c) 2012-2017 Andrew Farmer All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Andrew Farmer nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. scotty-0.12/examples/basic.hs0000755000000000000000000000657607346545000014426 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} module Main (main) where import Web.Scotty import Network.Wai.Middleware.RequestLogger -- install wai-extra if you don't have this import Control.Monad import Control.Monad.Trans import System.Random (newStdGen, randomRs) import Network.HTTP.Types (status302) import Data.Text.Lazy.Encoding (decodeUtf8) import Data.String (fromString) import Prelude () import Prelude.Compat main :: IO () main = scotty 3000 $ do -- Add any WAI middleware, they are run top-down. middleware logStdoutDev -- get (function $ \req -> Just [("version", T.pack $ show $ httpVersion req)]) $ do -- v <- param "version" -- text v -- To demonstrate that routes are matched top-down. get "/" $ text "foobar" get "/" $ text "barfoo" -- Using a parameter in the query string. If it has -- not been given, a 500 page is generated. get "/foo" $ do v <- param "fooparam" html $ mconcat ["

", v, "

"] -- An uncaught error becomes a 500 page. get "/raise" $ raise "some error here" -- You can set status and headers directly. get "/redirect-custom" $ do status status302 setHeader "Location" "http://www.google.com" -- note first arg to header is NOT case-sensitive -- redirects preempt execution get "/redirect" $ do void $ redirect "http://www.google.com" raise "this error is never reached" -- Of course you can catch your own errors. get "/rescue" $ do (do void $ raise "a rescued error"; redirect "http://www.we-never-go-here.com") `rescue` (\m -> text $ "we recovered from " `mappend` m) -- Parts of the URL that start with a colon match -- any string, and capture that value as a parameter. -- URL captures take precedence over query string parameters. get "/foo/:bar/required" $ do v <- param "bar" html $ mconcat ["

", v, "

"] -- Files are streamed directly to the client. get "/404" $ file "404.html" -- You can stop execution of this action and keep pattern matching routes. get "/random" $ do void next redirect "http://www.we-never-go-here.com" -- You can do IO with liftIO, and you can return JSON content. get "/random" $ do g <- liftIO newStdGen json $ take 20 $ randomRs (1::Int,100) g get "/ints/:is" $ do is <- param "is" json $ [(1::Int)..10] ++ is get "/setbody" $ do html $ mconcat ["
" ,"" ,"" ,"
" ] post "/readbody" $ do b <- body text $ decodeUtf8 b get "/header" $ do agent <- header "User-Agent" maybe (raise "User-Agent header not found!") text agent -- Make a request to this URI, then type a line in the terminal, which -- will be the response. Using ctrl-c will cause getLine to fail. -- This demonstrates that IO exceptions are lifted into ActionM exceptions. get "/iofail" $ do msg <- liftIO $ liftM fromString getLine text msg {- If you don't want to use Warp as your webserver, you can use any WAI handler. import Network.Wai.Handler.FastCGI (run) main = do myApp <- scottyApp $ do get "/" $ text "hello world" run myApp -} scotty-0.12/examples/bodyecho.hs0000755000000000000000000000221207346545000015120 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} module Main (main) where import Web.Scotty import Control.Monad.IO.Class (liftIO) import qualified Blaze.ByteString.Builder as B import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL import qualified Data.Text.Lazy as T main :: IO () main = scotty 3000 $ do post "/echo" $ do rd <- bodyReader stream $ ioCopy rd $ return () post "/count" $ do wb <- body -- this must happen before first 'rd' rd <- bodyReader let step acc = do chunk <- rd putStrLn "got a chunk" let len = BS.length chunk if len > 0 then step $ acc + len else return acc len <- liftIO $ step 0 text $ T.pack $ "uploaded " ++ show len ++ " bytes, wb len is " ++ show (BSL.length wb) ioCopy :: IO BS.ByteString -> IO () -> (B.Builder -> IO ()) -> IO () -> IO () ioCopy reader close write flush = step >> flush where step = do chunk <- reader if (BS.length chunk > 0) then (write $ B.insertByteString chunk) >> step else close scotty-0.12/examples/cookies.hs0000755000000000000000000000376507346545000014776 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} -- This examples requires you to: cabal install cookie -- and: cabal install blaze-html module Main (main) where import Control.Monad (forM_) import Data.Text.Lazy (Text) import qualified Data.Text.Lazy.Encoding as T import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL import qualified Blaze.ByteString.Builder as B import qualified Text.Blaze.Html5 as H import Text.Blaze.Html5.Attributes import Text.Blaze.Html.Renderer.Text (renderHtml) import Web.Scotty import Web.Cookie makeCookie :: BS.ByteString -> BS.ByteString -> SetCookie makeCookie n v = def { setCookieName = n, setCookieValue = v } renderSetCookie' :: SetCookie -> Text renderSetCookie' = T.decodeUtf8 . B.toLazyByteString . renderSetCookie setCookie :: BS.ByteString -> BS.ByteString -> ActionM () setCookie n v = setHeader "Set-Cookie" (renderSetCookie' (makeCookie n v)) getCookies :: ActionM (Maybe CookiesText) getCookies = fmap (fmap (parseCookiesText . lazyToStrict . T.encodeUtf8)) $ header "Cookie" where lazyToStrict = BS.concat . BSL.toChunks renderCookiesTable :: CookiesText -> H.Html renderCookiesTable cs = H.table $ do H.tr $ do H.th "name" H.th "value" forM_ cs $ \(name', val) -> do H.tr $ do H.td (H.toMarkup name') H.td (H.toMarkup val) main :: IO () main = scotty 3000 $ do get "/" $ do cookies <- getCookies html $ renderHtml $ do case cookies of Just cs -> renderCookiesTable cs Nothing -> return () H.form H.! method "post" H.! action "/set-a-cookie" $ do H.input H.! type_ "text" H.! name "name" H.input H.! type_ "text" H.! name "value" H.input H.! type_ "submit" H.! value "set a cookie" post "/set-a-cookie" $ do name' <- param "name" value' <- param "value" setCookie name' value' redirect "/" scotty-0.12/examples/exceptions.hs0000755000000000000000000000407207346545000015513 0ustar0000000000000000{-# LANGUAGE OverloadedStrings, GeneralizedNewtypeDeriving, ScopedTypeVariables #-} module Main (main) where import Control.Monad.IO.Class import Data.String (fromString) import Network.HTTP.Types import Network.Wai.Middleware.RequestLogger import Prelude () import Prelude.Compat import System.Random import Web.Scotty.Trans -- Define a custom exception type. data Except = Forbidden | NotFound Int | StringEx String deriving (Show, Eq) -- The type must be an instance of 'ScottyError'. -- 'ScottyError' is essentially a combination of 'Error' and 'Show'. instance ScottyError Except where stringError = StringEx showError = fromString . show -- Handler for uncaught exceptions. handleEx :: Monad m => Except -> ActionT Except m () handleEx Forbidden = do status status403 html "

Scotty Says No

" handleEx (NotFound i) = do status status404 html $ fromString $ "

Can't find " ++ show i ++ ".

" handleEx (StringEx s) = do status status500 html $ fromString $ "

" ++ s ++ "

" main :: IO () main = scottyT 3000 id $ do -- note, we aren't using any additional transformer layers -- so we can just use 'id' for the runner. middleware logStdoutDev defaultHandler handleEx -- define what to do with uncaught exceptions get "/" $ do html $ mconcat ["Option 1 (Not Found)" ,"
" ,"Option 2 (Forbidden)" ,"
" ,"Option 3 (Random)" ] get "/switch/:val" $ do v <- param "val" _ <- if even v then raise Forbidden else raise (NotFound v) text "this will never be reached" get "/random" $ do rBool <- liftIO randomIO i <- liftIO randomIO let catchOne Forbidden = html "

Forbidden was randomly thrown, but we caught it." catchOne other = raise other raise (if rBool then Forbidden else NotFound i) `rescue` catchOne scotty-0.12/examples/globalstate.hs0000755000000000000000000000522407346545000015633 0ustar0000000000000000{-# LANGUAGE OverloadedStrings, GeneralizedNewtypeDeriving #-} -- An example of embedding a custom monad into -- Scotty's transformer stack, using ReaderT to provide access -- to a TVar containing global state. -- -- Note: this example is somewhat simple, as our top level -- is IO itself. The types of 'scottyT' and 'scottyAppT' are -- general enough to allow a Scotty application to be -- embedded into any MonadIO monad. module Main (main) where import Control.Concurrent.STM import Control.Monad.Reader import Data.Default.Class import Data.String import Data.Text.Lazy (Text) import Network.Wai.Middleware.RequestLogger import Prelude () import Prelude.Compat import Web.Scotty.Trans newtype AppState = AppState { tickCount :: Int } instance Default AppState where def = AppState 0 -- Why 'ReaderT (TVar AppState)' rather than 'StateT AppState'? -- With a state transformer, 'runActionToIO' (below) would have -- to provide the state to _every action_, and save the resulting -- state, using an MVar. This means actions would be blocking, -- effectively meaning only one request could be serviced at a time. -- The 'ReaderT' solution means only actions that actually modify -- the state need to block/retry. -- -- Also note: your monad must be an instance of 'MonadIO' for -- Scotty to use it. newtype WebM a = WebM { runWebM :: ReaderT (TVar AppState) IO a } deriving (Applicative, Functor, Monad, MonadIO, MonadReader (TVar AppState)) -- Scotty's monads are layered on top of our custom monad. -- We define this synonym for lift in order to be explicit -- about when we are operating at the 'WebM' layer. webM :: MonadTrans t => WebM a -> t WebM a webM = lift -- Some helpers to make this feel more like a state monad. gets :: (AppState -> b) -> WebM b gets f = ask >>= liftIO . readTVarIO >>= return . f modify :: (AppState -> AppState) -> WebM () modify f = ask >>= liftIO . atomically . flip modifyTVar' f main :: IO () main = do sync <- newTVarIO def -- 'runActionToIO' is called once per action. let runActionToIO m = runReaderT (runWebM m) sync scottyT 3000 runActionToIO app -- This app doesn't use raise/rescue, so the exception -- type is ambiguous. We can fix it by putting a type -- annotation just about anywhere. In this case, we'll -- just do it on the entire app. app :: ScottyT Text WebM () app = do middleware logStdoutDev get "/" $ do c <- webM $ gets tickCount text $ fromString $ show c get "/plusone" $ do webM $ modify $ \ st -> st { tickCount = tickCount st + 1 } redirect "/" get "/plustwo" $ do webM $ modify $ \ st -> st { tickCount = tickCount st + 2 } redirect "/" scotty-0.12/examples/gzip.hs0000755000000000000000000000106607346545000014303 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} module Main (main) where import Network.Wai.Middleware.RequestLogger import Network.Wai.Middleware.Gzip import Web.Scotty main :: IO () main = scotty 3000 $ do -- Note that files are not gzip'd by the default settings. middleware $ gzip $ def { gzipFiles = GzipCompress } middleware logStdoutDev -- gzip a normal response get "/" $ text "It works" -- gzip a file response (note non-default gzip settings above) get "/afile" $ do setHeader "content-type" "text/plain" file "gzip.hs" scotty-0.12/examples/options.hs0000755000000000000000000000106007346545000015017 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} module Main (main) where import Web.Scotty import Network.Wai.Middleware.RequestLogger -- install wai-extra if you don't have this import Data.Default.Class (def) import Network.Wai.Handler.Warp (setPort) -- Set some Scotty settings opts :: Options opts = def { verbose = 0 , settings = setPort 4000 $ settings def } -- This won't display anything at startup, and will listen on localhost:4000 main :: IO () main = scottyOpts opts $ do middleware logStdoutDev get "/" $ text "hello world" scotty-0.12/examples/reader.hs0000755000000000000000000000202607346545000014571 0ustar0000000000000000{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {- An example of embedding a custom monad into Scotty's transformer stack, using ReaderT to provide access to a global state. -} module Main where import Control.Monad.Reader (MonadIO, MonadReader, ReaderT, asks, lift, runReaderT) import Data.Default.Class (def) import Data.Text.Lazy (Text, pack) import Prelude () import Prelude.Compat import Web.Scotty.Trans (ScottyT, get, scottyOptsT, text) data Config = Config { environment :: String } deriving (Eq, Read, Show) newtype ConfigM a = ConfigM { runConfigM :: ReaderT Config IO a } deriving (Applicative, Functor, Monad, MonadIO, MonadReader Config) application :: ScottyT Text ConfigM () application = do get "/" $ do e <- lift $ asks environment text $ pack $ show e main :: IO () main = scottyOptsT def runIO application where runIO :: ConfigM a -> IO a runIO m = runReaderT (runConfigM m) config config :: Config config = Config { environment = "Development" } scotty-0.12/examples/static/0000755000000000000000000000000007346545000014257 5ustar0000000000000000scotty-0.12/examples/static/jquery-json.js0000755000000000000000000000420107346545000017103 0ustar0000000000000000 (function($){var escapeable=/["\\\x00-\x1f\x7f-\x9f]/g,meta={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'};$.toJSON=typeof JSON==='object'&&JSON.stringify?JSON.stringify:function(o){if(o===null){return'null';} var type=typeof o;if(type==='undefined'){return undefined;} if(type==='number'||type==='boolean'){return''+o;} if(type==='string'){return $.quoteString(o);} if(type==='object'){if(typeof o.toJSON==='function'){return $.toJSON(o.toJSON());} if(o.constructor===Date){var month=o.getUTCMonth()+1,day=o.getUTCDate(),year=o.getUTCFullYear(),hours=o.getUTCHours(),minutes=o.getUTCMinutes(),seconds=o.getUTCSeconds(),milli=o.getUTCMilliseconds();if(month<10){month='0'+month;} if(day<10){day='0'+day;} if(hours<10){hours='0'+hours;} if(minutes<10){minutes='0'+minutes;} if(seconds<10){seconds='0'+seconds;} if(milli<100){milli='0'+milli;} if(milli<10){milli='0'+milli;} return'"'+year+'-'+month+'-'+day+'T'+ hours+':'+minutes+':'+seconds+'.'+milli+'Z"';} if(o.constructor===Array){var ret=[];for(var i=0;i").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cl||(cl=c.createElement("iframe"),cl.frameBorder=cl.width=cl.height=0),b.appendChild(cl);if(!cm||!cl.createElement)cm=(cl.contentWindow||cl.contentDocument).document,cm.write((c.compatMode==="CSS1Compat"?"":"")+""),cm.close();d=cm.createElement(a),cm.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cl)}ck[a]=e}return ck[a]}function cu(a,b){var c={};f.each(cq.concat.apply([],cq.slice(0,b)),function(){c[this]=a});return c}function ct(){cr=b}function cs(){setTimeout(ct,0);return cr=f.now()}function cj(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ci(){try{return new a.XMLHttpRequest}catch(b){}}function cc(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;g=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.1",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
a",d=q.getElementsByTagName("*"),e=q.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=q.getElementsByTagName("input")[0],b={leadingWhitespace:q.firstChild.nodeType===3,tbody:!q.getElementsByTagName("tbody").length,htmlSerialize:!!q.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:q.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete q.test}catch(s){b.deleteExpando=!1}!q.addEventListener&&q.attachEvent&&q.fireEvent&&(q.attachEvent("onclick",function(){b.noCloneEvent=!1}),q.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),q.appendChild(i),k=c.createDocumentFragment(),k.appendChild(q.lastChild),b.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,k.removeChild(i),k.appendChild(q),q.innerHTML="",a.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",q.style.width="2px",q.appendChild(j),b.reliableMarginRight=(parseInt((a.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(q.attachEvent)for(o in{submit:1,change:1,focusin:1})n="on"+o,p=n in q,p||(q.setAttribute(n,"return;"),p=typeof q[n]=="function"),b[o+"Bubbles"]=p;k.removeChild(q),k=g=h=j=q=i=null,f(function(){var a,d,e,g,h,i,j,k,m,n,o,r=c.getElementsByTagName("body")[0];!r||(j=1,k="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",m="visibility:hidden;border:0;",n="style='"+k+"border:5px solid #000;padding:0;'",o="
"+""+"
",a=c.createElement("div"),a.style.cssText=m+"width:0;height:0;position:static;top:0;margin-top:"+j+"px",r.insertBefore(a,r.firstChild),q=c.createElement("div"),a.appendChild(q),q.innerHTML="
t
",l=q.getElementsByTagName("td"),p=l[0].offsetHeight===0,l[0].style.display="",l[1].style.display="none",b.reliableHiddenOffsets=p&&l[0].offsetHeight===0,q.innerHTML="",q.style.width=q.style.paddingLeft="1px",f.boxModel=b.boxModel=q.offsetWidth===2,typeof q.style.zoom!="undefined"&&(q.style.display="inline",q.style.zoom=1,b.inlineBlockNeedsLayout=q.offsetWidth===2,q.style.display="",q.innerHTML="
",b.shrinkWrapBlocks=q.offsetWidth!==2),q.style.cssText=k+m,q.innerHTML=o,d=q.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,i={doesNotAddBorder:e.offsetTop!==5,doesAddBorderForTableAndCells:h.offsetTop===5},e.style.position="fixed",e.style.top="20px",i.fixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",i.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,i.doesNotIncludeMarginInBodyOffset=r.offsetTop!==j,r.removeChild(a),q=a=null,f.extend(b,i))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;h=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/\bhover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")}; f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&i.push({elem:this,matches:d.slice(e)});for(j=0;j0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
","
"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function() {for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||!bc.test("<"+a.nodeName)?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");b===c?bh.appendChild(o):U(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return br.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bq,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bq.test(g)?g.replace(bq,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,b){var c,d,e;b=b.replace(bs,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b)));return c}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bt.test(f)&&bu.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bD=/%20/g,bE=/\[\]$/,bF=/\r?\n/g,bG=/#.*$/,bH=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bI=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bJ=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bK=/^(?:GET|HEAD)$/,bL=/^\/\//,bM=/\?/,bN=/)<[^<]*)*<\/script>/gi,bO=/^(?:select|textarea)/i,bP=/\s+/,bQ=/([?&])_=[^&]*/,bR=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bS=f.fn.load,bT={},bU={},bV,bW,bX=["*/"]+["*"];try{bV=e.href}catch(bY){bV=c.createElement("a"),bV.href="",bV=bV.href}bW=bR.exec(bV.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bS)return bS.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bN,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bO.test(this.nodeName)||bI.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bF,"\r\n")}}):{name:b.name,value:c.replace(bF,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b_(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b_(a,b);return a},ajaxSettings:{url:bV,isLocal:bJ.test(bW[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bX},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bZ(bT),ajaxTransport:bZ(bU),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cb(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cc(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bH.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bG,"").replace(bL,bW[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bP),d.crossDomain==null&&(r=bR.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bW[1]&&r[2]==bW[2]&&(r[3]||(r[1]==="http:"?80:443))==(bW[3]||(bW[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bT,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bK.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bM.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bQ,"$1_="+x);d.url=y+(y===d.url?(bM.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bX+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bU,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)ca(g,a[g],c,e);return d.join("&").replace(bD,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cd=f.now(),ce=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cd++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ce.test(b.url)||e&&ce.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ce,l),b.url===j&&(e&&(k=k.replace(ce,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cf=a.ActiveXObject?function(){for(var a in ch)ch[a](0,1)}:!1,cg=0,ch;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ci()||cj()}:ci,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cf&&delete ch[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cg,cf&&(ch||(ch={},f(a).unload(cf)),ch[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ck={},cl,cm,cn=/^(?:toggle|show|hide)$/,co=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cp,cq=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cr;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window);scotty-0.12/examples/upload.hs0000755000000000000000000000326207346545000014616 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} module Main (main) where import Web.Scotty import Control.Monad.IO.Class import Network.Wai.Middleware.RequestLogger import Network.Wai.Middleware.Static import Network.Wai.Parse import qualified Text.Blaze.Html5 as H import Text.Blaze.Html5.Attributes import Text.Blaze.Html.Renderer.Text (renderHtml) import qualified Data.ByteString.Lazy as B import qualified Data.ByteString.Char8 as BS import System.FilePath (()) import Prelude () import Prelude.Compat main :: IO () main = scotty 3000 $ do middleware logStdoutDev middleware $ staticPolicy (noDots >-> addBase "uploads") get "/" $ do html $ renderHtml $ H.html $ do H.body $ do H.form H.! method "post" H.! enctype "multipart/form-data" H.! action "/upload" $ do H.input H.! type_ "file" H.! name "foofile" H.br H.input H.! type_ "file" H.! name "barfile" H.br H.input H.! type_ "submit" post "/upload" $ do fs <- files let fs' = [ (fieldName, BS.unpack (fileName fi), fileContent fi) | (fieldName,fi) <- fs ] -- write the files to disk, so they will be served by the static middleware liftIO $ sequence_ [ B.writeFile ("uploads" fn) fc | (_,fn,fc) <- fs' ] -- generate list of links to the files just uploaded html $ mconcat [ mconcat [ fName , ": " , renderHtml $ H.a (H.toHtml fn) H.! (href $ H.toValue fn) >> H.br ] | (fName,fn,_) <- fs' ] scotty-0.12/examples/uploads/0000755000000000000000000000000007346545000014437 5ustar0000000000000000scotty-0.12/examples/uploads/.keep0000755000000000000000000000000007346545000015355 0ustar0000000000000000scotty-0.12/examples/urlshortener.hs0000755000000000000000000000403307346545000016063 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} module Main (main) where import Web.Scotty import Control.Concurrent.MVar import Control.Monad.IO.Class import qualified Data.Map as M import qualified Data.Text.Lazy as T import Network.Wai.Middleware.RequestLogger import Network.Wai.Middleware.Static import Prelude () import Prelude.Compat import qualified Text.Blaze.Html5 as H import Text.Blaze.Html5.Attributes -- Note: -- Scotty does not require blaze-html or -- wai-middleware-static, but this example does -- cabal install blaze-html wai-middleware-static import Text.Blaze.Html.Renderer.Text (renderHtml) -- TODO: -- Implement some kind of session and/or cookies -- Add links main :: IO () main = do m <- newMVar (0::Int,M.empty :: M.Map Int T.Text) scotty 3000 $ do middleware logStdoutDev middleware static get "/" $ do html $ renderHtml $ H.html $ do H.body $ do H.form H.! method "post" H.! action "/shorten" $ do H.input H.! type_ "text" H.! name "url" H.input H.! type_ "submit" post "/shorten" $ do url <- param "url" liftIO $ modifyMVar_ m $ \(i,db) -> return (i+1, M.insert i (T.pack url) db) redirect "/list" -- We have to be careful here, because this route can match pretty much anything. -- Thankfully, the type system knows that 'hash' must be an Int, so this route -- only matches if 'read' can successfully parse the hash capture as an Int. -- Otherwise, the pattern match will fail and Scotty will continue matching -- subsequent routes. get "/:hash" $ do hash <- param "hash" (_,db) <- liftIO $ readMVar m case M.lookup hash db of Nothing -> raise $ mconcat ["URL hash #", T.pack $ show $ hash, " not found in database!"] Just url -> redirect url -- We put /list down here to show that it will not match the '/:hash' route above. get "/list" $ do (_,db) <- liftIO $ readMVar m json $ M.toList db scotty-0.12/scotty.cabal0000644000000000000000000001211707346545000013465 0ustar0000000000000000Name: scotty Version: 0.12 Synopsis: Haskell web framework inspired by Ruby's Sinatra, using WAI and Warp Homepage: https://github.com/scotty-web/scotty Bug-reports: https://github.com/scotty-web/scotty/issues License: BSD3 License-file: LICENSE Author: Andrew Farmer Maintainer: Andrew Farmer Copyright: (c) 2012-Present Andrew Farmer Category: Web Stability: experimental Build-type: Simple Cabal-version: >= 1.10 Description: A Haskell web framework inspired by Ruby's Sinatra, using WAI and Warp. . @ {-# LANGUAGE OverloadedStrings #-} . import Web.Scotty . import Data.Monoid (mconcat) . main = scotty 3000 $ get "/:word" $ do beam <- param "word" html $ mconcat ["<h1>Scotty, ", beam, " me up!</h1>"] @ . . Scotty is the cheap and cheerful way to write RESTful, declarative web applications. . * A page is as simple as defining the verb, url pattern, and Text content. . * It is template-language agnostic. Anything that returns a Text value will do. . * Conforms to WAI Application interface. . * Uses very fast Warp webserver by default. . As for the name: Sinatra + Warp = Scotty. . [WAI] . [Warp] tested-with: GHC == 7.6.3 , GHC == 7.8.4 , GHC == 7.10.3 , GHC == 8.0.2 , GHC == 8.2.2 , GHC == 8.4.4 , GHC == 8.6.5 , GHC == 8.8.3 , GHC == 8.10.1 Extra-source-files: README.md changelog.md examples/404.html examples/LICENSE examples/*.hs examples/static/jquery.js examples/static/jquery-json.js examples/uploads/.keep Library Exposed-modules: Web.Scotty Web.Scotty.Trans Web.Scotty.Internal.Types other-modules: Web.Scotty.Action Web.Scotty.Route Web.Scotty.Util default-language: Haskell2010 build-depends: aeson >= 0.6.2.1 && < 1.5, base >= 4.6 && < 5, base-compat-batteries >= 0.10 && < 0.12, blaze-builder >= 0.3.3.0 && < 0.5, bytestring >= 0.10.0.2 && < 0.11, case-insensitive >= 1.0.0.1 && < 1.3, data-default-class >= 0.0.1 && < 0.2, exceptions >= 0.7 && < 0.11, fail, http-types >= 0.9.1 && < 0.13, monad-control >= 1.0.0.3 && < 1.1, mtl >= 2.1.2 && < 2.3, nats >= 0.1 && < 2, network >= 2.6.0.2 && < 3.2, regex-compat >= 0.95.1 && < 0.96, text >= 0.11.3.1 && < 1.3, transformers >= 0.3.0.0 && < 0.6, transformers-base >= 0.4.1 && < 0.5, transformers-compat >= 0.4 && < 0.7, wai >= 3.0.0 && < 3.3, wai-extra >= 3.0.0 && < 3.1, warp >= 3.0.13 && < 3.4 GHC-options: -Wall -fno-warn-orphans test-suite spec main-is: Spec.hs other-modules: Web.ScottySpec type: exitcode-stdio-1.0 default-language: Haskell2010 hs-source-dirs: test build-depends: async, base, bytestring, data-default-class, directory, hspec == 2.*, hspec-wai >= 0.6.3, http-types, lifted-base, network, scotty, text, wai build-tool-depends: hspec-discover:hspec-discover == 2.* GHC-options: -Wall -threaded -fno-warn-orphans benchmark weigh main-is: Main.hs type: exitcode-stdio-1.0 default-language: Haskell2010 hs-source-dirs: bench build-depends: base, scotty, lucid, bytestring, mtl, text, transformers, data-default-class, weigh == 0.0.16 GHC-options: -Wall -O2 -threaded source-repository head type: git location: git://github.com/scotty-web/scotty.git scotty-0.12/test/0000755000000000000000000000000007346545000012131 5ustar0000000000000000scotty-0.12/test/Spec.hs0000644000000000000000000000005407346545000013356 0ustar0000000000000000{-# OPTIONS_GHC -F -pgmF hspec-discover #-} scotty-0.12/test/Web/0000755000000000000000000000000007346545000012646 5ustar0000000000000000scotty-0.12/test/Web/ScottySpec.hs0000644000000000000000000002143407346545000015306 0ustar0000000000000000{-# LANGUAGE OverloadedStrings, CPP #-} module Web.ScottySpec (main, spec) where import Test.Hspec import Test.Hspec.Wai import Control.Applicative import Control.Monad import Data.Char import Data.String import Network.HTTP.Types import qualified Control.Exception.Lifted as EL import qualified Control.Exception as E import Web.Scotty as Scotty hiding (get, post, put, patch, delete, request, options) import qualified Web.Scotty as Scotty #if !defined(mingw32_HOST_OS) import Control.Concurrent.Async (withAsync) import Control.Exception (bracketOnError) import qualified Data.ByteString as BS import Data.ByteString (ByteString) import Data.Default.Class (def) import Network.Socket (Family(..), SockAddr(..), Socket, SocketOption(..), SocketType(..), bind, close, connect, listen, maxListenQueue, setSocketOption, socket) import Network.Socket.ByteString (send, recv) import System.Directory (removeFile) #endif main :: IO () main = hspec spec availableMethods :: [StdMethod] availableMethods = [GET, POST, HEAD, PUT, PATCH, DELETE, OPTIONS] spec :: Spec spec = do describe "ScottyM" $ do forM_ [ ("GET", Scotty.get, get) , ("POST", Scotty.post, (`post` "")) , ("PUT", Scotty.put, (`put` "")) , ("PATCH", Scotty.patch, (`patch` "")) , ("DELETE", Scotty.delete, delete) , ("OPTIONS", Scotty.options, options) ] $ \(method, route, makeRequest) -> do describe (map toLower method) $ do withApp (route "/scotty" $ html "") $ do it ("adds route for " ++ method ++ " requests") $ do makeRequest "/scotty" `shouldRespondWith` 200 withApp (route "/scotty" $ html "") $ do it ("properly handles extra slash routes for " ++ method ++ " requests") $ do makeRequest "//scotty" `shouldRespondWith` 200 describe "addroute" $ do forM_ availableMethods $ \method -> do withApp (addroute method "/scotty" $ html "") $ do it ("can be used to add route for " ++ show method ++ " requests") $ do request (renderStdMethod method) "/scotty" [] "" `shouldRespondWith` 200 describe "matchAny" $ do withApp (matchAny "/scotty" $ html "") $ do forM_ ("NONSTANDARD" : fmap renderStdMethod availableMethods) $ \method -> do it ("adds route that matches " ++ show method ++ " requests") $ do request method "/scotty" [] "" `shouldRespondWith` 200 describe "notFound" $ do withApp (notFound $ html "my custom not found page") $ do it "adds handler for requests that do not match any route" $ do get "/somewhere" `shouldRespondWith` "my custom not found page" {matchStatus = 404} withApp (notFound $ status status400 >> html "my custom not found page") $ do it "allows to customize the HTTP status code" $ do get "/somewhere" `shouldRespondWith` "my custom not found page" {matchStatus = 400} context "when not specified" $ do withApp (return ()) $ do it "returns 404 when no route matches" $ do get "/" `shouldRespondWith` "

404: File Not Found!

" {matchStatus = 404} describe "defaultHandler" $ do withApp (defaultHandler text >> Scotty.get "/" (liftAndCatchIO $ E.throwIO E.DivideByZero)) $ do it "sets custom exception handler" $ do get "/" `shouldRespondWith` "divide by zero" {matchStatus = 500} withApp (defaultHandler (\_ -> status status503) >> Scotty.get "/" (liftAndCatchIO $ E.throwIO E.DivideByZero)) $ do it "allows to customize the HTTP status code" $ do get "/" `shouldRespondWith` "" {matchStatus = 503} context "when not specified" $ do withApp (Scotty.get "/" $ liftAndCatchIO $ E.throwIO E.DivideByZero) $ do it "returns 500 on exceptions" $ do get "/" `shouldRespondWith` "

500 Internal Server Error

divide by zero" {matchStatus = 500} describe "ActionM" $ do withApp (Scotty.get "/" $ (undefined `EL.catch` ((\_ -> html "") :: E.SomeException -> ActionM ()))) $ do it "has a MonadBaseControl instance" $ do get "/" `shouldRespondWith` 200 withApp (Scotty.get "/dictionary" $ empty <|> param "word1" <|> empty <|> param "word2" >>= text) $ it "has an Alternative instance" $ do get "/dictionary?word1=haskell" `shouldRespondWith` "haskell" get "/dictionary?word2=scotty" `shouldRespondWith` "scotty" get "/dictionary?word1=a&word2=b" `shouldRespondWith` "a" describe "param" $ do withApp (Scotty.matchAny "/search" $ param "query" >>= text) $ do it "returns query parameter with given name" $ do get "/search?query=haskell" `shouldRespondWith` "haskell" context "when used with application/x-www-form-urlencoded data" $ do it "returns POST parameter with given name" $ do request "POST" "/search" [("Content-Type","application/x-www-form-urlencoded")] "query=haskell" `shouldRespondWith` "haskell" it "replaces non UTF-8 bytes with Unicode replacement character" $ do request "POST" "/search" [("Content-Type","application/x-www-form-urlencoded")] "query=\xe9" `shouldRespondWith` "\xfffd" describe "text" $ do let modernGreekText :: IsString a => a modernGreekText = "νέα ελληνικά" withApp (Scotty.get "/scotty" $ text modernGreekText) $ do it "sets body to given text" $ do get "/scotty" `shouldRespondWith` modernGreekText it "sets Content-Type header to \"text/plain; charset=utf-8\"" $ do get "/scotty" `shouldRespondWith` 200 {matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]} withApp (Scotty.get "/scotty" $ setHeader "Content-Type" "text/somethingweird" >> text modernGreekText) $ do it "doesn't override a previously set Content-Type header" $ do get "/scotty" `shouldRespondWith` 200 {matchHeaders = ["Content-Type" <:> "text/somethingweird"]} describe "html" $ do let russianLanguageTextInHtml :: IsString a => a russianLanguageTextInHtml = "

ру́сский язы́к

" withApp (Scotty.get "/scotty" $ html russianLanguageTextInHtml) $ do it "sets body to given text" $ do get "/scotty" `shouldRespondWith` russianLanguageTextInHtml it "sets Content-Type header to \"text/html; charset=utf-8\"" $ do get "/scotty" `shouldRespondWith` 200 {matchHeaders = ["Content-Type" <:> "text/html; charset=utf-8"]} withApp (Scotty.get "/scotty" $ setHeader "Content-Type" "text/somethingweird" >> html russianLanguageTextInHtml) $ do it "doesn't override a previously set Content-Type header" $ do get "/scotty" `shouldRespondWith` 200 {matchHeaders = ["Content-Type" <:> "text/somethingweird"]} describe "json" $ do withApp (Scotty.get "/scotty" $ setHeader "Content-Type" "text/somethingweird" >> json (Just (5::Int))) $ do it "doesn't override a previously set Content-Type header" $ do get "/scotty" `shouldRespondWith` 200 {matchHeaders = ["Content-Type" <:> "text/somethingweird"]} describe "finish" $ do withApp (Scotty.get "/scotty" $ finish) $ do it "responds with 200 by default" $ do get "/scotty" `shouldRespondWith` 200 withApp (Scotty.get "/scotty" $ status status400 >> finish >> status status200) $ do it "stops the execution of an action" $ do get "/scotty" `shouldRespondWith` 400 -- Unix sockets not available on Windows #if !defined(mingw32_HOST_OS) describe "scottySocket" . it "works with a unix socket" . withServer (Scotty.get "/scotty" $ html "") . E.bracket (socket AF_UNIX Stream 0) close $ \sock -> do connect sock $ SockAddrUnix socketPath _ <- send sock "GET /scotty HTTP/1.1\r\n\n" r1 <- recv sock 1024 _ <- send sock "GET /four-oh-four HTTP/1.1\r\n\n" r2 <- recv sock 1024 (BS.take (BS.length ok) r1, BS.take (BS.length no) r2) `shouldBe` (ok, no) where ok, no :: ByteString ok = "HTTP/1.1 200 OK" no = "HTTP/1.1 404 Not Found" withApp = with . scottyApp socketPath :: FilePath socketPath = "/tmp/scotty-test.socket" withServer :: ScottyM () -> IO a -> IO a withServer actions inner = E.bracket (listenOn socketPath) (\sock -> close sock >> removeFile socketPath) (\sock -> withAsync (Scotty.scottySocket def sock actions) $ const inner) -- See https://github.com/haskell/network/issues/318 listenOn :: String -> IO Socket listenOn path = bracketOnError (socket AF_UNIX Stream 0) close (\sock -> do setSocketOption sock ReuseAddr 1 bind sock (SockAddrUnix path) listen sock maxListenQueue return sock ) #endif