airship 0.3.0.0 → 0.4.0.0
raw patch · 13 files changed
+299/−470 lines, 13 filesdep +microlensdep +mmorphdep +wai-extradep −warpdep ~basedep ~eitherdep ~http-typesPVP ok
version bump matches the API change (PVP)
Dependencies added: microlens, mmorph, wai-extra
Dependencies removed: warp
Dependency ranges changed: base, either, http-types, tasty, tasty-quickcheck, wai
API changes (from Hackage documentation)
- Airship.Helpers: fromWaiRequest :: Request -> Request IO
- Airship.Resource: allowMissingPost :: Resource s m -> Handler s m Bool
- Airship.Resource: allowedMethods :: Resource s m -> Handler s m [Method]
- Airship.Resource: contentTypesAccepted :: Resource s m -> Handler s m [(MediaType, Handler s m ())]
- Airship.Resource: contentTypesProvided :: Resource s m -> Handler s m [(MediaType, Webmachine s m (ResponseBody m))]
- Airship.Resource: deleteCompleted :: Resource s m -> Handler s m Bool
- Airship.Resource: deleteResource :: Resource s m -> Handler s m Bool
- Airship.Resource: entityTooLarge :: Resource s m -> Handler s m Bool
- Airship.Resource: forbidden :: Resource s m -> Handler s m Bool
- Airship.Resource: generateETag :: Resource s m -> Handler s m (Maybe ETag)
- Airship.Resource: implemented :: Resource s m -> Handler s m Bool
- Airship.Resource: isAuthorized :: Resource s m -> Handler s m Bool
- Airship.Resource: isConflict :: Resource s m -> Handler s m Bool
- Airship.Resource: knownContentType :: Resource s m -> Handler s m Bool
- Airship.Resource: languageAvailable :: Resource s m -> Handler s m Bool
- Airship.Resource: lastModified :: Resource s m -> Handler s m (Maybe UTCTime)
- Airship.Resource: malformedRequest :: Resource s m -> Handler s m Bool
- Airship.Resource: movedPermanently :: Resource s m -> Handler s m (Maybe ByteString)
- Airship.Resource: movedTemporarily :: Resource s m -> Handler s m (Maybe ByteString)
- Airship.Resource: multipleChoices :: Resource s m -> Handler s m Bool
- Airship.Resource: previouslyExisted :: Resource s m -> Handler s m Bool
- Airship.Resource: processPost :: Resource s m -> Handler s m (PostResponse s m)
- Airship.Resource: resourceExists :: Resource s m -> Handler s m Bool
- Airship.Resource: serviceAvailable :: Resource s m -> Handler s m Bool
- Airship.Resource: uriTooLong :: Resource s m -> Handler s m Bool
- Airship.Resource: validContentHeaders :: Resource s m -> Handler s m Bool
- Airship.Resource.Static: _etag :: FileInfo -> ETag
- Airship.Resource.Static: _lastModified :: FileInfo -> UTCTime
- Airship.Resource.Static: _path :: FileInfo -> FilePath
- Airship.Resource.Static: _size :: FileInfo -> Integer
- Airship.Resource.Static: instance Eq FileInfo
- Airship.Resource.Static: instance Eq StaticOptions
- Airship.Resource.Static: instance Ord FileInfo
- Airship.Resource.Static: instance Show FileInfo
- Airship.Types: Request :: Method -> HttpVersion -> ByteString -> ByteString -> RequestHeaders -> Bool -> SockAddr -> [Text] -> Query -> m ByteString -> RequestBodyLength -> Maybe ByteString -> Maybe ByteString -> Request m
- Airship.Types: _dispatchPath :: ResponseState s m -> [Text]
- Airship.Types: _params :: ResponseState s m -> HashMap Text Text
- Airship.Types: _responseBody :: Response m -> ResponseBody m
- Airship.Types: _responseHeaders :: Response m -> ResponseHeaders
- Airship.Types: _responseStatus :: Response m -> Status
- Airship.Types: getState :: Handler s m s
- Airship.Types: httpVersion :: Request m -> HttpVersion
- Airship.Types: instance Eq ETag
- Airship.Types: instance Monad m => Applicative (Webmachine s m)
- Airship.Types: instance Monad m => Functor (Webmachine s m)
- Airship.Types: instance Monad m => Monad (Webmachine s m)
- Airship.Types: instance Monad m => MonadReader (RequestReader m) (Webmachine s m)
- Airship.Types: instance Monad m => MonadState (ResponseState s m) (Webmachine s m)
- Airship.Types: instance Monad m => MonadWriter Trace (Webmachine s m)
- Airship.Types: instance MonadBase b m => MonadBase b (Webmachine s m)
- Airship.Types: instance MonadBaseControl b m => MonadBaseControl b (Webmachine s m)
- Airship.Types: instance MonadIO m => MonadIO (Webmachine s m)
- Airship.Types: instance MonadTrans (Webmachine s)
- Airship.Types: instance Ord ETag
- Airship.Types: instance Show ETag
- Airship.Types: isSecure :: Request m -> Bool
- Airship.Types: modifyState :: (s -> s) -> Handler s m ()
- Airship.Types: pathInfo :: Request m -> [Text]
- Airship.Types: putState :: s -> Handler s m ()
- Airship.Types: queryString :: Request m -> Query
- Airship.Types: rawPathInfo :: Request m -> ByteString
- Airship.Types: rawQueryString :: Request m -> ByteString
- Airship.Types: remoteHost :: Request m -> SockAddr
- Airship.Types: requestBody :: Request m -> m ByteString
- Airship.Types: requestBodyLength :: Request m -> RequestBodyLength
- Airship.Types: requestHeaderHost :: Request m -> Maybe ByteString
- Airship.Types: requestHeaderRange :: Request m -> Maybe ByteString
- Airship.Types: requestHeaders :: Request m -> RequestHeaders
- Airship.Types: requestMethod :: Request m -> Method
- Airship.Types: stateBody :: ResponseState s m -> ResponseBody m
- Airship.Types: stateHeaders :: ResponseState s m -> ResponseHeaders
- Airship.Types: stateUser :: ResponseState s m -> s
- Airship.Types: type Handler s m a = Monad m => Webmachine s m a
+ Airship.Config: IncludeHeader :: HeaderInclusion
+ Airship.Config: OmitHeader :: HeaderInclusion
+ Airship.Config: data AirshipConfig
+ Airship.Config: data HeaderInclusion
+ Airship.Config: defaultAirshipConfig :: AirshipConfig
+ Airship.Config: includeQuipHeader :: Lens' AirshipConfig HeaderInclusion
+ Airship.Config: includeTraceHeader :: Lens' AirshipConfig HeaderInclusion
+ Airship.Config: instance GHC.Classes.Eq Airship.Config.HeaderInclusion
+ Airship.Config: instance GHC.Show.Show Airship.Config.HeaderInclusion
+ Airship.Helpers: parseFormData :: Request -> IO ([Param], [File ByteString])
+ Airship.Helpers: redirectPermanently :: Monad m => ByteString -> Webmachine m a
+ Airship.Helpers: redirectTemporarily :: Monad m => ByteString -> Webmachine m a
+ Airship.Helpers: resourceToWaiT :: Monad m => AirshipConfig -> (Request -> m Response -> IO Response) -> RoutingSpec m () -> Resource m -> Application
+ Airship.Resource: [allowMissingPost] :: Resource m -> Webmachine m Bool
+ Airship.Resource: [allowedMethods] :: Resource m -> Webmachine m [Method]
+ Airship.Resource: [contentTypesAccepted] :: Resource m -> Webmachine m [(MediaType, Webmachine m ())]
+ Airship.Resource: [contentTypesProvided] :: Resource m -> Webmachine m [(MediaType, Webmachine m ResponseBody)]
+ Airship.Resource: [deleteCompleted] :: Resource m -> Webmachine m Bool
+ Airship.Resource: [deleteResource] :: Resource m -> Webmachine m Bool
+ Airship.Resource: [entityTooLarge] :: Resource m -> Webmachine m Bool
+ Airship.Resource: [forbidden] :: Resource m -> Webmachine m Bool
+ Airship.Resource: [generateETag] :: Resource m -> Webmachine m (Maybe ETag)
+ Airship.Resource: [implemented] :: Resource m -> Webmachine m Bool
+ Airship.Resource: [isAuthorized] :: Resource m -> Webmachine m Bool
+ Airship.Resource: [isConflict] :: Resource m -> Webmachine m Bool
+ Airship.Resource: [knownContentType] :: Resource m -> Webmachine m Bool
+ Airship.Resource: [languageAvailable] :: Resource m -> Webmachine m Bool
+ Airship.Resource: [lastModified] :: Resource m -> Webmachine m (Maybe UTCTime)
+ Airship.Resource: [malformedRequest] :: Resource m -> Webmachine m Bool
+ Airship.Resource: [movedPermanently] :: Resource m -> Webmachine m (Maybe ByteString)
+ Airship.Resource: [movedTemporarily] :: Resource m -> Webmachine m (Maybe ByteString)
+ Airship.Resource: [multipleChoices] :: Resource m -> Webmachine m Bool
+ Airship.Resource: [previouslyExisted] :: Resource m -> Webmachine m Bool
+ Airship.Resource: [processPost] :: Resource m -> Webmachine m (PostResponse m)
+ Airship.Resource: [resourceExists] :: Resource m -> Webmachine m Bool
+ Airship.Resource: [serviceAvailable] :: Resource m -> Webmachine m Bool
+ Airship.Resource: [uriTooLong] :: Resource m -> Webmachine m Bool
+ Airship.Resource: [validContentHeaders] :: Resource m -> Webmachine m Bool
+ Airship.Resource.Static: [_etag] :: FileInfo -> ETag
+ Airship.Resource.Static: [_lastModified] :: FileInfo -> UTCTime
+ Airship.Resource.Static: [_path] :: FileInfo -> FilePath
+ Airship.Resource.Static: [_size] :: FileInfo -> Integer
+ Airship.Resource.Static: instance GHC.Classes.Eq Airship.Resource.Static.FileInfo
+ Airship.Resource.Static: instance GHC.Classes.Eq Airship.Resource.Static.StaticOptions
+ Airship.Resource.Static: instance GHC.Classes.Ord Airship.Resource.Static.FileInfo
+ Airship.Resource.Static: instance GHC.Show.Show Airship.Resource.Static.FileInfo
+ Airship.Types: [_dispatchPath] :: ResponseState -> [Text]
+ Airship.Types: [_params] :: ResponseState -> HashMap Text Text
+ Airship.Types: [_responseBody] :: Response -> ResponseBody
+ Airship.Types: [_responseHeaders] :: Response -> ResponseHeaders
+ Airship.Types: [_responseStatus] :: Response -> Status
+ Airship.Types: [stateBody] :: ResponseState -> ResponseBody
+ Airship.Types: [stateHeaders] :: ResponseState -> ResponseHeaders
+ Airship.Types: instance Control.Monad.Base.MonadBase b m => Control.Monad.Base.MonadBase b (Airship.Types.Webmachine m)
+ Airship.Types: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Airship.Types.Webmachine m)
+ Airship.Types: instance Control.Monad.Trans.Class.MonadTrans Airship.Types.Webmachine
+ Airship.Types: instance Control.Monad.Trans.Control.MonadBaseControl b m => Control.Monad.Trans.Control.MonadBaseControl b (Airship.Types.Webmachine m)
+ Airship.Types: instance GHC.Base.Monad m => Control.Monad.Reader.Class.MonadReader Airship.Types.RequestReader (Airship.Types.Webmachine m)
+ Airship.Types: instance GHC.Base.Monad m => Control.Monad.State.Class.MonadState Airship.Types.ResponseState (Airship.Types.Webmachine m)
+ Airship.Types: instance GHC.Base.Monad m => Control.Monad.Writer.Class.MonadWriter Airship.Types.Trace (Airship.Types.Webmachine m)
+ Airship.Types: instance GHC.Base.Monad m => GHC.Base.Applicative (Airship.Types.Webmachine m)
+ Airship.Types: instance GHC.Base.Monad m => GHC.Base.Functor (Airship.Types.Webmachine m)
+ Airship.Types: instance GHC.Base.Monad m => GHC.Base.Monad (Airship.Types.Webmachine m)
+ Airship.Types: instance GHC.Classes.Eq Airship.Types.ETag
+ Airship.Types: instance GHC.Classes.Ord Airship.Types.ETag
+ Airship.Types: instance GHC.Show.Show Airship.Types.ETag
- Airship.Headers: addResponseHeader :: Header -> Handler s m ()
+ Airship.Headers: addResponseHeader :: Monad m => Header -> Webmachine m ()
- Airship.Headers: modifyResponseHeaders :: (ResponseHeaders -> ResponseHeaders) -> Handler s m ()
+ Airship.Headers: modifyResponseHeaders :: Monad m => (ResponseHeaders -> ResponseHeaders) -> Webmachine m ()
- Airship.Helpers: contentTypeMatches :: [MediaType] -> Handler s m Bool
+ Airship.Helpers: contentTypeMatches :: Monad m => [MediaType] -> Webmachine m Bool
- Airship.Helpers: resourceToWai :: RoutingSpec s IO () -> Resource s IO -> s -> Application
+ Airship.Helpers: resourceToWai :: AirshipConfig -> RoutingSpec IO () -> Resource IO -> Application
- Airship.Resource: PostCreate :: [Text] -> PostResponse s m
+ Airship.Resource: PostCreate :: [Text] -> PostResponse m
- Airship.Resource: PostCreateRedirect :: [Text] -> PostResponse s m
+ Airship.Resource: PostCreateRedirect :: [Text] -> PostResponse m
- Airship.Resource: PostProcess :: (Handler s m ()) -> PostResponse s m
+ Airship.Resource: PostProcess :: (Webmachine m ()) -> PostResponse m
- Airship.Resource: PostProcessRedirect :: (Handler s m ByteString) -> PostResponse s m
+ Airship.Resource: PostProcessRedirect :: (Webmachine m ByteString) -> PostResponse m
- Airship.Resource: Resource :: Handler s m Bool -> Handler s m [Method] -> Handler s m [(MediaType, Handler s m ())] -> Handler s m [(MediaType, Webmachine s m (ResponseBody m))] -> Handler s m Bool -> Handler s m Bool -> Handler s m Bool -> Handler s m Bool -> Handler s m (Maybe ETag) -> Handler s m Bool -> Handler s m Bool -> Handler s m Bool -> Handler s m Bool -> Handler s m (Maybe UTCTime) -> Handler s m Bool -> Handler s m Bool -> Handler s m (Maybe ByteString) -> Handler s m (Maybe ByteString) -> Handler s m Bool -> Handler s m Bool -> Handler s m (PostResponse s m) -> Handler s m Bool -> Handler s m Bool -> Handler s m Bool -> Handler s m Bool -> Resource s m
+ Airship.Resource: Resource :: Webmachine m Bool -> Webmachine m [Method] -> Webmachine m [(MediaType, Webmachine m ())] -> Webmachine m [(MediaType, Webmachine m ResponseBody)] -> Webmachine m Bool -> Webmachine m Bool -> Webmachine m Bool -> Webmachine m Bool -> Webmachine m (Maybe ETag) -> Webmachine m Bool -> Webmachine m Bool -> Webmachine m Bool -> Webmachine m Bool -> Webmachine m (Maybe UTCTime) -> Webmachine m Bool -> Webmachine m Bool -> Webmachine m (Maybe ByteString) -> Webmachine m (Maybe ByteString) -> Webmachine m Bool -> Webmachine m Bool -> Webmachine m (PostResponse m) -> Webmachine m Bool -> Webmachine m Bool -> Webmachine m Bool -> Webmachine m Bool -> Resource m
- Airship.Resource: data PostResponse s m
+ Airship.Resource: data PostResponse m
- Airship.Resource: data Resource s m
+ Airship.Resource: data Resource m
- Airship.Resource: defaultResource :: Resource s m
+ Airship.Resource: defaultResource :: Monad m => Resource m
- Airship.Resource: serverError :: Handler m s a
+ Airship.Resource: serverError :: Monad m => Webmachine m a
- Airship.Resource.Static: staticResource :: StaticOptions -> FilePath -> IO (Resource s m)
+ Airship.Resource.Static: staticResource :: Monad m => StaticOptions -> FilePath -> IO (Resource m)
- Airship.Route: data RoutingSpec s m a
+ Airship.Route: data RoutingSpec m a
- Airship.Types: Empty :: ResponseBody m
+ Airship.Types: Empty :: ResponseBody
- Airship.Types: Response :: Status -> ResponseHeaders -> ResponseBody m -> Response m
+ Airship.Types: Response :: Status -> ResponseHeaders -> ResponseBody -> Response
- Airship.Types: ResponseBuilder :: Builder -> ResponseBody m
+ Airship.Types: ResponseBuilder :: Builder -> ResponseBody
- Airship.Types: ResponseFile :: FilePath -> (Maybe FilePart) -> ResponseBody m
+ Airship.Types: ResponseFile :: FilePath -> (Maybe FilePart) -> ResponseBody
- Airship.Types: ResponseState :: s -> ResponseHeaders -> ResponseBody m -> HashMap Text Text -> [Text] -> ResponseState s m
+ Airship.Types: ResponseState :: ResponseHeaders -> ResponseBody -> HashMap Text Text -> [Text] -> ResponseState
- Airship.Types: ResponseStream :: (StreamingBody m) -> ResponseBody m
+ Airship.Types: ResponseStream :: StreamingBody -> ResponseBody
- Airship.Types: data Request m
+ Airship.Types: data Request :: *
- Airship.Types: data Response m
+ Airship.Types: data Response
- Airship.Types: data ResponseBody m
+ Airship.Types: data ResponseBody
- Airship.Types: data ResponseState s m
+ Airship.Types: data ResponseState
- Airship.Types: data Webmachine s m a
+ Airship.Types: data Webmachine m a
- Airship.Types: defaultRequest :: Monad m => Request m
+ Airship.Types: defaultRequest :: Request
- Airship.Types: dispatchPath :: Handler s m [Text]
+ Airship.Types: dispatchPath :: Handler m [Text]
- Airship.Types: eitherResponse :: Monad m => UTCTime -> HashMap Text Text -> [Text] -> Request m -> s -> Handler s m (Response m) -> m (Response m, Trace)
+ Airship.Types: eitherResponse :: Monad m => UTCTime -> HashMap Text Text -> [Text] -> Request -> Webmachine m Response -> m (Response, Trace)
- Airship.Types: entireRequestBody :: Monad m => Request m -> m ByteString
+ Airship.Types: entireRequestBody :: MonadIO m => Request -> m ByteString
- Airship.Types: escapedResponse :: Text -> ResponseBody m
+ Airship.Types: escapedResponse :: Text -> ResponseBody
- Airship.Types: finishWith :: Response m -> Handler s m a
+ Airship.Types: finishWith :: Monad m => Response -> Webmachine m a
- Airship.Types: getResponseBody :: Handler s m (ResponseBody m)
+ Airship.Types: getResponseBody :: Handler m ResponseBody
- Airship.Types: getResponseHeaders :: Handler s m ResponseHeaders
+ Airship.Types: getResponseHeaders :: Handler m ResponseHeaders
- Airship.Types: halt :: Status -> Handler m s a
+ Airship.Types: halt :: Monad m => Status -> Webmachine m a
- Airship.Types: params :: Handler s m (HashMap Text Text)
+ Airship.Types: params :: Handler m (HashMap Text Text)
- Airship.Types: putResponseBS :: ByteString -> Handler s m ()
+ Airship.Types: putResponseBS :: Monad m => ByteString -> Webmachine m ()
- Airship.Types: putResponseBody :: ResponseBody m -> Handler s m ()
+ Airship.Types: putResponseBody :: ResponseBody -> Handler m ()
- Airship.Types: request :: Handler s m (Request m)
+ Airship.Types: request :: Handler m Request
- Airship.Types: requestTime :: Handler s m UTCTime
+ Airship.Types: requestTime :: Handler m UTCTime
- Airship.Types: runWebmachine :: Monad m => UTCTime -> HashMap Text Text -> [Text] -> Request m -> s -> Handler s m a -> m (Either (Response m) a, Trace)
+ Airship.Types: runWebmachine :: Monad m => UTCTime -> HashMap Text Text -> [Text] -> Request -> Webmachine m a -> m (Either (Response) a, Trace)
Files
- airship.cabal +12/−23
- bin/Main.hs +0/−135
- src/Airship.hs +6/−1
- src/Airship/Config.hs +41/−0
- src/Airship/Headers.hs +4/−4
- src/Airship/Helpers.hs +5/−2
- src/Airship/Internal/Decision.hs +76/−94
- src/Airship/Internal/Helpers.hs +51/−34
- src/Airship/Internal/Route.hs +6/−6
- src/Airship/Resource.hs +33/−33
- src/Airship/Resource/Static.hs +5/−5
- src/Airship/Types.hs +51/−111
- test/unit/test.hs +9/−22
airship.cabal view
@@ -1,7 +1,9 @@ name: airship synopsis: A Webmachine-inspired HTTP library description: A Webmachine-inspired HTTP library-version: 0.3.0.0+homepage: https://github.com/helium/airship/+Bug-reports: https://github.com/helium/airship/issues+version: 0.4.0.0 license: MIT license-file: LICENSE author: Reid Draper and Patrick Thomson@@ -19,6 +21,7 @@ hs-source-dirs: src ghc-options: -Wall exposed-modules: Airship+ , Airship.Config , Airship.Headers , Airship.Helpers , Airship.Types@@ -41,14 +44,16 @@ , case-insensitive , cryptohash == 0.11.6.* , directory- , either == 4.3.*+ , either >= 4.3 && < 4.6 , filepath >= 1.3 && < 1.5 , http-date , http-media- , http-types >= 0.7+ , http-types == 0.9.* , lifted-base == 0.2.*- , mime-types == 0.1.0.*+ , microlens , monad-control >= 1.0+ , mime-types == 0.1.0.*+ , mmorph == 1.0.* , mtl , network , old-locale@@ -60,23 +65,7 @@ , unix == 2.7.* , unordered-containers , wai == 3.0.*--executable airship-example- main-is: Main.hs- hs-source-dirs: bin- ghc-options: -Wall- default-language: Haskell2010- build-depends: base >=4.7 && < 5- , airship- , blaze-builder >=0.3 && < 0.5- , bytestring- , http-types >= 0.7- , mtl- , text- , time- , unordered-containers- , wai == 3.0.2.*- , warp == 3.0.*+ , wai-extra == 3.0.* test-suite unit default-language: Haskell2010@@ -87,8 +76,8 @@ , airship , text == 1.2.* , bytestring >= 0.9.1 && < 0.11- , tasty == 0.10.*- , tasty-quickcheck == 0.8.3.*+ , tasty >= 0.10.1 && < 0.12+ , tasty-quickcheck >= 0.8.3 && < 0.8.5 , tasty-hunit >= 0.9.1 && < 0.10 , transformers , wai == 3.0.*
− bin/Main.hs
@@ -1,135 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}--module Main where--import Airship-import Airship.Resource.Static (StaticOptions(..), staticResource)--import Blaze.ByteString.Builder.Html.Utf8 (fromHtmlEscapedText)--#if __GLASGOW_HASKELL__ < 710-import Control.Applicative ((<$>))-#endif-import Control.Concurrent.MVar-import Control.Monad.Trans (liftIO)--import Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as HM-import qualified Data.ByteString.Lazy as LB-import Data.ByteString.Lazy.Char8 (unpack)-import Data.Maybe (fromMaybe)-import Data.Monoid ((<>))-import Data.Text(Text, pack)-import Data.Time.Clock--import qualified Network.HTTP.Types as HTTP-import Network.Wai.Handler.Warp ( runSettings- , defaultSettings- , setPort- , setHost- )---- ***************************************************************************--- Helpers--- ***************************************************************************--getBody :: Handler s IO LB.ByteString-getBody = do- req <- request- liftIO (entireRequestBody req)--readBody :: Handler s IO Integer-readBody = read . unpack <$> getBody--routingParam :: Text -> Handler s m Text-routingParam t = do- p <- params- return (p HM.! t)--newtype State = State { _getState :: MVar (HashMap Text Integer) }--resourceWithBody :: Text -> Resource State IO-resourceWithBody t = defaultResource { contentTypesProvided = return [("text/plain", return (escapedResponse t))]- , lastModified = Just <$> liftIO getCurrentTime- , generateETag = return $ Just $ Strong "abc123"- }--accountResource :: Resource State IO-accountResource = defaultResource- { allowedMethods = return [ HTTP.methodGet- , HTTP.methodHead- , HTTP.methodPost- , HTTP.methodPut- ]- , knownContentType = contentTypeMatches ["text/plain"]-- , contentTypesProvided = do- let textAction = do- s <- getState- m <- liftIO (readMVar (_getState s))- accountNameM <- HM.lookup "name" <$> params- let val = fromMaybe 0 (accountNameM >>= flip HM.lookup m)- return $ ResponseBuilder (fromHtmlEscapedText- (pack (show val) <> "\n"))- return [("text/plain", textAction)]-- , allowMissingPost = return False-- , lastModified = Just <$> liftIO getCurrentTime-- , resourceExists = do- accountName <- routingParam "name"- s <- getState- m <- liftIO (readMVar (_getState s))- return $ HM.member accountName m-- -- POST'ing to this resource adds the integer to the current value- , processPost = return (PostProcess $ do- (val, accountName, s) <- postPutStates- liftIO (modifyMVar_ (_getState s) (return . HM.insertWith (+) accountName val))- return ()- )-- , contentTypesAccepted = return [("text/plain", do- (val, accountName, s) <- postPutStates- liftIO (modifyMVar_ (_getState s) (return . HM.insert accountName val))- return ()- )]- }--postPutStates :: Handler State IO (Integer, Text, State)-postPutStates = do- val <- readBody- accountName <- routingParam "name"- s <- getState- return (val, accountName, s)--myRoutes :: Resource State IO -> RoutingSpec State IO ()-myRoutes static = do- root #> resourceWithBody "Just the root resource"- "account" </> var "name" #> accountResource- "static" </> star #> static--main :: IO ()-main = do- static <- staticResource Cache "assets"- let port = 3000- host = "127.0.0.1"- settings = setPort port (setHost host defaultSettings)- routes = myRoutes static- response404 = escapedResponse "<html><head></head><body><h1>404 Not Found</h1></body></html>"- resource404 = defaultResource { resourceExists = return False- , contentTypesProvided = return- [ ( "text/html"- , return response404- )- ]- }-- mvar <- newMVar HM.empty- let s = State mvar- putStrLn "Listening on port 3000"- runSettings settings (resourceToWai routes resource404 s)
src/Airship.hs view
@@ -1,11 +1,16 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+ module Airship- ( module Airship.Resource+ ( module Airship.Config+ , module Airship.Resource , module Airship.Headers , module Airship.Helpers , module Airship.Route , module Airship.Types ) where +import Airship.Config import Airship.Headers import Airship.Helpers import Airship.Resource
+ src/Airship/Config.hs view
@@ -0,0 +1,41 @@+module Airship.Config+ ( AirshipConfig+ , HeaderInclusion (..)+ , includeTraceHeader+ , includeQuipHeader+ , defaultAirshipConfig+ ) where++import Lens.Micro (Lens', lens)++-- | An opaque data type encapsulating all Airship-specific configuration options.+--+-- We use lenses to modify 'AirshipConfig' values -- though Airship only depends on the+-- microlens library, its lenses are compatible with Control.Lens.+data AirshipConfig = AirshipConfig+ { _includeTraceHeader :: HeaderInclusion+ , _includeQuipHeader :: HeaderInclusion+ }++data HeaderInclusion = IncludeHeader | OmitHeader deriving (Eq, Show)++-- | Determines whether or not the @Airship-Trace@ header, which traces the execution of+-- a given request in the Airship decision tree, is included in every HTTP response.+-- While exposing the decision tree is usually innocuous (and makes for significantly easier+-- debugging), you may want to turn it off in certain circumstances.+--+-- Defaults to 'IncludeHeader' (enabled).+includeTraceHeader :: Lens' AirshipConfig HeaderInclusion+includeTraceHeader = lens _includeTraceHeader (\s n -> s { _includeTraceHeader = n })++-- | Determines whether or not the @Airship-Quip@ header, which includes a pithy+-- quote in your response headers, is included in every HTTP response.+--+-- Defaults to 'IncludeHeader' (enabled).+includeQuipHeader :: Lens' AirshipConfig HeaderInclusion+includeQuipHeader = lens _includeQuipHeader (\s n -> s { _includeQuipHeader = n })++-- | The default configuration. Use this, in conjunction with the lenses declared+-- above, to get and modify an 'AirshipConfig' to pass to 'resourceToWai'.+defaultAirshipConfig :: AirshipConfig+defaultAirshipConfig = AirshipConfig IncludeHeader IncludeHeader
src/Airship/Headers.hs view
@@ -5,15 +5,15 @@ , modifyResponseHeaders ) where -import Airship.Types (Handler, ResponseState(..))+import Airship.Types (Webmachine, ResponseState(..)) import Control.Monad.State.Class (modify) import Network.HTTP.Types (ResponseHeaders, Header) --- | Applies the given function to the 'ResponseHeaders' present in this 'Handler''s 'ResponseState'.-modifyResponseHeaders :: (ResponseHeaders -> ResponseHeaders) -> Handler s m ()+-- | Applies the given function to the 'ResponseHeaders' present in this handlers 'ResponseState'.+modifyResponseHeaders :: Monad m => (ResponseHeaders -> ResponseHeaders) -> Webmachine m () modifyResponseHeaders f = modify updateHeaders where updateHeaders rs@ResponseState{stateHeaders = h} = rs { stateHeaders = f h } -- | Adds a given 'Header' to this handler's 'ResponseState'.-addResponseHeader :: Header -> Handler s m ()+addResponseHeader :: Monad m => Header -> Webmachine m () addResponseHeader h = modifyResponseHeaders (h :)
src/Airship/Helpers.hs view
@@ -1,7 +1,10 @@ module Airship.Helpers- ( contentTypeMatches- , fromWaiRequest+ ( parseFormData+ , contentTypeMatches+ , redirectTemporarily+ , redirectPermanently , resourceToWai+ , resourceToWaiT ) where import Airship.Internal.Helpers
src/Airship/Internal/Decision.hs view
@@ -35,65 +35,49 @@ get, modify) import Control.Monad.Writer.Class (tell) -import Data.ByteString (ByteString) import Blaze.ByteString.Builder (toByteString)-import Data.Maybe (fromJust, isJust)+import Data.Maybe (isJust) import Data.Text (Text) import Data.Time.Clock (UTCTime)+import Data.ByteString (ByteString, intercalate) import Network.HTTP.Media import qualified Network.HTTP.Types as HTTP----------------------------------------------------------------------------------- HTTP Headers--- These are headers not defined for us already in--- Network.HTTP.Types---------------------------------------------------------------------------------hAcceptCharset :: HTTP.HeaderName-hAcceptCharset = "Accept-Charset"--hAcceptEncoding :: HTTP.HeaderName-hAcceptEncoding = "Accept-Encoding"--hIfMatch :: HTTP.HeaderName-hIfMatch = "If-Match"--hIfUnmodifiedSince :: HTTP.HeaderName-hIfUnmodifiedSince = "If-Unmodified-Since"--hIfNoneMatch :: HTTP.HeaderName-hIfNoneMatch = "If-None-Match"--hIfModifiedSince :: HTTP.HeaderName-hIfModifiedSince = "If-Modified-Since"+import qualified Network.HTTP.Types.Header as HTTP ------------------------------------------------------------------------------ -- FlowState: StateT used for recording information as we walk the decision -- tree ------------------------------------------------------------------------------ -data FlowState s m = FlowState- { _contentType :: Maybe (MediaType, Webmachine s m (ResponseBody m)) }+data FlowState m = FlowState+ { _contentType :: Maybe (MediaType, Webmachine m ResponseBody) } -type FlowStateT s m a = StateT (FlowState s m) (Webmachine s m) a+type FlowStateT m a = StateT (FlowState m) (Webmachine m) a -type Flow s m = Resource s m -> FlowStateT s m (Response m)+type Flow m = Resource m -> FlowStateT m Response -initFlowState :: FlowState s m+initFlowState :: FlowState m initFlowState = FlowState Nothing -flow :: Monad m => Resource s m -> Webmachine s m (Response m)+flow :: Monad m => Resource m -> Webmachine m Response flow r = evalStateT (b13 r) initFlowState -trace :: Monad m => Text -> FlowStateT s m ()+trace :: Monad m => Text -> FlowStateT m () trace t = lift $ tell [t] ------------------------------------------------------------------------------+-- Header value data newtypes+------------------------------------------------------------------------------++newtype IfMatch = IfMatch ByteString+newtype IfNoneMatch = IfNoneMatch ByteString++------------------------------------------------------------------------------ -- Decision Helpers ------------------------------------------------------------------------------ -negotiateContentTypesAccepted :: Monad m => Resource s m -> FlowStateT s m ()+negotiateContentTypesAccepted :: Monad m => Resource m -> FlowStateT m () negotiateContentTypesAccepted Resource{..} = do req <- lift request accepted <- lift contentTypesAccepted@@ -105,13 +89,13 @@ (Just process) -> lift process Nothing -> lift $ halt HTTP.status415 -appendRequestPath :: Monad m => [Text] -> Webmachine s m ByteString+appendRequestPath :: Monad m => [Text] -> Webmachine m ByteString appendRequestPath ts = do currentPath <- pathInfo <$> request return $ toByteString (HTTP.encodePathSegments (currentPath ++ ts)) requestHeaderDate :: Monad m => HTTP.HeaderName ->- Webmachine s m (Maybe UTCTime)+ Webmachine m (Maybe UTCTime) requestHeaderDate headerName = do req <- request let reqHeaders = requestHeaders req@@ -119,7 +103,7 @@ parsedDate = dateHeader >>= parseRfc1123Date return parsedDate -writeCacheTags :: Monad m => Resource s m -> FlowStateT s m ()+writeCacheTags :: Monad m => Resource m -> FlowStateT m () writeCacheTags Resource{..} = lift $ do etag <- generateETag case etag of@@ -134,21 +118,24 @@ -- Type definitions for all decision nodes ------------------------------------------------------------------------------ -b13, b12, b11, b10, b09, b08, b07, b06, b05, b04, b03 :: Monad m => Flow s m-c04, c03 :: Monad m => Flow s m-d05, d04 :: Monad m => Flow s m-e06, e05 :: Monad m => Flow s m-f07, f06 :: Monad m => Flow s m-g11, g09, g08, g07 :: Monad m => Flow s m-h12, h11, h10, h07 :: Monad m => Flow s m-i13, i12, i07, i04 :: Monad m => Flow s m-j18 :: Monad m => Flow s m-k13, k07, k05 :: Monad m => Flow s m-l17, l15, l14, l13, l07, l05 :: Monad m => Flow s m-m20, m16, m07, m05 :: Monad m => Flow s m-n16, n11, n05 :: Monad m => Flow s m-o20, o18, o16, o14 :: Monad m => Flow s m-p11, p03 :: Monad m => Flow s m+b13, b12, b11, b10, b09, b08, b07, b06, b05, b04, b03 :: Monad m => Flow m+c04, c03 :: Monad m => Flow m+d05, d04 :: Monad m => Flow m+e06, e05 :: Monad m => Flow m+f07, f06 :: Monad m => Flow m+g11, g09 :: Monad m => IfMatch -> Flow m+g08, g07 :: Monad m => Flow m+h12, h11, h10, h07 :: Monad m => Flow m+i13 :: Monad m => IfNoneMatch -> Flow m+i12, i07, i04 :: Monad m => Flow m+j18 :: Monad m => Flow m+k13 :: Monad m => IfNoneMatch -> Flow m+k07, k05 :: Monad m => Flow m+l17, l15, l14, l13, l07, l05 :: Monad m => Flow m+m20, m16, m07, m05 :: Monad m => Flow m+n16, n11, n05 :: Monad m => Flow m+o20, o18, o16, o14 :: Monad m => Flow m+p11, p03 :: Monad m => Flow m ------------------------------------------------------------------------------ -- B column@@ -192,7 +179,9 @@ allowed <- lift allowedMethods if requestMethod req `elem` allowed then b09 r- else lift $ halt HTTP.status405+ else do+ lift $ addResponseHeader ("Allow", intercalate "," allowed)+ lift $ halt HTTP.status405 b09 r@Resource{..} = do trace "b09"@@ -239,8 +228,11 @@ b03 r@Resource{..} = do trace "b03" req <- lift request+ allowed <- lift allowedMethods if requestMethod req == HTTP.methodOptions- then lift $ halt HTTP.status200+ then do+ lift $ addResponseHeader ("Allow", intercalate "," allowed)+ lift $ halt HTTP.status204 else c03 r ------------------------------------------------------------------------------@@ -313,7 +305,7 @@ trace "e05" req <- lift request let reqHeaders = requestHeaders req- case lookup hAcceptCharset reqHeaders of+ case lookup HTTP.hAcceptCharset reqHeaders of (Just _h) -> e06 r Nothing ->@@ -332,7 +324,7 @@ trace "f06" req <- lift request let reqHeaders = requestHeaders req- case lookup hAcceptEncoding reqHeaders of+ case lookup HTTP.hAcceptEncoding reqHeaders of (Just _h) -> f07 r Nothing ->@@ -342,34 +334,29 @@ -- G column ------------------------------------------------------------------------------ -g11 r@Resource{..} = do+g11 (IfMatch ifMatch) r@Resource{..} = do trace "g11"- req <- lift request- let reqHeaders = requestHeaders req- ifMatch = fromJust (lookup hIfMatch reqHeaders)- etags = parseEtagList ifMatch+ let etags = parseEtagList ifMatch if null etags then lift $ halt HTTP.status412 else h10 r -g09 r@Resource{..} = do+g09 ifMatch r@Resource{..} = do trace "g09"- req <- lift request- let reqHeaders = requestHeaders req- case fromJust (lookup hIfMatch reqHeaders) of+ case ifMatch of -- TODO: should we be stripping whitespace here?- "*" ->+ (IfMatch "*") -> h10 r _ ->- g11 r+ g11 ifMatch r g08 r@Resource{..} = do trace "g08" req <- lift request let reqHeaders = requestHeaders req- case lookup hIfMatch reqHeaders of- (Just _h) ->- g09 r+ case IfMatch <$> lookup HTTP.hIfMatch reqHeaders of+ (Just h) ->+ g09 h r Nothing -> h10 r @@ -388,7 +375,7 @@ h12 r@Resource{..} = do trace "h12" modified <- lift lastModified- parsedDate <- lift $ requestHeaderDate hIfUnmodifiedSince+ parsedDate <- lift $ requestHeaderDate HTTP.hIfUnmodifiedSince let maybeGreater = do lastM <- modified headerDate <- parsedDate@@ -399,7 +386,7 @@ h11 r@Resource{..} = do trace "h11"- parsedDate <- lift $ requestHeaderDate hIfUnmodifiedSince+ parsedDate <- lift $ requestHeaderDate HTTP.hIfUnmodifiedSince if isJust parsedDate then h12 r else i12 r@@ -408,7 +395,7 @@ trace "h10" req <- lift request let reqHeaders = requestHeaders req- case lookup hIfUnmodifiedSince reqHeaders of+ case lookup HTTP.hIfUnmodifiedSince reqHeaders of (Just _h) -> h11 r Nothing ->@@ -418,7 +405,7 @@ trace "h07" req <- lift request let reqHeaders = requestHeaders req- case lookup hIfMatch reqHeaders of+ case lookup HTTP.hIfMatch reqHeaders of -- TODO: should we be stripping whitespace here? (Just "*") -> lift $ halt HTTP.status412@@ -429,24 +416,22 @@ -- I column ------------------------------------------------------------------------------ -i13 r@Resource{..} = do+i13 ifNoneMatch r@Resource{..} = do trace "i13"- req <- lift request- let reqHeaders = requestHeaders req- case fromJust (lookup hIfNoneMatch reqHeaders) of+ case ifNoneMatch of -- TODO: should we be stripping whitespace here?- "*" ->+ (IfNoneMatch "*") -> j18 r _ ->- k13 r+ k13 ifNoneMatch r i12 r@Resource{..} = do trace "i12" req <- lift request let reqHeaders = requestHeaders req- case lookup hIfNoneMatch reqHeaders of- (Just _h) ->- i13 r+ case IfNoneMatch <$> lookup HTTP.hIfNoneMatch reqHeaders of+ (Just h) ->+ i13 h r Nothing -> l13 r @@ -485,12 +470,9 @@ -- K column ------------------------------------------------------------------------------ -k13 r@Resource{..} = do+k13 (IfNoneMatch ifNoneMatch) r@Resource{..} = do trace "k13"- req <- lift request- let reqHeaders = requestHeaders req- ifNoneMatch = fromJust (lookup hIfNoneMatch reqHeaders)- etags = parseEtagList ifNoneMatch+ let etags = parseEtagList ifNoneMatch if null etags then l13 r else j18 r@@ -518,7 +500,7 @@ l17 r@Resource{..} = do trace "l17"- parsedDate <- lift $ requestHeaderDate hIfModifiedSince+ parsedDate <- lift $ requestHeaderDate HTTP.hIfModifiedSince modified <- lift lastModified let maybeGreater = do lastM <- modified@@ -530,7 +512,7 @@ l15 r@Resource{..} = do trace "l15"- parsedDate <- lift $ requestHeaderDate hIfModifiedSince+ parsedDate <- lift $ requestHeaderDate HTTP.hIfModifiedSince now <- lift requestTime let maybeGreater = (> now) <$> parsedDate if maybeGreater == Just True@@ -541,7 +523,7 @@ trace "l14" req <- lift request let reqHeaders = requestHeaders req- dateHeader = lookup hIfModifiedSince reqHeaders+ dateHeader = lookup HTTP.hIfModifiedSince reqHeaders validDate = isJust (dateHeader >>= parseRfc1123Date) if validDate then l15 r@@ -551,7 +533,7 @@ trace "l13" req <- lift request let reqHeaders = requestHeaders req- case lookup hIfModifiedSince reqHeaders of+ case lookup HTTP.hIfModifiedSince reqHeaders of (Just _h) -> l14 r Nothing ->@@ -623,13 +605,13 @@ n11 r@Resource{..} = trace "n11" >> lift processPost >>= flip processPostAction r -create :: Monad m => [Text] -> Resource s m -> FlowStateT s m ()+create :: Monad m => [Text] -> Resource m -> FlowStateT m () create ts r = do loc <- lift (appendRequestPath ts) lift (addResponseHeader ("Location", loc)) negotiateContentTypesAccepted r -processPostAction :: Monad m => PostResponse s m -> Flow s m+processPostAction :: Monad m => PostResponse m -> Flow m processPostAction (PostCreate ts) r = do create ts r p11 r
src/Airship/Internal/Helpers.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE CPP #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-}@@ -8,29 +8,40 @@ #if __GLASGOW_HASKELL__ < 710 import Control.Applicative #endif-import Data.ByteString (ByteString)+import Data.ByteString (ByteString)+import qualified Data.ByteString.Lazy as LazyBS import Data.Maybe #if __GLASGOW_HASKELL__ < 710 import Data.Monoid #endif-import Data.Text (Text, intercalate)+import Data.Text (Text, intercalate) import Data.Text.Encoding-import Data.Time (getCurrentTime)+import Data.Time (getCurrentTime)+import Lens.Micro ((^.)) import Network.HTTP.Media-import qualified Network.HTTP.Types as HTTP-import qualified Network.Wai as Wai+import qualified Network.HTTP.Types as HTTP+import qualified Network.Wai as Wai+import Network.Wai.Parse import System.Random +import Airship.Config+import Airship.Headers import Airship.Internal.Decision+import Airship.Internal.Route import Airship.Resource import Airship.Types-import Airship.Internal.Route +-- | Parse form data uploaded with a @Content-Type@ of either+-- @www-form-urlencoded@ or @multipart/form-data@ to return a+-- list of parameter names and values and a list of uploaded+-- files and their information.+parseFormData :: Request -> IO ([Param], [File LazyBS.ByteString])+parseFormData r = parseRequestBody lbsBackEnd r -- | Returns @True@ if the request's @Content-Type@ header is one of the -- provided media types. If the @Content-Type@ header is not present, -- this function will return True.-contentTypeMatches :: [MediaType] -> Handler s m Bool+contentTypeMatches :: Monad m => [MediaType] -> Webmachine m Bool contentTypeMatches validTypes = do headers <- requestHeaders <$> request let cType = lookup HTTP.hContentType headers@@ -38,26 +49,19 @@ Nothing -> True Just t -> isJust $ matchAccept validTypes t --- | Construct an Airship 'Request' from a WAI request.-fromWaiRequest :: Wai.Request -> Request IO-fromWaiRequest req = Request- { requestMethod = Wai.requestMethod req- , httpVersion = Wai.httpVersion req- , rawPathInfo = Wai.rawPathInfo req- , rawQueryString = Wai.rawQueryString req- , requestHeaders = Wai.requestHeaders req- , isSecure = Wai.isSecure req- , remoteHost = Wai.remoteHost req- , pathInfo = Wai.pathInfo req- , queryString = Wai.queryString req- , requestBody = Wai.requestBody req- , requestBodyLength = Wai.requestBodyLength req- , requestHeaderHost = Wai.requestHeaderHost req- , requestHeaderRange = Wai.requestHeaderRange req- }+-- | Issue an HTTP 302 (Found) response, with `location' as the destination.+redirectTemporarily :: Monad m => ByteString -> Webmachine m a+redirectTemporarily location =+ addResponseHeader ("Location", location) >> halt HTTP.status302 -toWaiResponse :: Response IO -> ByteString -> ByteString -> Wai.Response-toWaiResponse Response{..} trace quip =+-- | Issue an HTTP 301 (Moved Permantently) response,+-- with `location' as the destination.+redirectPermanently :: Monad m => ByteString -> Webmachine m a+redirectPermanently location =+ addResponseHeader ("Location", location) >> halt HTTP.status301++toWaiResponse :: Response -> AirshipConfig -> ByteString -> ByteString -> Wai.Response+toWaiResponse Response{..} cfg trace quip = case _responseBody of (ResponseBuilder b) -> Wai.responseBuilder _responseStatus headers b@@ -67,20 +71,32 @@ Wai.responseStream _responseStatus headers streamer Empty -> Wai.responseBuilder _responseStatus headers mempty- where headers = _responseHeaders ++ [("Airship-Trace", trace), ("Airship-Quip", quip)]+ where+ headers = traced ++ quipHeader ++ _responseHeaders+ traced = if cfg^.includeTraceHeader == IncludeHeader+ then [("Airship-Trace", trace)]+ else [] + quipHeader = if cfg^.includeQuipHeader == IncludeHeader+ then [("Airship-Quip", quip)]+ else []+ -- | Given a 'RoutingSpec', a 404 resource, and a user state @s@, construct a WAI 'Application'.-resourceToWai :: RoutingSpec s IO () -> Resource s IO -> s -> Wai.Application-resourceToWai routes resource404 s req respond = do+resourceToWai :: AirshipConfig -> RoutingSpec IO () -> Resource IO -> Wai.Application+resourceToWai cfg routes resource404 =+ resourceToWaiT cfg (const id) routes resource404++-- | Given a 'RoutingSpec', a 404 resource, and a user state @s@, construct a WAI 'Application'.+resourceToWaiT :: Monad m => AirshipConfig -> (Request -> m Wai.Response -> IO Wai.Response) -> RoutingSpec m () -> Resource m -> Wai.Application+resourceToWaiT cfg run routes resource404 req respond = do let routeMapping = runRouter routes pInfo = Wai.pathInfo req- airshipReq = fromWaiRequest req (resource, (params', matched)) = route routeMapping pInfo resource404 nowTime <- getCurrentTime quip <- getQuip- (response, trace) <- eitherResponse nowTime params' matched airshipReq s (flow resource)- let traceHeaderValue = traceHeader trace- respond (toWaiResponse response traceHeaderValue quip)+ (=<<) respond . run req $ do+ (response, trace) <- eitherResponse nowTime params' matched req (flow resource)+ return $ toWaiResponse response cfg (traceHeader trace) quip getQuip :: IO ByteString getQuip = do@@ -95,6 +111,7 @@ , "evacuation not done in time" , "javascript doesn't have integers" , "WARNING: ulimit -n is 1024"+ , "shut it down" ] traceHeader :: [Text] -> ByteString
src/Airship/Internal/Route.hs view
@@ -15,7 +15,8 @@ #if __GLASGOW_HASKELL__ < 710 import Control.Applicative #endif-import Control.Monad.Writer (Writer, execWriter)+import Control.Monad.Identity+import Control.Monad.Writer (Writer, WriterT (..), execWriter) import Control.Monad.Writer.Class (MonadWriter) import Data.String (IsString, fromString)@@ -32,7 +33,7 @@ instance IsString Route where fromString s = Route [Bound (fromString s)] -runRouter :: RoutingSpec s m a -> [(Route, Resource s m)]+runRouter :: RoutingSpec m a -> [(Route, Resource m)] runRouter routes = execWriter (getRouter routes) -- | @a '</>' b@ separates the path components @a@ and @b@ with a slash.@@ -68,7 +69,7 @@ -- | Represents a fully-specified set of routes that map paths (represented as 'Route's) to 'Resource's. 'RoutingSpec's are declared with do-notation, to wit: -- -- @--- myRoutes :: RoutingSpec MyState IO ()+-- myRoutes :: RoutingSpec IO () -- myRoutes = do -- root #> myRootResource -- "blog" '</>' var "date" '</>' var "post" #> blogPostResource@@ -76,9 +77,8 @@ -- "anything" '</>' star #> wildcardResource -- @ ---newtype RoutingSpec s m a = RoutingSpec { getRouter :: Writer [(Route, Resource s m)] a }- deriving (Functor, Applicative, Monad, MonadWriter [(Route, Resource s m)])-+newtype RoutingSpec m a = RoutingSpec { getRouter :: Writer [(Route, Resource m)] a }+ deriving (Functor, Applicative, Monad, MonadWriter [(Route, Resource m)]) route :: [(Route, a)] -> [Text] -> a -> (a, (HashMap Text Text, [Text])) route routes pInfo resource404 = foldr' (matchRoute pInfo) (resource404, (mempty, mempty)) routes
src/Airship/Resource.hs view
@@ -23,96 +23,96 @@ -- | Used when processing POST requests so as to handle the outcome of the binary decisions between -- handling a POST as a create request and whether to redirect after the POST is done. -- Credit for this idea goes to Richard Wallace (purefn) on Webcrank.-data PostResponse s m+data PostResponse m = PostCreate [Text] -- ^ Treat this request as a PUT. | PostCreateRedirect [Text] -- ^ Treat this request as a PUT, then redirect.- | PostProcess (Handler s m ()) -- ^ Process as a POST, but don't redirect.- | PostProcessRedirect (Handler s m ByteString) -- ^ Process and redirect.+ | PostProcess (Webmachine m ()) -- ^ Process as a POST, but don't redirect.+ | PostProcessRedirect (Webmachine m ByteString) -- ^ Process and redirect. -data Resource s m =+data Resource m = Resource { -- | Whether to allow HTTP POSTs to a missing resource. Default: false.- allowMissingPost :: Handler s m Bool+ allowMissingPost :: Webmachine m Bool -- | The set of HTTP methods that this resource allows. Default: @GET@ and @HEAD@. -- If a request arrives with an HTTP method not included herein, @501 Not Implemented@ is returned.- , allowedMethods :: Handler s m [Method]- -- | An association list of 'MediaType's and 'Handler' actions that correspond to the accepted+ , allowedMethods :: Webmachine m [Method]+ -- | An association list of 'MediaType's and 'Webmachine' actions that correspond to the accepted -- @Content-Type@ values that this resource can accept in a request body. If a @Content-Type@ header -- is present but not accounted for in 'contentTypesAccepted', processing will halt with @415 Unsupported Media Type@.- -- Otherwise, the corresponding 'Handler' action will be executed and processing will continue.- , contentTypesAccepted :: Handler s m [(MediaType, Handler s m ())]+ -- Otherwise, the corresponding 'Webmachine' action will be executed and processing will continue.+ , contentTypesAccepted :: Webmachine m [(MediaType, Webmachine m ())] -- | An association list of 'MediaType' values and 'ResponseBody' values. The response will be chosen -- by looking up the 'MediaType' that most closely matches the @Content-Type@ header. Should there be no match, -- processing will halt with @406 Not Acceptable@.- , contentTypesProvided :: Handler s m [(MediaType, Webmachine s m (ResponseBody m))]+ , contentTypesProvided :: Webmachine m [(MediaType, Webmachine m ResponseBody)] -- | When a @DELETE@ request is enacted (via a @True@ value returned from 'deleteResource'), a -- @False@ value returns a @202 Accepted@ response. Returning @True@ will continue processing, -- usually ending up with a @204 No Content@ response. Default: False.- , deleteCompleted :: Handler s m Bool+ , deleteCompleted :: Webmachine m Bool -- | When processing a @DELETE@ request, a @True@ value allows processing to continue. -- Returns @500 Forbidden@ if False. Default: false.- , deleteResource :: Handler s m Bool+ , deleteResource :: Webmachine m Bool -- | Returns @413 Request Entity Too Large@ if true. Default: false.- , entityTooLarge :: Handler s m Bool+ , entityTooLarge :: Webmachine m Bool -- | Checks if the given request is allowed to access this resource. -- Returns @403 Forbidden@ if true. Default: false.- , forbidden :: Handler s m Bool+ , forbidden :: Webmachine m Bool -- | If this returns a non-'Nothing' 'ETag', its value will be added to every HTTP response -- in the @ETag:@ field.- , generateETag :: Handler s m (Maybe ETag)+ , generateETag :: Webmachine m (Maybe ETag) -- | Checks if this resource has actually implemented a handler for a given HTTP method. -- Returns @501 Not Implemented@ if false. Default: true.- , implemented :: Handler s m Bool+ , implemented :: Webmachine m Bool -- | Returns @401 Unauthorized@ if false. Default: true.- , isAuthorized :: Handler s m Bool+ , isAuthorized :: Webmachine m Bool -- | When processing @PUT@ requests, a @True@ value returned here will halt processing with a @409 Created@.- , isConflict :: Handler s m Bool+ , isConflict :: Webmachine m Bool -- | Returns @415 Unsupported Media Type@ if false. We recommend you use the 'contentTypeMatches' helper function, which accepts a list of -- 'MediaType' values, so as to simplify proper MIME type handling. Default: true.- , knownContentType :: Handler s m Bool+ , knownContentType :: Webmachine m Bool -- | In the presence of an @If-Modified-Since@ header, returning a @Just@ value from 'lastModifed' allows -- the server to halt with @304 Not Modified@ if appropriate.- , lastModified :: Handler s m (Maybe UTCTime)+ , lastModified :: Webmachine m (Maybe UTCTime) -- | If an @Accept-Language@ value is present in the HTTP request, and this function returns @False@, -- processing will halt with @406 Not Acceptable@.- , languageAvailable :: Handler s m Bool+ , languageAvailable :: Webmachine m Bool -- | Returns @400 Bad Request@ if true. Default: false.- , malformedRequest :: Handler s m Bool+ , malformedRequest :: Webmachine m Bool -- wondering if this should be text, -- or some 'path' type -- | When processing a resource for which 'resourceExists' returned @False@, returning a @Just@ value -- halts with a @301 Moved Permanently@ response. The contained 'ByteString' will be added to the -- HTTP response under the @Location:@ header.- , movedPermanently :: Handler s m (Maybe ByteString)+ , movedPermanently :: Webmachine m (Maybe ByteString) -- | Like 'movedPermanently', except with a @307 Moved Temporarily@ response.- , movedTemporarily :: Handler s m (Maybe ByteString)+ , movedTemporarily :: Webmachine m (Maybe ByteString) -- | When handling a @PUT@ request, returning @True@ here halts processing with @300 Multiple Choices@. Default: False.- , multipleChoices :: Handler s m Bool+ , multipleChoices :: Webmachine m Bool -- | When processing a request for which 'resourceExists' returned @False@, returning @True@ here -- allows the 'movedPermanently' and 'movedTemporarily' functions to process the request.- , previouslyExisted :: Handler s m Bool+ , previouslyExisted :: Webmachine m Bool -- | When handling @POST@ requests, the value returned determines whether to treat the request as a @PUT@, -- a @PUT@ and a redirect, or a plain @POST@. See the documentation for 'PostResponse' for more information. -- The default implemetation returns a 'PostProcess' with an empty handler.- , processPost :: Handler s m (PostResponse s m)+ , processPost :: Webmachine m (PostResponse m) -- | Does the resource at this path exist? -- Returning false from this usually entails a @404 Not Found@ response. -- (If 'allowMissingPost' returns @True@ or an @If-Match: *@ header is present, it may not).- , resourceExists :: Handler s m Bool+ , resourceExists :: Webmachine m Bool -- | Returns @503 Service Unavailable@ if false. Default: true.- , serviceAvailable :: Handler s m Bool+ , serviceAvailable :: Webmachine m Bool -- | Returns @414 Request URI Too Long@ if true. Default: false.- , uriTooLong :: Handler s m Bool+ , uriTooLong :: Webmachine m Bool -- | Returns @501 Not Implemented@ if false. Default: true.- , validContentHeaders :: Handler s m Bool+ , validContentHeaders :: Webmachine m Bool } -- | A helper function that terminates execution with @500 Internal Server Error@.-serverError :: Handler m s a+serverError :: Monad m => Webmachine m a serverError = finishWith (Response status500 [] Empty) -- | The default Airship resource, with "sensible" values filled in for each entry. -- You construct new resources by extending the default resource with your own handlers.-defaultResource :: Resource s m+defaultResource :: Monad m => Resource m defaultResource = Resource { allowMissingPost = return False , allowedMethods = return [methodGet, methodHead] , contentTypesAccepted = return []
src/Airship/Resource/Static.hs view
@@ -19,7 +19,7 @@ import Airship.Headers (addResponseHeader) import Airship.Types ( ETag(Strong) , ResponseBody(ResponseFile)- , Handler+ , Webmachine , dispatchPath , halt )@@ -113,10 +113,10 @@ let infos = fileInfos (zipWith (\(a,b) c -> (a,b,c)) regularFiles etags) return (FileTree (Trie.fromList infos) (T.pack f)) -staticResource :: StaticOptions -> FilePath -> IO (Resource s m)+staticResource :: Monad m => StaticOptions -> FilePath -> IO (Resource m) staticResource options p = staticResource' options <$> directoryTree p -staticResource' :: StaticOptions -> FileTree -> Resource s m+staticResource' :: Monad m => StaticOptions -> FileTree -> Resource m staticResource' options FileTree{..} = defaultResource { allowedMethods = return [ HTTP.methodGet, HTTP.methodHead ] , resourceExists = getFileInfo >> return True@@ -137,7 +137,7 @@ return [ (mediaType, response) , ("application/octet-stream", response)] }- where getFileInfo :: Handler s m FileInfo+ where getFileInfo :: Monad m => Webmachine m FileInfo getFileInfo = do dispath <- dispatchPath let key = encodeUtf8 (T.intercalate "/" (root:dispath))@@ -146,7 +146,7 @@ (Just r) -> return r Nothing -> halt HTTP.status404 -addNoCacheHeaders :: Handler s m ()+addNoCacheHeaders :: Monad m => Webmachine m () addNoCacheHeaders = do addResponseHeader (HTTP.hCacheControl, "no-cache, no-store, must-revalidate") addResponseHeader ("Pragma", "no-cache")
src/Airship/Types.hs view
@@ -12,7 +12,6 @@ module Airship.Types ( ETag(..) , Webmachine- , Handler , Request(..) , Response(..) , ResponseState(..)@@ -25,9 +24,6 @@ , runWebmachine , request , requestTime- , getState- , putState- , modifyState , getResponseHeaders , getResponseBody , params@@ -49,10 +45,10 @@ #endif import Control.Monad (liftM) import Control.Monad.Base (MonadBase)-import Control.Monad.IO.Class (MonadIO)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.Morph import Control.Monad.Reader.Class (MonadReader, ask) import Control.Monad.State.Class (MonadState, get, modify)-import Control.Monad.Trans.Class (MonadTrans(..)) import Control.Monad.Trans.Control (MonadBaseControl(..)) import Control.Monad.Trans.Either (EitherT(..), runEitherT, left) import Control.Monad.Trans.RWS.Strict (RWST(..), runRWST)@@ -63,62 +59,25 @@ import Data.Text (Text) import Data.Time.Clock (UTCTime) -import Network.Socket (SockAddr(..))-import qualified Network.HTTP.Types as HTTP import Network.HTTP.Types ( ResponseHeaders- , RequestHeaders- , Query , Status- , Method- , HttpVersion )+ ) import qualified Network.Wai as Wai+import Network.Wai (Request (..), defaultRequest) --- | Very similar to WAI's @Request@ type, except generalized to an arbitrary monad @m@.-data Request m =- Request { requestMethod :: Method -- ^ The request method -- @GET@, @POST@, @DELETE@, et cetera.- , httpVersion :: HttpVersion -- ^ The HTTP version (usually 1.1; hopefully someday 2.0).- , rawPathInfo :: ByteString -- ^ The unparsed path information yielded from the WAI server. You probably want 'pathInfo'.- , rawQueryString :: ByteString -- ^ The query string, if any, yielded from the WAI server. You probably want 'queryString'.- , requestHeaders :: RequestHeaders -- ^ An association list of (headername, value) pairs. See "Network.HTTP.Types.Header" for the possible values.- , isSecure :: Bool -- ^ Was this request made over SSL/TLS?- , remoteHost :: SockAddr -- ^ The address information of the client.- , pathInfo :: [Text] -- ^ The URL, stripped of hostname and port, split on forward-slashes- , queryString :: Query -- ^ Parsed query string information.- , requestBody :: m ByteString -- ^ A monadic action that extracts a (possibly-empty) chunk of the request body.- , requestBodyLength :: Wai.RequestBodyLength -- ^ Either @ChunkedBody@ or a @KnownLength 'Word64'@.- , requestHeaderHost :: Maybe ByteString -- ^ Contains the Host header.- , requestHeaderRange :: Maybe ByteString -- ^ Contains the Range header.- } -defaultRequest :: Monad m => Request m-defaultRequest = Request- { requestMethod = HTTP.methodGet- , httpVersion = HTTP.http10- , rawPathInfo = BS.empty- , rawQueryString = BS.empty- , requestHeaders = []- , isSecure = False- , remoteHost = SockAddrInet 0 0- , pathInfo = []- , queryString = []- , requestBody = return BS.empty- , requestBodyLength = Wai.KnownLength 0- , requestHeaderHost = Nothing- , requestHeaderRange = Nothing- }- -- | Reads the entirety of the request body in a single string. -- This turns the chunks obtained from repeated invocations of 'requestBody' into a lazy 'ByteString'.-entireRequestBody :: Monad m => Request m -> m LB.ByteString-entireRequestBody req = requestBody req >>= strictRequestBody' LB.empty+entireRequestBody :: MonadIO m => Request -> m LB.ByteString+entireRequestBody req = liftIO (requestBody req) >>= strictRequestBody' LB.empty where strictRequestBody' acc prev | BS.null prev = return acc- | otherwise = requestBody req >>= strictRequestBody' (acc <> LB.fromStrict prev)+ | otherwise = liftIO (requestBody req) >>= strictRequestBody' (acc <> LB.fromStrict prev) -data RequestReader m = RequestReader { _now :: UTCTime- , _request :: Request m- }+data RequestReader = RequestReader { _now :: UTCTime+ , _request :: Request+ } data ETag = Strong ByteString | Weak ByteString@@ -130,51 +89,47 @@ etagToByteString (Strong bs) = "\"" <> bs <> "\"" etagToByteString (Weak bs) = "W/\"" <> bs <> "\"" -type StreamingBody m = (Builder -> m ()) -> m () -> m ()---- | Basically Wai's unexported 'Response' type, but generalized to any monad,--- 'm'.-data ResponseBody m+-- | Basically Wai's unexported 'Response' type.+data ResponseBody = ResponseFile FilePath (Maybe Wai.FilePart) | ResponseBuilder Builder- | ResponseStream (StreamingBody m)+ | ResponseStream Wai.StreamingBody | Empty -- ResponseRaw ... (not implemented yet, but useful for websocket upgrades) -- | Helper function for building a `ResponseBuilder` out of HTML-escaped text.-escapedResponse :: Text -> ResponseBody m+escapedResponse :: Text -> ResponseBody escapedResponse = ResponseBuilder . fromHtmlEscapedText -data Response m = Response { _responseStatus :: Status- , _responseHeaders :: ResponseHeaders- , _responseBody :: ResponseBody m- }+data Response = Response { _responseStatus :: Status+ , _responseHeaders :: ResponseHeaders+ , _responseBody :: ResponseBody+ } -data ResponseState s m = ResponseState { stateUser :: s- , stateHeaders :: ResponseHeaders- , stateBody :: ResponseBody m- , _params :: HashMap Text Text- , _dispatchPath :: [Text]- }+data ResponseState = ResponseState { stateHeaders :: ResponseHeaders+ , stateBody :: ResponseBody+ , _params :: HashMap Text Text+ , _dispatchPath :: [Text]+ } type Trace = [Text] -newtype Webmachine s m a =- Webmachine { getWebmachine :: EitherT (Response m) (RWST (RequestReader m) Trace (ResponseState s m) m) a }+newtype Webmachine m a =+ Webmachine { getWebmachine :: EitherT Response (RWST RequestReader Trace ResponseState m) a } deriving (Functor, Applicative, Monad, MonadIO, MonadBase b,- MonadReader (RequestReader m),+ MonadReader RequestReader, MonadWriter Trace,- MonadState (ResponseState s m))+ MonadState ResponseState) -instance MonadTrans (Webmachine s) where+instance MonadTrans Webmachine where lift = Webmachine . EitherT . (>>= return . Right) . lift -newtype StMWebmachine s m a = StMWebmachine {- unStMWebmachine :: StM (EitherT (Response m) (RWST (RequestReader m) Trace (ResponseState s m) m)) a+newtype StMWebmachine m a = StMWebmachine {+ unStMWebmachine :: StM (EitherT Response (RWST RequestReader Trace ResponseState m)) a } -instance MonadBaseControl b m => MonadBaseControl b (Webmachine s m) where- type StM (Webmachine s m) a = StMWebmachine s m a+instance MonadBaseControl b m => MonadBaseControl b (Webmachine m) where+ type StM (Webmachine m) a = StMWebmachine m a liftBaseWith f = Webmachine $ liftBaseWith $ \g' -> f@@ -183,68 +138,53 @@ restoreM = Webmachine . restoreM . unStMWebmachine -- | A convenience synonym that writes the @Monad@ type constraint for you.-type Handler s m a = Monad m => Webmachine s m a+type Handler m a = Monad m => Webmachine m a -- Functions inside the Webmachine Monad ------------------------------------- ------------------------------------------------------------------------------ -- | Returns the 'Request' that this 'Handler' is currently processing.-request :: Handler s m (Request m)+request :: Handler m Request request = _request <$> ask -- | Returns the bound routing parameters extracted from the routing system (see "Airship.Route").-params :: Handler s m (HashMap Text Text)+params :: Handler m (HashMap Text Text) params = _params <$> get -dispatchPath :: Handler s m [Text]+dispatchPath :: Handler m [Text] dispatchPath = _dispatchPath <$> get -- | Returns the time at which this request began processing.-requestTime :: Handler s m UTCTime+requestTime :: Handler m UTCTime requestTime = _now <$> ask --- | Returns the user state (of type @s@) in the provided @'Handler' s m@.-getState :: Handler s m s-getState = stateUser <$> get---- | Sets the user state.-putState :: s -> Handler s m ()-putState s = modify updateState- where updateState rs = rs {stateUser = s}---- | Applies the provided function to the user state.-modifyState :: (s -> s) -> Handler s m ()-modifyState f = modify modifyState'- where modifyState' rs@ResponseState{stateUser=uState} =- rs {stateUser = f uState}- -- | Returns the 'ResponseHeaders' stored in the current 'Handler'.-getResponseHeaders :: Handler s m ResponseHeaders+getResponseHeaders :: Handler m ResponseHeaders getResponseHeaders = stateHeaders <$> get -- | Returns the current 'ResponseBody' that this 'Handler' is storing.-getResponseBody :: Handler s m (ResponseBody m)+getResponseBody :: Handler m ResponseBody getResponseBody = stateBody <$> get -- | Given a new 'ResponseBody', replaces the stored body with the new one.-putResponseBody :: ResponseBody m -> Handler s m ()+putResponseBody :: ResponseBody -> Handler m () putResponseBody b = modify updateState where updateState rs = rs {stateBody = b} -- | Stores the provided 'ByteString' as the responseBody. This is a shortcut for -- creating a response body with a 'ResponseBuilder' and a bytestring 'Builder'.-putResponseBS :: ByteString -> Handler s m ()+putResponseBS :: Monad m => ByteString -> Webmachine m () putResponseBS bs = putResponseBody $ ResponseBuilder $ fromByteString bs -- | Immediately halts processing with the provided 'Status' code.--- The contents of the 'Handler''s response body will be streamed back to the client.+-- The contents of the 'Webmachine''s response body will be streamed back to the client. -- This is a shortcut for constructing a 'Response' with 'getResponseHeaders' and 'getResponseBody' -- and passing that response to 'finishWith'.-halt :: Status -> Handler m s a-halt status = finishWith =<< Response <$> pure status <*> getResponseHeaders <*> getResponseBody+halt :: Monad m => Status -> Webmachine m a+halt status = finishWith =<< Response <$> return status <*> getResponseHeaders <*> getResponseBody -- | Immediately halts processing and writes the provided 'Response' back to the client.-finishWith :: Response m -> Handler s m a+finishWith :: Monad m => Response -> Webmachine m a finishWith = Webmachine . left -- | The @#>@ operator provides syntactic sugar for the construction of association lists.@@ -272,14 +212,14 @@ both :: Either a a -> a both = either id id -eitherResponse :: Monad m => UTCTime -> HashMap Text Text -> [Text] -> Request m -> s -> Handler s m (Response m) -> m (Response m, Trace)-eitherResponse reqDate reqParams dispatched req s resource = do- (e, trace) <- runWebmachine reqDate reqParams dispatched req s resource+eitherResponse :: Monad m => UTCTime -> HashMap Text Text -> [Text] -> Request -> Webmachine m Response -> m (Response, Trace)+eitherResponse reqDate reqParams dispatched req resource = do+ (e, trace) <- runWebmachine reqDate reqParams dispatched req resource return (both e, trace) -runWebmachine :: Monad m => UTCTime -> HashMap Text Text -> [Text] -> Request m -> s -> Handler s m a -> m (Either (Response m) a, Trace)-runWebmachine reqDate reqParams dispatched req s w = do- let startingState = ResponseState s [] Empty reqParams dispatched+runWebmachine :: Monad m => UTCTime -> HashMap Text Text -> [Text] -> Request -> Webmachine m a -> m (Either (Response) a, Trace)+runWebmachine reqDate reqParams dispatched req w = do+ let startingState = ResponseState [] Empty reqParams dispatched requestReader = RequestReader reqDate req (e, _, t) <- runRWST (runEitherT (getWebmachine w)) requestReader startingState return (e, t)
test/unit/test.hs view
@@ -3,9 +3,7 @@ module Main where import Airship-import Control.Monad.Trans.State.Strict (State, evalState, get, put)-import qualified Data.ByteString as BS-import qualified Data.ByteString.Lazy as LB+import Control.Concurrent import Data.ByteString (ByteString) import Test.Tasty@@ -24,27 +22,16 @@ exampleTests = testGroup "ExampleTests" [ bodyTest ] -type RequestState = State [ByteString]- bodyChunks :: [ByteString] bodyChunks = ["one", "two", "three", "four", "five"] -nextBody :: RequestState ByteString-nextBody = do- s <- get- if null s- then return BS.empty- else do- let (h:tl) = s- put tl- return h+bodyChunksIO :: IO (IO ByteString)+bodyChunksIO = do+ v <- newMVar bodyChunks+ return $ modifyMVar v (\l -> return $ case l of { [] -> ([], ""); h : t -> (t, h) }) bodyTest :: TestTree-bodyTest = testCase "entireRequestBody returns the body in the correct order" bodyTest'- where bodyTest' = evalState state bodyChunks @?= "onetwothreefourfive"- state :: RequestState LB.ByteString- state = entireRequestBody req- req = defRequest { requestBody = nextBody }- -- for some reason this type signature seems to be necessary- defRequest :: Request RequestState- defRequest = defaultRequest+bodyTest = testCase "entireRequestBody returns the body in the correct order" $ do+ nextBody <- bodyChunksIO+ b <- entireRequestBody defaultRequest { requestBody = nextBody }+ b @?= "onetwothreefourfive"