-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2025 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module Testlib.HTTP where

import qualified Control.Exception as E
import Control.Monad.Reader
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Aeson
import Data.Bifunctor (Bifunctor (bimap))
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as C8
import qualified Data.ByteString.Lazy as L
import qualified Data.CaseInsensitive as CI
import Data.Function
import Data.List
import Data.List.Split (splitOn)
import qualified Data.Map as Map
import Data.Maybe
import Data.String
import Data.String.Conversions (cs)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Tuple.Extra
import GHC.Generics
import GHC.Stack
import qualified Network.HTTP.Client as HTTP
import Network.HTTP.Types (hContentLength, hLocation)
import qualified Network.HTTP.Types as HTTP
import Network.HTTP.Types.URI (parseQuery)
import Network.URI (URI (..), URIAuth (..), parseURI)
import Testlib.Assertions
import Testlib.Env
import Testlib.JSON
import Testlib.Types
import Web.Cookie
import Prelude

splitHttpPath :: String -> [String]
splitHttpPath :: String -> [String]
splitHttpPath String
path = (String -> Bool) -> [String] -> [String]
forall a. (a -> Bool) -> [a] -> [a]
filter (Bool -> Bool
not (Bool -> Bool) -> (String -> Bool) -> String -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null) (String -> String -> [String]
forall a. Eq a => [a] -> [a] -> [[a]]
splitOn String
"/" String
path)

joinHttpPath :: [String] -> String
joinHttpPath :: [String] -> String
joinHttpPath = String -> [String] -> String
forall a. [a] -> [[a]] -> [a]
intercalate String
"/"

addJSONObject :: [Aeson.Pair] -> HTTP.Request -> HTTP.Request
addJSONObject :: [Pair] -> Request -> Request
addJSONObject = Value -> Request -> Request
forall a. ToJSON a => a -> Request -> Request
addJSON (Value -> Request -> Request)
-> ([Pair] -> Value) -> [Pair] -> Request -> Request
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Pair] -> Value
Aeson.object

addJSON :: (Aeson.ToJSON a) => a -> HTTP.Request -> HTTP.Request
addJSON :: forall a. ToJSON a => a -> Request -> Request
addJSON a
obj = RequestBody -> String -> Request -> Request
addBody (ByteString -> RequestBody
HTTP.RequestBodyLBS (a -> ByteString
forall a. ToJSON a => a -> ByteString
Aeson.encode a
obj)) String
"application/json"

addXML :: ByteString -> HTTP.Request -> HTTP.Request
addXML :: ByteString -> Request -> Request
addXML ByteString
xml = RequestBody -> String -> Request -> Request
addBody (ByteString -> RequestBody
HTTP.RequestBodyBS ByteString
xml) String
"application/xml"

addClientIP :: HTTP.Request -> HTTP.Request
addClientIP :: Request -> Request
addClientIP = String -> String -> Request -> Request
addHeader String
"X-Forwarded-For" String
"127.0.0.42"

addUrlEncodedForm :: [(String, String)] -> HTTP.Request -> HTTP.Request
addUrlEncodedForm :: [(String, String)] -> Request -> Request
addUrlEncodedForm [(String, String)]
form Request
req =
  Request
req
    { HTTP.requestBody = HTTP.RequestBodyLBS (L.fromStrict (HTTP.renderSimpleQuery False (both C8.pack <$> form))),
      HTTP.requestHeaders =
        (fromString "Content-Type", fromString "application/x-www-form-urlencoded")
          : HTTP.requestHeaders req
    }

addBody :: HTTP.RequestBody -> String -> HTTP.Request -> HTTP.Request
addBody :: RequestBody -> String -> Request -> Request
addBody RequestBody
body String
contentType Request
req =
  Request
req
    { HTTP.requestBody = body,
      HTTP.requestHeaders =
        (fromString "Content-Type", fromString contentType)
          : HTTP.requestHeaders req
    }

addMLS :: ByteString -> HTTP.Request -> HTTP.Request
addMLS :: ByteString -> Request -> Request
addMLS ByteString
bytes Request
req =
  Request
req
    { HTTP.requestBody = HTTP.RequestBodyBS bytes,
      HTTP.requestHeaders =
        (fromString "Content-Type", fromString "message/mls")
          : HTTP.requestHeaders req
    }

addProtobuf :: ByteString -> HTTP.Request -> HTTP.Request
addProtobuf :: ByteString -> Request -> Request
addProtobuf ByteString
bytes Request
req =
  Request
req
    { HTTP.requestBody = HTTP.RequestBodyBS bytes,
      HTTP.requestHeaders = (fromString "Content-Type", fromString "application/x-protobuf") : HTTP.requestHeaders req
    }

