packages feed

rfc-0.0.0.23: src/RFC/Miso/Routing.hs

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FunctionalDependencies    #-}
{-# LANGUAGE MultiParamTypeClasses     #-}
{-# LANGUAGE NamedFieldPuns            #-}
{-# LANGUAGE ScopedTypeVariables       #-}

module RFC.Miso.Routing
  ( module RFC.Miso.Routing
  , URI(..)
  ) where

import           Data.Map                  (Map)
import qualified Data.Map                  as Map
import           Data.Proxy
import           Miso.Effect
import           Miso.Html                 (View (..))
import           Miso.Subscription.History (URI (..), getCurrentURI)
import qualified Network.URL               as URL
import           RFC.Prelude

newtype URIHash = URIHash String deriving (Eq)
newtype URIQuery = URIQuery (Map String [String]) deriving (Eq)
type RoutingURI = (URIHash, URIQuery)

parseURI :: URI -> RoutingURI
parseURI URI{uriFragment,uriQuery} =
    (URIHash $ parseHash uriFragment, URIQuery $ parseQuery uriQuery)
  where
    parseHash ('#':rest) = parseHash rest
    parseHash ('!':rest) = parseHash rest
    parseHash ('/':rest) = parseHash rest
    parseHash hash       = hash
    parseQuery :: String -> Map String [String]
    parseQuery ('?':rest) = parseQuery rest
    parseQuery query =
      case URL.importParams query of
        Nothing ->
          Map.empty
        Just pairs ->
          Map.map sort $ Map.fromListWith (++) $ map (second listify) pairs
    listify :: String -> [String]
    listify ""  = []
    listify val = [val]

parseCurrentURI :: IO (URIHash,URIQuery)
parseCurrentURI = parseURI <$> getCurrentURI

data ViewSpec parentModel parentAction = ViewSpec (parentModel -> View parentAction, RoutingURI)

instance Eq (ViewSpec model action) where
  (==) (ViewSpec(_,left)) (ViewSpec(_,right)) = left == right

class (Typeable model) => RouteConfig model action
      | model -> action, action -> model
  where
    routeUpdate   :: model -> action -> Effect action model
    routeView     :: model -> View action
    runRoute      :: RoutingURI -> Maybe (Effect action model)

class (RouteConfig model action) => RouteConvert parentModel parentAction model action
      | parentAction -> parentModel, parentModel -> parentAction
  where
    wrapAction    :: action -> model -> parentAction
    unwrapAction  :: parentAction -> model -> Maybe action
    wrapModel     :: parentModel -> model -> parentModel
    unwrapModel   :: parentModel -> model
    wrapEffect    :: parentModel -> Effect action model -> Effect parentAction parentModel
    wrapEffect parentModel (Effect model childIOs) =
      Effect
          (wrapModel parentModel model)
          (map (fmap (\act -> wrapAction act model)) childIOs)

    wrappedRunRoute :: Proxy model -> WrappedRun parentModel parentAction
    wrappedRunRoute _ uriPair currentParentModel = do
        childEffect <- runRoute uriPair
        return (effectWrapper childEffect, ViewSpec (view, uriPair))
      where
        effectWrapper :: Effect action model -> Effect parentAction parentModel
        effectWrapper = wrapEffect currentParentModel
        view :: parentModel -> View parentAction
        view parentModel = fmap (\action -> wrapAction action childModel) $ routeView childModel
          where
            childModel :: model
            childModel = unwrapModel parentModel

    wrappedRouteUpdate :: Proxy model -> WrappedUpdate parentModel parentAction
    wrappedRouteUpdate _ parentModel parentAction =
      case unwrapAction parentAction model of
        Nothing     -> noEff parentModel
        Just action -> wrapEffect parentModel $ routeUpdate model action
      where
        model :: model
        model = unwrapModel parentModel

type WrappedRun parentModel parentAction =
  RoutingURI -> parentModel -> Maybe (Effect parentAction parentModel, ViewSpec parentModel parentAction)

type WrappedUpdate parentModel parentAction = parentModel -> parentAction -> Effect parentAction parentModel

data RoutingTable parentModel parentAction =
  RoutingTable [(WrappedRun parentModel parentAction, WrappedUpdate parentModel parentAction)]

addRoute :: (RouteConvert parentModel parentAction model action) =>
  RoutingTable parentModel parentAction -> Proxy model -> RoutingTable parentModel parentAction
addRoute (RoutingTable table) pxy =
  RoutingTable $ (wrappedRunRoute pxy, wrappedRouteUpdate pxy):table

runTable ::
  RoutingTable parentModel parentAction ->
  (parentModel -> View parentAction) ->
  RoutingURI ->
  parentModel ->
  (Effect parentAction parentModel, ViewSpec parentModel parentAction)
runTable (RoutingTable routes) notFoundView routingURI parentModel =
    case safeHead $ catMaybes routeRunResults of
      Nothing -> (noEff parentModel, ViewSpec (notFoundView, routingURI))
      Just results -> results
  where
    routeRunResults = map (\run -> run routingURI parentModel) $ map fst routes

updateTable ::
  RoutingTable parentModel parentAction ->
  parentModel ->
  parentAction ->
  Effect parentAction parentModel
updateTable (RoutingTable tbl) initialParentModel parentAction =
    foldr doMerge initialEffect updateCalls
  where
    initialEffect = Effect initialParentModel []
    updateCalls = map snd tbl
    doMerge call (Effect parentModel ios) =
      let (Effect newParentModel moreIOs) = call parentModel parentAction in
      Effect newParentModel (ios++moreIOs)