{-# LANGUAGE LambdaCase #-} {-# LANGUAGE PartialTypeSignatures #-} {-# LANGUAGE NoFieldSelectors #-} module Web.Hyperbole.Effect where import Control.Monad (join) import Data.Bifunctor (first) import Data.ByteString qualified as BS import Data.ByteString.Lazy qualified as BL import Data.List qualified as List import Data.String.Conversions import Data.Text import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Effectful import Effectful.Dispatch.Dynamic import Effectful.Error.Static import Effectful.State.Static.Local import Network.HTTP.Types import Web.FormUrlEncoded (Form, urlDecodeForm) import Web.HttpApiData (FromHttpApiData, ToHttpApiData (..), parseQueryParam) import Web.Hyperbole.HyperView import Web.Hyperbole.Route import Web.Hyperbole.Session as Session import Web.View newtype Host = Host {text :: BS.ByteString} deriving (Show) data Request = Request { host :: Host , path :: [Text] , query :: Query , body :: BL.ByteString , method :: Method , cookies :: [(BS.ByteString, BS.ByteString)] } deriving (Show) data Response = Response (View () ()) | NotFound | Redirect Url | Err ResponseError | Empty data ResponseError = ErrParse Text | ErrParam Text | ErrOther Text | ErrNotHandled (Event Text Text) | ErrAuth deriving (Show) newtype Page es a = Page (Eff es a) deriving newtype (Applicative, Monad, Functor) data Event id act = Event { viewId :: id , action :: act } instance (Show act, Show id) => Show (Event id act) where show e = "Event " <> show e.viewId <> " " <> show e.action routeRequest :: (Hyperbole :> es, Route route) => (route -> Eff es Response) -> Eff es Response routeRequest actions = do path <- reqPath case findRoute path of Nothing -> send $ RespondEarly NotFound Just rt -> actions rt data Server :: Effect where LoadRequest :: Server m Request SendResponse :: Session -> Response -> Server m () type instance DispatchOf Server = 'Dynamic data Hyperbole :: Effect where GetRequest :: Hyperbole m Request RespondEarly :: Response -> Hyperbole m a SetSession :: (ToHttpApiData a) => Text -> a -> Hyperbole m () DelSession :: Text -> Hyperbole m () GetSession :: (FromHttpApiData a) => Text -> Hyperbole m (Maybe a) type instance DispatchOf Hyperbole = 'Dynamic data HyperState = HyperState { request :: Request , session :: Session } runHyperbole :: (Server :> es) => Eff (Hyperbole : es) Response -> Eff es Response runHyperbole = fmap combine $ reinterpret runLocal $ \_ -> \case GetRequest -> do gets @HyperState (.request) RespondEarly r -> do s <- gets @HyperState (.session) send $ SendResponse s r throwError r SetSession k a -> do modify $ \st -> st{session = sessionSet k a st.session} :: HyperState DelSession k -> do modify $ \st -> st{session = sessionDel k st.session} :: HyperState GetSession k -> do s <- gets @HyperState (.session) pure $ sessionLookup k s where runLocal :: (Server :> es) => Eff (State HyperState : Error Response : es) a -> Eff es (Either Response (a, HyperState)) runLocal eff = do -- Load the request ONCE right when we start r <- send LoadRequest let st = HyperState r (sessionFromCookies r.cookies) runErrorNoCallStack @Response . runState st $ eff combine :: (Server :> es) => Eff es (Either Response (Response, HyperState)) -> Eff es Response combine eff = do er <- eff case er of Left res -> -- responded early, don't need to respond again pure res Right (res, st) -> do send $ SendResponse st.session res pure res request :: (Hyperbole :> es) => Eff es Request request = send GetRequest reqPath :: (Hyperbole :> es) => Eff es [Text] reqPath = (.path) <$> request formData :: (Hyperbole :> es) => Eff es Form formData = do b <- (.body) <$> request let ef = urlDecodeForm b -- not going to work. we need a way to `throwError` or it doesn't work... either (send . RespondEarly . Err . ErrParse) pure ef getEvent :: (HyperView id, Hyperbole :> es, Show id, Show (Action id)) => Eff es (Maybe (Event id (Action id))) getEvent = do q <- reqParams pure $ parseEvent q lookupParam :: BS.ByteString -> Query -> Maybe Text lookupParam p q = fmap cs <$> join $ lookup p q parseEvent :: (HyperView id, Show id, Show (Action id)) => Query -> Maybe (Event id (Action id)) parseEvent q = do Event ti ta <- lookupEvent q vid <- parseParam ti act <- parseParam ta pure $ Event vid act lookupEvent :: Query -> Maybe (Event Text Text) lookupEvent q' = Event <$> lookupParam "id" q' <*> lookupParam "action" q' session :: (Hyperbole :> es, FromHttpApiData a) => Text -> Eff es (Maybe a) session k = send $ GetSession k setSession :: (Hyperbole :> es, ToHttpApiData a) => Text -> a -> Eff es () setSession k v = send $ SetSession k v -- Or, do we clear the whole thing? clearSession :: (Hyperbole :> es) => Text -> Eff es () clearSession k = send $ DelSession k reqParams :: (Hyperbole :> es) => Eff es Query reqParams = (.query) <$> request reqParam :: (Hyperbole :> es, FromHttpApiData a) => Text -> Eff es a reqParam p = do q <- reqParams (er :: Either Response a) <- pure $ do mv <- require $ List.lookup (encodeUtf8 p) q v <- require mv first (Err . ErrParam) $ parseQueryParam (decodeUtf8 v) case er of Left e -> send $ RespondEarly e Right a -> pure a where require :: Maybe x -> Either Response x require Nothing = Left $ Err $ ErrParam $ "Missing: " <> p require (Just a) = pure a notFound :: (Hyperbole :> es) => Eff es a notFound = send $ RespondEarly NotFound parseError :: (Hyperbole :> es) => Text -> Eff es a parseError = send . RespondEarly . Err . ErrParse redirect :: (Hyperbole :> es) => Url -> Eff es a redirect = send . RespondEarly . Redirect -- | Set the response to the view. Note that `page` already expects a view to be returned from the effect view :: (Hyperbole :> es) => View () () -> Eff es Response view vw = do pure $ Response vw respondEarly :: (Hyperbole :> es, HyperView id) => id -> View id () -> Eff es () respondEarly vid vw = do let res = Response $ viewId vid vw send $ RespondEarly res -- | Load the entire page when no HyperViews match load :: (Hyperbole :> es) => Eff es (View () ()) -> Page es Response load run = Page $ do r <- request case lookupEvent r.query of -- Are id and action set to sometjhing? Just e -> pure $ Err $ ErrNotHandled e Nothing -> do vw <- run view vw -- | Handle a HyperView. If the event matches our handler, respond with the fragment hyper :: forall id es . (Hyperbole :> es, HyperView id, Show id, Show (Action id)) => (id -> Action id -> Eff es (View id ())) -> Page es () hyper run = Page $ do -- Get an event matching our type. If it doesn't match, skip to the next handler mev <- getEvent @id case mev of Just event -> do vw <- run event.viewId event.action send $ RespondEarly $ Response $ viewId event.viewId vw _ -> pure () page :: (Hyperbole :> es) => Page es Response -> Eff es Response page (Page eff) = eff