addHeader :: String -> String -> HTTP.Request -> HTTP.Request
addHeader :: String -> String -> Request -> Request
addHeader String
name String
value Request
req =
  Request
req {HTTP.requestHeaders = (CI.mk . C8.pack $ name, C8.pack value) : HTTP.requestHeaders req}

setCookie :: String -> HTTP.Request -> HTTP.Request
setCookie :: String -> Request -> Request
setCookie String
c Request
r =
  String -> String -> Request -> Request
addHeader String
"Cookie" (String -> String
forall a b. ConvertibleStrings a b => a -> b
cs String
c) Request
r

getCookie :: String -> Response -> Maybe String
getCookie :: String -> Response -> Maybe String
getCookie String
name Response
resp = do
  cookieHeader <- HeaderName -> [(HeaderName, ByteString)] -> Maybe ByteString
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup (ByteString -> HeaderName
forall s. FoldCase s => s -> CI s
CI.mk (ByteString -> HeaderName) -> ByteString -> HeaderName
forall a b. (a -> b) -> a -> b
$ String -> ByteString
forall a b. ConvertibleStrings a b => a -> b
cs String
"set-cookie") Response
resp.headers
  cs <$> lookup (cs name) (parseCookies cookieHeader)

addQueryParams :: [(String, String)] -> HTTP.Request -> HTTP.Request
addQueryParams :: [(String, String)] -> Request -> Request
addQueryParams [(String, String)]
params Request
req =
  [(ByteString, Maybe ByteString)] -> Request -> Request
HTTP.setQueryString (((String, String) -> (ByteString, Maybe ByteString))
-> [(String, String)] -> [(ByteString, Maybe ByteString)]
forall a b. (a -> b) -> [a] -> [b]
map (\(String
k, String
v) -> (String -> ByteString
forall a b. ConvertibleStrings a b => a -> b
cs String
k, ByteString -> Maybe ByteString
forall a. a -> Maybe a
Just (String -> ByteString
forall a b. ConvertibleStrings a b => a -> b
cs String
v))) [(String, String)]
params) Request
req

contentTypeMixed :: HTTP.Request -> HTTP.Request
contentTypeMixed :: Request -> Request
contentTypeMixed = String -> String -> Request -> Request
addHeader String
"Content-Type" String
"multipart/mixed"

bindResponse :: (HasCallStack) => App Response -> (Response -> App a) -> App a
bindResponse :: forall a.
HasCallStack =>
App Response -> (Response -> App a) -> App a
bindResponse App Response
m Response -> App a
k = App Response
m App Response -> (Response -> App a) -> App a
forall a b. App a -> (a -> App b) -> App b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \Response
r -> Response -> (Response -> App a) -> App a
forall a. HasCallStack => Response -> (Response -> App a) -> App a
withResponse Response
r Response -> App a
k

infixl 1 `bindResponse`

withResponse :: (HasCallStack) => Response -> (Response -> App a) -> App a
withResponse :: forall a. HasCallStack => Response -> (Response -> App a) -> App a
withResponse Response
r Response -> App a
k = Response -> App a -> App a
forall a. HasCallStack => Response -> App a -> App a
onFailureAddResponse Response
r (Response -> App a
k Response
r)

-- | Check response status code, then return body.
getBody :: (HasCallStack) => Int -> Response -> App ByteString
getBody :: HasCallStack => Int -> Response -> App ByteString
getBody Int
status = (Response -> (Response -> App ByteString) -> App ByteString)
-> (Response -> App ByteString) -> Response -> App ByteString
forall a b c. (a -> b -> c) -> b -> a -> c
flip Response -> (Response -> App ByteString) -> App ByteString
forall a. HasCallStack => Response -> (Response -> App a) -> App a
withResponse \Response
resp -> do
  Response
resp.status Int -> Int -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` Int
status
  ByteString -> App ByteString
forall a. a -> App a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Response
resp.body

-- | Check response status code, then return JSON body.
getJSON :: (HasCallStack) => Int -> Response -> App Aeson.Value
getJSON :: HasCallStack => Int -> Response -> App Value
getJSON Int
status = (Response -> (Response -> App Value) -> App Value)
-> (Response -> App Value) -> Response -> App Value
forall a b c. (a -> b -> c) -> b -> a -> c
flip Response -> (Response -> App Value) -> App Value
forall a. HasCallStack => Response -> (Response -> App a) -> App a
withResponse \Response
resp -> do
  Response
resp.status Int -> Int -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` Int
status
  Response
resp.json

