packages feed

alto-0: src/Alto/Menu.hs

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Alto.Menu where

import           Control.Lens
import qualified Control.Monad.Catch as E
import qualified Data.Aeson as JS
import qualified Data.Aeson.TH as JS
import           Data.Aeson (FromJSON, ToJSON)
import           Data.ByteString (ByteString)
import           Data.Char (toLower)
import           Data.Map (Map)
import qualified Data.Map as Map
import           Data.Set (Set)
import           Data.String
import           Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import           System.Directory (listDirectory, createDirectory)
import           System.FilePath
import           GHC.Generics

type MenuID = Text

type Tag = Text

data ClientState =
  ClientState
  { _clientTags :: Map Tag Text
  }
  deriving (Read, Show, Eq, Ord, Generic)

makeLenses ''ClientState
JS.deriveJSON JS.defaultOptions{JS.fieldLabelModifier = drop 7, JS.constructorTagModifier = map toLower} ''ClientState

data TagLogic =
   Always
 | TagSet Tag
 | TagUnset Tag
 | TLAnd [TagLogic]
 | TLOr [TagLogic]
 | TLNot TagLogic
 deriving (Read, Show, Eq, Ord, Generic, ToJSON, FromJSON)

data TagChange =
  TagChange
  { _setTags :: Map Tag Text
  , _unsetTags :: Set Tag
  }
 deriving (Read, Show, Eq, Ord, Generic)

instance Semigroup TagChange where
  (<>) (TagChange a1 a2) (TagChange b1 b2) = TagChange (a1 <> b1) (a2 <> b2)

instance Monoid TagChange where
  mempty = TagChange mempty mempty

makeLenses ''TagChange
JS.deriveJSON JS.defaultOptions{JS.fieldLabelModifier = drop 1, JS.sumEncoding = JS.UntaggedValue} ''TagChange

data EmbedSize =
   EFullPage
 | ENative
 | ESize { _x :: Int, _y :: Int }
 deriving (Read, Show, Eq, Ord, Generic)

makeLenses ''EmbedSize
JS.deriveJSON JS.defaultOptions{JS.fieldLabelModifier = drop 1} ''EmbedSize

data Action =
   ColapseMenu
 | Nav { _url :: Text }
 | Embed { _url :: Text, _size :: EmbedSize }
 | Download { _url :: Text, _filename :: Text }
 | JSCall { _jsCall :: Text }
 deriving (Read, Show, Eq, Ord, Generic)

makeLenses ''Action
JS.deriveJSON JS.defaultOptions{JS.fieldLabelModifier = drop 1} ''Action

data EntryType =
   Action { _onAction :: TagChange, _act :: Maybe Action }
   -- ^ When the entry is clicked it does the above
 | SubMenu { _onAction :: TagChange, _subMenu :: MenuID, _subIdPostfix :: Maybe Tag }
   -- ^ When the entry is selected, the submenu is displayed
 -- | CallBack SomeHMACedThing
 deriving (Read, Show, Eq, Ord, Generic)

makeLenses ''EntryType
JS.deriveJSON JS.defaultOptions{JS.fieldLabelModifier = drop 1} ''EntryType

data MenuEntry =
  MEntry
  { _icon :: Maybe Text
  , _label :: Text
  , _display :: TagLogic
  , _active :: TagLogic
  , _reaction :: EntryType
  }
  deriving (Read, Show, Eq, Ord, Generic)

makeLenses ''MenuEntry
JS.deriveJSON JS.defaultOptions{JS.fieldLabelModifier = drop 1} ''MenuEntry

instance IsString MenuEntry where
  fromString l = MEntry Nothing (T.pack l) Always Always (Action mempty Nothing)

data Menu =
  Menu
  { _mid :: MenuID
  , _onLeave :: TagChange
  , _entries :: [MenuEntry]
  }
  deriving (Read, Show, Eq, Ord, Generic)

makeLenses ''Menu
JS.deriveJSON JS.defaultOptions{JS.fieldLabelModifier = dropWhile (=='m') . drop 1} ''Menu

data Root =
  MenuRoot
  { _rootState :: ClientState
  , _rootMenu :: Menu
  }
  deriving (Read, Show, Eq, Ord, Generic)

makeLenses ''Root
JS.deriveJSON JS.defaultOptions{JS.fieldLabelModifier = drop 5} ''Root

data MenuSystem =
  MenuSystem
  { _menuMap :: Map MenuID Menu
  , _topMenu :: Menu
  }
  deriving (Read, Show, Eq, Ord, Generic, ToJSON, FromJSON)

makeLenses ''MenuSystem

data CompState =
  CSt
  { _salt :: ByteString
    -- ^ A pseudo-salt derived expensively from the overall name. 
  , _menus :: Map MenuID Menu
  , _tags :: Set Tag
  }
  deriving (Read, Show, Eq, Ord, Generic)

makeLenses ''CompState

existDirectory :: FilePath -> IO ()
existDirectory fp = E.catch (createDirectory fp) (\(_::E.SomeException) -> return ())

-- | Load a menu from a file
loadMenu :: FilePath -> IO Menu
loadMenu fp = do
  either error return =<< JS.eitherDecodeFileStrict' fp

-- | Loads a MenuSystem directory. The format is:
--   FP:
--     - root <- file containing root menu's ID
--     - menus/ <- directory of one file per menu
loadMenus :: IO MenuSystem
loadMenus = do
  root <- either error return =<< JS.eitherDecodeFileStrict' ("graph" </> "root")
  mns <- (fmap (("graph"</>"menu")</>) <$> listDirectory ("graph" </> "menu")) >>=
         (fmap (Map.fromList . map (\a -> (a ^.mid, a))) . mapM loadMenu)
  return . MenuSystem mns $ root^.rootMenu

-- | Save a MenuSystem so it can be reloaded later for serving or use as a
--   subcomponent of another MenuSystem.
saveMenus :: MenuSystem -> IO ()
saveMenus ms = do
  existDirectory "graph"
  JS.encodeFile ("graph"</>"root") . MenuRoot (ClientState mempty) $ ms^.topMenu
  storeSubMenus ms

storeSubMenus :: MenuSystem -> IO ()
storeSubMenus ms = do
  existDirectory $ "graph" </> "menu"
  ifor_ (ms^.menuMap) $ \i m ->
    JS.encodeFile (("graph"</>"menu")</>(T.unpack i)) m

saveSubGraph :: Text -> MenuSystem -> IO ()
saveSubGraph subname ms = do
  existDirectory "graph"
  storeSubMenus ms
  existDirectory $ "graph" </> "subgraph"
  TIO.writeFile (("graph"</>"subgraph")</>(T.unpack subname)) (ms^.topMenu.mid)

refSubGraph :: Text -> IO Menu
refSubGraph subname = do
  mnId <- TIO.readFile (("graph"</>"subgraph")</>(T.unpack subname))
  loadMenu $ ("graph"</>"menu")</>(T.unpack mnId)