packages feed

webgear-server 0.1.0 → 0.2.0

raw patch · 28 files changed

+1731/−729 lines, 28 filesdep +base64-bytestringdep +bytestring-conversiondep +case-insensitivedep −taggeddep ~QuickCheckdep ~aesondep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: base64-bytestring, bytestring-conversion, case-insensitive, network

Dependencies removed: tagged

Dependency ranges changed: QuickCheck, aeson, base, tasty, template-haskell

API changes (from Hackage documentation)

- WebGear.Middlewares: badRequest :: Monad m => m (Linked '[] (Response a))
- WebGear.Middlewares: noContent :: (Monad m, IsString s) => m (Linked '[] (Response s))
- WebGear.Middlewares: notFound :: Monad m => m (Linked '[] (Response a))
- WebGear.Middlewares: ok :: Monad m => a -> m (Linked '[] (Response a))
- WebGear.Middlewares.Header: requestContentType :: forall c m req res a. (KnownSymbol c, MonadRouter m) => RequestMiddleware m req (HeaderMatch "Content-Type" c : req) res a
- WebGear.Route: class (Alternative m, MonadPlus m) => MonadRouter m
- WebGear.Route: failHandler :: MonadRouter m => Response ByteString -> m a
- WebGear.Route: instance GHC.Base.Monad m => WebGear.Route.MonadRouter (WebGear.Route.RouterT m)
- WebGear.Route: rejectRoute :: MonadRouter m => m a
- WebGear.Route: runRoute :: Monad m => Handler (RouterT m) '[] res ByteString -> Request -> m Response
- WebGear.Route: type RouterT m = ExceptT (Maybe (First (Response ByteString))) m
- WebGear.Trait: CheckFail :: Fail t a -> CheckResult t a
- WebGear.Trait: CheckSuccess :: a -> Val t a -> CheckResult t a
- WebGear.Trait: check :: Trait t a m => a -> m (CheckResult t a)
- WebGear.Trait: data CheckResult t a
- WebGear.Trait: instance forall a1 (t :: a1) a2 (m :: * -> *) (ts :: [a1]). (WebGear.Trait.Trait t a2 m, WebGear.Trait.Trait ts a2 m) => WebGear.Trait.Trait (t : ts) a2 m
- WebGear.Trait: instance forall k a (t :: k). (GHC.Classes.Eq a, GHC.Classes.Eq (WebGear.Trait.Val t a), GHC.Classes.Eq (WebGear.Trait.Fail t a)) => GHC.Classes.Eq (WebGear.Trait.CheckResult t a)
- WebGear.Trait: instance forall k a (t :: k). (GHC.Read.Read a, GHC.Read.Read (WebGear.Trait.Val t a), GHC.Read.Read (WebGear.Trait.Fail t a)) => GHC.Read.Read (WebGear.Trait.CheckResult t a)
- WebGear.Trait: instance forall k a (t :: k). (GHC.Show.Show a, GHC.Show.Show (WebGear.Trait.Val t a), GHC.Show.Show (WebGear.Trait.Fail t a)) => GHC.Show.Show (WebGear.Trait.CheckResult t a)
- WebGear.Trait: linkminus :: Linked (t : ts) a -> Linked ts a
- WebGear.Trait: linkplus :: Trait t a m => Linked ts a -> m (Either (Fail t a) (Linked (t : ts) a))
- WebGear.Trait: linkzero :: a -> Linked '[] a
- WebGear.Trait: traitValue :: Has t ts => Linked ts a -> Tagged t (Val t a)
- WebGear.Trait.Body: data JSONRequestBody (t :: Type)
- WebGear.Trait.Body: instance (Data.Aeson.Types.FromJSON.FromJSON t, Control.Monad.IO.Class.MonadIO m) => WebGear.Trait.Trait (WebGear.Trait.Body.JSONRequestBody t) Network.Wai.Internal.Request m
- WebGear.Trait.Header: HeaderMismatch :: ByteString -> Maybe ByteString -> HeaderMismatch
- WebGear.Trait.Header: HeaderNotFound :: HeaderFail
- WebGear.Trait.Header: HeaderParseError :: Text -> HeaderFail
- WebGear.Trait.Header: [actualHeader] :: HeaderMismatch -> Maybe ByteString
- WebGear.Trait.Header: [expectedHeader] :: HeaderMismatch -> ByteString
- WebGear.Trait.Header: data Header (s :: Symbol) (t :: Type)
- WebGear.Trait.Header: data HeaderFail
- WebGear.Trait.Header: data HeaderMatch (s :: Symbol) (t :: Symbol)
- WebGear.Trait.Header: data HeaderMismatch
- WebGear.Trait.Header: instance (GHC.TypeLits.KnownSymbol s, GHC.TypeLits.KnownSymbol t, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Trait.Header.HeaderMatch s t) Network.Wai.Internal.Request m
- WebGear.Trait.Header: instance (GHC.TypeLits.KnownSymbol s, Web.Internal.HttpApiData.FromHttpApiData t, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Trait.Header.Header s t) Network.Wai.Internal.Request m
- WebGear.Trait.Header: instance GHC.Classes.Eq WebGear.Trait.Header.HeaderFail
- WebGear.Trait.Header: instance GHC.Classes.Eq WebGear.Trait.Header.HeaderMismatch
- WebGear.Trait.Header: instance GHC.Read.Read WebGear.Trait.Header.HeaderFail
- WebGear.Trait.Header: instance GHC.Read.Read WebGear.Trait.Header.HeaderMismatch
- WebGear.Trait.Header: instance GHC.Show.Show WebGear.Trait.Header.HeaderFail
- WebGear.Trait.Header: instance GHC.Show.Show WebGear.Trait.Header.HeaderMismatch
- WebGear.Trait.Method: MethodMismatch :: Method -> Method -> MethodMismatch
- WebGear.Trait.Method: [actualMethod] :: MethodMismatch -> Method
- WebGear.Trait.Method: [expectedMethod] :: MethodMismatch -> Method
- WebGear.Trait.Method: class IsStdMethod t
- WebGear.Trait.Method: data Method (t :: StdMethod)
- WebGear.Trait.Method: data MethodMismatch
- WebGear.Trait.Method: instance (GHC.Base.Monad m, WebGear.Trait.Method.IsStdMethod t) => WebGear.Trait.Trait (WebGear.Trait.Method.Method t) Network.Wai.Internal.Request m
- WebGear.Trait.Method: instance WebGear.Trait.Method.IsStdMethod 'Network.HTTP.Types.Method.CONNECT
- WebGear.Trait.Method: instance WebGear.Trait.Method.IsStdMethod 'Network.HTTP.Types.Method.DELETE
- WebGear.Trait.Method: instance WebGear.Trait.Method.IsStdMethod 'Network.HTTP.Types.Method.GET
- WebGear.Trait.Method: instance WebGear.Trait.Method.IsStdMethod 'Network.HTTP.Types.Method.HEAD
- WebGear.Trait.Method: instance WebGear.Trait.Method.IsStdMethod 'Network.HTTP.Types.Method.OPTIONS
- WebGear.Trait.Method: instance WebGear.Trait.Method.IsStdMethod 'Network.HTTP.Types.Method.PATCH
- WebGear.Trait.Method: instance WebGear.Trait.Method.IsStdMethod 'Network.HTTP.Types.Method.POST
- WebGear.Trait.Method: instance WebGear.Trait.Method.IsStdMethod 'Network.HTTP.Types.Method.PUT
- WebGear.Trait.Method: instance WebGear.Trait.Method.IsStdMethod 'Network.HTTP.Types.Method.TRACE
- WebGear.Trait.Method: toStdMethod :: IsStdMethod t => Proxy t -> StdMethod
- WebGear.Trait.Path: PathVarNotFound :: PathVarFail
- WebGear.Trait.Path: PathVarParseError :: Text -> PathVarFail
- WebGear.Trait.Path: data Path (s :: Symbol)
- WebGear.Trait.Path: data PathVar tag (val :: Type)
- WebGear.Trait.Path: data PathVarFail
- WebGear.Trait.Path: instance (GHC.TypeLits.KnownSymbol s, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Trait.Path.Path s) Network.Wai.Internal.Request m
- WebGear.Trait.Path: instance GHC.Classes.Eq WebGear.Trait.Path.PathVarFail
- WebGear.Trait.Path: instance GHC.Read.Read WebGear.Trait.Path.PathVarFail
- WebGear.Trait.Path: instance GHC.Show.Show WebGear.Trait.Path.PathVarFail
- WebGear.Trait.Path: instance forall k val (m :: * -> *) (tag :: k). (Web.Internal.HttpApiData.FromHttpApiData val, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Trait.Path.PathVar tag val) Network.Wai.Internal.Request m
- WebGear.Types: [respBody] :: Response a -> Maybe a
- WebGear.Types: [respHeaders] :: Response a -> HashMap HeaderName ByteString
- WebGear.Types: [respStatus] :: Response a -> Status
- WebGear.Types: addResponseHeader :: Header -> Response a -> Response a
- WebGear.Types: setPathInfo :: [Text] -> Request -> Request
+ WebGear.Middlewares.Auth.Basic: AuthHeaderError :: BasicAuthError
+ WebGear.Middlewares.Auth.Basic: AuthSchemeMismatch :: BasicAuthError
+ WebGear.Middlewares.Auth.Basic: Credentials :: !Username -> !Password -> Credentials
+ WebGear.Middlewares.Auth.Basic: Password :: ByteString -> Password
+ WebGear.Middlewares.Auth.Basic: Realm :: ByteString -> Realm
+ WebGear.Middlewares.Auth.Basic: Username :: ByteString -> Username
+ WebGear.Middlewares.Auth.Basic: [credentialsPassword] :: Credentials -> !Password
+ WebGear.Middlewares.Auth.Basic: [credentialsUsername] :: Credentials -> !Username
+ WebGear.Middlewares.Auth.Basic: basicAuth :: forall m req a. MonadRouter m => Realm -> (Credentials -> m Bool) -> RequestMiddleware' m req (BasicAuth : req) a
+ WebGear.Middlewares.Auth.Basic: data BasicAuth
+ WebGear.Middlewares.Auth.Basic: data BasicAuthError
+ WebGear.Middlewares.Auth.Basic: data Credentials
+ WebGear.Middlewares.Auth.Basic: instance Data.String.IsString WebGear.Middlewares.Auth.Basic.Password
+ WebGear.Middlewares.Auth.Basic: instance Data.String.IsString WebGear.Middlewares.Auth.Basic.Realm
+ WebGear.Middlewares.Auth.Basic: instance Data.String.IsString WebGear.Middlewares.Auth.Basic.Username
+ WebGear.Middlewares.Auth.Basic: instance GHC.Base.Monad m => WebGear.Trait.Trait WebGear.Middlewares.Auth.Basic.BasicAuth Network.Wai.Internal.Request m
+ WebGear.Middlewares.Auth.Basic: instance GHC.Classes.Eq WebGear.Middlewares.Auth.Basic.BasicAuthError
+ WebGear.Middlewares.Auth.Basic: instance GHC.Classes.Eq WebGear.Middlewares.Auth.Basic.Credentials
+ WebGear.Middlewares.Auth.Basic: instance GHC.Classes.Eq WebGear.Middlewares.Auth.Basic.Password
+ WebGear.Middlewares.Auth.Basic: instance GHC.Classes.Eq WebGear.Middlewares.Auth.Basic.Realm
+ WebGear.Middlewares.Auth.Basic: instance GHC.Classes.Eq WebGear.Middlewares.Auth.Basic.Username
+ WebGear.Middlewares.Auth.Basic: instance GHC.Classes.Ord WebGear.Middlewares.Auth.Basic.BasicAuthError
+ WebGear.Middlewares.Auth.Basic: instance GHC.Classes.Ord WebGear.Middlewares.Auth.Basic.Credentials
+ WebGear.Middlewares.Auth.Basic: instance GHC.Classes.Ord WebGear.Middlewares.Auth.Basic.Password
+ WebGear.Middlewares.Auth.Basic: instance GHC.Classes.Ord WebGear.Middlewares.Auth.Basic.Realm
+ WebGear.Middlewares.Auth.Basic: instance GHC.Classes.Ord WebGear.Middlewares.Auth.Basic.Username
+ WebGear.Middlewares.Auth.Basic: instance GHC.Read.Read WebGear.Middlewares.Auth.Basic.BasicAuthError
+ WebGear.Middlewares.Auth.Basic: instance GHC.Read.Read WebGear.Middlewares.Auth.Basic.Credentials
+ WebGear.Middlewares.Auth.Basic: instance GHC.Read.Read WebGear.Middlewares.Auth.Basic.Password
+ WebGear.Middlewares.Auth.Basic: instance GHC.Read.Read WebGear.Middlewares.Auth.Basic.Realm
+ WebGear.Middlewares.Auth.Basic: instance GHC.Read.Read WebGear.Middlewares.Auth.Basic.Username
+ WebGear.Middlewares.Auth.Basic: instance GHC.Show.Show WebGear.Middlewares.Auth.Basic.BasicAuthError
+ WebGear.Middlewares.Auth.Basic: instance GHC.Show.Show WebGear.Middlewares.Auth.Basic.Credentials
+ WebGear.Middlewares.Auth.Basic: instance GHC.Show.Show WebGear.Middlewares.Auth.Basic.Password
+ WebGear.Middlewares.Auth.Basic: instance GHC.Show.Show WebGear.Middlewares.Auth.Basic.Realm
+ WebGear.Middlewares.Auth.Basic: instance GHC.Show.Show WebGear.Middlewares.Auth.Basic.Username
+ WebGear.Middlewares.Auth.Basic: newtype Password
+ WebGear.Middlewares.Auth.Basic: newtype Realm
+ WebGear.Middlewares.Auth.Basic: newtype Username
+ WebGear.Middlewares.Body: data JSONRequestBody (t :: Type)
+ WebGear.Middlewares.Body: instance (Data.Aeson.Types.FromJSON.FromJSON t, Control.Monad.IO.Class.MonadIO m) => WebGear.Trait.Trait (WebGear.Middlewares.Body.JSONRequestBody t) Network.Wai.Internal.Request m
+ WebGear.Middlewares.Header: HeaderMismatch :: ByteString -> ByteString -> HeaderMismatch
+ WebGear.Middlewares.Header: HeaderNotFound :: HeaderNotFound
+ WebGear.Middlewares.Header: HeaderParseError :: Text -> HeaderParseError
+ WebGear.Middlewares.Header: [actualHeader] :: HeaderMismatch -> ByteString
+ WebGear.Middlewares.Header: [expectedHeader] :: HeaderMismatch -> ByteString
+ WebGear.Middlewares.Header: addResponseHeader :: forall t m req a. (ToHttpApiData t, Monad m) => HeaderName -> t -> ResponseMiddleware' m req a a
+ WebGear.Middlewares.Header: data Header' (e :: Existence) (p :: ParseStyle) (name :: Symbol) (val :: Type)
+ WebGear.Middlewares.Header: data HeaderMatch' (e :: Existence) (name :: Symbol) (val :: Symbol)
+ WebGear.Middlewares.Header: data HeaderMismatch
+ WebGear.Middlewares.Header: data HeaderNotFound
+ WebGear.Middlewares.Header: header :: forall name val m req a. (KnownSymbol name, FromHttpApiData val, MonadRouter m) => RequestMiddleware' m req (Header name val : req) a
+ WebGear.Middlewares.Header: headerMatch :: forall name val m req a. (KnownSymbol name, KnownSymbol val, MonadRouter m) => RequestMiddleware' m req (HeaderMatch name val : req) a
+ WebGear.Middlewares.Header: instance (GHC.TypeLits.KnownSymbol name, GHC.TypeLits.KnownSymbol val, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Middlewares.Header.HeaderMatch' 'WebGear.Modifiers.Optional name val) Network.Wai.Internal.Request m
+ WebGear.Middlewares.Header: instance (GHC.TypeLits.KnownSymbol name, GHC.TypeLits.KnownSymbol val, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Middlewares.Header.HeaderMatch' 'WebGear.Modifiers.Required name val) Network.Wai.Internal.Request m
+ WebGear.Middlewares.Header: instance (GHC.TypeLits.KnownSymbol name, Web.Internal.HttpApiData.FromHttpApiData val, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Middlewares.Header.Header' 'WebGear.Modifiers.Optional 'WebGear.Modifiers.Lenient name val) (WebGear.Types.Response a) m
+ WebGear.Middlewares.Header: instance (GHC.TypeLits.KnownSymbol name, Web.Internal.HttpApiData.FromHttpApiData val, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Middlewares.Header.Header' 'WebGear.Modifiers.Optional 'WebGear.Modifiers.Lenient name val) Network.Wai.Internal.Request m
+ WebGear.Middlewares.Header: instance (GHC.TypeLits.KnownSymbol name, Web.Internal.HttpApiData.FromHttpApiData val, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Middlewares.Header.Header' 'WebGear.Modifiers.Optional 'WebGear.Modifiers.Strict name val) (WebGear.Types.Response a) m
+ WebGear.Middlewares.Header: instance (GHC.TypeLits.KnownSymbol name, Web.Internal.HttpApiData.FromHttpApiData val, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Middlewares.Header.Header' 'WebGear.Modifiers.Optional 'WebGear.Modifiers.Strict name val) Network.Wai.Internal.Request m
+ WebGear.Middlewares.Header: instance (GHC.TypeLits.KnownSymbol name, Web.Internal.HttpApiData.FromHttpApiData val, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Middlewares.Header.Header' 'WebGear.Modifiers.Required 'WebGear.Modifiers.Lenient name val) (WebGear.Types.Response a) m
+ WebGear.Middlewares.Header: instance (GHC.TypeLits.KnownSymbol name, Web.Internal.HttpApiData.FromHttpApiData val, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Middlewares.Header.Header' 'WebGear.Modifiers.Required 'WebGear.Modifiers.Lenient name val) Network.Wai.Internal.Request m
+ WebGear.Middlewares.Header: instance (GHC.TypeLits.KnownSymbol name, Web.Internal.HttpApiData.FromHttpApiData val, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Middlewares.Header.Header' 'WebGear.Modifiers.Required 'WebGear.Modifiers.Strict name val) (WebGear.Types.Response a) m
+ WebGear.Middlewares.Header: instance (GHC.TypeLits.KnownSymbol name, Web.Internal.HttpApiData.FromHttpApiData val, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Middlewares.Header.Header' 'WebGear.Modifiers.Required 'WebGear.Modifiers.Strict name val) Network.Wai.Internal.Request m
+ WebGear.Middlewares.Header: instance GHC.Classes.Eq WebGear.Middlewares.Header.HeaderMismatch
+ WebGear.Middlewares.Header: instance GHC.Classes.Eq WebGear.Middlewares.Header.HeaderNotFound
+ WebGear.Middlewares.Header: instance GHC.Classes.Eq WebGear.Middlewares.Header.HeaderParseError
+ WebGear.Middlewares.Header: instance GHC.Read.Read WebGear.Middlewares.Header.HeaderMismatch
+ WebGear.Middlewares.Header: instance GHC.Read.Read WebGear.Middlewares.Header.HeaderNotFound
+ WebGear.Middlewares.Header: instance GHC.Read.Read WebGear.Middlewares.Header.HeaderParseError
+ WebGear.Middlewares.Header: instance GHC.Show.Show WebGear.Middlewares.Header.HeaderMismatch
+ WebGear.Middlewares.Header: instance GHC.Show.Show WebGear.Middlewares.Header.HeaderNotFound
+ WebGear.Middlewares.Header: instance GHC.Show.Show WebGear.Middlewares.Header.HeaderParseError
+ WebGear.Middlewares.Header: lenientHeader :: forall name val m req a. (KnownSymbol name, FromHttpApiData val, MonadRouter m) => RequestMiddleware' m req (Header' Required Lenient name val : req) a
+ WebGear.Middlewares.Header: newtype HeaderParseError
+ WebGear.Middlewares.Header: optionalHeader :: forall name val m req a. (KnownSymbol name, FromHttpApiData val, MonadRouter m) => RequestMiddleware' m req (Header' Optional Strict name val : req) a
+ WebGear.Middlewares.Header: optionalHeaderMatch :: forall name val m req a. (KnownSymbol name, KnownSymbol val, MonadRouter m) => RequestMiddleware' m req (HeaderMatch' Optional name val : req) a
+ WebGear.Middlewares.Header: optionalLenientHeader :: forall name val m req a. (KnownSymbol name, FromHttpApiData val, MonadRouter m) => RequestMiddleware' m req (Header' Optional Lenient name val : req) a
+ WebGear.Middlewares.Header: requestContentTypeHeader :: forall val m req a. (KnownSymbol val, MonadRouter m) => RequestMiddleware' m req (HeaderMatch "Content-Type" val : req) a
+ WebGear.Middlewares.Header: type Header (name :: Symbol) (val :: Type) = Header' Required Strict name val
+ WebGear.Middlewares.Header: type HeaderMatch (name :: Symbol) (val :: Symbol) = HeaderMatch' Required name val
+ WebGear.Middlewares.Method: MethodMismatch :: Method -> Method -> MethodMismatch
+ WebGear.Middlewares.Method: [actualMethod] :: MethodMismatch -> Method
+ WebGear.Middlewares.Method: [expectedMethod] :: MethodMismatch -> Method
+ WebGear.Middlewares.Method: class IsStdMethod t
+ WebGear.Middlewares.Method: data Method (t :: StdMethod)
+ WebGear.Middlewares.Method: data MethodMismatch
+ WebGear.Middlewares.Method: instance (WebGear.Middlewares.Method.IsStdMethod t, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Middlewares.Method.Method t) Network.Wai.Internal.Request m
+ WebGear.Middlewares.Method: instance WebGear.Middlewares.Method.IsStdMethod 'Network.HTTP.Types.Method.CONNECT
+ WebGear.Middlewares.Method: instance WebGear.Middlewares.Method.IsStdMethod 'Network.HTTP.Types.Method.DELETE
+ WebGear.Middlewares.Method: instance WebGear.Middlewares.Method.IsStdMethod 'Network.HTTP.Types.Method.GET
+ WebGear.Middlewares.Method: instance WebGear.Middlewares.Method.IsStdMethod 'Network.HTTP.Types.Method.HEAD
+ WebGear.Middlewares.Method: instance WebGear.Middlewares.Method.IsStdMethod 'Network.HTTP.Types.Method.OPTIONS
+ WebGear.Middlewares.Method: instance WebGear.Middlewares.Method.IsStdMethod 'Network.HTTP.Types.Method.PATCH
+ WebGear.Middlewares.Method: instance WebGear.Middlewares.Method.IsStdMethod 'Network.HTTP.Types.Method.POST
+ WebGear.Middlewares.Method: instance WebGear.Middlewares.Method.IsStdMethod 'Network.HTTP.Types.Method.PUT
+ WebGear.Middlewares.Method: instance WebGear.Middlewares.Method.IsStdMethod 'Network.HTTP.Types.Method.TRACE
+ WebGear.Middlewares.Method: toStdMethod :: IsStdMethod t => Proxy t -> StdMethod
+ WebGear.Middlewares.Params: ParamNotFound :: ParamNotFound
+ WebGear.Middlewares.Params: ParamParseError :: Text -> ParamParseError
+ WebGear.Middlewares.Params: data ParamNotFound
+ WebGear.Middlewares.Params: data QueryParam' (e :: Existence) (p :: ParseStyle) (name :: Symbol) val
+ WebGear.Middlewares.Params: instance (GHC.TypeLits.KnownSymbol name, Web.Internal.HttpApiData.FromHttpApiData val, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Middlewares.Params.QueryParam' 'WebGear.Modifiers.Optional 'WebGear.Modifiers.Lenient name val) Network.Wai.Internal.Request m
+ WebGear.Middlewares.Params: instance (GHC.TypeLits.KnownSymbol name, Web.Internal.HttpApiData.FromHttpApiData val, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Middlewares.Params.QueryParam' 'WebGear.Modifiers.Optional 'WebGear.Modifiers.Strict name val) Network.Wai.Internal.Request m
+ WebGear.Middlewares.Params: instance (GHC.TypeLits.KnownSymbol name, Web.Internal.HttpApiData.FromHttpApiData val, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Middlewares.Params.QueryParam' 'WebGear.Modifiers.Required 'WebGear.Modifiers.Lenient name val) Network.Wai.Internal.Request m
+ WebGear.Middlewares.Params: instance (GHC.TypeLits.KnownSymbol name, Web.Internal.HttpApiData.FromHttpApiData val, GHC.Base.Monad m) => WebGear.Trait.Trait (WebGear.Middlewares.Params.QueryParam' 'WebGear.Modifiers.Required 'WebGear.Modifiers.Strict name val) Network.Wai.Internal.Request m
+ WebGear.Middlewares.Params: instance GHC.Classes.Eq WebGear.Middlewares.Params.ParamNotFound
+ WebGear.Middlewares.Params: instance GHC.Classes.Eq WebGear.Middlewares.Params.ParamParseError
+ WebGear.Middlewares.Params: instance GHC.Read.Read WebGear.Middlewares.Params.ParamNotFound
+ WebGear.Middlewares.Params: instance GHC.Read.Read WebGear.Middlewares.Params.ParamParseError
+ WebGear.Middlewares.Params: instance GHC.Show.Show WebGear.Middlewares.Params.ParamNotFound
+ WebGear.Middlewares.Params: instance GHC.Show.Show WebGear.Middlewares.Params.ParamParseError
+ WebGear.Middlewares.Params: lenientQueryParam :: forall name val m req a. (KnownSymbol name, FromHttpApiData val, MonadRouter m) => RequestMiddleware' m req (QueryParam' Required Lenient name val : req) a
+ WebGear.Middlewares.Params: newtype ParamParseError
+ WebGear.Middlewares.Params: optionalLenientQueryParam :: forall name val m req a. (KnownSymbol name, FromHttpApiData val, MonadRouter m) => RequestMiddleware' m req (QueryParam' Optional Lenient name val : req) a
+ WebGear.Middlewares.Params: optionalQueryParam :: forall name val m req a. (KnownSymbol name, FromHttpApiData val, MonadRouter m) => RequestMiddleware' m req (QueryParam' Optional Strict name val : req) a
+ WebGear.Middlewares.Params: queryParam :: forall name val m req a. (KnownSymbol name, FromHttpApiData val, MonadRouter m) => RequestMiddleware' m req (QueryParam name val : req) a
+ WebGear.Middlewares.Params: type QueryParam (name :: Symbol) val = QueryParam' Required Strict name val
+ WebGear.Middlewares.Path: PathVarNotFound :: PathVarError
+ WebGear.Middlewares.Path: PathVarParseError :: Text -> PathVarError
+ WebGear.Middlewares.Path: data Path (s :: Symbol)
+ WebGear.Middlewares.Path: data PathEnd
+ WebGear.Middlewares.Path: data PathVar tag val
+ WebGear.Middlewares.Path: data PathVarError
+ WebGear.Middlewares.Path: instance (GHC.TypeLits.KnownSymbol s, Control.Monad.State.Class.MonadState WebGear.Types.PathInfo m) => WebGear.Trait.Trait (WebGear.Middlewares.Path.Path s) Network.Wai.Internal.Request m
+ WebGear.Middlewares.Path: instance Control.Monad.State.Class.MonadState WebGear.Types.PathInfo m => WebGear.Trait.Trait WebGear.Middlewares.Path.PathEnd Network.Wai.Internal.Request m
+ WebGear.Middlewares.Path: instance GHC.Classes.Eq WebGear.Middlewares.Path.PathVarError
+ WebGear.Middlewares.Path: instance GHC.Read.Read WebGear.Middlewares.Path.PathVarError
+ WebGear.Middlewares.Path: instance GHC.Show.Show WebGear.Middlewares.Path.PathVarError
+ WebGear.Middlewares.Path: instance forall k val (m :: * -> *) (tag :: k). (Web.Internal.HttpApiData.FromHttpApiData val, Control.Monad.State.Class.MonadState WebGear.Types.PathInfo m) => WebGear.Trait.Trait (WebGear.Middlewares.Path.PathVar tag val) Network.Wai.Internal.Request m
+ WebGear.Middlewares.Path: pathEnd :: MonadRouter m => RequestMiddleware' m ts (PathEnd : ts) a
+ WebGear.Middlewares.Path: route :: QuasiQuoter
+ WebGear.Modifiers: Lenient :: ParseStyle
+ WebGear.Modifiers: Optional :: Existence
+ WebGear.Modifiers: Required :: Existence
+ WebGear.Modifiers: Strict :: ParseStyle
+ WebGear.Modifiers: data Existence
+ WebGear.Modifiers: data ParseStyle
+ WebGear.Trait: -- could be an error message, parse error etc.
+ WebGear.Trait: Found :: Attribute t a -> Result t a
+ WebGear.Trait: NotFound :: Absence t a -> Result t a
+ WebGear.Trait: data Result t a
+ WebGear.Trait: get :: Has t ts => Proxy t -> Linked ts a -> Attribute t a
+ WebGear.Trait: instance forall a1 (t :: a1) a2 (m :: * -> *) (ts :: [a1]). (WebGear.Trait.Trait t a2 m, WebGear.Trait.Trait ts a2 m, GHC.Base.Monad m) => WebGear.Trait.Trait (t : ts) a2 m
+ WebGear.Trait: instance forall k (t :: k). (TypeError ...) => WebGear.Trait.Has t '[]
+ WebGear.Trait: link :: a -> Linked '[] a
+ WebGear.Trait: probe :: forall t ts a m. Trait t a m => Linked ts a -> m (Either (Absence t a) (Linked (t : ts) a))
+ WebGear.Trait: remove :: Linked (t : ts) a -> Linked ts a
+ WebGear.Trait: toAttribute :: Trait t a m => a -> m (Result t a)
+ WebGear.Trait: type MissingTrait t = Text "The request doesn't have the trait \8216" :<>: ShowType t :<>: Text "\8217." :$$: Text "" :$$: Text "Did you use a wrong trait type?" :$$: Text "For e.g., \8216PathVar \"foo\" Int\8217 instead of \8216PathVar \"foo\" String\8217?" :$$: Text "" :$$: Text "Or did you forget to apply an appropriate middleware?" :$$: Text "For e.g. The trait \8216JSONRequestBody Foo\8217 can be used with \8216jsonRequestBody @Foo\8217 middleware." :$$: Text ""
+ WebGear.Types: ErrorResponse :: Response ByteString -> RouteError
+ WebGear.Types: PathInfo :: [Text] -> PathInfo
+ WebGear.Types: RouteMismatch :: RouteError
+ WebGear.Types: Router :: StateT PathInfo (ExceptT RouteError IO) a -> Router a
+ WebGear.Types: [responseBody] :: Response a -> Maybe a
+ WebGear.Types: [responseHeaders] :: Response a -> HashMap HeaderName ByteString
+ WebGear.Types: [responseStatus] :: Response a -> Status
+ WebGear.Types: [unRouter] :: Router a -> StateT PathInfo (ExceptT RouteError IO) a
+ WebGear.Types: accepted202 :: a -> Response a
+ WebGear.Types: badGateway502 :: a -> Response a
+ WebGear.Types: badRequest400 :: a -> Response a
+ WebGear.Types: catchErrorResponse :: MonadRouter m => m a -> (Response ByteString -> m a) -> m a
+ WebGear.Types: class (MonadState PathInfo m, Alternative m, MonadPlus m) => MonadRouter m
+ WebGear.Types: conflict409 :: a -> Response a
+ WebGear.Types: continue100 :: Response a
+ WebGear.Types: created201 :: a -> Response a
+ WebGear.Types: data RouteError
+ WebGear.Types: errorResponse :: MonadRouter m => Response ByteString -> m a
+ WebGear.Types: expectationFailed417 :: a -> Response a
+ WebGear.Types: forbidden403 :: a -> Response a
+ WebGear.Types: found302 :: a -> Response a
+ WebGear.Types: gatewayTimeout504 :: a -> Response a
+ WebGear.Types: gone410 :: a -> Response a
+ WebGear.Types: httpVersionNotSupported505 :: a -> Response a
+ WebGear.Types: imATeapot418 :: a -> Response a
+ WebGear.Types: instance Control.Monad.Error.Class.MonadError WebGear.Types.RouteError WebGear.Types.Router
+ WebGear.Types: instance Control.Monad.IO.Class.MonadIO WebGear.Types.Router
+ WebGear.Types: instance Control.Monad.State.Class.MonadState WebGear.Types.PathInfo WebGear.Types.Router
+ WebGear.Types: instance GHC.Base.Alternative WebGear.Types.Router
+ WebGear.Types: instance GHC.Base.Applicative WebGear.Types.Router
+ WebGear.Types: instance GHC.Base.Functor WebGear.Types.Response
+ WebGear.Types: instance GHC.Base.Functor WebGear.Types.Router
+ WebGear.Types: instance GHC.Base.Monad WebGear.Types.Router
+ WebGear.Types: instance GHC.Base.MonadPlus WebGear.Types.Router
+ WebGear.Types: instance GHC.Base.Monoid WebGear.Types.RouteError
+ WebGear.Types: instance GHC.Base.Semigroup WebGear.Types.RouteError
+ WebGear.Types: instance GHC.Classes.Eq WebGear.Types.RouteError
+ WebGear.Types: instance GHC.Classes.Eq a => GHC.Classes.Eq (WebGear.Types.Response a)
+ WebGear.Types: instance GHC.Classes.Ord WebGear.Types.RouteError
+ WebGear.Types: instance GHC.Classes.Ord a => GHC.Classes.Ord (WebGear.Types.Response a)
+ WebGear.Types: instance GHC.Show.Show WebGear.Types.RouteError
+ WebGear.Types: instance GHC.Show.Show a => GHC.Show.Show (WebGear.Types.Response a)
+ WebGear.Types: instance WebGear.Types.MonadRouter WebGear.Types.Router
+ WebGear.Types: internalServerError500 :: a -> Response a
+ WebGear.Types: lengthRequired411 :: a -> Response a
+ WebGear.Types: methodNotAllowed405 :: a -> Response a
+ WebGear.Types: movedPermanently301 :: a -> Response a
+ WebGear.Types: multipleChoices300 :: a -> Response a
+ WebGear.Types: networkAuthenticationRequired511 :: a -> Response a
+ WebGear.Types: newtype PathInfo
+ WebGear.Types: newtype Router a
+ WebGear.Types: noContent204 :: Response a
+ WebGear.Types: nonAuthoritative203 :: a -> Response a
+ WebGear.Types: notAcceptable406 :: a -> Response a
+ WebGear.Types: notFound404 :: Response a
+ WebGear.Types: notImplemented501 :: a -> Response a
+ WebGear.Types: notModified304 :: Response a
+ WebGear.Types: ok200 :: a -> Response a
+ WebGear.Types: partialContent206 :: a -> Response a
+ WebGear.Types: paymentRequired402 :: a -> Response a
+ WebGear.Types: permanentRedirect308 :: a -> Response a
+ WebGear.Types: preconditionFailed412 :: a -> Response a
+ WebGear.Types: preconditionRequired428 :: a -> Response a
+ WebGear.Types: proxyAuthenticationRequired407 :: a -> Response a
+ WebGear.Types: rejectRoute :: MonadRouter m => m a
+ WebGear.Types: requestEntityTooLarge413 :: a -> Response a
+ WebGear.Types: requestHeaderFieldsTooLarge431 :: a -> Response a
+ WebGear.Types: requestTimeout408 :: a -> Response a
+ WebGear.Types: requestURITooLong414 :: a -> Response a
+ WebGear.Types: requestedRangeNotSatisfiable416 :: a -> Response a
+ WebGear.Types: resetContent205 :: Response a
+ WebGear.Types: respond :: Status -> Maybe a -> Response a
+ WebGear.Types: responseHeader :: HeaderName -> Response a -> Maybe ByteString
+ WebGear.Types: runRoute :: ToByteString a => Handler '[] a -> Request -> IO Response
+ WebGear.Types: seeOther303 :: a -> Response a
+ WebGear.Types: serviceUnavailable503 :: a -> Response a
+ WebGear.Types: setResponseHeader :: HeaderName -> ByteString -> Response a -> Response a
+ WebGear.Types: switchingProtocols101 :: Response a
+ WebGear.Types: temporaryRedirect307 :: a -> Response a
+ WebGear.Types: toApplication :: ToByteString a => Handler '[] a -> Application
+ WebGear.Types: tooManyRequests429 :: a -> Response a
+ WebGear.Types: transform :: (forall x. m x -> n x) -> Handler' m req a -> Handler' n req a
+ WebGear.Types: type Handler' m req a = Kleisli m (Linked req Request) (Response a)
+ WebGear.Types: type Middleware' m req req' a' a = Handler' m req' a' -> Handler' m req a
+ WebGear.Types: type RequestMiddleware' m req req' a = Middleware' m req req' a a
+ WebGear.Types: type ResponseMiddleware' m req a' a = Middleware' m req req a' a
+ WebGear.Types: unauthorized401 :: a -> Response a
+ WebGear.Types: unprocessableEntity422 :: a -> Response a
+ WebGear.Types: unsupportedMediaType415 :: a -> Response a
- WebGear.Middlewares.Body: jsonRequestBody :: forall t m req res a. (FromJSON t, MonadRouter m, MonadIO m) => RequestMiddleware m req (JSONRequestBody t : req) res a
+ WebGear.Middlewares.Body: jsonRequestBody :: forall t m req a. (FromJSON t, MonadRouter m, MonadIO m) => RequestMiddleware' m req (JSONRequestBody t : req) a
- WebGear.Middlewares.Body: jsonResponseBody :: (ToJSON t, Monad m) => Middleware m req req res '[] t ByteString
+ WebGear.Middlewares.Body: jsonResponseBody :: (ToJSON t, Monad m) => ResponseMiddleware' m req t ByteString
- WebGear.Middlewares.Method: method :: forall t m req res a. (IsStdMethod t, MonadRouter m) => RequestMiddleware m req (Method t : req) res a
+ WebGear.Middlewares.Method: method :: forall t m req a. (IsStdMethod t, MonadRouter m) => RequestMiddleware' m req (Method t : req) a
- WebGear.Middlewares.Path: path :: forall s ts res m a. (KnownSymbol s, MonadRouter m) => RequestMiddleware m ts (Path s : ts) res a
+ WebGear.Middlewares.Path: path :: forall s ts m a. (KnownSymbol s, MonadRouter m) => RequestMiddleware' m ts (Path s : ts) a
- WebGear.Middlewares.Path: pathVar :: forall tag val ts res m a. (FromHttpApiData val, MonadRouter m) => RequestMiddleware m ts (PathVar tag val : ts) res a
+ WebGear.Middlewares.Path: pathVar :: forall tag val ts m a. (FromHttpApiData val, MonadRouter m) => RequestMiddleware' m ts (PathVar tag val : ts) a
- WebGear.Trait: -- | Type of check failures
+ WebGear.Trait: -- | Type that indicates that the trait does not exist for a value. This
- WebGear.Types: type Handler m req res a = Kleisli m (Linked req Request) (Linked res (Response a))
+ WebGear.Types: type Handler req a = Handler' Router req a
- WebGear.Types: type Middleware m req req' res' res a' a = Handler m req' res' a' -> Handler m req res a
+ WebGear.Types: type Middleware req req' a' a = Middleware' Router req req' a' a
- WebGear.Types: type RequestMiddleware m req req' res a = Middleware m req req' res res a a
+ WebGear.Types: type RequestMiddleware req req' a = RequestMiddleware' Router req req' a
- WebGear.Types: type ResponseMiddleware m req res' res a = Middleware m req req res' res a a
+ WebGear.Types: type ResponseMiddleware req a' a = ResponseMiddleware' Router req a' a

Files

ChangeLog.md view
@@ -2,13 +2,24 @@  ## Unreleased changes -## [0.1.0] - 2020-08-16+## [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 -[0.1.0]: https://github.com/rkaippully/webgear/compare/0.0.0...0.1.0-[Unreleased]: https://github.com/rkaippully/webgear/compare/0.1.0...HEAD+[Unreleased]: https://github.com/rkaippully/webgear/compare/v0.2.0...HEAD+[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
src/WebGear.hs view
@@ -1,4 +1,5 @@ {-# OPTIONS_GHC -Wno-unused-imports #-}+{-# OPTIONS_HADDOCK ignore-exports #-} -- | -- Copyright        : (c) Raghu Kaippully, 2020 -- License          : MPL-2.0@@ -28,21 +29,34 @@      -- * 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.Applicative (Alternative ((<|>))) import Control.Arrow (Kleisli (..))-import Web.HttpApiData (FromHttpApiData)+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.Route import WebGear.Trait-import WebGear.Trait.Body-import WebGear.Trait.Header-import WebGear.Trait.Method-import WebGear.Trait.Path import WebGear.Types  @@ -53,10 +67,14 @@ -- takes a request as input and produces a response as output in a -- monadic context. ----- @--- handler :: Monad m => 'Request' -> m 'Response'--- @+-- > 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@@ -67,33 +85,37 @@ -- $traits -- -- A trait is an attribute associated with a value. For example, a--- @Request@ might have a header that we are interested in; the--- 'Header' trait represents that. All traits have instances of--- 'Trait' typeclass. This typeclass helps to 'check' the presence of--- the trait. It also has two associated types - 'Val' and 'Fail' - to--- represent the result of 'check'ing the presence of a trait.+-- @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 'check' function evaluates to a 'CheckSuccess' value--- if the header exists and can be converted to an attribute via the--- 'FromHttpApiData' typeclass. Otherwise, it evaluates to a--- 'CheckFail' value.+-- 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 or--- response at type level. The 'Linked' data type associates a--- 'Request' or 'Response' with a list of traits. This linking--- guarantees that the Request or Response has the specified trait.+-- 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: -----   * 'linkzero': Establish a link between a value and an empty list---     of traits. This always succeeds.---   * 'linkplus': Attempts to establish a link between a linked value---     with an additional trait.---   * 'linkminus': Removes a trait from the list of linked traits.+--   * '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.---   * 'traitValue': Extract a 'Val' associated with a trait from a+--+--   * '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@@ -101,21 +123,19 @@ -- request value with this trait using: -- -- @--- linkedRequest :: Monad m => 'Request' -> m (Either 'MethodMismatch' ('Linked' '['Method' GET] 'Request'))--- linkedRequest = 'linkplus' @('Method' GET) . 'linkzero'+-- 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 :: Monad m => Linked req Request -> m (Linked res Response)+-- > handler :: Linked req Request -> Router Response -- -- Here, @req@ is a type-level list of traits associated with the--- @Request@ that this handler requires and @res@ is a type-level list--- of traits associated with the @Response@ that this handler will--- produce. This implies that this handler can be called only with a--- request possessing certain traits and it is guaranteed to produce a--- response having certain traits.+-- @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@@ -124,7 +144,9 @@ -- above. -- -- @--- type 'Handler' m req res a = 'Kleisli' m ('Linked' req 'Request') ('Linked' res ('Response' a))+-- 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@@ -132,36 +154,37 @@ -- 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--- 'traitValue' function. It can also use 'linkplus' function to prove--- the presence of traits in the response before returning it.+-- 'get' function. -- -- -- $middlewares -- -- A middleware is a higher-order function that takes a handler as--- input and produces another handler with potentially different lists--- of request and response traits. Thus middlewares can augment the+-- 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) res a -> 'Handler' m req res a--- method handler = 'Kleisli' $ 'linkplus' \@('Method' t) >=> 'either' ('const' 'rejectRoute') ('runKleisli' handler)+-- 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 @linkplus \@(Method t)@ function is used to prove the presence--- of the method @t@ in the request and the @handler@ is invoked only--- if the method matches. In case of a mismatch, this route is--- rejected by calling 'rejectRoute'.+-- 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.+-- logic. For example: -- -- @ -- putUser = 'method' \@PUT---           $ 'requestContentType' \@"application/json"+--           $ 'requestContentTypeHeader' \@"application/json" --           $ 'jsonRequestBody' \@User --           $ 'jsonResponseBody' \@User --           $ putUserHandler@@ -181,14 +204,15 @@ -- @ -- class (Alternative m, MonadPlus m) => 'MonadRouter' m where --   'rejectRoute' :: m a---   'failHandler' :: 'Response' ByteString -> m a+--   'errorResponse' :: 'Response' 'ByteString' -> m a+--   'catchErrorResponse' :: m a -> ('Response' 'ByteString' -> m a) -> m a -- @ ----- The 'failHandler' can be used in cases where we find a matching+-- 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, 'failHandler' can be used to abort and return an+-- 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@@ -199,45 +223,68 @@ -- example: -- -- @--- allRoutes :: 'MonadRouter' m => 'Handler' m '[] '[] ByteString--- allRoutes = ['match'| v1\/users\/userId:Int |]    -- non-TH version: 'path' \@"v1/users" . 'pathVar' \@"userId" \@Int+-- 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 :: ('MonadRouter' m, 'Has' IntUserId req) => 'Handler' m req '[] ByteString+-- getUser :: 'Has' IntUserId req => 'Handler' req 'ByteString' -- getUser = 'method' \@GET getUserHandler ----- putUser :: ('MonadRouter' m, 'Has' IntUserId req) => 'Handler' m req '[] ByteString+-- putUser :: 'Has' IntUserId req => 'Handler' req 'ByteString' -- putUser = 'method' \@PUT---           $ 'requestContentType' \@"application/json"+--           $ 'requestContentTypeHeader' \@"application/json" --           $ 'jsonRequestBody' \@User --           $ putUserHandler ----- deleteUser :: ('MonadRouter' m, 'Has' IntUserId req) => 'Handler' m req '[] ByteString+-- deleteUser :: 'Has' IntUserId req => 'Handler' req 'ByteString' -- deleteUser = 'method' \@DELETE deleteUserHandler -- @ -- -- -- $running ----- Routable handlers can be converted to a regular function using--- 'runRoute':+-- Routable handlers can be converted to a Wai 'Wai.Application' using+-- 'toApplication': -- -- @--- runRoute :: Monad m => 'Handler' (RouterT m) '[] res ByteString -> ('Wai.Request' -> m 'Wai.Response')+-- toApplication :: 'ToByteString' a => 'Handler' '[] a -> 'Wai.Application' -- @ ----- This function converts a WebGear handler to a function from--- 'Wai.Request' to 'Wai.Response' in a monadic context @m@. Then it--- is trivial to convert that to a WAI 'Wai.Application' and run it as a--- warp server:+-- This Wai application can then be run as a Warp web server. -- -- @--- application :: 'Wai.Application'--- application req respond = 'runRoute' allRoutes req >>= respond--- -- main :: IO ()--- main = Warp.run 3000 application+-- 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 view
@@ -6,41 +6,17 @@ -- Middlewares provided by WebGear. -- module WebGear.Middlewares-  ( ok-  , noContent-  , badRequest-  , notFound--  , module WebGear.Middlewares.Method+  ( 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 Data.String (IsString)-+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-import WebGear.Trait (Linked, linkzero)-import WebGear.Types (Response (..))--import qualified Network.HTTP.Types as HTTP----- | Respond with a 200 OK-ok :: Monad m => a -> m (Linked '[] (Response a))-ok = pure . linkzero . Response HTTP.ok200 mempty . Just---- | Respond with a 400 Bad Request-badRequest :: Monad m => m (Linked '[] (Response a))-badRequest = pure $ linkzero $ Response HTTP.badRequest400 mempty Nothing---- | Respond with a 404 NotFound-notFound :: Monad m => m (Linked '[] (Response a))-notFound = pure $ linkzero $ Response HTTP.notFound404 mempty Nothing---- | Respond with a 204 NoContent-noContent :: (Monad m, IsString s) => m (Linked '[] (Response s))-noContent = pure $ linkzero $ Response HTTP.noContent204 mempty $ Just ""
+ src/WebGear/Middlewares/Auth/Basic.hs view
@@ -0,0 +1,120 @@+-- |+-- 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 view
@@ -5,26 +5,40 @@ -- -- Middlewares related to HTTP body. module WebGear.Middlewares.Body-  ( jsonRequestBody+  ( JSONRequestBody+  , jsonRequestBody   , jsonResponseBody   ) where  import Control.Arrow (Kleisli (..)) import Control.Monad ((>=>))-import Control.Monad.IO.Class (MonadIO)-import Data.Aeson (FromJSON, ToJSON, encode)-import Data.ByteString.Lazy (ByteString, fromStrict)-import Data.HashMap.Strict (fromList, insert)-import Data.Text (Text)+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 (badRequest400, hContentType)+import Network.HTTP.Types (hContentType) -import WebGear.Route (MonadRouter (..))-import WebGear.Trait (linkplus, linkzero, unlink)-import WebGear.Trait.Body (JSONRequestBody)-import WebGear.Types (Middleware, RequestMiddleware, Response (..))+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. --@@ -32,17 +46,14 @@ -- -- > jsonRequestBody @t handler ---jsonRequestBody :: forall t m req res a. (FromJSON t, MonadRouter m, MonadIO m)-                => RequestMiddleware m req (JSONRequestBody t:req) res a+-- 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 $-  linkplus @(JSONRequestBody t) >=> either (failHandler . mkError) (runKleisli handler)+  probe @(JSONRequestBody t) >=> either (errorResponse . mkError) (runKleisli handler)   where     mkError :: Text -> Response ByteString-    mkError e = Response-          { respStatus  = badRequest400-          , respHeaders = fromList []-          , respBody    = Just $ fromStrict $ encodeUtf8 $ "Error parsing request body: " <> e-          }+    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.@@ -54,11 +65,7 @@ -- -- > jsonResponseBody @t handler ---jsonResponseBody :: (ToJSON t, Monad m) => Middleware m req req res '[] t ByteString+jsonResponseBody :: (ToJSON t, Monad m) => ResponseMiddleware' m req t ByteString jsonResponseBody handler = Kleisli $ \req -> do-  x <- unlink <$> runKleisli handler req-  pure $ linkzero $ Response-    { respStatus  = respStatus x-    , respHeaders = insert hContentType "application/json" $ respHeaders x-    , respBody    = encode <$> respBody x-    }+  x <- runKleisli handler req+  pure $ setResponseHeader hContentType "application/json" $ encode <$> x
src/WebGear/Middlewares/Header.hs view
@@ -4,44 +4,353 @@ -- Maintainer       : rkaippully@gmail.com -- -- Middlewares related to HTTP headers.+-- module WebGear.Middlewares.Header-  ( requestContentType+  ( -- * 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.Lazy (ByteString)-import Data.HashMap.Strict (fromList)+import Data.ByteString (ByteString)+import Data.Kind (Type)+import Data.Proxy (Proxy (..)) import Data.String (fromString)-import GHC.TypeLits (KnownSymbol)-import Network.HTTP.Types (badRequest400)+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.Route (MonadRouter (..))-import WebGear.Trait (linkplus)-import WebGear.Trait.Header (HeaderMatch, HeaderMismatch (..))-import WebGear.Types (RequestMiddleware, Response (..))+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. ----- Typical usage:+-- Example usage: ----- > requestContentType @"application/json" handler+-- > requestContentTypeHeader @"application/json" handler ---requestContentType :: forall c m req res a. (KnownSymbol c, MonadRouter m)-                   => RequestMiddleware m req (HeaderMatch "Content-Type" c:req) res a-requestContentType handler = Kleisli $-  linkplus @(HeaderMatch "Content-Type" c) >=> either (failHandler . mkError) (runKleisli handler)-  where-    mkError :: HeaderMismatch -> Response ByteString-    mkError err = Response-                  { respStatus  = badRequest400-                  , respHeaders = fromList []-                  , respBody    = Just $ fromString $-                    case (expectedHeader err, actualHeader err) of-                      (ex, Nothing) -> printf "Expected Content-Type header %s but not found" (show ex)-                      (ex, Just h)  -> printf "Expected Content-Type header %s but found %s" (show ex) (show h)-                  }+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 view
@@ -5,18 +5,71 @@ -- -- Middlewares related to HTTP methods. module WebGear.Middlewares.Method-  ( method+  ( Method+  , IsStdMethod (..)+  , MethodMismatch (..)+  , method   ) where  import Control.Arrow (Kleisli (..)) import Control.Monad ((>=>))+import Data.Proxy (Proxy (..)) -import WebGear.Route (MonadRouter (..))-import WebGear.Trait (linkplus)-import WebGear.Trait.Method (IsStdMethod, Method)-import WebGear.Types (RequestMiddleware)+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. --@@ -27,6 +80,6 @@ -- 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 res a. (IsStdMethod t, MonadRouter m)-       => RequestMiddleware m req (Method t:req) res a-method handler = Kleisli $ linkplus @(Method t) >=> either (const rejectRoute) (runKleisli handler)+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 view
@@ -0,0 +1,186 @@+-- |+-- 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 view
@@ -5,30 +5,103 @@ -- -- Middlewares related to route paths. module WebGear.Middlewares.Path-  ( 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 (..), toList)-import GHC.TypeLits (KnownSymbol)+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 Web.HttpApiData (FromHttpApiData)+import Prelude hiding (drop, filter, take)+import Web.HttpApiData (FromHttpApiData (..))  import WebGear.Middlewares.Method (method)-import WebGear.Route (MonadRouter (..))-import WebGear.Trait (linkplus)-import WebGear.Trait.Path (Path, PathVar)-import WebGear.Types (RequestMiddleware)+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@@ -40,10 +113,10 @@ -- -- > path @"a/b/c" handler ---path :: forall s ts res m a. (KnownSymbol s, MonadRouter m)-     => RequestMiddleware m ts (Path s:ts) res a+path :: forall s ts m a. (KnownSymbol s, MonadRouter m)+     => RequestMiddleware' m ts (Path s:ts) a path handler = Kleisli $-  linkplus @(Path s) >=> either (const rejectRoute) (runKleisli handler)+  probe @(Path s) >=> either (const rejectRoute) (runKleisli handler)  -- | A middleware that captures a path variable from a single path -- component.@@ -58,52 +131,101 @@ -- -- > pathVar @"objId" @Int handler ---pathVar :: forall tag val ts res m a. (FromHttpApiData val, MonadRouter m)-        => RequestMiddleware m ts (PathVar tag val:ts) res a+pathVar :: forall tag val ts m a. (FromHttpApiData val, MonadRouter m)+        => RequestMiddleware' m ts (PathVar tag val:ts) a pathVar handler = Kleisli $-  linkplus @(PathVar tag val) >=> either (const rejectRoute) (runKleisli handler)+  probe @(PathVar tag val) >=> either (const rejectRoute) (runKleisli handler) --- | Produces middleware(s) to match an optional HTTP method and path.+-- | 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: ----- * @[match|a\/b\/c]@ is equivalent to @'path' \@\"a\/b\/c\"@--- * @[match|a\/b\/objId:Int\/d]@ is equivalent to---   @'path' \@\"a\/b\" . 'pathVar' \@\"objId\" \@Int . 'path' @\"d\"@--- * @[match|GET a\/b\/c]@ is equivalent to---   @'method' \@GET $ 'path' \@\"a\/b\/c\"@--- * @[match|GET a\/b\/objId:Int\/d]@ is equivalent to---   @'method' \@GET . 'path' \@\"a\/b\" . 'pathVar' \@\"objId\" \@Int . 'path' \@\"d\"@+-- +---------------------------------------+---------------------------------------------------------------------------------------++-- | 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  = toExp+  { 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"   }-  where-    toExp :: String -> Q Exp-    toExp 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 "match expects an HTTP method and a path or just a path" +-- | 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 s []                    = [s]-    joinPath (s:|[]) ((s':|[]) : xs) = ((s <> "/" <> s') :| []) : xs+    joinPath p []                    = [p]+    joinPath (p:|[]) ((p':|[]) : xs) = ((p <> "/" <> p') :| []) : xs     joinPath y (x:xs)                = y:x:xs      toPathExp :: NonEmpty String -> Q Exp@@ -111,5 +233,5 @@     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 ".")+compose :: Exp -> Exp -> Exp+compose l = UInfixE l (VarE $ mkName ".")
+ src/WebGear/Modifiers.hs view
@@ -0,0 +1,18 @@+-- |+-- 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/Route.hs
@@ -1,81 +0,0 @@--- |--- Copyright        : (c) Raghu Kaippully, 2020--- License          : MPL-2.0--- Maintainer       : rkaippully@gmail.com------ Types and functions to route HTTP requests.-module WebGear.Route-  ( RouterT-  , MonadRouter (..)-  , runRoute-  ) where--import Control.Applicative (Alternative)-import Control.Arrow (Kleisli (..))-import Control.Monad (MonadPlus (..))-import Control.Monad.Except (ExceptT, MonadError (..), runExceptT)-import Data.ByteString.Lazy (ByteString)-import Data.HashMap.Strict (fromList)-import Data.Semigroup (First (..))-import Data.String (fromString)-import Data.Version (showVersion)-import Network.HTTP.Types (Header, hServer, notFound404)--import Paths_webgear_server (version)-import WebGear.Trait (linkzero, unlink)-import WebGear.Types (Handler, Response (..), waiResponse)--import qualified Network.Wai as Wai----- | The monad transformer stack for routing.------ * The 'ExceptT' provides short-circuiting behaviour for---   'rejectRoute' and 'failHandler'.------ * In case of 'rejectRoute', a 'Nothing' value is returned and in---   case of 'failHandler', a @Response ByteString@ is returned.------ * The 'First' wrapper is provided to get instances of 'Alternative'---   and 'MonadPlus' for 'RouterT'.----type RouterT m = ExceptT (Maybe (First (Response ByteString))) m---- | HTTP request routing with short circuiting behavior.-class (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-  failHandler :: Response ByteString -> m a--instance Monad m => MonadRouter (RouterT m) where-  rejectRoute :: RouterT m a-  rejectRoute = mzero--  failHandler :: Response ByteString -> RouterT m a-  failHandler = throwError . Just . First---- | Convert a routable handler into a plain function.------ This function is typically used to convert WebGear routes to a--- 'Wai.Application'.-runRoute :: Monad m-         => Handler (RouterT m) '[] res ByteString-         -> (Wai.Request -> m Wai.Response)-runRoute route req = waiResponse . addServerHeader . either (maybe notFoundResponse getFirst) id <$> runExceptT f-  where-    f = unlink <$> runKleisli route (linkzero req)--    notFoundResponse :: Response ByteString-    notFoundResponse = Response-      { respStatus  = notFound404-      , respHeaders = fromList []-      , respBody    = Just "Not Found"-      }--    addServerHeader :: Response ByteString -> Response ByteString-    addServerHeader r = r { respHeaders = respHeaders r <> fromList [serverHeader] }--    serverHeader :: Header-    serverHeader = (hServer, fromString $ "WebGear/" ++ showVersion version)
src/WebGear/Trait.hs view
@@ -4,133 +4,152 @@ -- License          : MPL-2.0 -- Maintainer       : rkaippully@gmail.com ----- Traits are optional attributes that a value might posess. For example,--- a list containing totally ordered values might have a @Maximum@ trait--- where the associated attribute is the maximum value. The trait exists--- only if the list is non-empty.+-- 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 access these attributes in a type-safe manner.+-- 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.+-- arbitrary attributes to be associated with a value instead of only+-- a predicate.+-- module WebGear.Trait   ( -- * Core Types     Trait (..)-  , CheckResult (..)+  , Result (..)   , Linked-  , Traits -    -- * Linking values with traits-  , linkzero-  , linkplus-  , linkminus+    -- * 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.Tagged (Tagged (..))+import Data.Proxy (Proxy (..))+import GHC.TypeLits (ErrorMessage (..), TypeError)  --- | A 'Trait' is an optional attribute @t@ associated with a value @a@.------ The 'check' function validates the presence of the trait for a--- given value. Checking the presence of the trait can optionally--- modify the value as well.+-- | 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-  type Val t a+  -- | Type of the associated attribute when the trait holds for a+  -- value+  type Attribute t a :: Type -  -- | Type of check failures-  type Fail t a+  -- | 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 -  -- | Checks the presence of the associated attribute.-  check :: a -> m (CheckResult t a)+  -- | 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) --- | Result of a 'check' operation-data CheckResult t a = CheckSuccess a (Val t a)-                     | CheckFail (Fail t a) -deriving instance (Eq a, Eq (Val t a), Eq (Fail t a)) => Eq (CheckResult t a)-deriving instance (Show a, Show (Val t a), Show (Fail t a)) => Show (CheckResult t a)-deriving instance (Read a, Read (Val t a), Read (Fail t a)) => Read (CheckResult 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 trait that is always present and whose attribute does--- not carry any meaningful information.+-- | 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 Val '[] a = ()-  type Fail '[] a = ()+  type Attribute '[] a = ()+  type Absence '[] a = () -  check :: a -> m (CheckResult '[] a)-  check a = pure $ CheckSuccess a ()+  toAttribute :: a -> m (Result '[] a)+  toAttribute = const $ pure $ Found () --- | Combination of many traits all of which are present for a value.-instance (Trait t a m, Trait ts a m) => Trait (t:ts) a m where-  type Val (t:ts) a = (Val t a, Val ts a)-  type Fail (t:ts) a = Either (CheckResult t a) (CheckResult ts a)+-- | 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) -  check :: a -> m (CheckResult (t:ts) a)-  check a = check @t a >>= \case-    e@(CheckFail _)   -> pure $ CheckFail $ Left e-    CheckSuccess a' l -> check @ts a' >>= \case-      e@(CheckFail _)    -> pure $ CheckFail $ Right e-      CheckSuccess a'' r -> pure $ CheckSuccess a'' (l, r)+  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) --- | Constraint for functions that use multiple traits-type family Traits ts a m :: Constraint where-  Traits '[]    a m = ()-  Traits (t:ts) a m = (Trait t a m, Traits ts a m)  -- | A value linked with a type-level list of traits. data Linked (ts :: [Type]) a = Linked-    { linkVal :: !(Val ts a)-    , unlink  :: !a           -- ^ Retrive the value from a linked value+    { linkAttribute :: !(Attribute ts a)+    , unlink        :: !a                 -- ^ Retrive the value from a linked value     } --- | Link a value with the trivial trait-linkzero :: a -> Linked '[] a-linkzero = Linked ()+-- | 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-linkplus :: Trait t a m => Linked ts a -> m (Either (Fail t a) (Linked (t:ts) a))-linkplus l = do-  v <- check (unlink l)+-- 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 :: CheckResult t a -> Linked ts a -> Either (Fail t a) (Linked (t:ts) a)-    mkLinked (CheckSuccess a left) lv = Right $ Linked (left, linkVal lv) a-    mkLinked (CheckFail e) _          = Left e+    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 linked value-linkminus :: Linked (t:ts) a -> Linked ts a-linkminus l = Linked (snd $ linkVal l) (unlink l)+-- | 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 somewhere in--- the list of traits @ts@.+-- | Constraint that proves that the trait @t@ is present in the list+-- of traits @ts@. class Has t ts where-  traitValue :: Linked ts a -> Tagged t (Val t a)+  -- | 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-  traitValue :: Linked (t : ts) a -> Tagged t (Val t a)-  traitValue (Linked (lv, _) _) = Tagged lv+  get :: Proxy t -> Linked (t:ts) a -> Attribute t a+  get _ (Linked (lv, _) _) = lv  instance {-# OVERLAPPABLE #-} Has t ts => Has t (t':ts) where-  traitValue :: Linked (t':ts) a -> Tagged t (Val t a)-  traitValue l = traitValue (rightLinked l)+  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--- present in the list @qs@.+-- 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/Trait/Body.hs
@@ -1,34 +0,0 @@--- |--- Copyright        : (c) Raghu Kaippully, 2020--- License          : MPL-2.0--- Maintainer       : rkaippully@gmail.com------ Traits related to HTTP body.-module WebGear.Trait.Body-  ( JSONRequestBody-  ) where--import Control.Monad.IO.Class (MonadIO (..))-import Data.Aeson (FromJSON, eitherDecode')-import Data.ByteString.Lazy (fromChunks)-import Data.Kind (Type)-import Data.Text (Text, pack)--import WebGear.Trait (CheckResult (..), Trait (..))-import WebGear.Types (Request, getRequestBodyChunk)-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 Val (JSONRequestBody t) Request = t-  type Fail (JSONRequestBody t) Request = Text--  check :: Request -> m (CheckResult (JSONRequestBody t) Request)-  check r = do-    chunks <- takeWhileM (/= mempty) $ repeat $ liftIO $ getRequestBodyChunk r-    pure $ case eitherDecode' (fromChunks chunks) of-             Left e  -> CheckFail (pack e)-             Right t -> CheckSuccess r t
− src/WebGear/Trait/Header.hs
@@ -1,71 +0,0 @@--- |--- Copyright        : (c) Raghu Kaippully, 2020--- License          : MPL-2.0--- Maintainer       : rkaippully@gmail.com------ Traits related to HTTP headers.-module WebGear.Trait.Header-  ( Header-  , HeaderFail (..)-  , HeaderMatch-  , HeaderMismatch (..)-  ) where--import Data.ByteString (ByteString)-import Data.Kind (Type)-import Data.Proxy (Proxy (..))-import Data.String (fromString)-import Data.Text (Text)-import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)-import Web.HttpApiData (FromHttpApiData (..))--import WebGear.Trait (CheckResult (..), Trait (..))-import WebGear.Types (Request, requestHeader)----- | A 'Trait' for capturing a header with name @s@ in a request or--- response and convert it to some type @t@ via 'FromHttpApiData'.-data Header (s :: Symbol) (t :: Type)---- | Failure in extracting a header value-data HeaderFail = HeaderNotFound | HeaderParseError Text-  deriving stock (Read, Show, Eq)--instance (KnownSymbol s, FromHttpApiData t, Monad m) => Trait (Header s t) Request m where-  type Val (Header s t) Request = t-  type Fail (Header s t) Request = HeaderFail--  check :: Request -> m (CheckResult (Header s t) Request)-  check r = pure $-    let s = fromString $ symbolVal (Proxy @s)-    in case parseHeader <$> requestHeader s r of-         Nothing        -> CheckFail HeaderNotFound-         Just (Left e)  -> CheckFail $ HeaderParseError e-         Just (Right x) -> CheckSuccess r x---- | A 'Trait' for ensuring that a header named @s@ has value @t@.-data HeaderMatch (s :: Symbol) (t :: Symbol)---- | Failure in extracting a header value-data HeaderMismatch = HeaderMismatch-  { expectedHeader :: ByteString-  , actualHeader   :: Maybe ByteString-  }-  deriving stock (Eq, Read, Show)--instance (KnownSymbol s, KnownSymbol t, Monad m) => Trait (HeaderMatch s t) Request m where-  type Val (HeaderMatch s t) Request = ByteString-  type Fail (HeaderMatch s t) Request = HeaderMismatch--  check :: Request -> m (CheckResult (HeaderMatch s t) Request)-  check r = pure $-    let-      name = fromString $ symbolVal (Proxy @s)-      expected = fromString $ symbolVal (Proxy @t)-    in-      case requestHeader name r of-        Nothing                  -> CheckFail HeaderMismatch-                                      {expectedHeader = expected, actualHeader = Nothing}-        Just hv | hv == expected -> CheckSuccess r hv-                | otherwise      -> CheckFail HeaderMismatch-                                      {expectedHeader = expected, actualHeader = Just hv}
− src/WebGear/Trait/Method.hs
@@ -1,68 +0,0 @@--- |--- Copyright        : (c) Raghu Kaippully, 2020--- License          : MPL-2.0--- Maintainer       : rkaippully@gmail.com------ Trait capturing the HTTP method in a request.-module WebGear.Trait.Method-  ( Method-  , IsStdMethod (..)-  , MethodMismatch (..)-  ) where--import Data.Proxy (Proxy (..))--import WebGear.Trait (CheckResult (..), Trait (..))-import WebGear.Types (Request, 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 (Monad m, IsStdMethod t) => Trait (Method t) Request m where-  type Val (Method t) Request = HTTP.Method-  type Fail (Method t) Request = MethodMismatch--  check :: Request -> m (CheckResult (Method t) Request)-  check r =-    let-      expected = HTTP.renderStdMethod $ toStdMethod $ Proxy @t-      actual = requestMethod r-    in-      pure $ if expected == actual-             then CheckSuccess r actual-             else CheckFail $ MethodMismatch expected actual----- | A typeclass implemented by all 'HTTP.StdMethod's to convert them--- 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
− src/WebGear/Trait/Path.hs
@@ -1,66 +0,0 @@--- |--- Copyright        : (c) Raghu Kaippully, 2020--- License          : MPL-2.0--- Maintainer       : rkaippully@gmail.com------ Traits related to the route path of a request.-module WebGear.Trait.Path-  ( Path-  , PathVar-  , PathVarFail (..)-  ) where--import Data.Kind (Type)-import Data.List (stripPrefix)-import Data.List.NonEmpty (toList)-import Data.Proxy (Proxy (..))-import Data.Text (Text, pack)-import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)-import Web.HttpApiData (FromHttpApiData (..))--import WebGear.Trait (CheckResult (..), Trait (..))-import WebGear.Types (Request, pathInfo, setPathInfo)-import WebGear.Util (splitOn)----- | A path component which is literally matched against the request--- but discarded after that.-data Path (s :: Symbol)--instance (KnownSymbol s, Monad m) => Trait (Path s) Request m where-  type Val (Path s) Request = ()--  -- | The path that could not be matched-  type Fail (Path s) Request = ()--  check :: Request -> m (CheckResult (Path s) Request)-  check r = pure $-    let expected = map pack $ toList $ splitOn '/' $ symbolVal $ Proxy @s-        actual = pathInfo r-    in-      case stripPrefix expected actual of-        Nothing   -> CheckFail ()-        Just rest -> CheckSuccess (setPathInfo rest r) ()----- | 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 :: Type)---- | Failure to extract a 'PathVar'-data PathVarFail = PathVarNotFound | PathVarParseError Text-  deriving (Eq, Show, Read)--instance (FromHttpApiData val, Monad m) => Trait (PathVar tag val) Request m where-  type Val (PathVar tag val) Request = val-  type Fail (PathVar tag val) Request = PathVarFail--  check :: Request -> m (CheckResult (PathVar tag val) Request)-  check r = pure $-    case pathInfo r of-      []     -> CheckFail PathVarNotFound-      (x:xs) ->-        case parseUrlPiece @val x of-          Left e  -> CheckFail $ PathVarParseError e-          Right h -> CheckSuccess (setPathInfo xs r) h
src/WebGear/Types.hs view
@@ -4,87 +4,352 @@ -- Maintainer       : rkaippully@gmail.com -- -- Common types and functions used throughout WebGear.+-- module WebGear.Types   ( -- * WebGear Request-    -- | WebGear requests are WAI requests. This module reexports a number-    -- of useful functions that operate on requests from "Network.Wai"-    -- module.     Request   , remoteHost   , httpVersion   , isSecure   , requestMethod   , pathInfo-  , setPathInfo   , queryString-  , requestHeaders   , requestHeader+  , requestHeaders   , requestBodyLength   , getRequestBodyChunk      -- * WebGear Response   , Response (..)+  , responseHeader+  , setResponseHeader   , waiResponse-  , addResponseHeader +    -- * 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.Arrow (Kleisli)+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 Network.HTTP.Types (Header, HeaderName, Status)+import Data.Version (showVersion)+import GHC.Exts (fromList) import Network.Wai (Request, getRequestBodyChunk, httpVersion, isSecure, pathInfo, queryString,                     remoteHost, requestBodyLength, requestHeaders, requestMethod) -import WebGear.Trait (Linked)+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 :: HeaderName -> Request -> Maybe ByteString+requestHeader :: HTTP.HeaderName -> Request -> Maybe ByteString requestHeader h r = snd <$> find ((== h) . fst) (requestHeaders r) --- | Get request with an updated URL path info.-setPathInfo :: [Text] -> Request -> Request-setPathInfo p r = r { pathInfo = p }---- | A response sent from the server to the client.+-- | 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-    { respStatus  :: Status                            -- ^ Response status code-    , respHeaders :: HM.HashMap HeaderName ByteString  -- ^ Response headers-    , respBody    :: Maybe a                           -- ^ Optional response body+    { 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 respStatus (HM.toList respHeaders) (fromMaybe "" respBody)+waiResponse Response{..} = Wai.responseLBS+  responseStatus+  (HM.toList responseHeaders)+  (fromMaybe "" responseBody) --- | Create or update a response header.-addResponseHeader :: Header -> Response a -> Response a-addResponseHeader (name, val) resp = resp { respHeaders = HM.insertWith f name val (respHeaders resp) }-  where-    f = flip const +-- | 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. The handler will produce a response that--- satisfies all the traits in the type level list @res@.-type Handler m req res a = Kleisli m (Linked req Request) (Linked res (Response a))+-- 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. --@@ -92,16 +357,114 @@ -- handling such as: -- --   * Change the request traits before invoking the handler.---   * Change the response traits before passing it back to its caller.---   * Use the linked value of any of the request or response traits.+--   * Use the linked value of any of the request traits. --   * Change the response body. ---type Middleware m req req' res' res a' a = Handler m req' res' a' -> Handler m req res a+type Middleware' m req req' a' a = Handler' m req' a' -> Handler' m req a --- | A middleware that manipulates only the request traits and leaves--- the response unchanged.-type RequestMiddleware m req req' res a = Middleware m req req' res res a 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 response traits and leaves--- the request unchanged.-type ResponseMiddleware m req res' res a = Middleware m req req res' res 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 view
@@ -7,6 +7,7 @@ module WebGear.Util   ( takeWhileM   , splitOn+  , maybeToRight   ) where  import Data.List.NonEmpty (NonEmpty (..), toList)@@ -25,3 +26,7 @@   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/Properties.hs view
@@ -9,15 +9,19 @@  import Test.Tasty (TestTree, testGroup) +import qualified Properties.Trait.Auth.Basic as Basic 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   propertyTests :: TestTree-propertyTests = testGroup "Property Tests" [ Body.tests-                                           , Header.tests-                                           , Method.tests+propertyTests = testGroup "Property Tests" [ Method.tests                                            , Path.tests+                                           , Header.tests+                                           , Params.tests+                                           , Body.tests+                                           , Basic.tests                                            ]
+ test/Properties/Trait/Auth/Basic.hs view
@@ -0,0 +1,43 @@+module Properties.Trait.Auth.Basic+  ( tests+  ) where++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 Test.QuickCheck.Instances ()+import Test.Tasty (TestTree)+import Test.Tasty.QuickCheck (testProperties)++import WebGear.Middlewares.Auth.Basic+import WebGear.Trait+import WebGear.Types+++prop_basicAuth :: Property+prop_basicAuth = property f+  where+    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)+++-- Hack for TH splicing+return []++tests :: TestTree+tests = testProperties "Trait.Auth.Basic" $allProperties
test/Properties/Trait/Body.hs view
@@ -7,15 +7,16 @@ import Control.Monad.IO.Class (MonadIO, liftIO) import Data.IORef (newIORef, readIORef, writeIORef) import Data.String (fromString)-import Network.Wai (Request, defaultRequest, requestBody)+import Network.Wai (defaultRequest, requestBody) import Test.QuickCheck (Property, allProperties, counterexample, property) import Test.QuickCheck.Instances () 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.Trait.Body+import WebGear.Types   bodyToRequest :: (MonadIO m, Show a) => a -> m Request@@ -27,23 +28,23 @@ prop_emptyRequestBodyFails :: Property prop_emptyRequestBodyFails = monadicIO $ do   req <- bodyToRequest ("" :: String)-  check @(JSONRequestBody Int) req >>= \case-    CheckSuccess _ _ -> monitor (counterexample "Unexpected success") >> assert False-    CheckFail _      -> assert True+  toAttribute @(JSONRequestBody Int) req >>= \case+    Found _    -> monitor (counterexample "Unexpected success") >> assert False+    NotFound _ -> assert True  prop_validBodyParses :: Property prop_validBodyParses = property $ \n -> monadicIO $ do   req <- bodyToRequest (n :: Integer)-  check @(JSONRequestBody Integer) req >>= \case-    CheckSuccess _ n' -> assert (n == n')-    CheckFail _       -> assert False+  toAttribute @(JSONRequestBody Integer) req >>= \case+    Found n'   -> assert (n == n')+    NotFound _ -> assert False  prop_invalidBodyFails :: Property prop_invalidBodyFails = property $ \n -> monadicIO $ do   req <- bodyToRequest (n :: Integer)-  check @(JSONRequestBody String) req >>= \case-    CheckSuccess _ _ -> assert False-    CheckFail _      -> assert True+  toAttribute @(JSONRequestBody String) req >>= \case+    Found _    -> assert False+    NotFound _ -> assert True   -- Hack for TH splicing
test/Properties/Trait/Header.hs view
@@ -5,14 +5,15 @@ import Data.Functor.Identity (runIdentity) import Data.String (fromString) import Data.Text.Encoding (encodeUtf8)-import Network.Wai (defaultRequest, requestHeaders)+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.Header import WebGear.Trait-import WebGear.Trait.Header+import WebGear.Types   prop_headerParseError :: Property@@ -21,31 +22,33 @@     hval' = "test-" <> hval     req = defaultRequest { requestHeaders = [("foo", encodeUtf8 hval')] }   in-    case runIdentity (check @(Header "foo" Int) req) of-      CheckFail e      ->-        e === HeaderParseError ("could not parse: `" <> hval' <> "' (input does not start with a digit)")-      CheckSuccess _ v ->+    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)")  prop_headerParseSuccess :: Property prop_headerParseSuccess = property $ \(n :: Int) ->   let     req = defaultRequest { requestHeaders = [("foo", fromString $ show n)] }   in-    case runIdentity (check @(Header "foo" Int) req) of-      CheckFail e       ->+    case runIdentity (toAttribute @(Header "foo" Int) req) of+      Found n'   -> n === n'+      NotFound e ->         counterexample ("Unexpected result: " <> show e) (property False)-      CheckSuccess _ n' -> n === n'  prop_headerMatch :: Property prop_headerMatch = property $ \v ->   let     req = defaultRequest { requestHeaders = [("foo", v)] }   in-    case runIdentity (check @(HeaderMatch "foo" "bar") req) of-      CheckFail e       ->-        expectedHeader e === "bar" .&&. actualHeader e =/= Nothing .&&. actualHeader e =/= Just "bar"-      CheckSuccess _ v' -> v === "bar" .&&. v === v'+    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"   -- Hack for TH splicing
test/Properties/Trait/Method.hs view
@@ -4,14 +4,15 @@  import Data.Functor.Identity (runIdentity) import Network.HTTP.Types (StdMethod (..), methodGet, renderStdMethod)-import Network.Wai (defaultRequest, requestMethod)+import Network.Wai (defaultRequest) 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.Trait.Method+import WebGear.Types   newtype MethodWrapper = MethodWrapper StdMethod@@ -25,9 +26,9 @@   let     req = defaultRequest { requestMethod = renderStdMethod v }   in-    case runIdentity (check @(Method GET) req) of-      CheckSuccess _ v' -> v === GET .&&. v' === methodGet-      CheckFail e       ->+    case runIdentity (toAttribute @(Method GET) req) of+      Found _    -> v === GET+      NotFound e ->         expectedMethod e === methodGet .&&. actualMethod e =/= methodGet  
+ test/Properties/Trait/Params.hs view
@@ -0,0 +1,46 @@+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
@@ -2,16 +2,17 @@   ( tests   ) where -import Data.Functor.Identity (runIdentity)+import Control.Monad.State.Strict (evalState) import Data.String (fromString)-import Network.Wai (defaultRequest, pathInfo)-import Test.QuickCheck (Property, allProperties, property, (.&&.), (=/=), (===))+import Network.Wai (defaultRequest)+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.Trait.Path+import WebGear.Types   prop_pathMatch :: Property@@ -20,9 +21,9 @@     rest = ["foo", "bar"]     req = defaultRequest { pathInfo = h:rest }   in-    case runIdentity (check @(Path "a") req) of-      CheckSuccess req' _ -> h === "a" .&&. pathInfo req' === rest-      CheckFail _         -> h =/= "a"+    case evalState (toAttribute @(Path "a") req) (PathInfo $ h:rest) of+      Found _    -> h === "a"+      NotFound _ -> h =/= "a"  prop_pathVarMatch :: Property prop_pathVarMatch = property $ \(n :: Int) ->@@ -30,9 +31,9 @@     rest = ["foo", "bar"]     req = defaultRequest { pathInfo = fromString (show n):rest }   in-    case runIdentity (check @(PathVar "tag" Int) req) of-      CheckSuccess req' n' -> n' === n .&&. pathInfo req' === rest-      CheckFail _          -> property False+    case evalState (toAttribute @(PathVar "tag" Int) req) (PathInfo $ pathInfo req) of+      Found n'   -> n' === n+      NotFound _ -> property False  prop_pathVarParseError :: Property prop_pathVarParseError = property $ \(p, ps) ->@@ -40,9 +41,9 @@     p' = "test-" <> p     req = defaultRequest { pathInfo = p':ps }   in-    case runIdentity (check @(PathVar "tag" Int) req) of-      CheckSuccess _ _ -> property False-      CheckFail e      -> e === PathVarParseError ("could not parse: `" <> p' <> "' (input does not start with a digit)")+    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)")   -- Hack for TH splicing
test/Unit/Trait/Header.hs view
@@ -7,36 +7,35 @@   ( tests   ) where -import Network.Wai (defaultRequest, requestHeaders)+import Network.Wai (defaultRequest) import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (assertFailure, testCase, (@?=)) +import WebGear.Middlewares.Header import WebGear.Trait-import WebGear.Trait.Header+import WebGear.Types   testMissingHeaderFails :: TestTree testMissingHeaderFails = testCase "Missing header fails Header trait" $ do   let req = defaultRequest { requestHeaders = [] }-  check @(Header "foo" Int) req >>= \case-    CheckSuccess _ _ -> assertFailure "unexpected success"-    CheckFail e      -> e @?= HeaderNotFound+  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")] }-  check @(HeaderMatch "foo" "bar") req >>= \case-    CheckSuccess _ v -> v @?= "bar"-    CheckFail e      -> assertFailure $ "Unexpected result: " <> show e+  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 = [] }-  check @(HeaderMatch "foo" "bar") req >>= \case-    CheckSuccess _ _ -> assertFailure "unexpected success"-    CheckFail e      -> e @?= HeaderMismatch { expectedHeader = "bar"-                                             , actualHeader = Nothing-                                             }+  toAttribute @(HeaderMatch "foo" "bar") req >>= \case+    Found _      -> assertFailure "unexpected success"+    NotFound e -> e @?= Nothing  tests :: TestTree tests = testGroup "Trait.Header" [ testMissingHeaderFails
test/Unit/Trait/Path.hs view
@@ -7,20 +7,22 @@   ( tests   ) where -import Network.Wai (defaultRequest, pathInfo)+import Control.Monad.State (evalState)+import Network.Wai (defaultRequest) import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (assertFailure, testCase, (@?=)) +import WebGear.Middlewares.Path import WebGear.Trait-import WebGear.Trait.Path+import WebGear.Types   testMissingPathVar :: TestTree testMissingPathVar = testCase "PathVar match: missing variable" $ do   let req = defaultRequest { pathInfo = [] }-  check @(PathVar "tag" Int) req >>= \case-    CheckSuccess _ _ -> assertFailure "unexpected success"-    CheckFail e      -> e @?= PathVarNotFound+  case evalState (toAttribute @(PathVar "tag" Int) req) (PathInfo []) of+    Found _    -> assertFailure "unexpected success"+    NotFound e -> e @?= PathVarNotFound  tests :: TestTree tests = testGroup "Trait.Path" [ testMissingPathVar ]
webgear-server.cabal view
@@ -1,6 +1,6 @@ cabal-version:       2.4 name:                webgear-server-version:             0.1.0+version:             0.2.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.@@ -28,78 +28,61 @@  common webgear-common   default-language:   Haskell2010-  default-extensions: ApplicativeDo-                      BangPatterns-                      ConstraintKinds-                      DataKinds-                      DefaultSignatures-                      DeriveAnyClass-                      DeriveFoldable+  default-extensions: DataKinds                       DeriveFunctor-                      DeriveGeneric-                      DeriveLift-                      DeriveTraversable                       DerivingStrategies-                      DerivingVia-                      EmptyCase-                      ExistentialQuantification                       FlexibleContexts                       FlexibleInstances-                      FunctionalDependencies-                      GADTs                       GeneralizedNewtypeDeriving                       InstanceSigs                       KindSignatures                       LambdaCase                       MultiParamTypeClasses-                      MultiWayIf-                      NamedFieldPuns                       OverloadedStrings-                      PatternSynonyms                       PolyKinds                       RankNTypes                       RecordWildCards                       ScopedTypeVariables-                      StandaloneDeriving-                      TemplateHaskell-                      TupleSections+                      TemplateHaskellQuotes                       TypeApplications                       TypeFamilies-                      TypeFamilyDependencies                       TypeOperators-  build-depends:      base                  >=4.13.0.0 && <5-                    , template-haskell      >=2.15.0.0 && <3+  build-depends:      base                  >=4.12.0.0 && <5+                    , template-haskell      >=2.14.0.0 && <3                     , mtl                   ==2.2.*                     , unordered-containers  ==0.2.*                     , wai                   ==3.2.*                     , bytestring            ==0.10.*                     , text                  ==1.2.*+                    , case-insensitive      ==1.2.*+                    , base64-bytestring     >=1.0.0.3 && <1.3+                    , bytestring-conversion ==0.3.*                     , http-types            ==0.12.*                     , http-api-data         ==0.4.*-                    , aeson                 ==1.4.*-                    , tagged                ==0.8.*-  ghc-options:        -fwrite-ide-info-                      -hiedir=.hie-                      -Wall+                    , aeson                 >=1.4 && <1.6+                    , network               >=2.8 && <3.2+  ghc-options:        -Wall                       -Wno-unticked-promoted-constructors                       -Wincomplete-record-updates                       -Wincomplete-uni-patterns                       -Wredundant-constraints +  if impl(ghc >= 8.8.1)+    ghc-options:      -fwrite-ide-info+                      -hiedir=.hie+                       library   import:             webgear-common   exposed-modules:    WebGear+                    , WebGear.Modifiers                     , WebGear.Trait-                    , WebGear.Trait.Method-                    , WebGear.Trait.Header-                    , WebGear.Trait.Path-                    , WebGear.Trait.Body                     , WebGear.Types-                    , WebGear.Route                     , WebGear.Middlewares+                    , WebGear.Middlewares.Auth.Basic                     , WebGear.Middlewares.Body                     , WebGear.Middlewares.Header                     , WebGear.Middlewares.Method+                    , WebGear.Middlewares.Params                     , WebGear.Middlewares.Path   other-modules:      Paths_webgear_server                     , WebGear.Util@@ -116,15 +99,18 @@                     , Properties                     , Properties.Trait.Body                     , Properties.Trait.Header+                    , Properties.Trait.Params                     , Properties.Trait.Method                     , Properties.Trait.Path+                    , Properties.Trait.Auth.Basic   hs-source-dirs:     test+  default-extensions: TemplateHaskell   ghc-options:        -threaded                       -rtsopts                       -with-rtsopts=-N-  build-depends:      tasty                 ==1.2.*+  build-depends:      tasty                 >=1.2 && <1.4                     , tasty-quickcheck      ==0.10.*                     , tasty-hunit           ==0.10.*-                    , QuickCheck            ==2.13.*+                    , QuickCheck            >=2.13 && <2.15                     , quickcheck-instances  ==0.3.*                     , webgear-server