-- | assert a response code in the 2** range
assertSuccess :: (HasCallStack) => Response -> App ()
assertSuccess :: HasCallStack => Response -> App ()
assertSuccess = (Response -> (Response -> App ()) -> App ())
-> (Response -> App ()) -> Response -> App ()
forall a b c. (a -> b -> c) -> b -> a -> c
flip Response -> (Response -> App ()) -> App ()
forall a. HasCallStack => Response -> (Response -> App a) -> App a
withResponse \Response
resp -> Response
resp.status Int -> (Int, Int) -> App ()
forall a. (MakesValue a, HasCallStack) => a -> (Int, Int) -> App ()
`shouldMatchRange` (Int
200, Int
299)

-- | assert a response status code
assertStatus :: (HasCallStack) => Int -> Response -> App ()
assertStatus :: HasCallStack => Int -> Response -> App ()
assertStatus Int
status = (Response -> (Response -> App ()) -> App ())
-> (Response -> App ()) -> Response -> App ()
forall a b c. (a -> b -> c) -> b -> a -> c
flip Response -> (Response -> App ()) -> App ()
forall a. HasCallStack => Response -> (Response -> App a) -> App a
withResponse \Response
resp -> Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
status

-- | assert a failure with some failure code and label
assertLabel :: (HasCallStack) => Int -> String -> Response -> App ()
assertLabel :: HasCallStack => Int -> String -> Response -> App ()
assertLabel Int
status String
label Response
resp = do
  j <- HasCallStack => Int -> Response -> App Value
Int -> Response -> App Value
getJSON Int
status Response
resp
  j %. "label" `shouldMatch` label

onFailureAddResponse :: (HasCallStack) => Response -> App a -> App a
onFailureAddResponse :: forall a. HasCallStack => Response -> App a -> App a
onFailureAddResponse Response
r App a
m = ReaderT Env IO a -> App a
forall a. ReaderT Env IO a -> App a
App (ReaderT Env IO a -> App a) -> ReaderT Env IO a -> App a
forall a b. (a -> b) -> a -> b
$ do
  e <- ReaderT Env IO Env
forall r (m :: * -> *). MonadReader r m => m r
ask
  liftIO $ E.catch (runAppWithEnv e m) $ \(AssertionFailure CallStack
stack Maybe Response
_ Maybe String
ctx String
msg) -> do
    AssertionFailure -> IO a
forall a e. (HasCallStack, Exception e) => e -> a
E.throw (CallStack
-> Maybe Response -> Maybe String -> String -> AssertionFailure
AssertionFailure CallStack
stack (Response -> Maybe Response
forall a. a -> Maybe a
Just Response
r) Maybe String
ctx String
msg)

data Versioned = Versioned | Unversioned | ExplicitVersion Int
  deriving stock (Versioned -> Versioned -> Bool
(Versioned -> Versioned -> Bool)
-> (Versioned -> Versioned -> Bool) -> Eq Versioned
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Versioned -> Versioned -> Bool
== :: Versioned -> Versioned -> Bool
$c/= :: Versioned -> Versioned -> Bool
/= :: Versioned -> Versioned -> Bool
Eq, (forall x. Versioned -> Rep Versioned x)
-> (forall x. Rep Versioned x -> Versioned) -> Generic Versioned
forall x. Rep Versioned x -> Versioned
forall x. Versioned -> Rep Versioned x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. Versioned -> Rep Versioned x
from :: forall x. Versioned -> Rep Versioned x
$cto :: forall x. Rep Versioned x -> Versioned
to :: forall x. Rep Versioned x -> Versioned
Generic)

