Matching a request
Linnet provides plenty of functions & endpoints for request matching to
combine together with help of (//)
and (|+|)
operators.
Methods matching
Following functions are available for matching against request HTTP method.
In case of method mismatch, resulting endpoint always returns NotMatched
:
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeSynonymInstances #-}
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import Data.Text
import Data.List.NonEmpty
import Linnet
import Linnet.Endpoint (root)
import Network.Wai
getEndpoint :: Endpoint IO Request
getEndpoint = get(root)
postEndpoint :: Endpoint IO Request
postEndpoint = post(root)
putEndpoint :: Endpoint IO Request
putEndpoint = put(root)
patchEndpoint :: Endpoint IO Request
patchEndpoint = patch(root)
deleteEndpoint :: Endpoint IO Request
deleteEndpoint = delete(root)
-- etc
Check out Methods
module API for more functions
Paths
Matching of request's path is vital part of routing logic. Usually, path endpoints are combined
together using product (//)
operator:
Constant segment
pathConst
or p'
alias allows to match a single segment of request against predefined constant:
boolConstEndpoint :: Endpoint IO Bool
boolConstEndpoint = get(p' "foo" // p' "bar") ~>> (return $ ok True)
Mind that {-# LANGUAGE OverloadedStrings #-}
extension should be enabled.
Variable segment
path
endpoint decodes a single segment of request path that is represented as value of resulting endpoint:
pathVariableEndpoint :: Endpoint IO Int
pathVariableEndpoint = (* 2) <$> get(path @Int)
Here {-# LANGUAGE TypeApplications #-}
pragma adds the support of @Type
syntax to explicitly declare expected type.
Consume all segments
Endpoint paths
consumes the reminder of input's path and always matches the request as far as types match:
pathsEndpoint :: Endpoint IO [Int]
pathsEndpoint = (fmap (* 2)) <$> get(paths @Int)
Again, TypeApplications
should be enabled.
Empty path
pathEmpty
matches iff the reminder path is empty. Could be handy to match root requests with empty path:
rootRequest :: Endpoint IO ()
rootRequest = get(pathEmpty) ~>> (return $ ok ())
Params
Linnet provides set of endpoints for extracting query parameters from the request. As their type is still Endpoint m a
,
it's usual to combine them together with other endpoints using sequential combination of (//)
.
Notice
All the endpoints for extracting query parameters below always match the request but might raise an error in context ofm
monad, therefore requireMonadThrow
constraint.
Single required parameter
param
endpoint extracts and decode value from request query part:
paramEndpoint :: Endpoint IO Int
paramEndpoint = get(param @Int "paramName")
Again, {-# LANGUAGE OverloadedStrings #-}
extension should be enabled.
In case if parameter is missing or malformed, LinnetError
is be thrown in context of m
.
Single optional parameter
paramMaybeEndpoint :: Endpoint IO (Maybe Int)
paramMaybeEndpoint = get(paramMaybe @Int "paramName")
In case if parameter is missing, Nothing
is returned. Throws an exception on malformed value.
List of values
params
endpoint retrieves list of values of repeating parameters:
paramsEndpoint :: Endpoint IO [Int]
paramsEndpoint = get(params @Int "paramName")
In case if parameter is missing, empty list is returned. Throws an exception on malformed value.
Non-empty list of values
paramsNel
is similar to params
endpoint but throws an exception in case of missing parameter:
paramsNelEndpoint :: Endpoint IO (NonEmpty Int)
paramsNelEndpoint = get(paramsNel @Int "paramName")
Headers
Just as in case of params, headers are composable with the rest of the endpoints using operators (//)
or (|+|)
.
In Linnet, everything is Endpoint
.
Notice
All the endpoints for extracting headers below always match the request but might raise an error in context ofm
monad, therefore requireMonadThrow
constraint.
Required header extraction
header
allows to extract and decode value of specific request header:
headerEndpoint :: Endpoint IO Text
headerEndpoint = get(header @Text "My-Header")
{-# LANGUAGE OverloadedStrings #-}
extension should be enabled.
In case if header is missing or malformed, LinnetError
is be thrown in context of m
.
Optional header extraction
headerMaybeEndpoint :: Endpoint IO (Maybe Text)
headerMaybeEndpoint = get(headerMaybe @Text "My-Header")
In case if header is missing, Nothing
is returned. Throws an exception on malformed value.
Bodies
Body endpoints take a special treatment in Linnet ecosystem. There are multiple key features:
- All
body*
endpoints haveDecode ct a
constraint that is used to decode request body of Content-Typect
into the specific value of typea
- All
body*
endpoints require non-chunked request with predeterminedContent-Length
size, otherwise they might not match the request
-- Example of Decode instance for text/plain Content-Type and strict ByteString type
instance Decode TextPlain BS.ByteString where
decode = Right . BL.toStrict
Decoding request body
bodyEndpoint :: Endpoint IO BS.ByteString
bodyEndpoint = post(body @TextPlain @BS.ByteString)
- In case if
Content-Length
of request is being chunked or missing, endpoint isn't matched. - In case if
Content-Length
is 0,LinnetError
is raised. - In case if
Decode
can't decode a malformed body of a request,LinnetError
is raised.
Decoding optional body
bodyMaybeEndpoint :: Endpoint IO (Maybe BS.ByteString)
bodyMaybeEndpoint = post(bodyMaybe @TextPlain @BS.ByteString)
- In case if
Content-Length
of request is being chunked or missing, endpoint isn't matched. - In case if
Content-Length
is 0,Nothing
is returned - In case if
Decode
can't decode a malformed body of a request,LinnetError
is raised.
Linnet also exposes couple of useful aliases to avoid type application for Content-Type
every time:
jsonBody @a == body @ApplicationJson @a
jsonBodyMaybe @a == bodyMaybe @ApplicationJson @a
textBody @a == body @TextPlain @a
textBodyMaybe @a == bodyMaybe @TextPlain @a
Cookies
In case of cookies, similar to headers*
endpoints are available:
Extract cookie
cookieEndpoint :: Endpoint IO Int
cookieEndpoint = get(cookie @Int "cookieName")
Extract optional cookie
cookieMaybeEndpoint :: Endpoint IO (Maybe Int)
cookieMaybeEndpoint = get(cookieMaybe @Int "cookieName")
Mind that cookieName
is case-sensitive.