-- | -- 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)