packages feed

trasa-reflex-0.2: src/Trasa/Reflex.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -Wall -Werror #-}
module Trasa.Reflex
  ( requestWith
  , requestManyWith
  , ResponseHandler(..)
  , requestMultiWith
  , serve
  , Arguments
  , handler) where

import Data.Kind (Type)

import qualified Data.Text.Encoding as TE
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS (toStrict,fromStrict)

import Data.Functor.Identity (Identity(..))
import Data.Foldable (toList)
import qualified Data.Map.Strict as M
import qualified Network.HTTP.Types.Status as N
import qualified Network.HTTP.Media as N
import Reflex.Dom

import Trasa.Core hiding (requestWith,Arguments,handler)

import Reflex.PopState

-- | Replaces 'Trasa.Core.Arguments' with one that does not deal with request bodies
type family Arguments (caps :: [Type]) (qrys :: [Param]) (resp :: Type) (result :: Type) :: Type where
  Arguments '[] '[] resp result = resp -> result
  Arguments '[] (q:qs) resp result = ParamBase q -> Arguments '[] qs resp result
  Arguments (cap:caps) qs resp result = cap -> Arguments caps qs resp result

-- | "Trasa.Reflex.handler" is to "Trasa.Core.handler" as "Trasa.Reflex.Arguments" is to "Trasa.Core.Arguments"
handler :: Rec Identity caps -> Rec Parameter qrys -> ResponseBody Identity resp -> Arguments caps qrys resp x -> x
handler = go
  where
    go :: Rec Identity caps -> Rec Parameter qrys -> ResponseBody Identity resp -> Arguments caps qrys resp x -> x
    go RNil RNil (ResponseBody (Identity response)) f = f response
    go RNil (q :& qs) respBody f = go RNil qs respBody (f (demoteParameter q))
    go (Identity cap :& caps) qs respBody f = go caps qs respBody (f cap)

-- | Used when you want to perform an action for any response type
data ResponseHandler route a = forall resp. ResponseHandler
  !(Prepared route resp)
  !(ResponseBody (Many BodyDecoding) resp)
  !(resp -> a)

data Pair a b = Pair !a !b
  deriving (Functor, Foldable, Traversable)

newtype Preps route resp f a = Preps (f (Pair (ResponseHandler route resp) a))
  deriving (Functor,Foldable,Traversable)

-- | Single request version of 'requestManyWith'
requestWith :: forall t m route response.
  MonadWidget t m
  => (forall caps qrys req resp. route caps qrys req resp -> Method)
  -- ^
  -> (forall caps qrys req resp. route caps qrys req resp -> Path CaptureEncoding caps)
  -> (forall caps qrys req resp. route caps qrys req resp -> Rec (Query CaptureEncoding) qrys)
  -> (forall caps qrys req resp. route caps qrys req resp -> RequestBody (Many BodyEncoding) req)
  -> (forall caps qrys req resp. route caps qrys req resp -> ResponseBody (Many BodyDecoding) resp)
  -> Event t (Prepared route response)
  -> m (Event t (Either TrasaErr response))
requestWith toMethod toCapEncs toQuerys toReqBody toRespBody prepared =
  coerceEvent <$> requestManyWith toMethod toCapEncs toQuerys toReqBody toRespBody preparedId
  where preparedId = coerceEvent prepared :: Event t (Identity (Prepared route response))

-- | Perform n requests and collect the results
requestManyWith :: forall t m f route response.
  (MonadWidget t m, Traversable f)
  => (forall caps qrys req resp. route caps qrys req resp -> Method)
  -- ^ Get the method from a route
  -> (forall caps qrys req resp. route caps qrys req resp -> Path CaptureEncoding caps)
  -- ^ How to encode the path pieces from a route
  -> (forall caps qrys req resp. route caps qrys req resp -> Rec (Query CaptureEncoding) qrys)
  -- ^ How to encode the query parameters from a route
  -> (forall caps qrys req resp. route caps qrys req resp -> RequestBody (Many BodyEncoding) req)
  -- ^ How to encode the request body from a route
  -> (forall caps qrys req resp. route caps qrys req resp -> ResponseBody (Many BodyDecoding) resp)
  -- ^ How to decode a response from a route
  -> Event t (f (Prepared route response))
  -- ^ The routes to request
  -> m (Event t (f (Either TrasaErr response)))
requestManyWith toMethod toCapEncs toQuerys toReqBody toRespBody prepared =
  requestMultiWith toMethod toCapEncs toQuerys toReqBody toRespBody (fmap toResponseHandler <$> prepared)
  where toResponseHandler p@(Prepared route _ _ _) = ResponseHandler p (toRespBody route) id

-- | Internal function but exported because it subsumes the function of all the other functions in this package.
-- | Very powerful function
requestMultiWith :: forall t m f route a.
  (MonadWidget t m, Traversable f)
  => (forall caps qrys req resp. route caps qrys req resp -> Method)
  -> (forall caps qrys req resp. route caps qrys req resp -> Path CaptureEncoding caps)
  -> (forall caps qrys req resp. route caps qrys req resp -> Rec (Query CaptureEncoding) qrys)
  -> (forall caps qrys req resp. route caps qrys req resp -> RequestBody (Many BodyEncoding) req)
  -> (forall caps qrys req resp. route caps qrys req resp -> ResponseBody (Many BodyDecoding) resp)
  -> Event t (f (ResponseHandler route a))
  -> m (Event t (f (Either TrasaErr a)))
