webgear-server 0.2.1 → 1.0.0
raw patch · 39 files changed
+1062/−2396 lines, 39 filesdep +arrowsdep +http-mediadep +josedep −case-insensitivedep −networkdep −template-haskelldep ~aesondep ~basedep ~bytestring
Dependencies added: arrows, http-media, jose, monad-time, webgear-core
Dependencies removed: case-insensitive, network, template-haskell
Dependency ranges changed: aeson, base, bytestring
Files
- CHANGELOG.md +38/−0
- ChangeLog.md +0/−31
- README.md +2/−5
- src/WebGear.hs +0/−290
- src/WebGear/Middlewares.hs +0/−22
- src/WebGear/Middlewares/Auth/Basic.hs +0/−120
- src/WebGear/Middlewares/Body.hs +0/−71
- src/WebGear/Middlewares/Header.hs +0/−356
- src/WebGear/Middlewares/Method.hs +0/−85
- src/WebGear/Middlewares/Params.hs +0/−186
- src/WebGear/Middlewares/Path.hs +0/−237
- src/WebGear/Modifiers.hs +0/−18
- src/WebGear/Server.hs +39/−0
- src/WebGear/Server/Handler.hs +183/−0
- src/WebGear/Server/Trait/Auth/Basic.hs +71/−0
- src/WebGear/Server/Trait/Auth/JWT.hs +53/−0
- src/WebGear/Server/Trait/Body.hs +82/−0
- src/WebGear/Server/Trait/Header.hs +101/−0
- src/WebGear/Server/Trait/Method.hs +21/−0
- src/WebGear/Server/Trait/Path.hs +41/−0
- src/WebGear/Server/Trait/QueryParam.hs +80/−0
- src/WebGear/Server/Trait/Status.hs +22/−0
- src/WebGear/Server/Traits.hs +15/−0
- src/WebGear/Trait.hs +0/−155
- src/WebGear/Types.hs +0/−470
- src/WebGear/Util.hs +0/−32
- test/Main.hs +0/−6
- test/Properties.hs +14/−17
- test/Properties/Trait/Auth/Basic.hs +47/−23
- test/Properties/Trait/Body.hs +23/−21
- test/Properties/Trait/Header.hs +24/−39
- test/Properties/Trait/Method.hs +27/−20
- test/Properties/Trait/Params.hs +0/−46
- test/Properties/Trait/Path.hs +33/−32
- test/Properties/Trait/QueryParam.hs +44/−0
- test/Unit.hs +9/−12
- test/Unit/Trait/Header.hs +17/−36
- test/Unit/Trait/Path.hs +17/−20
- webgear-server.cabal +59/−46
+ CHANGELOG.md view
@@ -0,0 +1,38 @@+# Changelog for webgear-server++## [Unreleased]++## [1.0.0] - 2022-01-08++### Changed+- New home at https://github.com/haskell-webgear/webgear+- New arrow based API++## [0.2.1] - 2021-01-11++### Changed+- Upgrade to latest version of LTS and deps++## [0.2.0] - 2020-09-11++### Added+- Support GHC 8.10 and 8.6 (#10)+- Added more traits and middlewares (#7)+- Performance benchmarks (#6)+- Set up a website (#13)++### Changed+- A lot of refactorings (#20, #21, #22, #23)++## [0.1.0] - 2020-08-16++### Added+- Support basic traits and middlewares+- Automated tests+- Documentation++[Unreleased]: https://github.com/haskell-webgear/webgear/compare/v1.0.0...HEAD+[1.0.0]: https://github.com/haskell-webgear/webgear/releases/tag/v1.0.0+[0.2.1]: https://github.com/haskell-webgear/webgear-server/compare/v0.2.0...v0.2.1+[0.2.0]: https://github.com/haskell-webgear/webgear-server/compare/v0.1.0...v0.2.0+[0.1.0]: https://github.com/haskell-webgear/webgear-server/releases/tag/v0.1.0
− ChangeLog.md
@@ -1,31 +0,0 @@-# Changelog for webgear-server--## Unreleased changes--## [0.2.1] - 2021-01-11--### Changed-- Upgrade to latest version of LTS and deps--## [0.2.0] - 2020-09-11--### Added-- Support GHC 8.10 and 8.6 (#10)-- Added more traits and middlewares (#7)-- Performance benchmarks (#6)-- Set up a website (#13)--### Changed-- A lot of refactorings (#20, #21, #22, #23)--## [0.1.0] - 2020-08-16--### Added-- Support basic traits and middlewares-- Automated tests-- Documentation--[Unreleased]: https://github.com/rkaippully/webgear/compare/v0.2.0...HEAD-[0.2.1]: https://github.com/rkaippully/webgear/compare/v0.2.0...v0.2.1-[0.2.0]: https://github.com/rkaippully/webgear/compare/v0.1.0...v0.2.0-[0.1.0]: https://github.com/rkaippully/webgear/releases/tag/v0.1.0
README.md view
@@ -1,9 +1,6 @@ # WebGear - HTTP API server [](https://hackage.haskell.org/package/webgear-server)-[](https://github.com/rkaippully/webgear/actions?query=workflow%3A%22Haskell+CI%22+branch%3Amaster) -WebGear is a Haskell library for building composable, type-safe HTTP API servers. It focuses on good documentation and-usability.--See the documentation of WebGear module to get started.+WebGear is a Haskell library for building composable, type-safe HTTP APIs. This package helps to generate+[WAI](https://hackage.haskell.org/package/wai) applications based on WebGear API specifications.
− src/WebGear.hs
@@ -1,290 +0,0 @@-{-# OPTIONS_GHC -Wno-unused-imports #-}-{-# OPTIONS_HADDOCK ignore-exports #-}--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com------ WebGear helps to build composable, type-safe HTTP API servers.------ The documentation below gives an overview of WebGear. Example--- programs built using WebGear are available at--- https://github.com/rkaippully/webgear/tree/master/webgear-examples.----module WebGear- ( -- * Serving HTTP APIs- -- $serving-- -- * Traits and Linking- -- $traits-- -- * Handlers- -- $handlers-- -- * Middlewares- -- $middlewares-- -- * Routing- -- $routing-- -- * Running the Server- -- $running-- -- * Servers with other monads- -- $otherMonads-- module Control.Applicative- , module Control.Arrow- , module Data.ByteString.Lazy- , module Data.ByteString.Conversion.To- , module Data.Proxy- , module Data.Text- , module Web.HttpApiData- , module WebGear.Middlewares- , module WebGear.Trait- , module WebGear.Types- ) where--import Control.Applicative (Alternative ((<|>)))-import Control.Arrow (Kleisli (..))-import Data.ByteString.Conversion.To-import Data.ByteString.Lazy (ByteString)-import Data.Proxy (Proxy (..))-import Data.Text-import Web.HttpApiData (FromHttpApiData (..))--import qualified Network.Wai as Wai--import WebGear.Middlewares-import WebGear.Trait-import WebGear.Types-------- $serving------ An HTTP API server handler can be thought of as a function that--- takes a request as input and produces a response as output in a--- monadic context.------ > handler :: Monad m => Request -> m Response------ For reasons that will be explained later, WebGear uses the 'Router'--- monad for running handlers. Thus the above type signature changes--- to:------ > handler :: Request -> Router Response------ Most APIs will require extracting some information from the--- request, processing it and then producing a response. For example,--- the server might require access to some HTTP header values, query--- parameters, or the request body. WebGear allows to access such--- information using traits.--------- $traits------ A trait is an attribute associated with a value. For example, a--- @Request@ might have a header that we are interested in, which is--- represented by the 'Header' trait. All traits have instances of the--- 'Trait' typeclass. The 'toAttribute' function helps to check--- presence of the trait. It also has two associated types ---- 'Attribute' and 'Absence' - to represent the result of the--- extraction.------ For example, the 'Header' trait has an instance of the 'Trait'--- typeclass. The 'toAttribute' function evaluates to a 'Found' or--- 'NotFound' value depending on whether we can successfully retrieve--- the header value.------ WebGear provides type-safety by linking traits to the request at--- type level. The 'Linked' data type associates a 'Request' with a--- list of traits. This linking guarantees that the Request has the--- specified trait.------ These functions work with traits and linked values:------ * 'link': Establish a link between a value and an empty list of--- traits. This always succeeds.------ * 'unlink': Convert a linked value to a regular value without any--- type-level traits.------ * 'probe': Attempts to establish a link between a linked value--- with an additional trait using 'toAttribute'.------ * 'remove': Removes a trait from the list of linked traits.------ * 'get': Extract an 'Attribute' associated with a trait from a--- linked value.------ For example, we make use of the @'Method' \@GET@ trait to ensure--- that our handler is called only for GET requests. We can link a--- request value with this trait using:------ @--- linkedRequest :: Monad m => 'Request' -> 'Router' (Either 'MethodMismatch' ('Linked' '['Method' GET] 'Request'))--- linkedRequest = 'probe' @('Method' GET) . 'link'--- @------ Let us modify the type signature of our handler to use linked--- values instead of regular values:------ > handler :: Linked req Request -> Router Response------ Here, @req@ is a type-level list of traits associated with the--- @Request@ that this handler requires. This ensures that this--- handler can only be called with a request possessing certain--- traits thus providing type-safety to our handlers.--------- $handlers------ Handlers in WebGear are defined with a type very similar to the--- above.------ @--- type 'Handler'' m req a = 'Kleisli' m ('Linked' req 'Request') ('Response' a)------ type 'Handler' req a = 'Handler'' 'Router' req a--- @------ It is a 'Kleisli' arrow as described in the above section with--- type-level trait lists. However, the response is parameterized by--- the type variable @a@, which represents the type of the response--- body.------ 'Handler'' can work with any monad while 'Handler' works with--- 'Router'.------ A handler can extract some trait attribute of a request with the--- 'get' function.--------- $middlewares------ A middleware is a higher-order function that takes a handler as--- input and produces another handler with potentially different--- request and response types. Thus middlewares can augment the--- functionality of another handler.------ For example, here is the definition of the 'method' middleware:------ @--- method :: ('IsStdMethod' t, 'MonadRouter' m) => 'Handler'' m ('Method' t:req) a -> 'Handler'' m req a--- method handler = 'Kleisli' $ 'probe' \@('Method' t) >=> 'either' ('const' 'rejectRoute') ('runKleisli' handler)--- @------ The @probe \@(Method t)@ function is used to ensure that the--- request has method @t@ before invoking the @handler@. In case of a--- mismatch, this route is rejected by calling 'rejectRoute'.------ Many middlewares can be composed to form complex request handling--- logic. For example:------ @--- putUser = 'method' \@PUT--- $ 'requestContentTypeHeader' \@"application/json"--- $ 'jsonRequestBody' \@User--- $ 'jsonResponseBody' \@User--- $ putUserHandler--- @--------- $routing------ A typical server will have many routes and we would like to pick--- one based on the URL path, HTTP method etc. We need a couple of--- things to achieve this.------ First, we need a way to indicate that a handler cannot handle a--- request, possibly because the path or method did not match with--- what was expected. This is achieved by the 'rejectRoute' function:------ @--- class (Alternative m, MonadPlus m) => 'MonadRouter' m where--- 'rejectRoute' :: m a--- 'errorResponse' :: 'Response' 'ByteString' -> m a--- 'catchErrorResponse' :: m a -> ('Response' 'ByteString' -> m a) -> m a--- @------ The 'errorResponse' can be used in cases where we find a matching--- route but the request handling is aborted for some reason. For--- example, if a route requires the request Content-type header to--- have a particular value but the actual request had a different--- Content-type, 'errorResponse' can be used to abort and return an--- error response.------ Second, we need a mechanism to try an alternate route when one--- route is rejected. Since 'MonadRouter' is an 'Alternative', we can--- use '<|>' to combine many routes. When a request arrives, a match--- will be attempted against each route sequentially and the first--- matching route handler will process the request. Here is an--- example:------ @--- allRoutes :: 'Handler' '[] 'ByteString'--- allRoutes = ['match'| /v1\/users\/userId:Int |] -- non-TH version: 'path' \@"/v1/users" . 'pathVar' \@"userId" \@Int--- $ getUser \<|\> putUser \<|\> deleteUser------ type IntUserId = 'PathVar' "userId" Int------ getUser :: 'Has' IntUserId req => 'Handler' req 'ByteString'--- getUser = 'method' \@GET getUserHandler------ putUser :: 'Has' IntUserId req => 'Handler' req 'ByteString'--- putUser = 'method' \@PUT--- $ 'requestContentTypeHeader' \@"application/json"--- $ 'jsonRequestBody' \@User--- $ putUserHandler------ deleteUser :: 'Has' IntUserId req => 'Handler' req 'ByteString'--- deleteUser = 'method' \@DELETE deleteUserHandler--- @--------- $running------ Routable handlers can be converted to a Wai 'Wai.Application' using--- 'toApplication':------ @--- toApplication :: 'ToByteString' a => 'Handler' '[] a -> 'Wai.Application'--- @------ This Wai application can then be run as a Warp web server.------ @--- main :: IO ()--- main = Warp.run 3000 $ 'toApplication' allRoutes--- @--------- $otherMonads------ It may not be practical to use 'Router' monad for your handlers. In--- most cases, you would need your own monad transformer stack or--- algebraic effect runners. WebGear supports that easily.------ Let us say, the @putUserHandler@ from the above example runs on--- some monad other than 'Router'. You can still use it as a handler thus:------ @--- putUser = 'method' \@PUT--- $ 'requestContentTypeHeader' \@"application/json"--- $ 'jsonRequestBody' \@User--- $ 'jsonResponseBody' \@User--- $ 'transform' customMonadToRouter putUserHandler------ putUserHandler :: 'Handler'' MyCustomMonad req User--- putUserHandler = ....------ customMonadToRouter :: MyCustomMonad a -> Router a--- customMonadToRouter = ...--- @------ As long as you have a way of transforming values in your custom--- monad to a 'Router' monadic value, you can use 'transform' to--- convert the handlers in that custom monad to handlers running in--- 'Router' monad.---
− src/WebGear/Middlewares.hs
@@ -1,22 +0,0 @@--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com------ Middlewares provided by WebGear.----module WebGear.Middlewares- ( module WebGear.Middlewares.Method- , module WebGear.Middlewares.Path- , module WebGear.Middlewares.Header- , module WebGear.Middlewares.Body- , module WebGear.Middlewares.Params- , module WebGear.Middlewares.Auth.Basic- ) where--import WebGear.Middlewares.Auth.Basic-import WebGear.Middlewares.Body-import WebGear.Middlewares.Header-import WebGear.Middlewares.Method-import WebGear.Middlewares.Params-import WebGear.Middlewares.Path
− src/WebGear/Middlewares/Auth/Basic.hs
@@ -1,120 +0,0 @@--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com------ Basic authentication support.----module WebGear.Middlewares.Auth.Basic- ( BasicAuth- , Realm (..)- , Username (..)- , Password (..)- , Credentials (..)- , BasicAuthError (..)- , basicAuth- ) where--import Control.Arrow (Kleisli (..))-import Control.Monad (when, (>=>))-import Control.Monad.Except (throwError)-import Data.ByteString (ByteString, intercalate)-import Data.ByteString.Base64 (decodeLenient)-import Data.ByteString.Char8 (split)-import Data.CaseInsensitive (CI, mk)-import Data.Proxy (Proxy (..))-import Data.String (IsString)--import WebGear.Trait (Has (..), Linked, Result (..), Trait (..), probe)-import WebGear.Types (MonadRouter (..), Request, RequestMiddleware', Response (..), forbidden403,- requestHeader, setResponseHeader, unauthorized401)-import WebGear.Util (maybeToRight)----- | Trait for HTTP basic authentication: https://tools.ietf.org/html/rfc7617-data BasicAuth---- | The protection space for basic authentication-newtype Realm = Realm ByteString- deriving newtype (Eq, Ord, Show, Read, IsString)---- | Username for basic authentication. Valid usernames cannot contain--- \':\' characters.-newtype Username = Username ByteString- deriving newtype (Eq, Ord, Show, Read, IsString)---- | Password for basic authentication.-newtype Password = Password ByteString- deriving newtype (Eq, Ord, Show, Read, IsString)---- | Basic authentication credentials retrieved from an HTTP request-data Credentials = Credentials- { credentialsUsername :: !Username- , credentialsPassword :: !Password- }- deriving (Eq, Ord, Show, Read)---- | Error extracting credentials from an HTTP request-data BasicAuthError = AuthHeaderError -- ^ Authorization header is missing or badly formatted- | AuthSchemeMismatch -- ^ Authorization scheme is not "Basic"- deriving (Eq, Ord, Show, Read)--instance Monad m => Trait BasicAuth Request m where- type Attribute BasicAuth Request = Credentials- type Absence BasicAuth Request = BasicAuthError-- toAttribute :: Request -> m (Result BasicAuth Request)- toAttribute r = pure $ either NotFound Found $ do- h <- getAuthHeader r- (scheme, creds) <- parseAuthHeader h- when (scheme /= "Basic") $- throwError AuthSchemeMismatch- parseCreds creds--type Scheme = CI ByteString-type EncodedPassword = ByteString--getAuthHeader :: Request -> Either BasicAuthError ByteString-getAuthHeader r = maybeToRight AuthHeaderError $ requestHeader "Authorization" r--parseAuthHeader :: ByteString -> Either BasicAuthError (Scheme, EncodedPassword)-parseAuthHeader s =- case split ' ' s of- [x, y] -> pure (mk x, y)- _ -> throwError AuthHeaderError--parseCreds :: EncodedPassword -> Either BasicAuthError Credentials-parseCreds enc =- case split ':' (decodeLenient enc) of- [] -> throwError AuthHeaderError- u:ps -> pure $ Credentials (Username u) (Password $ intercalate ":" ps)---- | Middleware to add basic authentication protection for a handler.------ Example usage:------ > basicAuth "realm" isValidCredentials handler------ This middleware returns a 401 response if no credentials are found--- in the request. It returns a 403 response if credentials are--- present but isValidCredentials returns False.----basicAuth :: forall m req a. MonadRouter m- => Realm- -> (Credentials -> m Bool)- -> RequestMiddleware' m req (BasicAuth : req) a-basicAuth (Realm realm) credCheck handler = Kleisli $- probe @BasicAuth >=> either unauthorized (validateCredentials >=> runKleisli handler)- where- unauthorized :: BasicAuthError -> m (Response a)- unauthorized = const $ errorResponse- $ setResponseHeader "WWW-Authenticate" ("Basic realm=\"" <> realm <> "\"")- $ unauthorized401 "Unauthorized"-- validateCredentials :: Linked (BasicAuth : req) Request- -> m (Linked (BasicAuth : req) Request)- validateCredentials req = do- valid <- credCheck $ get (Proxy @BasicAuth) req- if valid- then pure req- else errorResponse $ forbidden403 "Forbidden"
− src/WebGear/Middlewares/Body.hs
@@ -1,71 +0,0 @@--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com------ Middlewares related to HTTP body.-module WebGear.Middlewares.Body- ( JSONRequestBody- , jsonRequestBody- , jsonResponseBody- ) where--import Control.Arrow (Kleisli (..))-import Control.Monad ((>=>))-import Control.Monad.IO.Class (MonadIO, liftIO)-import Data.Aeson (FromJSON, ToJSON, eitherDecode', encode)-import Data.ByteString.Lazy (ByteString, fromChunks, fromStrict)-import Data.Kind (Type)-import Data.Text (Text, pack)-import Data.Text.Encoding (encodeUtf8)-import Network.HTTP.Types (hContentType)--import WebGear.Trait (Result (..), Trait (..), probe)-import WebGear.Types (MonadRouter (..), Request, RequestMiddleware', Response (..),- ResponseMiddleware', badRequest400, getRequestBodyChunk, setResponseHeader)-import WebGear.Util (takeWhileM)---- | A 'Trait' for converting a JSON request body into a value.-data JSONRequestBody (t :: Type)--instance (FromJSON t, MonadIO m) => Trait (JSONRequestBody t) Request m where- type Attribute (JSONRequestBody t) Request = t- type Absence (JSONRequestBody t) Request = Text-- toAttribute :: Request -> m (Result (JSONRequestBody t) Request)- toAttribute r = do- chunks <- takeWhileM (/= mempty) $ repeat $ liftIO $ getRequestBodyChunk r- pure $ case eitherDecode' (fromChunks chunks) of- Left e -> NotFound (pack e)- Right t -> Found t---- | A middleware to parse the request body as JSON and convert it to--- a value via a 'FromJSON' instance.------ Usage for a type @t@ which has a 'FromJSON' instance:------ > jsonRequestBody @t handler------ Returns a 400 Bad Request response on failure to parse body.-jsonRequestBody :: forall t m req a. (FromJSON t, MonadRouter m, MonadIO m)- => RequestMiddleware' m req (JSONRequestBody t:req) a-jsonRequestBody handler = Kleisli $- probe @(JSONRequestBody t) >=> either (errorResponse . mkError) (runKleisli handler)- where- mkError :: Text -> Response ByteString- mkError e = badRequest400 $ fromStrict $ encodeUtf8 $ "Error parsing request body: " <> e---- | A middleware that converts the response that has a 'ToJSON'--- instance to a 'ByteString' response.------ This will also set the "Content-Type" header of the response to--- "application/json".------ Usage for a type @t@ which has a 'ToJSON' instance:------ > jsonResponseBody @t handler----jsonResponseBody :: (ToJSON t, Monad m) => ResponseMiddleware' m req t ByteString-jsonResponseBody handler = Kleisli $ \req -> do- x <- runKleisli handler req- pure $ setResponseHeader hContentType "application/json" $ encode <$> x
− src/WebGear/Middlewares/Header.hs
@@ -1,356 +0,0 @@--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com------ Middlewares related to HTTP headers.----module WebGear.Middlewares.Header- ( -- * Traits- Header- , Header'- , HeaderNotFound (..)- , HeaderParseError (..)- , HeaderMatch- , HeaderMatch'- , HeaderMismatch (..)-- -- * Middlewares- , header- , optionalHeader- , lenientHeader- , optionalLenientHeader- , headerMatch- , optionalHeaderMatch- , requestContentTypeHeader- , addResponseHeader- ) where--import Control.Arrow (Kleisli (..))-import Control.Monad ((>=>))-import Data.ByteString (ByteString)-import Data.Kind (Type)-import Data.Proxy (Proxy (..))-import Data.String (fromString)-import Data.Text (Text)-import Data.Void (Void, absurd)-import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)-import Network.HTTP.Types (HeaderName)-import Text.Printf (printf)-import Web.HttpApiData (FromHttpApiData (..), ToHttpApiData (..))--import WebGear.Modifiers (Existence (..), ParseStyle (..))-import WebGear.Trait (Result (..), Trait (..), probe)-import WebGear.Types (MonadRouter (..), Request, RequestMiddleware', Response (..),- ResponseMiddleware', badRequest400, requestHeader, responseHeader,- setResponseHeader)--import qualified Data.ByteString.Lazy as LBS----- | A 'Trait' for capturing an HTTP header of specified @name@ and--- converting it to some type @val@ via 'FromHttpApiData'. The--- modifiers @e@ and @p@ determine how missing headers and parsing--- errors are handled. The header name is compared case-insensitively.-data Header' (e :: Existence) (p :: ParseStyle) (name :: Symbol) (val :: Type)---- | A 'Trait' for capturing a header with name @name@ in a request or--- response and convert it to some type @val@ via 'FromHttpApiData'.-type Header (name :: Symbol) (val :: Type) = Header' Required Strict name val---- | Indicates a missing header-data HeaderNotFound = HeaderNotFound- deriving stock (Read, Show, Eq)---- | Error in converting a header-newtype HeaderParseError = HeaderParseError Text- deriving stock (Read, Show, Eq)--deriveRequestHeader :: (KnownSymbol name, FromHttpApiData val)- => Proxy name -> Request -> (Maybe (Either Text val) -> r) -> r-deriveRequestHeader proxy req cont =- let s = fromString $ symbolVal proxy- in cont $ parseHeader <$> requestHeader s req--deriveResponseHeader :: (KnownSymbol name, FromHttpApiData val)- => Proxy name -> Response a -> (Maybe (Either Text val) -> r) -> r-deriveResponseHeader proxy res cont =- let s = fromString $ symbolVal proxy- in cont $ parseHeader <$> responseHeader s res---instance (KnownSymbol name, FromHttpApiData val, Monad m) => Trait (Header' Required Strict name val) Request m where- type Attribute (Header' Required Strict name val) Request = val- type Absence (Header' Required Strict name val) Request = Either HeaderNotFound HeaderParseError-- toAttribute :: Request -> m (Result (Header' Required Strict name val) Request)- toAttribute r = pure $ deriveRequestHeader (Proxy @name) r $ \case- Nothing -> NotFound (Left HeaderNotFound)- Just (Left e) -> NotFound (Right $ HeaderParseError e)- Just (Right x) -> Found x---instance (KnownSymbol name, FromHttpApiData val, Monad m) => Trait (Header' Optional Strict name val) Request m where- type Attribute (Header' Optional Strict name val) Request = Maybe val- type Absence (Header' Optional Strict name val) Request = HeaderParseError-- toAttribute :: Request -> m (Result (Header' Optional Strict name val) Request)- toAttribute r = pure $ deriveRequestHeader (Proxy @name) r $ \case- Nothing -> Found Nothing- Just (Left e) -> NotFound $ HeaderParseError e- Just (Right x) -> Found (Just x)---instance (KnownSymbol name, FromHttpApiData val, Monad m) => Trait (Header' Required Lenient name val) Request m where- type Attribute (Header' Required Lenient name val) Request = Either Text val- type Absence (Header' Required Lenient name val) Request = HeaderNotFound-- toAttribute :: Request -> m (Result (Header' Required Lenient name val) Request)- toAttribute r = pure $ deriveRequestHeader (Proxy @name) r $ \case- Nothing -> NotFound HeaderNotFound- Just (Left e) -> Found (Left e)- Just (Right x) -> Found (Right x)---instance (KnownSymbol name, FromHttpApiData val, Monad m) => Trait (Header' Optional Lenient name val) Request m where- type Attribute (Header' Optional Lenient name val) Request = Maybe (Either Text val)- type Absence (Header' Optional Lenient name val) Request = Void-- toAttribute :: Request -> m (Result (Header' Optional Lenient name val) Request)- toAttribute r = pure $ deriveRequestHeader (Proxy @name) r $ \case- Nothing -> Found Nothing- Just (Left e) -> Found (Just (Left e))- Just (Right x) -> Found (Just (Right x))---instance (KnownSymbol name, FromHttpApiData val, Monad m) => Trait (Header' Required Strict name val) (Response a) m where- type Attribute (Header' Required Strict name val) (Response a) = val- type Absence (Header' Required Strict name val) (Response a) = Either HeaderNotFound HeaderParseError-- toAttribute :: Response a -> m (Result (Header' Required Strict name val) (Response a))- toAttribute r = pure $ deriveResponseHeader (Proxy @name) r $ \case- Nothing -> NotFound (Left HeaderNotFound)- Just (Left e) -> NotFound (Right $ HeaderParseError e)- Just (Right x) -> Found x---instance (KnownSymbol name, FromHttpApiData val, Monad m) => Trait (Header' Optional Strict name val) (Response a) m where- type Attribute (Header' Optional Strict name val) (Response a) = Maybe val- type Absence (Header' Optional Strict name val) (Response a) = HeaderParseError-- toAttribute :: Response a -> m (Result (Header' Optional Strict name val) (Response a))- toAttribute r = pure $ deriveResponseHeader (Proxy @name) r $ \case- Nothing -> Found Nothing- Just (Left e) -> NotFound $ HeaderParseError e- Just (Right x) -> Found (Just x)---instance (KnownSymbol name, FromHttpApiData val, Monad m) => Trait (Header' Required Lenient name val) (Response a) m where- type Attribute (Header' Required Lenient name val) (Response a) = Either Text val- type Absence (Header' Required Lenient name val) (Response a) = HeaderNotFound-- toAttribute :: Response a -> m (Result (Header' Required Lenient name val) (Response a))- toAttribute r = pure $ deriveResponseHeader (Proxy @name) r $ \case- Nothing -> NotFound HeaderNotFound- Just (Left e) -> Found (Left e)- Just (Right x) -> Found (Right x)---instance (KnownSymbol name, FromHttpApiData val, Monad m) => Trait (Header' Optional Lenient name val) (Response a) m where- type Attribute (Header' Optional Lenient name val) (Response a) = Maybe (Either Text val)- type Absence (Header' Optional Lenient name val) (Response a) = ()-- toAttribute :: Response a -> m (Result (Header' Optional Lenient name val) (Response a))- toAttribute r = pure $ deriveResponseHeader (Proxy @name) r $ \case- Nothing -> Found Nothing- Just (Left e) -> Found (Just (Left e))- Just (Right x) -> Found (Just (Right x))----- | A 'Trait' for ensuring that an HTTP header with specified @name@--- has value @val@. The modifier @e@ determines how missing headers--- are handled. The header name is compared case-insensitively.-data HeaderMatch' (e :: Existence) (name :: Symbol) (val :: Symbol)---- | A 'Trait' for ensuring that a header with a specified @name@ has--- value @val@.-type HeaderMatch (name :: Symbol) (val :: Symbol) = HeaderMatch' Required name val---- | Failure in extracting a header value-data HeaderMismatch = HeaderMismatch- { expectedHeader :: ByteString- , actualHeader :: ByteString- }- deriving stock (Eq, Read, Show)---instance (KnownSymbol name, KnownSymbol val, Monad m) => Trait (HeaderMatch' Required name val) Request m where- type Attribute (HeaderMatch' Required name val) Request = ()- type Absence (HeaderMatch' Required name val) Request = Maybe HeaderMismatch-- toAttribute :: Request -> m (Result (HeaderMatch' Required name val) Request)- toAttribute r = pure $- let- name = fromString $ symbolVal (Proxy @name)- expected = fromString $ symbolVal (Proxy @val)- in- case requestHeader name r of- Nothing -> NotFound Nothing- Just hv | hv == expected -> Found ()- | otherwise -> NotFound $ Just HeaderMismatch {expectedHeader = expected, actualHeader = hv}--instance (KnownSymbol name, KnownSymbol val, Monad m) => Trait (HeaderMatch' Optional name val) Request m where- type Attribute (HeaderMatch' Optional name val) Request = Maybe ()- type Absence (HeaderMatch' Optional name val) Request = HeaderMismatch-- toAttribute :: Request -> m (Result (HeaderMatch' Optional name val) Request)- toAttribute r = pure $- let- name = fromString $ symbolVal (Proxy @name)- expected = fromString $ symbolVal (Proxy @val)- in- case requestHeader name r of- Nothing -> Found Nothing- Just hv | hv == expected -> Found (Just ())- | otherwise -> NotFound HeaderMismatch {expectedHeader = expected, actualHeader = hv}----- | A middleware to extract a header value and convert it to a value--- of type @val@ using 'FromHttpApiData'.------ Example usage:------ > header @"Content-Length" @Integer handler------ The associated trait attribute has type @val@. A 400 Bad Request--- response is returned if the header is not found or could not be--- parsed.-header :: forall name val m req a.- (KnownSymbol name, FromHttpApiData val, MonadRouter m)- => RequestMiddleware' m req (Header name val:req) a-header handler = Kleisli $- probe @(Header name val) >=> either (errorResponse . mkError) (runKleisli handler)- where- headerName :: String- headerName = symbolVal $ Proxy @name-- mkError :: Either HeaderNotFound HeaderParseError -> Response LBS.ByteString- mkError (Left HeaderNotFound) = badRequest400 $ fromString $ printf "Could not find header %s" headerName- mkError (Right (HeaderParseError _)) = badRequest400 $ fromString $- printf "Invalid value for header %s" headerName---- | A middleware to extract a header value and convert it to a value--- of type @val@ using 'FromHttpApiData'.------ Example usage:------ > optionalHeader @"Content-Length" @Integer handler------ The associated trait attribute has type @Maybe val@; a @Nothing@--- value indicates that the header is missing from the request. A 400--- Bad Request response is returned if the header could not be parsed.-optionalHeader :: forall name val m req a.- (KnownSymbol name, FromHttpApiData val, MonadRouter m)- => RequestMiddleware' m req (Header' Optional Strict name val:req) a-optionalHeader handler = Kleisli $- probe @(Header' Optional Strict name val) >=> either (errorResponse . mkError) (runKleisli handler)- where- headerName :: String- headerName = symbolVal $ Proxy @name-- mkError :: HeaderParseError -> Response LBS.ByteString- mkError (HeaderParseError _) = badRequest400 $ fromString $- printf "Invalid value for header %s" headerName---- | A middleware to extract a header value and convert it to a value--- of type @val@ using 'FromHttpApiData'.------ Example usage:------ > lenientHeader @"Content-Length" @Integer handler------ The associated trait attribute has type @Either Text val@. A 400--- Bad Request reponse is returned if the header is missing. The--- parsing is done leniently; the trait attribute is set to @Left--- Text@ in case of parse errors or @Right val@ on success.-lenientHeader :: forall name val m req a.- (KnownSymbol name, FromHttpApiData val, MonadRouter m)- => RequestMiddleware' m req (Header' Required Lenient name val:req) a-lenientHeader handler = Kleisli $- probe @(Header' Required Lenient name val) >=> either (errorResponse . mkError) (runKleisli handler)- where- headerName :: String- headerName = symbolVal $ Proxy @name-- mkError :: HeaderNotFound -> Response LBS.ByteString- mkError HeaderNotFound = badRequest400 $ fromString $ printf "Could not find header %s" headerName---- | A middleware to extract an optional header value and convert it--- to a value of type @val@ using 'FromHttpApiData'.------ Example usage:------ > optionalLenientHeader @"Content-Length" @Integer handler------ The associated trait attribute has type @Maybe (Either Text--- val)@. This middleware never fails.-optionalLenientHeader :: forall name val m req a.- (KnownSymbol name, FromHttpApiData val, MonadRouter m)- => RequestMiddleware' m req (Header' Optional Lenient name val:req) a-optionalLenientHeader handler = Kleisli $- probe @(Header' Optional Lenient name val) >=> either absurd (runKleisli handler)---- | A middleware to ensure that a header in the request has a--- specific value. Fails the handler with a 400 Bad Request response--- if the header does not exist or does not match.-headerMatch :: forall name val m req a.- (KnownSymbol name, KnownSymbol val, MonadRouter m)- => RequestMiddleware' m req (HeaderMatch name val:req) a-headerMatch handler = Kleisli $- probe @(HeaderMatch name val) >=> either (errorResponse . mkError) (runKleisli handler)- where- headerName :: String- headerName = symbolVal $ Proxy @name-- mkError :: Maybe HeaderMismatch -> Response LBS.ByteString- mkError Nothing = badRequest400 $ fromString $ printf "Could not find header %s" headerName- mkError (Just e) = badRequest400 $ fromString $- printf "Expected header %s to have value %s but found %s" headerName (show $ expectedHeader e) (show $ actualHeader e)---- | A middleware to ensure that an optional header in the request has--- a specific value. Fails the handler with a 400 Bad Request response--- if the header has a different value.-optionalHeaderMatch :: forall name val m req a.- (KnownSymbol name, KnownSymbol val, MonadRouter m)- => RequestMiddleware' m req (HeaderMatch' Optional name val:req) a-optionalHeaderMatch handler = Kleisli $- probe @(HeaderMatch' Optional name val) >=> either (errorResponse . mkError) (runKleisli handler)- where- headerName :: String- headerName = symbolVal $ Proxy @name-- mkError :: HeaderMismatch -> Response LBS.ByteString- mkError e = badRequest400 $ fromString $- printf "Expected header %s to have value %s but found %s" headerName (show $ expectedHeader e) (show $ actualHeader e)---- | A middleware to check that the Content-Type header in the request--- has a specific value. It will fail the handler if the header did--- not match.------ Example usage:------ > requestContentTypeHeader @"application/json" handler----requestContentTypeHeader :: forall val m req a. (KnownSymbol val, MonadRouter m)- => RequestMiddleware' m req (HeaderMatch "Content-Type" val:req) a-requestContentTypeHeader = headerMatch @"Content-Type" @val---- | A middleware to create or update a response header.------ Example usage:------ > addResponseHeader "Content-type" "application/json" handler----addResponseHeader :: forall t m req a. (ToHttpApiData t, Monad m)- => HeaderName -> t -> ResponseMiddleware' m req a a-addResponseHeader name val handler = Kleisli $ runKleisli handler >=> pure . setResponseHeader name (toHeader val)
− src/WebGear/Middlewares/Method.hs
@@ -1,85 +0,0 @@--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com------ Middlewares related to HTTP methods.-module WebGear.Middlewares.Method- ( Method- , IsStdMethod (..)- , MethodMismatch (..)- , method- ) where--import Control.Arrow (Kleisli (..))-import Control.Monad ((>=>))-import Data.Proxy (Proxy (..))--import WebGear.Trait (Result (..), Trait (..), probe)-import WebGear.Types (MonadRouter (..), Request, RequestMiddleware', requestMethod)--import qualified Network.HTTP.Types as HTTP----- | A 'Trait' for capturing the HTTP method of a request-data Method (t :: HTTP.StdMethod)---- | Failure to match method against an expected value-data MethodMismatch = MethodMismatch- { expectedMethod :: HTTP.Method- , actualMethod :: HTTP.Method- }--instance (IsStdMethod t, Monad m) => Trait (Method t) Request m where- type Attribute (Method t) Request = ()- type Absence (Method t) Request = MethodMismatch-- toAttribute :: Request -> m (Result (Method t) Request)- toAttribute r =- let- expected = HTTP.renderStdMethod $ toStdMethod $ Proxy @t- actual = requestMethod r- in- pure $ if expected == actual- then Found ()- else NotFound $ MethodMismatch expected actual----- | A typeclass to map a 'HTTP.StdMethod' from type level to term--- level.-class IsStdMethod t where- -- | Convert @t@ to term level.- toStdMethod :: Proxy t -> HTTP.StdMethod--instance IsStdMethod HTTP.GET where- toStdMethod = const HTTP.GET-instance IsStdMethod HTTP.POST where- toStdMethod = const HTTP.POST-instance IsStdMethod HTTP.HEAD where- toStdMethod = const HTTP.HEAD-instance IsStdMethod HTTP.PUT where- toStdMethod = const HTTP.PUT-instance IsStdMethod HTTP.DELETE where- toStdMethod = const HTTP.DELETE-instance IsStdMethod HTTP.TRACE where- toStdMethod = const HTTP.TRACE-instance IsStdMethod HTTP.CONNECT where- toStdMethod = const HTTP.CONNECT-instance IsStdMethod HTTP.OPTIONS where- toStdMethod = const HTTP.OPTIONS-instance IsStdMethod HTTP.PATCH where- toStdMethod = const HTTP.PATCH---- | A middleware to check whether the request has a specified HTTP--- method.------ Typically this would be used with a type application such as:------ > method @GET handler------ It is also idiomatic to use the template haskell quasiquoter--- 'WebGear.Middlewares.Path.match' in cases where both HTTP method--- and path needs to be matched.-method :: forall t m req a. (IsStdMethod t, MonadRouter m)- => RequestMiddleware' m req (Method t:req) a-method handler = Kleisli $ probe @(Method t) >=> either (const rejectRoute) (runKleisli handler)
− src/WebGear/Middlewares/Params.hs
@@ -1,186 +0,0 @@--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com------ Middlewares for handling query parameters----module WebGear.Middlewares.Params- ( -- * Traits- QueryParam- , QueryParam'- , ParamNotFound (..)- , ParamParseError (..)-- -- * Middlewares- , queryParam- , optionalQueryParam- , lenientQueryParam- , optionalLenientQueryParam- ) where--import Control.Arrow (Kleisli (..))-import Control.Monad ((>=>))-import Data.List (find)-import Data.Proxy (Proxy (..))-import Data.String (fromString)-import Data.Text (Text)-import Data.Void (Void, absurd)-import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)-import Network.HTTP.Types (queryToQueryText)-import Text.Printf (printf)-import Web.HttpApiData (FromHttpApiData (..))--import WebGear.Modifiers (Existence (..), ParseStyle (..))-import WebGear.Trait (Result (..), Trait (..), probe)-import WebGear.Types (MonadRouter (..), Request, RequestMiddleware', Response (..), badRequest400,- queryString)--import qualified Data.ByteString.Lazy as LBS----- | Capture a query parameter with a specified @name@ and convert it--- to a value of type @val@ via 'FromHttpApiData'.-type QueryParam (name :: Symbol) val = QueryParam' Required Strict name val---- | Capture a query parameter with a specified @name@ and convert it--- to a value of type @val@ via 'FromHttpApiData'. The type parameter--- @e@ denotes whether the query parameter is required to be--- present. The parse style parameter @p@ determines whether the--- conversion is applied strictly or leniently.-data QueryParam' (e :: Existence) (p :: ParseStyle) (name :: Symbol) val---- | Indicates a missing query parameter-data ParamNotFound = ParamNotFound- deriving stock (Read, Show, Eq)---- | Error in converting a query parameter-newtype ParamParseError = ParamParseError Text- deriving stock (Read, Show, Eq)--deriveRequestParam :: (KnownSymbol name, FromHttpApiData val)- => Proxy name -> Request -> (Maybe (Either Text val) -> r) -> r-deriveRequestParam proxy req cont =- let name = fromString $ symbolVal proxy- params = queryToQueryText $ queryString req- in cont $ parseQueryParam <$> (find ((== name) . fst) params >>= snd)--instance (KnownSymbol name, FromHttpApiData val, Monad m) => Trait (QueryParam' Required Strict name val) Request m where- type Attribute (QueryParam' Required Strict name val) Request = val- type Absence (QueryParam' Required Strict name val) Request = Either ParamNotFound ParamParseError-- toAttribute :: Request -> m (Result (QueryParam' Required Strict name val) Request)- toAttribute r = pure $ deriveRequestParam (Proxy @name) r $ \case- Nothing -> NotFound (Left ParamNotFound)- Just (Left e) -> NotFound (Right $ ParamParseError e)- Just (Right x) -> Found x--instance (KnownSymbol name, FromHttpApiData val, Monad m) => Trait (QueryParam' Optional Strict name val) Request m where- type Attribute (QueryParam' Optional Strict name val) Request = Maybe val- type Absence (QueryParam' Optional Strict name val) Request = ParamParseError-- toAttribute :: Request -> m (Result (QueryParam' Optional Strict name val) Request)- toAttribute r = pure $ deriveRequestParam (Proxy @name) r $ \case- Nothing -> Found Nothing- Just (Left e) -> NotFound $ ParamParseError e- Just (Right x) -> Found (Just x)--instance (KnownSymbol name, FromHttpApiData val, Monad m) => Trait (QueryParam' Required Lenient name val) Request m where- type Attribute (QueryParam' Required Lenient name val) Request = Either Text val- type Absence (QueryParam' Required Lenient name val) Request = ParamNotFound-- toAttribute :: Request -> m (Result (QueryParam' Required Lenient name val) Request)- toAttribute r = pure $ deriveRequestParam (Proxy @name) r $ \case- Nothing -> NotFound ParamNotFound- Just (Left e) -> Found (Left e)- Just (Right x) -> Found (Right x)--instance (KnownSymbol name, FromHttpApiData val, Monad m) => Trait (QueryParam' Optional Lenient name val) Request m where- type Attribute (QueryParam' Optional Lenient name val) Request = Maybe (Either Text val)- type Absence (QueryParam' Optional Lenient name val) Request = Void-- toAttribute :: Request -> m (Result (QueryParam' Optional Lenient name val) Request)- toAttribute r = pure $ deriveRequestParam (Proxy @name) r $ \case- Nothing -> Found Nothing- Just (Left e) -> Found (Just (Left e))- Just (Right x) -> Found (Just (Right x))----- | A middleware to extract a query parameter and convert it to a--- value of type @val@ using 'FromHttpApiData'.------ Example usage:------ > queryParam @"limit" @Int handler------ The associated trait attribute has type @val@. This middleware will--- respond with a 400 Bad Request response if the query parameter is--- not found or could not be parsed.-queryParam :: forall name val m req a. (KnownSymbol name, FromHttpApiData val, MonadRouter m)- => RequestMiddleware' m req (QueryParam name val:req) a-queryParam handler = Kleisli $ probe @(QueryParam name val) >=> either (errorResponse . mkError) (runKleisli handler)- where- paramName :: String- paramName = symbolVal $ Proxy @name-- mkError :: Either ParamNotFound ParamParseError -> Response LBS.ByteString- mkError err = badRequest400 $ fromString $- case err of- Left ParamNotFound -> printf "Could not find query parameter %s" paramName- Right (ParamParseError _) -> printf "Invalid value for query parameter %s" paramName---- | A middleware to extract an optional query parameter and convert--- it to a value of type @val@ using 'FromHttpApiData'.------ Example usage:------ > optionalQueryParam @"limit" @Int handler------ The associated trait attribute has type @Maybe val@; a @Nothing@--- value indicates a missing param. A 400 Bad Request response is--- returned if the query parameter could not be parsed.-optionalQueryParam :: forall name val m req a. (KnownSymbol name, FromHttpApiData val, MonadRouter m)- => RequestMiddleware' m req (QueryParam' Optional Strict name val:req) a-optionalQueryParam handler = Kleisli $ probe @(QueryParam' Optional Strict name val) >=> either (errorResponse . mkError) (runKleisli handler)- where- paramName :: String- paramName = symbolVal $ Proxy @name-- mkError :: ParamParseError -> Response LBS.ByteString- mkError _ = badRequest400 $ fromString $ printf "Invalid value for query parameter %s" paramName---- | A middleware to extract a query parameter and convert it to a--- value of type @val@ using 'FromHttpApiData'.------ Example usage:------ > lenientQueryParam @"limit" @Int handler------ The associated trait attribute has type @Either Text val@. A 400--- Bad Request reponse is returned if the query parameter is--- missing. The parsing is done leniently; the trait attribute is set--- to @Left Text@ in case of parse errors or @Right val@ on success.-lenientQueryParam :: forall name val m req a. (KnownSymbol name, FromHttpApiData val, MonadRouter m)- => RequestMiddleware' m req (QueryParam' Required Lenient name val:req) a-lenientQueryParam handler = Kleisli $- probe @(QueryParam' Required Lenient name val) >=> either (errorResponse . mkError) (runKleisli handler)- where- paramName :: String- paramName = symbolVal $ Proxy @name-- mkError :: ParamNotFound -> Response LBS.ByteString- mkError ParamNotFound = badRequest400 $ fromString $ printf "Could not find query parameter %s" paramName---- | A middleware to extract an optional query parameter and convert it--- to a value of type @val@ using 'FromHttpApiData'.------ Example usage:------ > optionalLenientHeader @"Content-Length" @Integer handler------ The associated trait attribute has type @Maybe (Either Text--- val)@. This middleware never fails.-optionalLenientQueryParam :: forall name val m req a. (KnownSymbol name, FromHttpApiData val, MonadRouter m)- => RequestMiddleware' m req (QueryParam' Optional Lenient name val:req) a-optionalLenientQueryParam handler = Kleisli $- probe @(QueryParam' Optional Lenient name val) >=> either absurd (runKleisli handler)
− src/WebGear/Middlewares/Path.hs
@@ -1,237 +0,0 @@--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com------ Middlewares related to route paths.-module WebGear.Middlewares.Path- ( Path- , PathVar- , PathVarError (..)- , PathEnd- , path- , pathVar- , pathEnd- , match- , route- ) where--import Control.Arrow (Kleisli (..))-import Control.Monad ((>=>))-import Control.Monad.State.Strict (MonadState (..))-import Data.Foldable (toList)-import Data.Function ((&))-import Data.List.NonEmpty (NonEmpty (..), filter)-import Data.Proxy (Proxy (..))-import Data.Text (Text, pack)-import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)-import Language.Haskell.TH.Quote (QuasiQuoter (..))-import Language.Haskell.TH.Syntax (Exp (..), Q, TyLit (..), Type (..), mkName)-import Prelude hiding (drop, filter, take)-import Web.HttpApiData (FromHttpApiData (..))--import WebGear.Middlewares.Method (method)-import WebGear.Trait (Result (..), Trait (..), probe)-import WebGear.Types (MonadRouter (..), PathInfo (..), Request, RequestMiddleware')-import WebGear.Util (splitOn)--import qualified Data.List as List----- | A path component which is literally matched against the request--- but discarded after that.-data Path (s :: Symbol)--instance (KnownSymbol s, MonadState PathInfo m) => Trait (Path s) Request m where- type Attribute (Path s) Request = ()- type Absence (Path s) Request = ()-- toAttribute :: Request -> m (Result (Path s) Request)- toAttribute _ = do- PathInfo actualPath <- get- case List.stripPrefix expectedPath actualPath of- Nothing -> pure $ NotFound ()- Just rest -> do- put $ PathInfo rest- pure $ Found ()-- where- expectedPath = Proxy @s- & symbolVal- & splitOn '/'- & filter (/= "")- & map pack----- | A path variable that is extracted and converted to a value of--- type @val@. The @tag@ is usually a type-level symbol (string) to--- uniquely identify this variable.-data PathVar tag val---- | Failure to extract a 'PathVar'-data PathVarError = PathVarNotFound | PathVarParseError Text- deriving (Eq, Show, Read)--instance (FromHttpApiData val, MonadState PathInfo m) => Trait (PathVar tag val) Request m where- type Attribute (PathVar tag val) Request = val- type Absence (PathVar tag val) Request = PathVarError-- toAttribute :: Request -> m (Result (PathVar tag val) Request)- toAttribute _ = do- PathInfo actualPath <- get- case actualPath of- [] -> pure $ NotFound PathVarNotFound- (x:xs) -> case parseUrlPiece @val x of- Left e -> pure $ NotFound $ PathVarParseError e- Right v -> do- put $ PathInfo xs- pure $ Found v---- | Trait to indicate that no more path components are present in the request-data PathEnd--instance MonadState PathInfo m => Trait PathEnd Request m where- type Attribute PathEnd Request = ()- type Absence PathEnd Request = ()-- toAttribute :: Request -> m (Result PathEnd Request)- toAttribute _ = do- PathInfo actualPath <- get- pure $ if null actualPath- then Found ()- else NotFound ()----- | A middleware that literally matches path @s@.------ The symbol @s@ could contain one or more parts separated by a--- forward slash character. The route will be rejected if there is no--- match.------ For example, the following code could be used to match the URL path--- \"a\/b\/c\" and then invoke @handler@:------ > path @"a/b/c" handler----path :: forall s ts m a. (KnownSymbol s, MonadRouter m)- => RequestMiddleware' m ts (Path s:ts) a-path handler = Kleisli $- probe @(Path s) >=> either (const rejectRoute) (runKleisli handler)---- | A middleware that captures a path variable from a single path--- component.------ The value captured is converted to a value of type @val@ via--- 'FromHttpApiData'. The route will be rejected if the value is not--- found or cannot be converted.------ For example, the following code could be used to read a path--- component as 'Int' tagged with the symbol \"objId\", and then--- invoke @handler@:------ > pathVar @"objId" @Int handler----pathVar :: forall tag val ts m a. (FromHttpApiData val, MonadRouter m)- => RequestMiddleware' m ts (PathVar tag val:ts) a-pathVar handler = Kleisli $- probe @(PathVar tag val) >=> either (const rejectRoute) (runKleisli handler)---- | A middleware that verifies that end of path is reached.-pathEnd :: MonadRouter m => RequestMiddleware' m ts (PathEnd:ts) a-pathEnd handler = Kleisli $- probe @PathEnd >=> either (const rejectRoute) (runKleisli handler)---- | Produces middleware(s) to match an optional HTTP method and some--- path components.------ This middleware matches a prefix of path components, the remaining--- components can be matched by subsequent uses of 'match'.------ This quasiquoter can be used in several ways:------ +---------------------------------------+---------------------------------------------------------------------------------------+--- | QuasiQuoter | Equivalent Middleware |--- +=======================================+=======================================================================================+--- | @[match| \/a\/b\/c |]@ | @'path' \@\"\/a\/b\/c\"@ |--- +---------------------------------------+---------------------------------------------------------------------------------------+--- | @[match| \/a\/b\/objId:Int\/d |]@ | @'path' \@\"\/a\/b\" . 'pathVar' \@\"objId\" \@Int . 'path' \@\"d\"@ |--- +---------------------------------------+---------------------------------------------------------------------------------------+--- | @[match| GET \/a\/b\/c |]@ | @'method' \@GET . 'path' \@\"\/a\/b\/c\"@ |--- +---------------------------------------+---------------------------------------------------------------------------------------+--- | @[match| GET \/a\/b\/objId:Int\/d |]@ | @'method' \@GET . 'path' \@\"\/a\/b\" . 'pathVar' \@\"objId\" \@Int . 'path' \@\"d\"@ |--- +---------------------------------------+---------------------------------------------------------------------------------------+----match :: QuasiQuoter-match = QuasiQuoter- { quoteExp = toMatchExp- , quotePat = const $ fail "match cannot be used in a pattern"- , quoteType = const $ fail "match cannot be used in a type"- , quoteDec = const $ fail "match cannot be used in a declaration"- }---- | Produces middleware(s) to match an optional HTTP method and the--- entire request path.------ This middleware is intended to be used in cases where the entire--- path needs to be matched. Use 'match' middleware to match only an--- initial portion of the path.------ This quasiquoter can be used in several ways:------ +---------------------------------------+---------------------------------------------------------------------------------------------------+--- | QuasiQuoter | Equivalent Middleware |--- +=======================================+===================================================================================================+--- | @[route| \/a\/b\/c |]@ | @'path' \@\"\/a\/b\/c\" . 'pathEnd'@ |--- +---------------------------------------+---------------------------------------------------------------------------------------------------+--- | @[route| \/a\/b\/objId:Int\/d |]@ | @'path' \@\"\/a\/b\" . 'pathVar' \@\"objId\" \@Int . 'path' \@\"d\" . 'pathEnd'@ |--- +---------------------------------------+---------------------------------------------------------------------------------------------------+--- | @[route| GET \/a\/b\/c |]@ | @'method' \@GET . 'path' \@\"\/a\/b\/c\" . 'pathEnd'@ |--- +---------------------------------------+---------------------------------------------------------------------------------------------------+--- | @[route| GET \/a\/b\/objId:Int\/d |]@ | @'method' \@GET . 'path' \@\"\/a\/b\" . 'pathVar' \@\"objId\" \@Int . 'path' \@\"d\" . 'pathEnd'@ |--- +---------------------------------------+---------------------------------------------------------------------------------------------------+----route :: QuasiQuoter-route = QuasiQuoter- { quoteExp = toRouteExp- , quotePat = const $ fail "route cannot be used in a pattern"- , quoteType = const $ fail "route cannot be used in a type"- , quoteDec = const $ fail "route cannot be used in a declaration"- }--toRouteExp :: String -> Q Exp-toRouteExp s = do- e <- toMatchExp s- pure $ compose e (VarE 'pathEnd)--toMatchExp :: String -> Q Exp-toMatchExp s = case List.words s of- [m, p] -> do- let methodExp = AppTypeE (VarE 'method) (ConT $ mkName m)- pathExps <- toPathExps p- pure $ List.foldr1 compose $ methodExp :| pathExps- [p] -> do- pathExps <- toPathExps p- pure $ List.foldr1 compose pathExps- _ -> fail "Expected an HTTP method and a path or just a path"-- where- toPathExps :: String -> Q [Exp]- toPathExps p = splitOn '/' p- & filter (/= "")- & fmap (splitOn ':')- & List.foldr joinPath []- & fmap toPathExp- & sequence-- joinPath :: NonEmpty String -> [NonEmpty String] -> [NonEmpty String]- joinPath p [] = [p]- joinPath (p:|[]) ((p':|[]) : xs) = ((p <> "/" <> p') :| []) : xs- joinPath y (x:xs) = y:x:xs-- toPathExp :: NonEmpty String -> Q Exp- toPathExp (p :| []) = pure $ AppTypeE (VarE 'path) (LitT $ StrTyLit p)- toPathExp (v :| [t]) = pure $ AppTypeE (AppTypeE (VarE 'pathVar) (LitT $ StrTyLit v)) (ConT $ mkName t)- toPathExp xs = fail $ "Invalid path component: " <> List.intercalate ":" (toList xs)--compose :: Exp -> Exp -> Exp-compose l = UInfixE l (VarE $ mkName ".")
− src/WebGear/Modifiers.hs
@@ -1,18 +0,0 @@--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com----module WebGear.Modifiers- ( Existence (..)- , ParseStyle (..)- ) where----- | Modifier used to indicate whether a trait is required or--- optional.-data Existence = Required | Optional---- | Modifier used to indicate whether a trait is parsed strictly or--- leniently.-data ParseStyle = Strict | Lenient
+ src/WebGear/Server.hs view
@@ -0,0 +1,39 @@+{- | Main module for WebGear server.++ Import this module to get all required types and functions to build a+ WebGear server. Alternatively, import individual modules under+ @WebGear.Server@.++ Typical usage to implement a server is:++@+ import WebGear.Server+ import Network.Wai (Application)+ import qualified Network.Wai.Handler.Warp as Warp++ -- | A monad stack for you application+ type App a = ....++ -- | Convert the App monad to IO+ runApp :: App a -> IO a+ runApp = ....++ -- | Handler for the server+ myHandler :: `Handler` h App => `RequestHandler` h '[]+ myHandler = ....++ app :: Application+ app = `toApplication` (`transform` runApp myHandler)++ main :: IO ()+ main = Warp.run port app+@+-}+module WebGear.Server (+ module WebGear.Core,+ module WebGear.Server.Handler,+) where++import WebGear.Core+import WebGear.Server.Handler+import WebGear.Server.Traits ()
+ src/WebGear/Server/Handler.hs view
@@ -0,0 +1,183 @@+{- |+ Server implementation of WebGear handlers+-}+module WebGear.Server.Handler (+ ServerHandler (..),+ RoutePath (..),+ runServerHandler,+ toApplication,+ transform,+) where++import Control.Arrow (Arrow (..), ArrowChoice (..), ArrowPlus (..), ArrowZero (..))+import Control.Arrow.Operations (ArrowError (..))+import qualified Control.Category as Cat+import Data.ByteString (ByteString)+import Data.Either (fromRight)+import qualified Data.HashMap.Strict as HM+import Data.String (fromString)+import Data.Version (showVersion)+import qualified Network.HTTP.Types as HTTP+import qualified Network.Wai as Wai+import Paths_webgear_server (version)+import WebGear.Core.Handler (Description, Handler (..), RouteMismatch (..), RoutePath (..), Summary)+import WebGear.Core.Request (Request (..))+import WebGear.Core.Response (Response (..), toWaiResponse)+import WebGear.Core.Trait (Linked, linkzero)++{- | An arrow implementing a WebGear server.++ It can be thought of equivalent to the function arrow @a -> m b@+ where @m@ is a monad. It also supports routing and possibly failing+ the computation when the route does not match.+-}+newtype ServerHandler m a b = ServerHandler {unServerHandler :: (a, RoutePath) -> m (Either RouteMismatch b, RoutePath)}++instance Monad m => Cat.Category (ServerHandler m) where+ {-# INLINEABLE id #-}+ id = ServerHandler $ \(a, s) -> pure (Right a, s)++ {-# INLINEABLE (.) #-}+ ServerHandler f . ServerHandler g = ServerHandler $ \(a, s) ->+ g (a, s) >>= \case+ (Left e, s') -> pure (Left e, s')+ (Right b, s') -> f (b, s')++instance Monad m => Arrow (ServerHandler m) where+ arr f = ServerHandler (\(a, s) -> pure (Right (f a), s))++ {-# INLINEABLE first #-}+ first (ServerHandler f) = ServerHandler $ \((a, c), s) ->+ f (a, s) >>= \case+ (Left e, s') -> pure (Left e, s')+ (Right b, s') -> pure (Right (b, c), s')++ {-# INLINEABLE second #-}+ second (ServerHandler f) = ServerHandler $ \((c, a), s) ->+ f (a, s) >>= \case+ (Left e, s') -> pure (Left e, s')+ (Right b, s') -> pure (Right (c, b), s')++instance Monad m => ArrowZero (ServerHandler m) where+ {-# INLINEABLE zeroArrow #-}+ zeroArrow = ServerHandler (\(_a, s) -> pure (Left mempty, s))++instance Monad m => ArrowPlus (ServerHandler m) where+ {-# INLINEABLE (<+>) #-}+ ServerHandler f <+> ServerHandler g = ServerHandler $ \(a, s) ->+ f (a, s) >>= \case+ (Left _e, _s') -> g (a, s)+ (Right b, s') -> pure (Right b, s')++instance Monad m => ArrowChoice (ServerHandler m) where+ {-# INLINEABLE left #-}+ left (ServerHandler f) = ServerHandler $ \(bd, s) ->+ case bd of+ Right d -> pure (Right (Right d), s)+ Left b ->+ f (b, s) >>= \case+ (Left e, s') -> pure (Left e, s')+ (Right c, s') -> pure (Right (Left c), s')++ {-# INLINEABLE right #-}+ right (ServerHandler f) = ServerHandler $ \(db, s) ->+ case db of+ Left d -> pure (Right (Left d), s)+ Right b ->+ f (b, s) >>= \case+ (Left e, s') -> pure (Left e, s')+ (Right c, s') -> pure (Right (Right c), s')++instance Monad m => ArrowError RouteMismatch (ServerHandler m) where+ {-# INLINEABLE raise #-}+ raise = ServerHandler $ \(e, s) -> pure (Left e, s)++ {-# INLINEABLE handle #-}+ (ServerHandler action) `handle` (ServerHandler errHandler) = ServerHandler $ \(a, s) ->+ action (a, s) >>= \case+ (Left e, s') -> errHandler ((a, e), s')+ (Right b, s') -> pure (Right b, s')++ {-# INLINEABLE tryInUnless #-}+ tryInUnless (ServerHandler action) (ServerHandler resHandler) (ServerHandler errHandler) =+ ServerHandler $ \(a, s) ->+ action (a, s) >>= \case+ (Left e, s') -> errHandler ((a, e), s')+ (Right b, s') -> resHandler ((a, b), s')++instance Monad m => Handler (ServerHandler m) m where+ {-# INLINEABLE arrM #-}+ arrM :: (a -> m b) -> ServerHandler m a b+ arrM f = ServerHandler $ \(a, s) -> f a >>= \b -> pure (Right b, s)++ {-# INLINEABLE consumeRoute #-}+ consumeRoute :: ServerHandler m RoutePath a -> ServerHandler m () a+ consumeRoute (ServerHandler h) = ServerHandler $+ \((), path) -> h (path, RoutePath [])++ {-# INLINEABLE setDescription #-}+ setDescription :: Description -> ServerHandler m a a+ setDescription _ = Cat.id++ {-# INLINEABLE setSummary #-}+ setSummary :: Summary -> ServerHandler m a a+ setSummary _ = Cat.id++-- | Run a ServerHandler to produce a result or a route mismatch error.+runServerHandler ::+ Monad m =>+ -- | The handler to run+ ServerHandler m a b ->+ -- | Path used for routing+ RoutePath ->+ -- | Input value to the arrow+ a ->+ -- | The result of the arrow+ m (Either RouteMismatch b)+runServerHandler (ServerHandler h) path a = fst <$> h (a, path)++-- | Convert a ServerHandler to a WAI application+toApplication :: ServerHandler IO (Linked '[] Request) Response -> Wai.Application+toApplication h rqt cont =+ runServerHandler h path request+ >>= cont . toWaiResponse . addServerHeader . mkWebGearResponse+ where+ request :: Linked '[] Request+ request = linkzero $ Request rqt++ path :: RoutePath+ path = RoutePath $ Wai.pathInfo rqt++ mkWebGearResponse :: Either RouteMismatch Response -> Response+ mkWebGearResponse = fromRight (Response HTTP.notFound404 [] mempty)++ addServerHeader :: Response -> Response+ addServerHeader resp@Response{..} = resp{responseHeaders = responseHeaders <> webGearServerHeader}++{- | Transform a `ServerHandler` running in one monad to another monad.++ This is useful in cases where the server is running in a custom+ monad but you would like to convert it to a WAI application using+ `toApplication`.++ Example usage with a ReaderT monad stack:++@+ `toApplication` (transform f server)+ where+ server :: `ServerHandler` (ReaderT r IO) (`Linked` '[] `Request`) `Response`+ server = ....++ f :: ReaderT r IO a -> IO a+ f action = runReaderT action r+@+-}+transform ::+ (forall x. m x -> n x) ->+ ServerHandler m a b ->+ ServerHandler n a b+transform f (ServerHandler g) =+ ServerHandler $ f . g++webGearServerHeader :: HM.HashMap HTTP.HeaderName ByteString+webGearServerHeader = HM.singleton HTTP.hServer (fromString $ "WebGear/" ++ showVersion version)
+ src/WebGear/Server/Trait/Auth/Basic.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Server implementation of the 'BasicAuth'' trait.+module WebGear.Server.Trait.Auth.Basic where++import Control.Arrow (arr, returnA, (>>>))+import Data.Bifunctor (first)+import Data.ByteString.Base64 (decodeLenient)+import Data.ByteString.Char8 (intercalate, split)+import Data.Void (Void)+import WebGear.Core.Handler (arrM)+import WebGear.Core.Modifiers+import WebGear.Core.Request (Request)+import WebGear.Core.Trait (Get (..), Linked)+import WebGear.Core.Trait.Auth.Basic (+ BasicAuth' (..),+ BasicAuthError (..),+ Credentials (..),+ Password (..),+ Username (..),+ )+import WebGear.Core.Trait.Auth.Common (+ AuthToken (..),+ AuthorizationHeader,+ getAuthorizationHeaderTrait,+ )+import WebGear.Server.Handler (ServerHandler)++instance+ ( Monad m+ , Get (ServerHandler m) (AuthorizationHeader scheme) Request+ ) =>+ Get (ServerHandler m) (BasicAuth' Required scheme m e a) Request+ where+ {-# INLINEABLE getTrait #-}+ getTrait ::+ BasicAuth' Required scheme m e a ->+ ServerHandler m (Linked ts Request) (Either (BasicAuthError e) a)+ getTrait BasicAuth'{..} = proc request -> do+ result <- getAuthorizationHeaderTrait @scheme -< request+ case result of+ Nothing -> returnA -< Left BasicAuthHeaderMissing+ (Just (Left _)) -> returnA -< Left BasicAuthSchemeMismatch+ (Just (Right token)) ->+ case parseCreds token of+ Left e -> returnA -< Left e+ Right c -> validateCreds -< c+ where+ parseCreds :: AuthToken scheme -> Either (BasicAuthError e) Credentials+ parseCreds AuthToken{..} =+ case split ':' (decodeLenient authToken) of+ [] -> Left BasicAuthCredsBadFormat+ u : ps -> Right $ Credentials (Username u) (Password $ intercalate ":" ps)++ validateCreds :: ServerHandler m Credentials (Either (BasicAuthError e) a)+ validateCreds = arrM $ \creds -> do+ res <- toBasicAttribute creds+ pure $ first BasicAuthAttributeError res++instance+ ( Monad m+ , Get (ServerHandler m) (AuthorizationHeader scheme) Request+ ) =>+ Get (ServerHandler m) (BasicAuth' Optional scheme m e a) Request+ where+ {-# INLINEABLE getTrait #-}+ getTrait ::+ BasicAuth' Optional scheme m e a ->+ ServerHandler m (Linked ts Request) (Either Void (Either (BasicAuthError e) a))+ getTrait BasicAuth'{..} = getTrait (BasicAuth'{..} :: BasicAuth' Required scheme m e a) >>> arr Right
+ src/WebGear/Server/Trait/Auth/JWT.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Server implementation of the 'JWTAuth'' trait.+module WebGear.Server.Trait.Auth.JWT where++import Control.Arrow (arr, returnA, (>>>))+import Control.Monad.Except (MonadError (throwError), lift, runExceptT, withExceptT)+import Control.Monad.Time (MonadTime)+import qualified Crypto.JWT as JWT+import Data.ByteString.Lazy (fromStrict)+import Data.Void (Void)+import WebGear.Core.Handler (arrM)+import WebGear.Core.Modifiers+import WebGear.Core.Request (Request)+import WebGear.Core.Trait (Get (..), Linked)+import WebGear.Core.Trait.Auth.Common (+ AuthToken (..),+ AuthorizationHeader,+ getAuthorizationHeaderTrait,+ )+import WebGear.Core.Trait.Auth.JWT (JWTAuth' (..), JWTAuthError (..))+import WebGear.Server.Handler (ServerHandler)++instance (MonadTime m, Get (ServerHandler m) (AuthorizationHeader scheme) Request) => Get (ServerHandler m) (JWTAuth' Required scheme m e a) Request where+ {-# INLINEABLE getTrait #-}+ getTrait ::+ JWTAuth' Required scheme m e a ->+ ServerHandler m (Linked ts Request) (Either (JWTAuthError e) a)+ getTrait JWTAuth'{..} = proc request -> do+ result <- getAuthorizationHeaderTrait @scheme -< request+ case result of+ Nothing -> returnA -< Left JWTAuthHeaderMissing+ (Just (Left _)) -> returnA -< Left JWTAuthSchemeMismatch+ (Just (Right token)) ->+ case parseJWT token of+ Left e -> returnA -< Left (JWTAuthTokenBadFormat e)+ Right jwt -> validateJWT -< jwt+ where+ parseJWT :: AuthToken scheme -> Either JWT.JWTError JWT.SignedJWT+ parseJWT AuthToken{..} = JWT.decodeCompact $ fromStrict authToken++ validateJWT :: ServerHandler m JWT.SignedJWT (Either (JWTAuthError e) a)+ validateJWT = arrM $ \jwt -> runExceptT $ do+ claims <- withExceptT JWTAuthTokenBadFormat $ JWT.verifyClaims jwtValidationSettings jwkSet jwt+ lift (toJWTAttribute claims) >>= either (throwError . JWTAuthAttributeError) pure++instance (MonadTime m, Get (ServerHandler m) (AuthorizationHeader scheme) Request) => Get (ServerHandler m) (JWTAuth' Optional scheme m e a) Request where+ {-# INLINEABLE getTrait #-}+ getTrait ::+ JWTAuth' Optional scheme m e a ->+ ServerHandler m (Linked ts Request) (Either Void (Either (JWTAuthError e) a))+ getTrait JWTAuth'{..} = getTrait (JWTAuth'{..} :: JWTAuth' Required scheme m e a) >>> arr Right
+ src/WebGear/Server/Trait/Body.hs view
@@ -0,0 +1,82 @@+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Server implementation of the `Body` trait.+module WebGear.Server.Trait.Body () where++import Control.Arrow (returnA)+import Control.Monad.IO.Class (MonadIO (..))+import qualified Data.Aeson as Aeson+import Data.ByteString.Conversion (FromByteString, ToByteString, parser, runParser', toByteString)+import Data.ByteString.Lazy (fromChunks)+import Data.Text (Text, pack)+import Network.HTTP.Media.RenderHeader (RenderHeader (renderHeader))+import Network.HTTP.Types (hContentType)+import WebGear.Core.Handler (Handler (..))+import WebGear.Core.Request (Request, getRequestBodyChunk)+import WebGear.Core.Response (Response (..))+import WebGear.Core.Trait (Get (..), Linked, Set (..), unlink)+import WebGear.Core.Trait.Body (Body (..), JSONBody (..))+import WebGear.Server.Handler (ServerHandler)++instance (MonadIO m, FromByteString val) => Get (ServerHandler m) (Body val) Request where+ {-# INLINEABLE getTrait #-}+ getTrait :: Body val -> ServerHandler m (Linked ts Request) (Either Text val)+ getTrait (Body _) = arrM $ \request -> do+ chunks <- takeWhileM (/= mempty) $ repeat $ liftIO $ getRequestBodyChunk $ unlink request+ pure $ case runParser' parser (fromChunks chunks) of+ Left e -> Left $ pack e+ Right t -> Right t++instance (Monad m, ToByteString val) => Set (ServerHandler m) (Body val) Response where+ {-# INLINEABLE setTrait #-}+ setTrait ::+ Body val ->+ (Linked ts Response -> Response -> val -> Linked (Body val : ts) Response) ->+ ServerHandler m (Linked ts Response, val) (Linked (Body val : ts) Response)+ setTrait (Body mediaType) f = proc (linkedResponse, val) -> do+ let response = (unlink linkedResponse)+ response' =+ response+ { responseBody = Just (toByteString val)+ , responseHeaders =+ responseHeaders response+ <> case mediaType of+ Just mt -> [(hContentType, renderHeader mt)]+ Nothing -> []+ }+ returnA -< f linkedResponse response' val++instance (MonadIO m, Aeson.FromJSON val) => Get (ServerHandler m) (JSONBody val) Request where+ {-# INLINEABLE getTrait #-}+ getTrait :: JSONBody val -> ServerHandler m (Linked ts Request) (Either Text val)+ getTrait (JSONBody _) = arrM $ \request -> do+ chunks <- takeWhileM (/= mempty) $ repeat $ liftIO $ getRequestBodyChunk $ unlink request+ pure $ case Aeson.eitherDecode' (fromChunks chunks) of+ Left e -> Left $ pack e+ Right t -> Right t++instance (Monad m, Aeson.ToJSON val) => Set (ServerHandler m) (JSONBody val) Response where+ {-# INLINEABLE setTrait #-}+ setTrait ::+ JSONBody val ->+ (Linked ts Response -> Response -> val -> Linked (JSONBody val : ts) Response) ->+ ServerHandler m (Linked ts Response, val) (Linked (JSONBody val : ts) Response)+ setTrait (JSONBody mediaType) f = proc (linkedResponse, val) -> do+ let response = unlink linkedResponse+ ctype = maybe "application/json" renderHeader mediaType+ response' =+ response+ { responseBody = Just (Aeson.encode val)+ , responseHeaders =+ responseHeaders response+ <> [(hContentType, ctype)]+ }+ returnA -< f linkedResponse response' val++takeWhileM :: Monad m => (a -> Bool) -> [m a] -> m [a]+takeWhileM _ [] = pure []+takeWhileM p (mx : mxs) = do+ x <- mx+ if p x+ then (x :) <$> takeWhileM p mxs+ else pure []
+ src/WebGear/Server/Trait/Header.hs view
@@ -0,0 +1,101 @@+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Server implementation of the `Header` trait.+module WebGear.Server.Trait.Header () where++import Control.Arrow (arr, returnA, (>>>))+import Data.ByteString.Conversion (ToByteString, toByteString')+import qualified Data.HashMap.Strict as HM+import Data.Proxy (Proxy (Proxy))+import Data.String (fromString)+import Data.Text (Text)+import Data.Void (Void)+import GHC.TypeLits (KnownSymbol, symbolVal)+import Network.HTTP.Types (HeaderName)+import Web.HttpApiData (FromHttpApiData, parseHeader)+import WebGear.Core.Modifiers+import WebGear.Core.Request (Request, requestHeader)+import WebGear.Core.Response (Response (..))+import WebGear.Core.Trait (Get (..), Linked, Set (..), unlink)+import WebGear.Core.Trait.Header (Header (..), HeaderNotFound (..), HeaderParseError (..))+import WebGear.Server.Handler (ServerHandler)++extractRequestHeader ::+ (Monad m, KnownSymbol name, FromHttpApiData val) =>+ Proxy name ->+ ServerHandler m (Linked ts Request) (Maybe (Either Text val))+extractRequestHeader proxy = proc req -> do+ let headerName :: HeaderName = fromString $ symbolVal proxy+ returnA -< parseHeader <$> requestHeader headerName (unlink req)++instance (Monad m, KnownSymbol name, FromHttpApiData val) => Get (ServerHandler m) (Header Required Strict name val) Request where+ {-# INLINEABLE getTrait #-}+ getTrait ::+ Header Required Strict name val ->+ ServerHandler m (Linked ts Request) (Either (Either HeaderNotFound HeaderParseError) val)+ getTrait Header = extractRequestHeader (Proxy @name) >>> arr f+ where+ f = \case+ Nothing -> Left $ Left HeaderNotFound+ Just (Left e) -> Left $ Right $ HeaderParseError e+ Just (Right x) -> Right x++instance (Monad m, KnownSymbol name, FromHttpApiData val) => Get (ServerHandler m) (Header Optional Strict name val) Request where+ {-# INLINEABLE getTrait #-}+ getTrait ::+ Header Optional Strict name val ->+ ServerHandler m (Linked ts Request) (Either HeaderParseError (Maybe val))+ getTrait Header = extractRequestHeader (Proxy @name) >>> arr f+ where+ f = \case+ Nothing -> Right Nothing+ Just (Left e) -> Left $ HeaderParseError e+ Just (Right x) -> Right $ Just x++instance (Monad m, KnownSymbol name, FromHttpApiData val) => Get (ServerHandler m) (Header Required Lenient name val) Request where+ {-# INLINEABLE getTrait #-}+ getTrait ::+ Header Required Lenient name val ->+ ServerHandler m (Linked ts Request) (Either HeaderNotFound (Either Text val))+ getTrait Header = extractRequestHeader (Proxy @name) >>> arr f+ where+ f = \case+ Nothing -> Left HeaderNotFound+ Just (Left e) -> Right $ Left e+ Just (Right x) -> Right $ Right x++instance (Monad m, KnownSymbol name, FromHttpApiData val) => Get (ServerHandler m) (Header Optional Lenient name val) Request where+ {-# INLINEABLE getTrait #-}+ getTrait ::+ Header Optional Lenient name val ->+ ServerHandler m (Linked ts Request) (Either Void (Maybe (Either Text val)))+ getTrait Header = extractRequestHeader (Proxy @name) >>> arr f+ where+ f = \case+ Nothing -> Right Nothing+ Just (Left e) -> Right $ Just $ Left e+ Just (Right x) -> Right $ Just $ Right x++instance (Monad m, KnownSymbol name, ToByteString val) => Set (ServerHandler m) (Header Required Strict name val) Response where+ {-# INLINEABLE setTrait #-}+ setTrait ::+ Header Required Strict name val ->+ (Linked ts Response -> Response -> val -> Linked (Header Required Strict name val : ts) Response) ->+ ServerHandler m (Linked ts Response, val) (Linked (Header Required Strict name val : ts) Response)+ setTrait Header f = proc (l, val) -> do+ let headerName :: HeaderName = fromString $ symbolVal $ Proxy @name+ response@Response{..} = unlink l+ response' = response{responseHeaders = HM.insert headerName (toByteString' val) responseHeaders}+ returnA -< f l response' val++instance (Monad m, KnownSymbol name, ToByteString val) => Set (ServerHandler m) (Header Optional Strict name val) Response where+ {-# INLINEABLE setTrait #-}+ setTrait ::+ Header Optional Strict name val ->+ (Linked ts Response -> Response -> Maybe val -> Linked (Header Optional Strict name val : ts) Response) ->+ ServerHandler m (Linked ts Response, Maybe val) (Linked (Header Optional Strict name val : ts) Response)+ setTrait Header f = proc (l, maybeVal) -> do+ let headerName :: HeaderName = fromString $ symbolVal $ Proxy @name+ response@Response{..} = unlink l+ response' = response{responseHeaders = HM.alter (const $ toByteString' <$> maybeVal) headerName responseHeaders}+ returnA -< f l response' maybeVal
+ src/WebGear/Server/Trait/Method.hs view
@@ -0,0 +1,21 @@+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Server implementation of the `Method` trait.+module WebGear.Server.Trait.Method where++import Control.Arrow (returnA)+import qualified Network.HTTP.Types as HTTP+import WebGear.Core.Request (Request, requestMethod)+import WebGear.Core.Trait (Get (..), Linked, unlink)+import WebGear.Core.Trait.Method (Method (..), MethodMismatch (..))+import WebGear.Server.Handler (ServerHandler)++instance Monad m => Get (ServerHandler m) Method Request where+ {-# INLINEABLE getTrait #-}+ getTrait :: Method -> ServerHandler m (Linked ts Request) (Either MethodMismatch HTTP.StdMethod)+ getTrait (Method method) = proc request -> do+ let expectedMethod = HTTP.renderStdMethod method+ actualMethod = requestMethod $ unlink request+ if actualMethod == expectedMethod+ then returnA -< Right method+ else returnA -< Left $ MethodMismatch{..}
+ src/WebGear/Server/Trait/Path.hs view
@@ -0,0 +1,41 @@+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Server implementation of the path traits.+module WebGear.Server.Trait.Path where++import qualified Data.List as List+import qualified Data.Text as Text+import Web.HttpApiData (FromHttpApiData (..))+import WebGear.Core.Handler (RoutePath (..))+import WebGear.Core.Request (Request)+import WebGear.Core.Trait (Get (..), Linked)+import WebGear.Core.Trait.Path (Path (..), PathEnd (..), PathVar (..), PathVarError (..))+import WebGear.Server.Handler (ServerHandler (..))++instance Monad m => Get (ServerHandler m) Path Request where+ {-# INLINEABLE getTrait #-}+ getTrait :: Path -> ServerHandler m (Linked ts Request) (Either () ())+ getTrait (Path p) = ServerHandler $ \(_, path@(RoutePath remaining)) -> do+ let expected = filter (/= "") $ Text.splitOn "/" p+ pure $ case List.stripPrefix expected remaining of+ Just ps -> (Right (Right ()), RoutePath ps)+ Nothing -> (Right (Left ()), path)++instance (Monad m, FromHttpApiData val) => Get (ServerHandler m) (PathVar tag val) Request where+ {-# INLINEABLE getTrait #-}+ getTrait :: PathVar tag val -> ServerHandler m (Linked ts Request) (Either PathVarError val)+ getTrait PathVar = ServerHandler $ \(_, path@(RoutePath remaining)) -> do+ pure $ case remaining of+ [] -> (Right (Left PathVarNotFound), path)+ (p : ps) ->+ case parseUrlPiece p of+ Left e -> (Right (Left $ PathVarParseError e), path)+ Right val -> (Right (Right val), RoutePath ps)++instance Monad m => Get (ServerHandler m) PathEnd Request where+ {-# INLINEABLE getTrait #-}+ getTrait :: PathEnd -> ServerHandler m (Linked ts Request) (Either () ())+ getTrait PathEnd = ServerHandler f+ where+ f (_, p@(RoutePath [])) = pure (Right $ Right (), p)+ f (_, p) = pure (Right $ Left (), p)
+ src/WebGear/Server/Trait/QueryParam.hs view
@@ -0,0 +1,80 @@+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Server implementation of the `QueryParam` trait.+module WebGear.Server.Trait.QueryParam () where++import Control.Arrow (arr, returnA, (>>>))+import Data.List (find)+import Data.Proxy (Proxy (Proxy))+import Data.String (fromString)+import Data.Text (Text)+import Data.Void (Void)+import GHC.TypeLits (KnownSymbol, symbolVal)+import Network.HTTP.Types (queryToQueryText)+import Web.HttpApiData (FromHttpApiData (..))+import WebGear.Core.Modifiers+import WebGear.Core.Request (Request, queryString)+import WebGear.Core.Trait (Get (..), Linked, unlink)+import WebGear.Core.Trait.QueryParam (+ ParamNotFound (..),+ ParamParseError (..),+ QueryParam (..),+ )+import WebGear.Server.Handler (ServerHandler)++extractQueryParam ::+ (Monad m, KnownSymbol name, FromHttpApiData val) =>+ Proxy name ->+ ServerHandler m (Linked ts Request) (Maybe (Either Text val))+extractQueryParam proxy = proc req -> do+ let name = fromString $ symbolVal proxy+ params = queryToQueryText $ queryString $ unlink req+ returnA -< parseQueryParam <$> (find ((== name) . fst) params >>= snd)++instance (Monad m, KnownSymbol name, FromHttpApiData val) => Get (ServerHandler m) (QueryParam Required Strict name val) Request where+ {-# INLINEABLE getTrait #-}+ getTrait ::+ QueryParam Required Strict name val ->+ ServerHandler m (Linked ts Request) (Either (Either ParamNotFound ParamParseError) val)+ getTrait QueryParam = extractQueryParam (Proxy @name) >>> arr f+ where+ f = \case+ Nothing -> Left $ Left ParamNotFound+ Just (Left e) -> Left $ Right $ ParamParseError e+ Just (Right x) -> Right x++instance (Monad m, KnownSymbol name, FromHttpApiData val) => Get (ServerHandler m) (QueryParam Optional Strict name val) Request where+ {-# INLINEABLE getTrait #-}+ getTrait ::+ QueryParam Optional Strict name val ->+ ServerHandler m (Linked ts Request) (Either ParamParseError (Maybe val))+ getTrait QueryParam = extractQueryParam (Proxy @name) >>> arr f+ where+ f = \case+ Nothing -> Right Nothing+ Just (Left e) -> Left $ ParamParseError e+ Just (Right x) -> Right $ Just x++instance (Monad m, KnownSymbol name, FromHttpApiData val) => Get (ServerHandler m) (QueryParam Required Lenient name val) Request where+ {-# INLINEABLE getTrait #-}+ getTrait ::+ QueryParam Required Lenient name val ->+ ServerHandler m (Linked ts Request) (Either ParamNotFound (Either Text val))+ getTrait QueryParam = extractQueryParam (Proxy @name) >>> arr f+ where+ f = \case+ Nothing -> Left ParamNotFound+ Just (Left e) -> Right $ Left e+ Just (Right x) -> Right $ Right x++instance (Monad m, KnownSymbol name, FromHttpApiData val) => Get (ServerHandler m) (QueryParam Optional Lenient name val) Request where+ {-# INLINEABLE getTrait #-}+ getTrait ::+ QueryParam Optional Lenient name val ->+ ServerHandler m (Linked ts Request) (Either Void (Maybe (Either Text val)))+ getTrait QueryParam = extractQueryParam (Proxy @name) >>> arr f+ where+ f = \case+ Nothing -> Right Nothing+ Just (Left e) -> Right $ Just $ Left e+ Just (Right x) -> Right $ Just $ Right x
+ src/WebGear/Server/Trait/Status.hs view
@@ -0,0 +1,22 @@+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Server implementation of the `Status` trait.+module WebGear.Server.Trait.Status where++import Control.Arrow (returnA)+import qualified Network.HTTP.Types.Status as HTTP+import WebGear.Core.Response (Response (responseStatus))+import WebGear.Core.Trait (Linked, Set, setTrait, unlink)+import WebGear.Core.Trait.Status (Status (..))+import WebGear.Server.Handler (ServerHandler)++instance Monad m => Set (ServerHandler m) Status Response where+ {-# INLINEABLE setTrait #-}+ setTrait ::+ Status ->+ (Linked ts Response -> Response -> HTTP.Status -> Linked (Status : ts) Response) ->+ ServerHandler m (Linked ts Response, HTTP.Status) (Linked (Status : ts) Response)+ setTrait (Status status) f = proc (linkedResponse, _) -> do+ let response = unlink linkedResponse+ response' = response{responseStatus = status}+ returnA -< f linkedResponse response' status
+ src/WebGear/Server/Traits.hs view
@@ -0,0 +1,15 @@+{- | Server implementation of all traits supported by WebGear.++ This modules only exports orphan instances imported from other+ modules. Hence the haddock documentation will be empty.+-}+module WebGear.Server.Traits () where++import WebGear.Server.Trait.Auth.Basic ()+import WebGear.Server.Trait.Auth.JWT ()+import WebGear.Server.Trait.Body ()+import WebGear.Server.Trait.Header ()+import WebGear.Server.Trait.Method ()+import WebGear.Server.Trait.Path ()+import WebGear.Server.Trait.QueryParam ()+import WebGear.Server.Trait.Status ()
− src/WebGear/Trait.hs
@@ -1,155 +0,0 @@-{-# LANGUAGE UndecidableInstances #-}--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com------ Traits are optional attributes associated with a value. For--- example, a list containing totally ordered values might have a--- @Maximum@ trait where the associated attribute is the maximum--- value. This trait exists only if the list is non-empty. The 'Trait'--- typeclass provides an interface to extract such trait attributes.------ Traits help to link attributes with values in a type-safe manner.------ Traits are somewhat similar to [refinement--- types](https://hackage.haskell.org/package/refined), but allow--- arbitrary attributes to be associated with a value instead of only--- a predicate.----module WebGear.Trait- ( -- * Core Types- Trait (..)- , Result (..)- , Linked-- -- * Linking values with attributes- , link- , unlink- , probe- , remove-- -- * Retrive trait attributes from linked values- , Has (..)- , Have-- , MissingTrait- ) where--import Data.Kind (Constraint, Type)-import Data.Proxy (Proxy (..))-import GHC.TypeLits (ErrorMessage (..), TypeError)----- | A trait is an optional attribute @t@ associated with a value--- @a@.-class Monad m => Trait t a m where- -- | Type of the associated attribute when the trait holds for a- -- value- type Attribute t a :: Type-- -- | Type that indicates that the trait does not exist for a- -- value. This could be an error message, parse error etc.- type Absence t a :: Type-- -- | Attempt to deduce the trait attribute from the value @a@. It is- -- possible that deducing a trait's presence can alter the value,- -- hence this function returns a possibly updated value along with- -- the trait attribute on success.- toAttribute :: a -> m (Result t a)----- | The result of 'toAttribute' - either a successful deduction of an--- attribute or an error.-data Result t a = NotFound (Absence t a)- | Found (Attribute t a)---- | A trivial derivable trait that is always present and whose--- attribute does not carry any meaningful information.-instance Monad m => Trait '[] a m where- type Attribute '[] a = ()- type Absence '[] a = ()-- toAttribute :: a -> m (Result '[] a)- toAttribute = const $ pure $ Found ()---- | Combination of many derivable traits all of which are present for--- a value.-instance (Trait t a m, Trait ts a m, Monad m) => Trait (t:ts) a m where- type Attribute (t:ts) a = (Attribute t a, Attribute ts a)- type Absence (t:ts) a = Either (Result t a) (Result ts a)-- toAttribute :: a -> m (Result (t:ts) a)- toAttribute a = toAttribute @t a >>= \case- e@(NotFound _) -> pure $ NotFound $ Left e- Found l -> toAttribute @ts a >>= \case- e@(NotFound _) -> pure $ NotFound $ Right e- Found r -> pure $ Found (l, r)----- | A value linked with a type-level list of traits.-data Linked (ts :: [Type]) a = Linked- { linkAttribute :: !(Attribute ts a)- , unlink :: !a -- ^ Retrive the value from a linked value- }---- | Wrap a value with an empty list of traits.-link :: a -> Linked '[] a-link = Linked ()---- | Attempt to link an additional trait with an already linked value--- via the 'toAttribute' operation. This can fail indicating an--- 'Absence' of the trait.-probe :: forall t ts a m. Trait t a m- => Linked ts a- -> m (Either (Absence t a) (Linked (t:ts) a))-probe l = do- v <- toAttribute @t (unlink l)- pure $ mkLinked v l- where- mkLinked :: Result t a -> Linked ts a -> Either (Absence t a) (Linked (t:ts) a)- mkLinked (Found left) lv = Right $ Linked (left, linkAttribute lv) (unlink lv)- mkLinked (NotFound e) _ = Left e---- | Remove the leading trait from the type-level list of traits-remove :: Linked (t:ts) a -> Linked ts a-remove l = Linked (snd $ linkAttribute l) (unlink l)----- | Constraint that proves that the trait @t@ is present in the list--- of traits @ts@.-class Has t ts where- -- | Get the attribute associated with @t@ from a linked value- get :: Proxy t -> Linked ts a -> Attribute t a--instance Has t (t:ts) where- get :: Proxy t -> Linked (t:ts) a -> Attribute t a- get _ (Linked (lv, _) _) = lv--instance {-# OVERLAPPABLE #-} Has t ts => Has t (t':ts) where- get :: Proxy t -> Linked (t':ts) a -> Attribute t a- get _ l = get (Proxy @t) (rightLinked l)- where- rightLinked :: Linked (q:qs) b -> Linked qs b- rightLinked (Linked (_, rv) a) = Linked rv a---- For better type errors-instance TypeError (MissingTrait t) => Has t '[] where- get = undefined---- | Type error for nicer UX of missing traits-type MissingTrait t = Text "The request doesn't have the trait ‘" :<>: ShowType t :<>: Text "’."- :$$: Text ""- :$$: Text "Did you use a wrong trait type?"- :$$: Text "For e.g., ‘PathVar \"foo\" Int’ instead of ‘PathVar \"foo\" String’?"- :$$: Text ""- :$$: Text "Or did you forget to apply an appropriate middleware?"- :$$: Text "For e.g. The trait ‘JSONRequestBody Foo’ can be used with ‘jsonRequestBody @Foo’ middleware."- :$$: Text ""----- | Constraint that proves that all the traits in the list @ts@ are--- also present in the list @qs@.-type family Have ts qs :: Constraint where- Have '[] qs = ()- Have (t:ts) qs = (Has t qs, Have ts qs)
− src/WebGear/Types.hs
@@ -1,470 +0,0 @@--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com------ Common types and functions used throughout WebGear.----module WebGear.Types- ( -- * WebGear Request- Request- , remoteHost- , httpVersion- , isSecure- , requestMethod- , pathInfo- , queryString- , requestHeader- , requestHeaders- , requestBodyLength- , getRequestBodyChunk-- -- * WebGear Response- , Response (..)- , responseHeader- , setResponseHeader- , waiResponse-- -- * Creating responses- , respond- , continue100- , switchingProtocols101- , ok200- , created201- , accepted202- , nonAuthoritative203- , noContent204- , resetContent205- , partialContent206- , multipleChoices300- , movedPermanently301- , found302- , seeOther303- , notModified304- , temporaryRedirect307- , permanentRedirect308- , badRequest400- , unauthorized401- , paymentRequired402- , forbidden403- , notFound404- , methodNotAllowed405- , notAcceptable406- , proxyAuthenticationRequired407- , requestTimeout408- , conflict409- , gone410- , lengthRequired411- , preconditionFailed412- , requestEntityTooLarge413- , requestURITooLong414- , unsupportedMediaType415- , requestedRangeNotSatisfiable416- , expectationFailed417- , imATeapot418- , unprocessableEntity422- , preconditionRequired428- , tooManyRequests429- , requestHeaderFieldsTooLarge431- , internalServerError500- , notImplemented501- , badGateway502- , serviceUnavailable503- , gatewayTimeout504- , httpVersionNotSupported505- , networkAuthenticationRequired511-- , Handler'- , Handler- , Middleware'- , Middleware- , RequestMiddleware'- , RequestMiddleware- , ResponseMiddleware'- , ResponseMiddleware-- , Router (..)- , MonadRouter (..)- , PathInfo (..)- , RouteError (..)- , transform- , runRoute- , toApplication- ) where--import Control.Applicative (Alternative)-import Control.Arrow (Kleisli (..))-import Control.Monad (MonadPlus)-import Control.Monad.Except (ExceptT, MonadError, catchError, runExceptT, throwError)-import Control.Monad.IO.Class (MonadIO)-import Control.Monad.State.Strict (MonadState, StateT, evalStateT)-import Data.ByteString (ByteString)-import Data.ByteString.Conversion.To (ToByteString, toByteString)-import Data.List (find)-import Data.Maybe (fromMaybe)-import Data.Semigroup (Semigroup (..), stimesIdempotent)-import Data.String (fromString)-import Data.Text (Text)-import Data.Version (showVersion)-import GHC.Exts (fromList)-import Network.Wai (Request, getRequestBodyChunk, httpVersion, isSecure, pathInfo, queryString,- remoteHost, requestBodyLength, requestHeaders, requestMethod)--import Paths_webgear_server (version)-import WebGear.Trait (Linked, link)--import qualified Data.ByteString.Lazy as LBS-import qualified Data.HashMap.Strict as HM-import qualified Network.HTTP.Types as HTTP-import qualified Network.Wai as Wai----- | Get the value of a request header-requestHeader :: HTTP.HeaderName -> Request -> Maybe ByteString-requestHeader h r = snd <$> find ((== h) . fst) (requestHeaders r)---- | An HTTP response sent from the server to the client.------ The response contains a status, optional headers and an optional--- body of type @a@.-data Response a = Response- { responseStatus :: HTTP.Status -- ^ Response status code- , responseHeaders :: HM.HashMap HTTP.HeaderName ByteString -- ^ Response headers- , responseBody :: Maybe a -- ^ Optional response body- }- deriving stock (Eq, Ord, Show, Functor)---- | Looks up a response header-responseHeader :: HTTP.HeaderName -> Response a -> Maybe ByteString-responseHeader h = HM.lookup h . responseHeaders---- | Set a response header value-setResponseHeader :: HTTP.HeaderName -> ByteString -> Response a -> Response a-setResponseHeader name val r = r { responseHeaders = HM.insert name val (responseHeaders r) }---- | Convert a WebGear response to a WAI Response.-waiResponse :: Response LBS.ByteString -> Wai.Response-waiResponse Response{..} = Wai.responseLBS- responseStatus- (HM.toList responseHeaders)- (fromMaybe "" responseBody)----- | Create a response with a given status and body-respond :: HTTP.Status -> Maybe a -> Response a-respond s = Response s mempty---- | Continue 100 response-continue100 :: Response a-continue100 = respond HTTP.continue100 Nothing---- | Switching Protocols 101 response-switchingProtocols101 :: Response a-switchingProtocols101 = respond HTTP.switchingProtocols101 Nothing---- | OK 200 response-ok200 :: a -> Response a-ok200 = respond HTTP.ok200 . Just---- | Created 201 response-created201 :: a -> Response a-created201 = respond HTTP.created201 . Just---- | Accepted 202 response-accepted202 :: a -> Response a-accepted202 = respond HTTP.accepted202 . Just---- | Non-Authoritative 203 response-nonAuthoritative203 :: a -> Response a-nonAuthoritative203 = respond HTTP.nonAuthoritative203 . Just---- | No Content 204 response-noContent204 :: Response a-noContent204 = respond HTTP.noContent204 Nothing---- | Reset Content 205 response-resetContent205 :: Response a-resetContent205 = respond HTTP.resetContent205 Nothing---- | Partial Content 206 response-partialContent206 :: a -> Response a-partialContent206 = respond HTTP.partialContent206 . Just---- | Multiple Choices 300 response-multipleChoices300 :: a -> Response a-multipleChoices300 = respond HTTP.multipleChoices300 . Just---- | Moved Permanently 301 response-movedPermanently301 :: a -> Response a-movedPermanently301 = respond HTTP.movedPermanently301 . Just---- | Found 302 response-found302 :: a -> Response a-found302 = respond HTTP.found302 . Just---- | See Other 303 response-seeOther303 :: a -> Response a-seeOther303 = respond HTTP.seeOther303 . Just---- | Not Modified 304 response-notModified304 :: Response a-notModified304 = respond HTTP.notModified304 Nothing---- | Temporary Redirect 307 response-temporaryRedirect307 :: a -> Response a-temporaryRedirect307 = respond HTTP.temporaryRedirect307 . Just---- | Permanent Redirect 308 response-permanentRedirect308 :: a -> Response a-permanentRedirect308 = respond HTTP.permanentRedirect308 . Just---- | Bad Request 400 response-badRequest400 :: a -> Response a-badRequest400 = respond HTTP.badRequest400 . Just---- | Unauthorized 401 response-unauthorized401 :: a -> Response a-unauthorized401 = respond HTTP.unauthorized401 . Just---- | Payment Required 402 response-paymentRequired402 :: a -> Response a-paymentRequired402 = respond HTTP.paymentRequired402 . Just---- | Forbidden 403 response-forbidden403 :: a -> Response a-forbidden403 = respond HTTP.forbidden403 . Just---- | Not Found 404 response-notFound404 :: Response a-notFound404 = respond HTTP.notFound404 Nothing---- | Method Not Allowed 405 response-methodNotAllowed405 :: a -> Response a-methodNotAllowed405 = respond HTTP.methodNotAllowed405 . Just---- | Not Acceptable 406 response-notAcceptable406 :: a -> Response a-notAcceptable406 = respond HTTP.notAcceptable406 . Just---- | Proxy Authentication Required 407 response-proxyAuthenticationRequired407 :: a -> Response a-proxyAuthenticationRequired407 = respond HTTP.proxyAuthenticationRequired407 . Just---- | Request Timeout 408 response-requestTimeout408 :: a -> Response a-requestTimeout408 = respond HTTP.requestTimeout408 . Just---- | Conflict 409 response-conflict409 :: a -> Response a-conflict409 = respond HTTP.conflict409 . Just---- | Gone 410 response-gone410 :: a -> Response a-gone410 = respond HTTP.gone410 . Just---- | Length Required 411 response-lengthRequired411 :: a -> Response a-lengthRequired411 = respond HTTP.lengthRequired411 . Just---- | Precondition Failed 412 response-preconditionFailed412 :: a -> Response a-preconditionFailed412 = respond HTTP.preconditionFailed412 . Just---- | Request Entity Too Large 413 response-requestEntityTooLarge413 :: a -> Response a-requestEntityTooLarge413 = respond HTTP.requestEntityTooLarge413 . Just---- | Request URI Too Long 414 response-requestURITooLong414 :: a -> Response a-requestURITooLong414 = respond HTTP.requestURITooLong414 . Just---- | Unsupported Media Type 415 response-unsupportedMediaType415 :: a -> Response a-unsupportedMediaType415 = respond HTTP.unsupportedMediaType415 . Just---- | Requested Range Not Satisfiable 416 response-requestedRangeNotSatisfiable416 :: a -> Response a-requestedRangeNotSatisfiable416 = respond HTTP.requestedRangeNotSatisfiable416 . Just---- | Expectation Failed 417 response-expectationFailed417 :: a -> Response a-expectationFailed417 = respond HTTP.expectationFailed417 . Just---- | I'm A Teapot 418 response-imATeapot418 :: a -> Response a-imATeapot418 = respond HTTP.imATeapot418 . Just---- | Unprocessable Entity 422 response-unprocessableEntity422 :: a -> Response a-unprocessableEntity422 = respond HTTP.unprocessableEntity422 . Just---- | Precondition Required 428 response-preconditionRequired428 :: a -> Response a-preconditionRequired428 = respond HTTP.preconditionRequired428 . Just---- | Too Many Requests 429 response-tooManyRequests429 :: a -> Response a-tooManyRequests429 = respond HTTP.tooManyRequests429 . Just---- | Request Header Fields Too Large 431 response-requestHeaderFieldsTooLarge431 :: a -> Response a-requestHeaderFieldsTooLarge431 = respond HTTP.requestHeaderFieldsTooLarge431 . Just---- | Internal Server Error 500 response-internalServerError500 :: a -> Response a-internalServerError500 = respond HTTP.internalServerError500 . Just---- | Not Implemented 501 response-notImplemented501 :: a -> Response a-notImplemented501 = respond HTTP.notImplemented501 . Just---- | Bad Gateway 502 response-badGateway502 :: a -> Response a-badGateway502 = respond HTTP.badGateway502 . Just---- | Service Unavailable 503 response-serviceUnavailable503 :: a -> Response a-serviceUnavailable503 = respond HTTP.serviceUnavailable503 . Just---- | Gateway Timeout 504 response-gatewayTimeout504 :: a -> Response a-gatewayTimeout504 = respond HTTP.gatewayTimeout504 . Just---- | HTTP Version Not Supported 505 response-httpVersionNotSupported505 :: a -> Response a-httpVersionNotSupported505 = respond HTTP.httpVersionNotSupported505 . Just---- | Network Authentication Required 511 response-networkAuthenticationRequired511 :: a -> Response a-networkAuthenticationRequired511 = respond HTTP.networkAuthenticationRequired511 . Just------ | A handler is a function from a request to response in a monadic--- context. Both the request and the response can have linked traits.------ The type level list @req@ contains all the traits expected to be--- present in the request.-type Handler' m req a = Kleisli m (Linked req Request) (Response a)---- | A handler that runs on the 'Router' monad.-type Handler req a = Handler' Router req a---- | A middleware takes a handler as input and produces another--- handler that usually adds some functionality.------ A middleware can do a number of things with the request--- handling such as:------ * Change the request traits before invoking the handler.--- * Use the linked value of any of the request traits.--- * Change the response body.----type Middleware' m req req' a' a = Handler' m req' a' -> Handler' m req a---- | A middleware that runs on the 'Router' monad.-type Middleware req req' a' a = Middleware' Router req req' a' a---- | A middleware that manipulates only the request traits and passes--- the response through.-type RequestMiddleware' m req req' a = Middleware' m req req' a a---- | A request middleware that runs on the 'Router' monad.-type RequestMiddleware req req' a = RequestMiddleware' Router req req' a---- | A middleware that manipulates only the response and passes the--- request through.-type ResponseMiddleware' m req a' a = Middleware' m req req a' a---- | A response middleware that runs on the 'Router' monad.-type ResponseMiddleware req a' a = ResponseMiddleware' Router req a' a---- | A natural transformation of handler monads.------ This is useful if you want to run a handler in a monad other than--- 'Router'.----transform :: (forall x. m x -> n x) -> Handler' m req a -> Handler' n req a-transform f (Kleisli mf) = Kleisli $ f . mf---- | The path components to be matched by routing machinery-newtype PathInfo = PathInfo [Text]---- | Responses that cause routes to abort execution-data RouteError = RouteMismatch- -- ^ A route did not match and the next one can be- -- tried- | ErrorResponse (Response LBS.ByteString)- -- ^ A route matched but returned a short circuiting- -- error response- deriving (Eq, Ord, Show)--instance Semigroup RouteError where- RouteMismatch <> e = e- e <> _ = e-- stimes :: Integral b => b -> RouteError -> RouteError- stimes = stimesIdempotent--instance Monoid RouteError where- mempty = RouteMismatch---- | The monad for routing.-newtype Router a = Router- { unRouter :: StateT PathInfo (ExceptT RouteError IO) a }- deriving newtype ( Functor, Applicative, Alternative, Monad, MonadPlus- , MonadError RouteError- , MonadState PathInfo- , MonadIO- )---- | HTTP request routing with short circuiting behavior.-class (MonadState PathInfo m, Alternative m, MonadPlus m) => MonadRouter m where- -- | Mark the current route as rejected, alternatives can be tried- rejectRoute :: m a-- -- | Short-circuit the current handler and return a response- errorResponse :: Response LBS.ByteString -> m a-- -- | Handle an error response- catchErrorResponse :: m a -> (Response LBS.ByteString -> m a) -> m a--instance MonadRouter Router where- rejectRoute :: Router a- rejectRoute = throwError RouteMismatch-- errorResponse :: Response LBS.ByteString -> Router a- errorResponse = throwError . ErrorResponse-- catchErrorResponse :: Router a -> (Response LBS.ByteString -> Router a) -> Router a- catchErrorResponse action handle = action `catchError` f- where- f RouteMismatch = rejectRoute- f (ErrorResponse res) = handle res----- | Convert a routable handler into a plain function from request to response.-runRoute :: ToByteString a => Handler '[] a -> (Wai.Request -> IO Wai.Response)-runRoute route req = waiResponse . addServerHeader . either routeErrorToResponse id <$> runRouter- where- runRouter :: IO (Either RouteError (Response LBS.ByteString))- runRouter = fmap (fmap (fmap toByteString))- $ runExceptT- $ flip evalStateT (PathInfo $ pathInfo req)- $ unRouter- $ runKleisli route- $ link req-- routeErrorToResponse :: RouteError -> Response LBS.ByteString- routeErrorToResponse RouteMismatch = notFound404- routeErrorToResponse (ErrorResponse r) = r-- addServerHeader :: Response LBS.ByteString -> Response LBS.ByteString- addServerHeader r = r { responseHeaders = responseHeaders r <> fromList [serverHeader] }-- serverHeader :: HTTP.Header- serverHeader = (HTTP.hServer, fromString $ "WebGear/" ++ showVersion version)---- | Convert a routable handler into a Wai application-toApplication :: ToByteString a => Handler '[] a -> Wai.Application-toApplication route request next = runRoute route request >>= next
− src/WebGear/Util.hs
@@ -1,32 +0,0 @@--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com------ Common utility functions.-module WebGear.Util- ( takeWhileM- , splitOn- , maybeToRight- ) where--import Data.List.NonEmpty (NonEmpty (..), toList)---takeWhileM :: Monad m => (a -> Bool) -> [m a] -> m [a]-takeWhileM _ [] = pure []-takeWhileM p (mx:mxs) = do- x <- mx- if p x- then (x :) <$> takeWhileM p mxs- else pure []--splitOn :: Eq a => a -> [a] -> NonEmpty [a]-splitOn sep = foldr f ([] :| [])- where- f x acc | x == sep = [] :| toList acc- f x (y :| ys) = (x:y) :| ys--maybeToRight :: a -> Maybe b -> Either a b-maybeToRight _ (Just x) = Right x-maybeToRight y Nothing = Left y
test/Main.hs view
@@ -1,15 +1,9 @@--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com--- module Main where import Test.Tasty (TestTree, defaultMain, testGroup) import Properties (propertyTests) import Unit (unitTests)- main :: IO () main = defaultMain allTests
test/Properties.hs view
@@ -1,11 +1,6 @@--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com----module Properties- ( propertyTests- ) where+module Properties (+ propertyTests,+) where import Test.Tasty (TestTree, testGroup) @@ -13,15 +8,17 @@ import qualified Properties.Trait.Body as Body import qualified Properties.Trait.Header as Header import qualified Properties.Trait.Method as Method-import qualified Properties.Trait.Params as Params import qualified Properties.Trait.Path as Path-+import qualified Properties.Trait.QueryParam as QueryParam propertyTests :: TestTree-propertyTests = testGroup "Property Tests" [ Method.tests- , Path.tests- , Header.tests- , Params.tests- , Body.tests- , Basic.tests- ]+propertyTests =+ testGroup+ "Property Tests"+ [ Method.tests+ , Path.tests+ , Header.tests+ , QueryParam.tests+ , Body.tests+ , Basic.tests+ ]
test/Properties/Trait/Auth/Basic.hs view
@@ -1,22 +1,40 @@-module Properties.Trait.Auth.Basic- ( tests- ) where+module Properties.Trait.Auth.Basic (+ tests,+) where +import Control.Arrow (returnA, (>>>)) import Data.ByteString.Base64 (encode) import Data.ByteString.Char8 (elem)-import Data.Functor.Identity (runIdentity)-import Network.Wai (defaultRequest)-import Prelude hiding (elem)-import Test.QuickCheck (Discard (..), Property, allProperties, counterexample, property, (.&&.),- (===))+import Data.Either (fromRight)+import Data.Functor.Identity (Identity, runIdentity)+import Network.Wai (defaultRequest, requestHeaders)+import Test.QuickCheck (+ Discard (..),+ Property,+ allProperties,+ counterexample,+ property,+ (.&&.),+ (===),+ ) import Test.QuickCheck.Instances () import Test.Tasty (TestTree) import Test.Tasty.QuickCheck (testProperties)--import WebGear.Middlewares.Auth.Basic-import WebGear.Trait-import WebGear.Types-+import WebGear.Core.Request (Request (..))+import WebGear.Core.Trait (Linked, getTrait, linkzero, probe)+import WebGear.Core.Trait.Auth.Basic (+ BasicAuth,+ BasicAuth' (..),+ Credentials (..),+ Password (..),+ Username (..),+ )+import WebGear.Core.Trait.Auth.Common (AuthorizationHeader)+import WebGear.Core.Trait.Header (Header (..))+import WebGear.Server.Handler (ServerHandler, runServerHandler)+import WebGear.Server.Trait.Auth.Basic ()+import WebGear.Server.Trait.Header ()+import Prelude hiding (elem) prop_basicAuth :: Property prop_basicAuth = property f@@ -24,17 +42,23 @@ f (username, password) | ':' `elem` username = property Discard | otherwise =- let- hval = "Basic " <> encode (username <> ":" <> password)- req = defaultRequest { requestHeaders = [("Authorization", hval)] }- in- case runIdentity (toAttribute @BasicAuth req) of- Found creds ->- credentialsUsername creds === Username username- .&&. credentialsPassword creds === Password password- NotFound e ->- counterexample ("Unexpected failure: " <> show e) (property False)+ let hval = "Basic " <> encode (username <> ":" <> password) + mkRequest :: ServerHandler Identity () (Linked '[AuthorizationHeader "Basic"] Request)+ mkRequest = proc () -> do+ let req = Request $ defaultRequest{requestHeaders = [("Authorization", hval)]}+ r <- probe Header -< linkzero req+ returnA -< fromRight undefined r++ authCfg :: BasicAuth Identity () Credentials+ authCfg = BasicAuth'{toBasicAttribute = pure . Right}+ in runIdentity $ do+ res <- runServerHandler (mkRequest >>> getTrait authCfg) [""] ()+ pure $ case res of+ Right (Right creds) ->+ credentialsUsername creds === Username username+ .&&. credentialsPassword creds === Password password+ e -> counterexample ("Unexpected failure: " <> show e) (property False) -- Hack for TH splicing return []
test/Properties/Trait/Body.hs view
@@ -1,8 +1,8 @@ {-# OPTIONS_GHC -Wno-deprecations #-} -module Properties.Trait.Body- ( tests- ) where+module Properties.Trait.Body (+ tests,+) where import Control.Monad.IO.Class (MonadIO, liftIO) import Data.IORef (newIORef, readIORef, writeIORef)@@ -13,39 +13,41 @@ import Test.QuickCheck.Monadic (assert, monadicIO, monitor) import Test.Tasty (TestTree) import Test.Tasty.QuickCheck (testProperties)--import WebGear.Middlewares.Body-import WebGear.Trait-import WebGear.Types+import WebGear.Core.Request (Request (..))+import WebGear.Core.Trait (Linked, getTrait, linkzero)+import WebGear.Core.Trait.Body (JSONBody (..))+import WebGear.Server.Handler (runServerHandler)+import WebGear.Server.Trait.Body () +jsonBody :: JSONBody t+jsonBody = JSONBody (Just "application/json") -bodyToRequest :: (MonadIO m, Show a) => a -> m Request+bodyToRequest :: (MonadIO m, Show a) => a -> m (Linked '[] Request) bodyToRequest x = do body <- liftIO $ newIORef $ Just $ fromString $ show x let f = readIORef body >>= maybe (pure "") (\s -> writeIORef body Nothing >> pure s)- return defaultRequest { requestBody = f }+ return $ linkzero $ Request $ defaultRequest{requestBody = f} prop_emptyRequestBodyFails :: Property prop_emptyRequestBodyFails = monadicIO $ do req <- bodyToRequest ("" :: String)- toAttribute @(JSONRequestBody Int) req >>= \case- Found _ -> monitor (counterexample "Unexpected success") >> assert False- NotFound _ -> assert True+ runServerHandler (getTrait (jsonBody :: JSONBody Int)) [""] req >>= \case+ Right (Left _) -> assert True+ e -> monitor (counterexample $ "Unexpected " <> show e) >> assert False prop_validBodyParses :: Property prop_validBodyParses = property $ \n -> monadicIO $ do req <- bodyToRequest (n :: Integer)- toAttribute @(JSONRequestBody Integer) req >>= \case- Found n' -> assert (n == n')- NotFound _ -> assert False+ runServerHandler (getTrait jsonBody) [""] req >>= \case+ Right (Right n') -> assert (n == n')+ _ -> assert False -prop_invalidBodyFails :: Property-prop_invalidBodyFails = property $ \n -> monadicIO $ do+prop_invalidBodyTypeFails :: Property+prop_invalidBodyTypeFails = property $ \n -> monadicIO $ do req <- bodyToRequest (n :: Integer)- toAttribute @(JSONRequestBody String) req >>= \case- Found _ -> assert False- NotFound _ -> assert True-+ runServerHandler (getTrait (jsonBody :: JSONBody String)) [""] req >>= \case+ Right (Left _) -> assert True+ _ -> assert False -- Hack for TH splicing return []
test/Properties/Trait/Header.hs view
@@ -1,55 +1,40 @@-module Properties.Trait.Header- ( tests- ) where+module Properties.Trait.Header (+ tests,+) where import Data.Functor.Identity (runIdentity) import Data.String (fromString) import Data.Text.Encoding (encodeUtf8)-import Network.Wai (defaultRequest)-import Test.QuickCheck (Property, allProperties, counterexample, property, (.&&.), (=/=), (===))+import Network.Wai (defaultRequest, requestHeaders)+import Test.QuickCheck (Property, allProperties, counterexample, property, (===)) import Test.QuickCheck.Instances () import Test.Tasty (TestTree) import Test.Tasty.QuickCheck (testProperties)--import WebGear.Middlewares.Header-import WebGear.Trait-import WebGear.Types-+import WebGear.Core.Request (Request (..))+import WebGear.Core.Trait (getTrait, linkzero)+import WebGear.Core.Trait.Header (Header (..), HeaderParseError (..), RequiredHeader)+import WebGear.Server.Handler (runServerHandler)+import WebGear.Server.Trait.Header () prop_headerParseError :: Property prop_headerParseError = property $ \hval ->- let- hval' = "test-" <> hval- req = defaultRequest { requestHeaders = [("foo", encodeUtf8 hval')] }- in- case runIdentity (toAttribute @(Header "foo" Int) req) of- Found v ->- counterexample ("Unexpected result: " <> show v) (property False)- NotFound e ->- e === Right (HeaderParseError $ "could not parse: `" <> hval' <> "' (input does not start with a digit)")+ let hval' = "test-" <> hval+ req = linkzero $ Request $ defaultRequest{requestHeaders = [("foo", encodeUtf8 hval')]}+ in runIdentity $ do+ res <- runServerHandler (getTrait (Header :: RequiredHeader "foo" Int)) [""] req+ pure $ case res of+ Right (Left e) ->+ e === Right (HeaderParseError $ "could not parse: `" <> hval' <> "' (input does not start with a digit)")+ v -> counterexample ("Unexpected result: " <> show v) (property False) prop_headerParseSuccess :: Property prop_headerParseSuccess = property $ \(n :: Int) ->- let- req = defaultRequest { requestHeaders = [("foo", fromString $ show n)] }- in- case runIdentity (toAttribute @(Header "foo" Int) req) of- Found n' -> n === n'- NotFound e ->- counterexample ("Unexpected result: " <> show e) (property False)--prop_headerMatch :: Property-prop_headerMatch = property $ \v ->- let- req = defaultRequest { requestHeaders = [("foo", v)] }- in- case runIdentity (toAttribute @(HeaderMatch "foo" "bar") req) of- Found _ -> v === "bar"- NotFound Nothing ->- counterexample "Unexpected result: Nothing" (property False)- NotFound (Just e) ->- expectedHeader e === "bar" .&&. actualHeader e =/= "bar"-+ let req = linkzero $ Request $ defaultRequest{requestHeaders = [("foo", fromString $ show n)]}+ in runIdentity $ do+ res <- runServerHandler (getTrait (Header :: RequiredHeader "foo" Int)) [""] req+ pure $ case res of+ Right (Right n') -> n === n'+ e -> counterexample ("Unexpected result: " <> show e) (property False) -- Hack for TH splicing return []
test/Properties/Trait/Method.hs view
@@ -1,36 +1,43 @@-module Properties.Trait.Method- ( tests- ) where+module Properties.Trait.Method (+ tests,+) where import Data.Functor.Identity (runIdentity) import Network.HTTP.Types (StdMethod (..), methodGet, renderStdMethod)-import Network.Wai (defaultRequest)-import Test.QuickCheck (Arbitrary (arbitrary), Property, allProperties, elements, property, (.&&.),- (=/=), (===))+import Network.Wai (defaultRequest, requestMethod)+import Test.QuickCheck (+ Arbitrary (arbitrary),+ Property,+ allProperties,+ elements,+ property,+ (.&&.),+ (=/=),+ (===),+ ) import Test.Tasty (TestTree) import Test.Tasty.QuickCheck (testProperties)--import WebGear.Middlewares.Method-import WebGear.Trait-import WebGear.Types-+import WebGear.Core.Request (Request (..))+import WebGear.Core.Trait (getTrait, linkzero)+import WebGear.Core.Trait.Method (Method (..), MethodMismatch (..))+import WebGear.Server.Handler (runServerHandler)+import WebGear.Server.Trait.Method () newtype MethodWrapper = MethodWrapper StdMethod deriving stock (Show) instance Arbitrary MethodWrapper where- arbitrary = elements $ MethodWrapper <$> [minBound..maxBound]+ arbitrary = elements $ MethodWrapper <$> [minBound .. maxBound] prop_methodMatch :: Property prop_methodMatch = property $ \(MethodWrapper v) ->- let- req = defaultRequest { requestMethod = renderStdMethod v }- in- case runIdentity (toAttribute @(Method GET) req) of- Found _ -> v === GET- NotFound e ->- expectedMethod e === methodGet .&&. actualMethod e =/= methodGet-+ let req = linkzero $ Request $ defaultRequest{requestMethod = renderStdMethod v}+ in runIdentity $ do+ res <- runServerHandler (getTrait (Method GET)) [""] req+ pure $ case res of+ Right (Right _) -> v === GET+ Right (Left e) -> expectedMethod e === methodGet .&&. actualMethod e =/= methodGet+ Left _ -> property False -- Hack for TH splicing return []
− test/Properties/Trait/Params.hs
@@ -1,46 +0,0 @@-module Properties.Trait.Params- ( tests- ) where--import Data.Functor.Identity (runIdentity)-import Data.String (fromString)-import Data.Text.Encoding (encodeUtf8)-import Network.Wai (defaultRequest)-import Test.QuickCheck (Property, allProperties, counterexample, property, (===))-import Test.QuickCheck.Instances ()-import Test.Tasty (TestTree)-import Test.Tasty.QuickCheck (testProperties)--import WebGear.Middlewares.Params-import WebGear.Trait-import WebGear.Types---prop_paramParseError :: Property-prop_paramParseError = property $ \hval ->- let- hval' = "test-" <> hval- req = defaultRequest { queryString = [("foo", Just $ encodeUtf8 hval')] }- in- case runIdentity (toAttribute @(QueryParam "foo" Int) req) of- Found v ->- counterexample ("Unexpected result: " <> show v) (property False)- NotFound e ->- e === Right (ParamParseError $ "could not parse: `" <> hval' <> "' (input does not start with a digit)")--prop_paramParseSuccess :: Property-prop_paramParseSuccess = property $ \(n :: Int) ->- let- req = defaultRequest { queryString = [("foo", Just $ fromString $ show n)] }- in- case runIdentity (toAttribute @(QueryParam "foo" Int) req) of- Found n' -> n === n'- NotFound e ->- counterexample ("Unexpected result: " <> show e) (property False)----- Hack for TH splicing-return []--tests :: TestTree-tests = testProperties "Trait.Params" $allProperties
test/Properties/Trait/Path.hs view
@@ -1,50 +1,51 @@-module Properties.Trait.Path- ( tests- ) where+module Properties.Trait.Path (+ tests,+) where -import Control.Monad.State.Strict (evalState)+import Data.Functor.Identity (runIdentity) import Data.String (fromString)-import Network.Wai (defaultRequest)+import Network.Wai (defaultRequest, pathInfo) import Test.QuickCheck (Property, allProperties, property, (=/=), (===)) import Test.QuickCheck.Instances () import Test.Tasty (TestTree) import Test.Tasty.QuickCheck (testProperties)--import WebGear.Middlewares.Path-import WebGear.Trait-import WebGear.Types-+import WebGear.Core.Trait.Path (Path (..), PathVar (..), PathVarError (..))+import WebGear.Core.Request (Request (..))+import WebGear.Core.Trait (getTrait, linkzero)+import WebGear.Server.Handler (RoutePath (..), runServerHandler)+import WebGear.Server.Trait.Path () prop_pathMatch :: Property prop_pathMatch = property $ \h ->- let- rest = ["foo", "bar"]- req = defaultRequest { pathInfo = h:rest }- in- case evalState (toAttribute @(Path "a") req) (PathInfo $ h:rest) of- Found _ -> h === "a"- NotFound _ -> h =/= "a"+ let rest = ["foo", "bar"]+ req = linkzero $ Request $ defaultRequest{pathInfo = h : rest}+ in runIdentity $ do+ res <- runServerHandler (getTrait $ Path "a") (RoutePath $ h : rest) req+ pure $ case res of+ Right (Right _) -> h === "a"+ Right (Left _) -> h =/= "a"+ Left _ -> property False prop_pathVarMatch :: Property prop_pathVarMatch = property $ \(n :: Int) ->- let- rest = ["foo", "bar"]- req = defaultRequest { pathInfo = fromString (show n):rest }- in- case evalState (toAttribute @(PathVar "tag" Int) req) (PathInfo $ pathInfo req) of- Found n' -> n' === n- NotFound _ -> property False+ let rest = ["foo", "bar"]+ p = fromString (show n) : rest+ req = linkzero $ Request $ defaultRequest{pathInfo = p}+ in runIdentity $ do+ res <- runServerHandler (getTrait (PathVar @"tag" @Int)) (RoutePath p) req+ pure $ case res of+ Right (Right n') -> n' === n+ _ -> property False prop_pathVarParseError :: Property prop_pathVarParseError = property $ \(p, ps) ->- let- p' = "test-" <> p- req = defaultRequest { pathInfo = p':ps }- in- case evalState (toAttribute @(PathVar "tag" Int) req) (PathInfo $ pathInfo req) of- Found _ -> property False- NotFound e -> e === PathVarParseError ("could not parse: `" <> p' <> "' (input does not start with a digit)")-+ let p' = "test-" <> p+ req = linkzero $ Request $ defaultRequest{pathInfo = p' : ps}+ in runIdentity $ do+ res <- runServerHandler (getTrait (PathVar @"tag" @Int)) (RoutePath $ p' : ps) req+ pure $ case res of+ Right (Left e) -> e === PathVarParseError ("could not parse: `" <> p' <> "' (input does not start with a digit)")+ _ -> property False -- Hack for TH splicing return []
+ test/Properties/Trait/QueryParam.hs view
@@ -0,0 +1,44 @@+module Properties.Trait.QueryParam (+ tests,+) where++import Data.Functor.Identity (runIdentity)+import Data.String (fromString)+import Data.Text.Encoding (encodeUtf8)+import Network.Wai (defaultRequest, queryString)+import Test.QuickCheck (Property, allProperties, counterexample, property, (===))+import Test.QuickCheck.Instances ()+import Test.Tasty (TestTree)+import Test.Tasty.QuickCheck (testProperties)+import WebGear.Core.Modifiers (Existence (..), ParseStyle (..))+import WebGear.Core.Request (Request (..))+import WebGear.Core.Trait (getTrait, linkzero)+import WebGear.Core.Trait.QueryParam (ParamParseError (..), QueryParam (..))+import WebGear.Server.Handler (runServerHandler)+import WebGear.Server.Trait.QueryParam ()++prop_paramParseError :: Property+prop_paramParseError = property $ \hval ->+ let hval' = "test-" <> hval+ req = linkzero $ Request $ defaultRequest{queryString = [("foo", Just $ encodeUtf8 hval')]}+ in runIdentity $ do+ res <- runServerHandler (getTrait (QueryParam :: QueryParam Required Strict "foo" Int)) [""] req+ pure $ case res of+ Right (Left e) ->+ e === Right (ParamParseError $ "could not parse: `" <> hval' <> "' (input does not start with a digit)")+ v -> counterexample ("Unexpected result: " <> show v) (property False)++prop_paramParseSuccess :: Property+prop_paramParseSuccess = property $ \(n :: Int) ->+ let req = linkzero $ Request $ defaultRequest{queryString = [("foo", Just $ fromString $ show n)]}+ in runIdentity $ do+ res <- runServerHandler (getTrait (QueryParam :: QueryParam Required Strict "foo" Int)) [""] req+ pure $ case res of+ Right (Right n') -> n === n'+ e -> counterexample ("Unexpected result: " <> show e) (property False)++-- Hack for TH splicing+return []++tests :: TestTree+tests = testProperties "Trait.Params" $allProperties
test/Unit.hs view
@@ -1,19 +1,16 @@--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com----module Unit- ( unitTests- ) where+module Unit (+ unitTests,+) where import Test.Tasty (TestTree, testGroup) import qualified Unit.Trait.Header as Header import qualified Unit.Trait.Path as Path - unitTests :: TestTree-unitTests = testGroup "Unit Tests" [ Header.tests- , Path.tests- ]+unitTests =+ testGroup+ "Unit Tests"+ [ Header.tests+ , Path.tests+ ]
test/Unit/Trait/Header.hs view
@@ -1,44 +1,25 @@--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com----module Unit.Trait.Header- ( tests- ) where+module Unit.Trait.Header (+ tests,+) where -import Network.Wai (defaultRequest)+import Data.Functor.Identity (runIdentity)+import Network.Wai (defaultRequest, requestHeaders) import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (assertFailure, testCase, (@?=))--import WebGear.Middlewares.Header-import WebGear.Trait-import WebGear.Types-+import WebGear.Core.Request (Request (..))+import WebGear.Core.Trait (getTrait, linkzero)+import WebGear.Core.Trait.Header (Header (..), HeaderNotFound (..), RequiredHeader)+import WebGear.Server.Handler (runServerHandler)+import WebGear.Server.Trait.Header () testMissingHeaderFails :: TestTree testMissingHeaderFails = testCase "Missing header fails Header trait" $ do- let req = defaultRequest { requestHeaders = [] }- toAttribute @(Header "foo" Int) req >>= \case- Found _ -> assertFailure "unexpected success"- NotFound e -> e @?= Left HeaderNotFound--testHeaderMatchPositive :: TestTree-testHeaderMatchPositive = testCase "Header match: positive" $ do- let req = defaultRequest { requestHeaders = [("foo", "bar")] }- toAttribute @(HeaderMatch "foo" "bar") req >>= \case- Found _ -> pure ()- NotFound e -> assertFailure $ "Unexpected result: " <> show e--testHeaderMatchMissingHeader :: TestTree-testHeaderMatchMissingHeader = testCase "Header match: missing header" $ do- let req = defaultRequest { requestHeaders = [] }- toAttribute @(HeaderMatch "foo" "bar") req >>= \case- Found _ -> assertFailure "unexpected success"- NotFound e -> e @?= Nothing+ let req = linkzero $ Request $ defaultRequest{requestHeaders = []}+ runIdentity $ do+ res <- runServerHandler (getTrait (Header :: RequiredHeader "foo" Int)) [""] req+ pure $ case res of+ Right (Left e) -> e @?= Left HeaderNotFound+ _ -> assertFailure "unexpected success" tests :: TestTree-tests = testGroup "Trait.Header" [ testMissingHeaderFails- , testHeaderMatchPositive- , testHeaderMatchMissingHeader- ]+tests = testGroup "Trait.Header" [testMissingHeaderFails]
test/Unit/Trait/Path.hs view
@@ -1,28 +1,25 @@--- |--- Copyright : (c) Raghu Kaippully, 2020--- License : MPL-2.0--- Maintainer : rkaippully@gmail.com----module Unit.Trait.Path- ( tests- ) where+module Unit.Trait.Path (+ tests,+) where -import Control.Monad.State (evalState)-import Network.Wai (defaultRequest)+import Data.Functor.Identity (runIdentity)+import Network.Wai (defaultRequest, pathInfo) import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (assertFailure, testCase, (@?=))--import WebGear.Middlewares.Path-import WebGear.Trait-import WebGear.Types-+import WebGear.Core.Trait.Path (PathVar (..), PathVarError (..))+import WebGear.Core.Request (Request (..))+import WebGear.Core.Trait (getTrait, linkzero)+import WebGear.Server.Handler (runServerHandler)+import WebGear.Server.Trait.Path () testMissingPathVar :: TestTree testMissingPathVar = testCase "PathVar match: missing variable" $ do- let req = defaultRequest { pathInfo = [] }- case evalState (toAttribute @(PathVar "tag" Int) req) (PathInfo []) of- Found _ -> assertFailure "unexpected success"- NotFound e -> e @?= PathVarNotFound+ let req = linkzero $ Request $ defaultRequest{pathInfo = []}+ runIdentity $ do+ res <- runServerHandler (getTrait (PathVar @"tag" @Int)) [] req+ pure $ case res of+ Right (Left e) -> e @?= PathVarNotFound+ _ -> assertFailure "unexpected success" tests :: TestTree-tests = testGroup "Trait.Path" [ testMissingPathVar ]+tests = testGroup "Trait.Path" [testMissingPathVar]
webgear-server.cabal view
@@ -1,15 +1,13 @@ cabal-version: 2.4 name: webgear-server-version: 0.2.1+version: 1.0.0 synopsis: Composable, type-safe library to build HTTP API servers description:- WebGear is a library to for building composable, type-safe HTTP API servers.-- WebGear focuses on good documentation and usability.-- See the documentation of WebGear module to get started.-homepage: https://github.com/rkaippully/webgear#readme-bug-reports: https://github.com/rkaippully/webgear/issues+ WebGear is a library to for building composable, type-safe HTTP API servers.+ WebGear focuses on good documentation and usability. See the documentation+ of WebGear module to get started.+homepage: https://github.com/haskell-webgear/webgear#readme+bug-reports: https://github.com/haskell-webgear/webgear/issues author: Raghu Kaippully maintainer: rkaippully@gmail.com copyright: 2020 Raghu Kaippully@@ -18,76 +16,91 @@ category: Web build-type: Simple extra-source-files: README.md- ChangeLog.md+ CHANGELOG.md source-repository head type: git- location: https://github.com/rkaippully/webgear+ location: https://github.com/haskell-webgear/webgear common webgear-common default-language: Haskell2010- default-extensions: DataKinds+ default-extensions: Arrows+ ConstraintKinds+ DataKinds DeriveFunctor+ DeriveGeneric DerivingStrategies+ DerivingVia FlexibleContexts FlexibleInstances+ FunctionalDependencies GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses+ NamedFieldPuns OverloadedStrings+ OverloadedLists PolyKinds RankNTypes RecordWildCards ScopedTypeVariables+ StandaloneDeriving TemplateHaskellQuotes TypeApplications TypeFamilies TypeOperators- build-depends: aeson >=1.4 && <1.6- , base >=4.12.0.0 && <5- , base64-bytestring >=1.0.0.3 && <1.3- , bytestring >=0.10.8.2 && <0.12- , bytestring-conversion ==0.3.*- , case-insensitive ==1.2.*- , http-api-data ==0.4.*- , http-types ==0.12.*- , mtl ==2.2.*- , network >=2.8 && <3.2- , template-haskell >=2.14.0.0 && <3- , text ==1.2.*- , unordered-containers ==0.2.*- , wai ==3.2.*+ build-depends: base >=4.13.0.0 && <4.17+ , base64-bytestring >=1.0.0.3 && <1.3+ , bytestring >=0.10.10.1 && <0.12+ , http-types ==0.12.*+ , text ==1.2.*+ , wai ==3.2.*+ , webgear-core ==1.0.0 ghc-options: -Wall -Wno-unticked-promoted-constructors+ -Wcompat+ -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns+ -Wmissing-fields+ -Wmissing-home-modules+ -Wmissing-deriving-strategies+ -Wpartial-fields -Wredundant-constraints+ -fshow-warning-groups - if impl(ghc >= 8.8.1)- ghc-options: -fwrite-ide-info- -hiedir=.hie+ if impl(ghc >= 8.10)+ ghc-options: -Wunused-packages library import: webgear-common- exposed-modules: WebGear- , WebGear.Modifiers- , WebGear.Trait- , WebGear.Types- , WebGear.Middlewares- , WebGear.Middlewares.Auth.Basic- , WebGear.Middlewares.Body- , WebGear.Middlewares.Header- , WebGear.Middlewares.Method- , WebGear.Middlewares.Params- , WebGear.Middlewares.Path+ exposed-modules: WebGear.Server+ , WebGear.Server.Handler+ , WebGear.Server.Traits+ , WebGear.Server.Trait.Auth.Basic+ , WebGear.Server.Trait.Auth.JWT+ , WebGear.Server.Trait.Body+ , WebGear.Server.Trait.Header+ , WebGear.Server.Trait.Method+ , WebGear.Server.Trait.Path+ , WebGear.Server.Trait.QueryParam+ , WebGear.Server.Trait.Status other-modules: Paths_webgear_server- , WebGear.Util autogen-modules: Paths_webgear_server hs-source-dirs: src+ build-depends: aeson >=1.4 && <2.1+ , arrows ==0.4.*+ , bytestring-conversion ==0.3.*+ , http-api-data ==0.4.*+ , http-media ==0.8.*+ , jose >=0.8.3.1 && <0.10+ , monad-time ==0.3.*+ , mtl ==2.2.*+ , unordered-containers ==0.2.* test-suite webgear-server-test import: webgear-common@@ -99,7 +112,7 @@ , Properties , Properties.Trait.Body , Properties.Trait.Header- , Properties.Trait.Params+ , Properties.Trait.QueryParam , Properties.Trait.Method , Properties.Trait.Path , Properties.Trait.Auth.Basic@@ -108,9 +121,9 @@ ghc-options: -threaded -rtsopts -with-rtsopts=-N- build-depends: QuickCheck >=2.13 && <2.15- , quickcheck-instances ==0.3.*- , tasty >=1.2 && <1.5- , tasty-hunit ==0.10.*- , tasty-quickcheck ==0.10.*+ build-depends: QuickCheck >=2.13 && <2.15+ , quickcheck-instances ==0.3.*+ , tasty >=1.2 && <1.5+ , tasty-hunit ==0.10.*+ , tasty-quickcheck ==0.10.* , webgear-server