packages feed

classy-miso-0.0.0.2: src/Miso/Classy.hs

{-# LANGUAGE CPP                       #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE InstanceSigs              #-}
{-# LANGUAGE NamedFieldPuns            #-}
{-# LANGUAGE OverloadedStrings         #-}
{-# LANGUAGE PolyKinds                 #-}
{-# LANGUAGE ScopedTypeVariables       #-}
{-# LANGUAGE TypeFamilyDependencies    #-}
{-# LANGUAGE TypeInType                #-}
{-# LANGUAGE TypeOperators             #-}
{-# LANGUAGE UnicodeSyntax             #-}
#if !MIN_VERSION_base(4,10,0)
{-# OPTIONS_GHC -fno-warn-deprecations #-}
#endif

module Miso.Classy
	( Component(..)
	, update'
	, WrappedComponent
	, wrapComponent
	, mapWrappedComponent
	, addSubcomponent
	, initSubcomponent
	, WrappedAction(..)
	, RouteParser
	, ClassyEffect
	, WrappedEffect
	, ClassyTransition
	, effectToTransition
	, ClassySub
	, ClassySink
	, WrappedView
	, WrappedSub
	, WrappedSink
	, wrapAction
	, wrapSub
	, routeToURI
	, toRoutePath
	, getCurrentURI
	, parseCurrentURI
	, RoutePath
	, actionToSub
	, actionToWrappedSub
	, wrappedActionToSub
	, viewSub
	, viewSubs
	, viewSubBy
	, viewSubsBy
	, emptyView
	, emptySub
	, emptyWrappedSub
	, fireEvent
	, addEventHandler
	, module Miso
	, module Network.URI
	, module RFC.Miso.String
	, Default(..)
	) where

import           Control.Lens                      hiding ( view )
import           Control.Monad.Trans.State.Strict  ( put )
import           Control.Monad.Trans.Writer.Strict ( tell )
import           Data.Default
import           Data.Map.Strict                   ( Map )
import qualified Data.Map.Strict                   as Map
import           Data.Typeable
import           Miso                              hiding ( App (..), getCurrentURI )
import qualified Miso.Html                         as Html
import           Network.URI                       ( URI (..), parseURI )
import qualified Network.URL                       as URL
import           RFC.Miso.String
import           RFC.Prelude                       hiding ( init )

-- Need the ability to compare proxies for equivalence
#if MIN_VERSION_base(4,10,0)
import Type.Reflection ( eqTypeRep )

eqProxy :: Proxy a -> Proxy b -> Maybe (a :~: b)
eqProxy a b = eqTypeRep (typeRep a) (typeRep b)
#else
-- | Hand-rolled and dangerous implementation of 'eqTypeRep'. BOO!
eqProxy :: (Typeable a, Typeable b) => Proxy a -> Proxy b -> Maybe (a :~: b)
eqProxy a b
	| typeRep a == typeRep b     = Just undefined
	| otherwise                 = Nothing
#endif

{-# ANN module ("HLint: ignore Use fewer imports"::String) #-}

-- | A 'View' where we wrap up the action type as a 'WrappedAction'.
type WrappedView = View WrappedAction

-- | An 'Effect' where we wrap up the action type as a 'WrappedAction'.
type WrappedEffect model = Effect WrappedAction model

-- | A 'Sink' where we consume the action type as a 'WrappedAction'.
type WrappedSink = WrappedAction -> IO ()

-- | A 'Sub' where we wrap up the action type as a 'WrappedSub'.
type WrappedSub = WrappedSink -> IO ()

-- | Handy alias specifying the 'Effect' in terms of type families.
type ClassyEffect model = Effect (Action model) model

-- | Handy alias specifying the 'Transition' in terms of type families.
type ClassyTransition model = Transition (Action model) model

-- | Handy alias specifying the 'Sink' in terms of type families.
type ClassySink model = Action model -> IO ()

-- | Handy alias specifying the 'Sub' in terms of type families.
type ClassySub model = ClassySink model -> IO ()

-- | Wraps the concept of th e
class (Eq model, Typeable model, Eq (Action model), Typeable (Action model), Default(EventHandler model)) => Component model where
	{-# MINIMAL init, view, eventHandlers, (update|transition) #-}

	-- | The actions for this component, such as those returned in an 'Effect'
	type Action model = action | action -> model

	-- | Defines the initial arguments that the component expects to receive.
	type InitArgs model = initargs | initargs -> model

	-- | Defines the kinds of events that the parent can handle.
	type EventHandler model

	init :: InitArgs model → IO model			-- ^ Initialize the model
	view :: model -> View (Action model)	-- ^ Render the model into a VDOM-friendly structure

	eventHandlers :: ALens' model [EventHandler model] -- ^ Holds onto all the event handlers.

	subcomponents :: ALens' model [WrappedComponent] -- ^ Holds onto all the subcomponents
	subcomponents = lens (const []) (const)
	{-# INLINE subcomponents #-}

	routeParser :: model -> RouteParser -- ^ Provides a parser that defines the route for this model
	routeParser _ _ = fail "Model is not a route"
	{-# INLINE routeParser #-}

	-- | Implement this if you want to use the classic way to update.
	update :: Action model -> model -> WrappedEffect model
	update = fromTransition . transition
	{-# INLINE update #-}

	-- | Implement this if you want to use the 'Transition'-based way to update.
	transition :: Action model -> Transition WrappedAction model ()
	transition = toTransition . update
	{-# INLINE transition #-}

	-- | Whether or not this action is really for this model. By default, we accept all actions. But, if you've
	--   got multiple components of the same type, you may need to be more picky.
	acceptAction :: Action model -> model -> Bool
	acceptAction _ _ = True
	{-# INLINE acceptAction #-}

-- | Converts an 'Effect' into a 'Transition'.
effectToTransition :: Effect action model -> Transition action model ()
effectToTransition (Effect model actions) = do
	lift $ tell actions
	put model
{-# INLINE effectToTransition #-}

fireEvent :: (Component model) => model -> (EventHandler model -> WrappedSub) -> WrappedEffect model
fireEvent model event = Effect model $ event <$> events
	where
		events = model^.(cloneLens eventHandlers)
{-# INLINE fireEvent #-}

-- | Utility method to add an 'EventHandler' instance to a 'Component'.
addEventHandler :: (Component model) => model -> EventHandler model -> model
addEventHandler model handler = model & (cloneLens eventHandlers) %~ ((:) handler)
{-# INLINE addEventHandler #-}

addSubcomponent :: (Component parent, Component child) => parent -> child -> parent
addSubcomponent parent child =
	parent & (cloneLens subcomponents) %~ ((:) $ wrapComponent child)
{-# INLINE addSubcomponent #-}

-- | Handy method for the common case when you can pass your args down to your subcomponents.
initSubcomponent :: (Component model, Component sub) =>
	InitArgs model -> model -> (InitArgs model -> IO (InitArgs sub)) -> EventHandler sub -> IO model
initSubcomponent parentArgs parent argPicker handler = addSub . addHandler <$> (subArgs >>= init)
	where
		addHandler sub = addEventHandler sub handler
		subArgs = argPicker parentArgs
		initSubs = parent^.(cloneLens subcomponents)
		addSub newSub = parent & (cloneLens subcomponents) .~ (wrapComponent newSub:initSubs)
{-# INLINE initSubcomponent #-}

-- | For the component itself, and then recursively for all the subcomponents, this attempts to cast
--	 the action (unwrapped from 'WrappedAction') to the relevant type for that component.
--	 If that works, we calls 'update' on that component.
update' :: (Component model) =>  WrappedAction -> model -> WrappedEffect model
update' wrapped@(WrappedAction action) initModel =
		Effect finalModel allEffects
	where
		(finalModel, allEffects) = (foldedModel, foldedEffects)
		Effect foldedModel foldedEffects =
			foldr foldImpl initEffect (initModel^.(cloneLens subcomponents))
		foldImpl (WrappedComponent (_,subcomp)) (Effect roundModel roundEffects) =
			let result = update' wrapped subcomp in -- TODO Do this recursion as a scheduled IO
			let Effect wrappedModel wrappedEffects = result &
				bimap
					id
					(\subModel -> roundModel & (cloneLens subcomponents) %~ ((:) (WrappedComponent (Proxy,subModel))))
			in
			Effect wrappedModel (wrappedEffects <> roundEffects)
		initEffect =
			case cast action of
				Nothing -> noEff initModel
				Just myAction ->
					if acceptAction myAction initModel then
						update myAction initModel
					else
						noEff initModel
{-# INLINABLE update' #-}

-- | The function for processing routes from route paths. It will consume the current
--	 'RoutePath', and (if it successfully parsed), it returns the component that handles the route.
type RouteParser = forall m. MonadFail m => RoutePath -> m WrappedComponent

-- | A wrapper around components that hides the existential quantification.
data WrappedComponent = forall child. Component child ⇒ WrappedComponent (Proxy child, child)
instance Eq WrappedComponent where
	(==) (WrappedComponent left) (WrappedComponent right) =
		maybe False (left ==) (cast right)
	{-# INLINE (==) #-}

-- | Smart constructor for 'WrappedComponent'.
wrapComponent :: Component child => child -> WrappedComponent
wrapComponent child = WrappedComponent (Proxy, child)
{-# INLINE wrapComponent #-}

-- | Allows you to effectively extract the component, but note that the type of the
--   wrapped component can't escape its scope.
mapWrappedComponent :: (forall model. (Component model, Typeable model) => model -> a) -> WrappedComponent -> a
mapWrappedComponent f (WrappedComponent(_,it)) = f it
{-# INLINE mapWrappedComponent #-}

-- | A wrapper around actions, such as those returned in an 'Effect', which
--	 hides the existential quantification.
data WrappedAction = forall child. Component child => WrappedAction (Action child)

instance Eq WrappedAction where
	(==) (WrappedAction left) (WrappedAction right) =
		maybe False (right ==) (cast left)
	{-# INLINE (==) #-}

-- | Utility function for wrapping up an action into a 'WrappedAction'
wrapAction :: Component model => Action model -> WrappedAction
wrapAction = WrappedAction
{-# INLINE wrapAction #-}

-- | Utility method for converting an action into a 'Sub'
actionToSub :: Action model -> Sub (Action model)
actionToSub action sink = sink action
{-# INLINE actionToSub #-}

-- | Utility method for converting a 'WrappedAction' into a 'WrappedSub'
wrappedActionToSub :: WrappedAction -> WrappedSub
wrappedActionToSub action sink = sink action
{-# INLINE wrappedActionToSub #-}

-- | Utility function for wrapping up a 'Sub' into a 'WrappedSub'
wrapSub :: Component model => Sub (Action model) -> WrappedSub
wrapSub sub sink = sub (sink . wrapAction)
{-# INLINE wrapSub #-}

-- | Ties together 'actionToSub' and 'wrapSub' for you,
--   so you can go all the way from an action to a 'WrappedSub'.
actionToWrappedSub :: Component model => Action model -> WrappedSub
actionToWrappedSub = wrapSub . actionToSub
{-# INLINE actionToWrappedSub #-}

-- | A single segment of a route (a piece between slashes in the URI fragment)
type RouteSegment = StrictText

-- | The value of the query section of a route
type RouteQuery = Map StrictText [StrictText]

-- | The full path of the route. The third URL is the "base URI",
--	 and does not necessarily contain the 'RouteSegment' and the
--	 'RouteQuery' pieces.
type RoutePath = ([RouteSegment], RouteQuery, URI)

-- | Generates a URI from the route path by merging the 'RouteQuery' into the 'uriQuery'
--	 and the '[RouteSegment]' into the 'uriFragment'.
routeToURI :: RoutePath -> URI
routeToURI (segs, qry, uri) =
	uri
		{ uriQuery = '?':query
		, uriFragment = '#':frag
		}
	where
		encodeString :: ConvertibleStrings a String => Bool -> a -> String -- Second argument is: "Is this a query string?"
		encodeString spaceToPlus = URL.encString spaceToPlus URL.ok_param . cs
		query :: String
		query = intercalate "&" . pairsToTerms $ Map.toList qry
		pairsToTerms [] = []
		pairsToTerms ((key,[]):rest)			 = (encodeString True key) : pairsToTerms rest
		pairsToTerms ((key,([val])):rest) = (encodeString True key <> "=" <> (encodeString True val)) : pairsToTerms rest
		pairsToTerms ((key,vals):rest)		 = (encodeString True key <> "=" <> intercalate "," (encodeString True <$> vals)) : pairsToTerms rest
		frag :: String
		frag = intercalate "/" (segToPath <$> segs)
		segToPath :: RouteSegment -> String
		segToPath = encodeString False
{-# INLINABLE routeToURI #-}


-- | Parses a 'URI' into a 'RoutePath'
toRoutePath :: URI -> RoutePath
toRoutePath uri@URI{uriFragment,uriQuery} =
			( pathToSegments . parseHash $ uriFragment
			, parseQuery uriQuery
			, uri
			)
	where
		decodeString :: String -> StrictText
		decodeString str = cs . fromMaybe str . URL.decString True $ str
		pathToSegments :: String -> [RouteSegment]
		pathToSegments "" = []
		pathToSegments ('/':rest) = pathToSegments rest
		pathToSegments path = (decodeString start) : pathToSegments rest
			where
				(start,rest) = splitOnSlash path
				splitOnSlash "" = ("","")
				splitOnSlash ('/':content) = splitOnSlash content
				splitOnSlash content =
					case span ('/' /=) content of
						([],[])								-> ("","")
						([], _:theRest)				-> splitOnSlash theRest
						(theStart, [])				-> (theStart, "")
						(theStart, _:theRest) -> (theStart, theRest)
		parseHash ('#':rest) = parseHash rest
		parseHash ('!':rest) = parseHash rest
		parseHash ('/':rest) = parseHash rest
		parseHash hash			 = hash
		parseQuery ('?':rest) = parseQuery rest
		parseQuery query =
			case URL.importParams query of
				Nothing ->
					Map.empty
				Just pairs ->
					fmap (second listify) pairs &
					Map.fromListWith (++) &
					Map.map sort &
					Map.map (fmap cs) &
					Map.mapKeys cs
		listify :: String -> [String]
		listify ""				= []
		listify (',':val) = listify val
		listify val				= start : listify rest
			where
				(start,rest) = splitOnComma val
				splitOnComma ""						 = ("","")
				splitOnComma (',':content) = splitOnComma content
				splitOnComma (' ':content) = splitOnComma content
				splitOnComma content =
					case span (',' /=) content of
						([], [])							-> ("", "")
						([], _:theRest)				-> splitOnComma theRest
						(theStart, [])				-> (theStart, "")
						(theStart, _:theRest) -> (theStart, theRest)
{-# INLINABLE toRoutePath #-}

-- | Retrieves the current location of the window from "window.location.href".
foreign import javascript safe "$r = (window && window.location && window.location.href) || '';"
	getWindowLocationHref :: IO MisoString

-- | Gets the current URI.
getCurrentURI :: (MonadIO m, MonadFail m) => m URI
getCurrentURI = do
	href <- fromMisoString <$> liftIO getWindowLocationHref
	case parseURI href of
		Nothing  -> fail $ "Co uld not parse URI from window.location: " <> href
		Just uri -> return uri
{-# INLINE getCurrentURI #-}

-- | Utility method to parse the current URI.
parseCurrentURI :: (MonadIO m) => m RoutePath
parseCurrentURI = toRoutePath <$> liftIO getCurrentURI
{-# INLINE parseCurrentURI #-}

-- | View a particular subcomponent by specifying a test. For the first (leftmost) subcomponent
--   that returns 'True', we will return their view. Failing that, we return an
--   empty (but defined) view.
viewSubBy :: (Component model) => model -> (WrappedComponent -> Bool) -> WrappedView
viewSubBy parent subTest = fromMaybe emptyView . safeHead $ viewSubsBy parent subTest
{-# INLINE viewSubBy #-}

-- | View a particular subcomponent by specifying its type. For the first (leftmost) subcomponent
--   whose type is equal to the given type, we will return their view. Failing that, we
--   return an empty (but defined) view.
viewSub :: (Component model, Component sub) => model -> Proxy sub -> WrappedView
viewSub parent subType = viewSubBy parent (wrappedComponentPxyEq subType)
{-# INLINE viewSub #-}

-- | View subcomponents by a given test. For every subcomponent that returns 'True', we will
--   include their 'WrappedView' in the resulting list.
viewSubsBy :: (Component model) => model -> (WrappedComponent -> Bool) -> [WrappedView]
viewSubsBy parent test =
		toWrappedView <$> filter test targets
	where
		targets = (wrapComponent parent : parent^.(cloneLens subcomponents))
		toWrappedView :: WrappedComponent -> WrappedView
		toWrappedView (WrappedComponent(_,sub)) = WrappedAction <$> view sub
{-# INLINE viewSubsBy #-}

-- | View subcomponents by specifying their type. For every subcomponent of whose type
--   is equal to the given type, we will include their 'WrappedView' in the resulting list.
viewSubs :: (Component model, Component sub) => model -> Proxy sub -> [WrappedView]
viewSubs parent subType = viewSubsBy parent (wrappedComponentPxyEq subType)
{-# INLINE viewSubs #-}

-- | Test that a 'WrappedComponent' is equal to a given type.
wrappedComponentPxyEq :: (Component target) => Proxy target -> WrappedComponent -> Bool
wrappedComponentPxyEq targetPxy (WrappedComponent(pxy,_)) = isJust $ eqProxy targetPxy pxy
{-# INLINE wrappedComponentPxyEq #-}

-- | A definition of an empty view, which has no impact on how the screen is rendered.
emptyView :: View anything
emptyView = Html.text ""
{-# INLINE emptyView #-}

-- | An empty (nop) 'Sub'
emptySub :: Sub (Action a)
emptySub = const $ return ()
{-# INLINE emptySub #-}

-- | A wrapped version of 'emptySub'
emptyWrappedSub :: WrappedSub
emptyWrappedSub = const $ return ()
{-# INLINE emptyWrappedSub #-}