-- | If you don't know what domain is for or what you should put in there, try `rawBaseRequest
-- OwnDomain ...`.
rawBaseRequest :: (HasCallStack, MakesValue domain) => domain -> Service -> Versioned -> String -> App HTTP.Request
rawBaseRequest :: forall domain.
(HasCallStack, MakesValue domain) =>
domain -> Service -> Versioned -> String -> App Request
rawBaseRequest domain
domain Service
service Versioned
versioned String
path = do
  domainV <- domain -> App String
forall a. (HasCallStack, MakesValue a) => a -> App String
objDomain domain
domain

  pathSegsPrefix <- case versioned of
    Versioned
Versioned -> do
      v <- String -> App Int
forall domain. MakesValue domain => domain -> App Int
getAPIVersionFor String
domainV
      pure ["v" <> show v]
    Versioned
Unversioned -> [String] -> App [String]
forall a. a -> App a
forall (f :: * -> *) a. Applicative f => a -> f a
pure []
    ExplicitVersion Int
v -> do
      [String] -> App [String]
forall a. a -> App a
forall (f :: * -> *) a. Applicative f => a -> f a
pure [String
"v" String -> String -> String
forall a. Semigroup a => a -> a -> a
<> Int -> String
forall a. Show a => a -> String
show Int
v]

  serviceMap <- getServiceMap domainV

  liftIO . HTTP.parseRequest $
    let HostPort h p = serviceHostPort serviceMap service
     in "http://" <> h <> ":" <> show p <> ("/" <> joinHttpPath (pathSegsPrefix <> splitHttpPath path))

-- | The bare minimum to ge a `HTTP.Request` given a URL
externalRequest :: String -> App HTTP.Request
externalRequest :: String -> App Request
externalRequest = IO Request -> App Request
forall a. IO a -> App a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO Request -> App Request)
-> (String -> IO Request) -> String -> App Request
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> IO Request
forall (m :: * -> *). MonadThrow m => String -> m Request
HTTP.parseRequest

getAPIVersionFor :: (MakesValue domain) => domain -> App Int
getAPIVersionFor :: forall domain. MakesValue domain => domain -> App Int
getAPIVersionFor domain
domain = do
  d <- domain -> App String
forall a. (HasCallStack, MakesValue a) => a -> App String
asString domain
domain
  versionMap <- asks (.apiVersionByDomain)
  case Map.lookup d versionMap of
    Maybe Int
Nothing -> (Env -> Int) -> App Int
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks (.defaultAPIVersion)
    Just Int
v -> Int -> App Int
forall a. a -> App a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Int
v

baseRequest :: (HasCallStack, MakesValue user) => user -> Service -> Versioned -> String -> App HTTP.Request
baseRequest :: forall domain.
(HasCallStack, MakesValue domain) =>
domain -> Service -> Versioned -> String -> App Request
baseRequest user
user Service
service Versioned
versioned String
path = do
  req <- user -> Service -> Versioned -> String -> App Request
forall domain.
(HasCallStack, MakesValue domain) =>
domain -> Service -> Versioned -> String -> App Request
rawBaseRequest user
user Service
service Versioned
versioned String
path
  uid <- objId user
  cli <-
    make user >>= \case
      Aeson.Object Object
_ -> do
        c <- user -> String -> App (Maybe Value)
forall a.
(HasCallStack, MakesValue a) =>
a -> String -> App (Maybe Value)
lookupField user
user String
"client_id"
        traverse asString c
      Value
_ -> Maybe String -> App (Maybe String)
forall a. a -> App a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Maybe String
forall a. Maybe a
Nothing
  pure $ req & zUser uid & maybe id zClient cli & zConnection "conn"

zUser :: String -> HTTP.Request -> HTTP.Request
zUser :: String -> Request -> Request
zUser = String -> String -> Request -> Request
addHeader String
"Z-User"

zProvider :: String -> HTTP.Request -> HTTP.Request
zProvider :: String -> Request -> Request
zProvider = String -> String -> Request -> Request
addHeader String
"Z-Provider"

zConnection :: String -> HTTP.Request -> HTTP.Request
zConnection :: String -> Request -> Request
zConnection = String -> String -> Request -> Request
addHeader String
"Z-Connection"

zClient :: String -> HTTP.Request -> HTTP.Request
zClient :: String -> Request -> Request
zClient = String -> String -> Request -> Request
addHeader String
"Z-Client"

zType :: String -> HTTP.Request -> HTTP.Request
zType :: String -> Request -> Request
zType = String -> String -> Request -> Request
addHeader String
"Z-Type"

zHost :: String -> HTTP.Request -> HTTP.Request
zHost :: String -> Request -> Request
zHost = String -> String -> Request -> Request
addHeader String
"Z-Host"

submit :: String -> HTTP.Request -> App Response
submit :: String -> Request -> App Response
submit String
method Request
req0 = do
  let req :: Request
req = Request
req0 {HTTP.method = T.encodeUtf8 (T.pack method)}
  manager <- (Env -> Manager) -> App Manager
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks (.manager)
  res <- liftIO $ HTTP.httpLbs req manager
  pure $
    Response
      { jsonBody = Aeson.decode (HTTP.responseBody res),
        body = L.toStrict (HTTP.responseBody res),
        status = HTTP.statusCode (HTTP.responseStatus res),
        headers = HTTP.responseHeaders res,
        request = req
      }

locationHeaderHost :: Response -> String
locationHeaderHost :: Response -> String
locationHeaderHost Response
resp =
  let location :: String
location = ByteString -> String
C8.unpack (ByteString -> String)
-> (Maybe (HeaderName, ByteString) -> ByteString)
-> Maybe (HeaderName, ByteString)
-> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (HeaderName, ByteString) -> ByteString
forall a b. (a, b) -> b
snd ((HeaderName, ByteString) -> ByteString)
-> (Maybe (HeaderName, ByteString) -> (HeaderName, ByteString))
-> Maybe (HeaderName, ByteString)
-> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Maybe (HeaderName, ByteString) -> (HeaderName, ByteString)
forall a. HasCallStack => Maybe a -> a
fromJust (Maybe (HeaderName, ByteString) -> String)
-> Maybe (HeaderName, ByteString) -> String
forall a b. (a -> b) -> a -> b
$ Response -> Maybe (HeaderName, ByteString)
locationHeader Response
resp
      locationURI :: URI
locationURI = Maybe URI -> URI
forall a. HasCallStack => Maybe a -> a
fromJust (Maybe URI -> URI) -> Maybe URI -> URI
forall a b. (a -> b) -> a -> b
$ String -> Maybe URI
parseURI String
location
      locationHost :: String
locationHost = URIAuth -> String
uriRegName (Maybe URIAuth -> URIAuth
forall a. HasCallStack => Maybe a -> a
fromJust (URI
locationURI URI -> (URI -> Maybe URIAuth) -> Maybe URIAuth
forall a b. a -> (a -> b) -> b
& URI -> Maybe URIAuth
uriAuthority))
   in String
locationHost

locationHeader :: Response -> Maybe (HTTP.HeaderName, ByteString)
locationHeader :: Response -> Maybe (HeaderName, ByteString)
locationHeader = HeaderName -> Response -> Maybe (HeaderName, ByteString)
findHeader HeaderName
hLocation

contentLengthHeader :: Response -> Maybe (HTTP.HeaderName, ByteString)
contentLengthHeader :: Response -> Maybe (HeaderName, ByteString)
contentLengthHeader = HeaderName -> Response -> Maybe (HeaderName, ByteString)
findHeader HeaderName
hContentLength

findHeader :: HTTP.HeaderName -> Response -> Maybe (HTTP.HeaderName, ByteString)
findHeader :: HeaderName -> Response -> Maybe (HeaderName, ByteString)
findHeader HeaderName
name Response
resp = ((HeaderName, ByteString) -> Bool)
-> [(HeaderName, ByteString)] -> Maybe (HeaderName, ByteString)
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
find (\(HeaderName
name', ByteString
_) -> HeaderName
name HeaderName -> HeaderName -> Bool
forall a. Eq a => a -> a -> Bool
== HeaderName
name') Response
resp.headers

getQueryParam :: String -> String -> Maybe (Maybe String)
getQueryParam :: String -> String -> Maybe (Maybe String)
getQueryParam String
name String
url =
  String -> Maybe URI
parseURI String
url
    Maybe URI -> (URI -> Maybe (Maybe String)) -> Maybe (Maybe String)
forall a b. Maybe a -> (a -> Maybe b) -> Maybe b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= String -> [(String, Maybe String)] -> Maybe (Maybe String)
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup String
name
      ([(String, Maybe String)] -> Maybe (Maybe String))
-> (URI -> [(String, Maybe String)]) -> URI -> Maybe (Maybe String)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ((ByteString, Maybe ByteString) -> (String, Maybe String))
-> [(ByteString, Maybe ByteString)] -> [(String, Maybe String)]
forall a b. (a -> b) -> [a] -> [b]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap ((ByteString -> String)
-> (Maybe ByteString -> Maybe String)
-> (ByteString, Maybe ByteString)
-> (String, Maybe String)
forall a b c d. (a -> b) -> (c -> d) -> (a, c) -> (b, d)
forall (p :: * -> * -> *) a b c d.
Bifunctor p =>
(a -> b) -> (c -> d) -> p a c -> p b d
bimap ByteString -> String
forall a b. ConvertibleStrings a b => a -> b
cs ((ByteString -> String) -> Maybe ByteString -> Maybe String
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
(<$>) ByteString -> String
forall a b. ConvertibleStrings a b => a -> b
cs))
      ([(ByteString, Maybe ByteString)] -> [(String, Maybe String)])
-> (URI -> [(ByteString, Maybe ByteString)])
-> URI
-> [(String, Maybe String)]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> [(ByteString, Maybe ByteString)]
parseQuery
      (ByteString -> [(ByteString, Maybe ByteString)])
-> (URI -> ByteString) -> URI -> [(ByteString, Maybe ByteString)]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> ByteString
forall a b. ConvertibleStrings a b => a -> b
cs
      (String -> ByteString) -> (URI -> String) -> URI -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. URI -> String
uriQuery