requestMultiWith toMethod toCapEncs toQuerys toReqBody toRespBody contResp =
  fmap parseXhrResponses <$> performRequestsAsync (buildXhrRequests <$> contResp)
  where parseXhrResponses :: Preps route a f XhrResponse -> f (Either TrasaErr a)
        parseXhrResponses (Preps res) = fmap parseOneXhrResponse res
        parseOneXhrResponse :: Pair (ResponseHandler route a) XhrResponse -> Either TrasaErr a
        parseOneXhrResponse (Pair (ResponseHandler (Prepared route _ _ _) _ fromResp)
                            (XhrResponse statusCode _ _ response headers)) =
          case statusCode < 400 of
            True -> case M.lookup "Content-Type" headers >>= N.parseAccept . TE.encodeUtf8 of
              Just content -> case response of
                Just txt -> let bs = LBS.fromStrict (TE.encodeUtf8 txt) in
                  fromResp <$> decodeResponseBody (toRespBody route) (Content content bs)
                Nothing -> Left (TrasaErr N.status400 "No body returned from server")
              Nothing -> Left (TrasaErr N.status406 "No content type from server")
            False -> Left (TrasaErr (N.mkStatus (fromIntegral statusCode) (maybe "" TE.encodeUtf8 response)) "")
        buildXhrRequests :: f (ResponseHandler route a) -> Preps route a f (XhrRequest BS.ByteString)
        buildXhrRequests = Preps . fmap buildOneXhrRequest
        buildOneXhrRequest :: ResponseHandler route a -> Pair (ResponseHandler route a) (XhrRequest BS.ByteString)
        buildOneXhrRequest w@(ResponseHandler p@(Prepared route _ _ _) _ _) =
          Pair w (XhrRequest (encodeMethod (toMethod route)) (encodeUrl (linkWith toCapEncs toQuerys p)) conf)
          where
            conf :: XhrRequestConfig BS.ByteString
            conf = def & xhrRequestConfig_sendData .~ maybe "" (LBS.toStrict . contentData) content
                        & xhrRequestConfig_headers .~ headers
                        & xhrRequestConfig_responseHeaders .~ AllHeaders
            -- headers = maybe acceptHeader (\ct -> M.insert "Content-Type" (contentType ct) acceptHeader) content
            headers = case content of
              Nothing -> acceptHeader
              Just (Content typ _) -> M.insert "Content-Type" (TE.decodeUtf8 (N.renderHeader typ)) acceptHeader
            acceptHeader = "Accept" =: (TE.decodeUtf8 . BS.intercalate ", " . fmap N.renderHeader . toList) accepts
            Payload _ content accepts = payloadWith toCapEncs toQuerys toReqBody toRespBody p

-- | Used to serve single page apps
serve :: forall t m route.
  MonadWidget t m
  => (forall caps qrys req resp. route caps qrys req resp -> Method)
  -> (forall caps qrys req resp. route caps qrys req resp -> Path CaptureEncoding caps)
  -> (forall caps qrys req resp. route caps qrys req resp -> Rec (Query CaptureCodec) qrys)
  -> (forall caps qrys req resp. route caps qrys req resp -> RequestBody (Many BodyCodec) req)
  -> (forall caps qrys req resp. route caps qrys req resp -> ResponseBody (Many BodyCodec) resp)
  -> Router route
  -> (forall caps qrys req resp.
      route caps qrys req resp ->
      Rec Identity caps ->
      Rec Parameter qrys ->
      ResponseBody Identity resp ->
      m (Event t (Concealed route)))
  -- ^ Build a widget from captures, query parameters, and a response body
  -> (TrasaErr -> m (Event t (Concealed route)))
  -> m ()
serve toMethod toCapEnc toQuerys toReqBody toRespBody router widgetize onErr = mdo
  -- Investigate why this is needed
  let newUrls = ffor (switch (current jumpsD)) $ \(Concealed route caps querys reqBody) ->
        linkWith toCapEnc toQueryEnc (Prepared route caps querys reqBody)
  (u0, urls) <- url newUrls
  pb <- getPostBuild
  let choice = ffor (leftmost [newUrls, urls, u0 <$ pb]) $ \us ->
        parseWith toQueryDec toReqBodyDec router "GET" us Nothing
      (failures, concealeds) = fanEither choice
  actions <- requestMultiWith toMethod toCapEnc toQueryEnc toReqBodyEnc toRespBodyDec (fromConcealed <$> concealeds)
  jumpsD <- widgetHold (return never) (leftmost [onErr <$> failures, either onErr id . runIdentity <$> actions])
  return ()
  where
    toQueryEnc :: route caps qrys req resp -> Rec (Query CaptureEncoding) qrys
    toQueryEnc = mapQuery captureCodecToCaptureEncoding . toQuerys
    toQueryDec :: route caps qrys req resp -> Rec (Query CaptureDecoding) qrys
    toQueryDec = mapQuery captureCodecToCaptureDecoding . toQuerys
    toReqBodyDec :: route caps qrys req resp -> RequestBody (Many BodyDecoding) req
    toReqBodyDec = mapRequestBody (mapMany bodyCodecToBodyDecoding) . toReqBody
    toReqBodyEnc :: route caps qrys req resp -> RequestBody (Many BodyEncoding) req
    toReqBodyEnc = mapRequestBody (mapMany bodyCodecToBodyEncoding) . toReqBody
    toRespBodyDec :: route caps qrys req resp -> ResponseBody (Many BodyDecoding) resp
    toRespBodyDec = mapResponseBody (mapMany bodyCodecToBodyDecoding) . toRespBody
    fromConcealed :: Concealed route -> Identity (ResponseHandler route (m (Event t (Concealed route))))
    fromConcealed (Concealed route caps querys reqBody) =
      Identity (ResponseHandler (Prepared route caps querys reqBody) (toRespBodyDec route)
               (widgetize route caps querys . ResponseBody . Identity))