diff --git a/back/src/Guide/Api/Guider.hs b/back/src/Guide/Api/Guider.hs index c073de6..3367d3a 100644 --- a/back/src/Guide/Api/Guider.hs +++ b/back/src/Guide/Api/Guider.hs @@ -4,6 +4,7 @@ module Guide.Api.Guider ( Guider (..) , GuiderServer + , Context (..) , guiderToHandler ) where @@ -12,25 +13,34 @@ import Imports import Servant (Handler (..), ServantErr) import Servant.Server.Generic +import Guide.Api.Utils (RequestDetails) import Guide.Config (Config) +import Guide.State (DB) --- | Custom 'Guider' type holds the 'Config' always on hand. +-- | A type for Guide handlers. Provides access to everything in 'Context'. newtype Guider a = Guider - { runGuider :: ReaderT Config IO a - } deriving (Functor, Applicative, Monad, MonadIO, MonadReader Config) + { runGuider :: ReaderT Context IO a + } deriving (Functor, Applicative, Monad, MonadIO, MonadReader Context) + +-- | Context of Guider +data Context = Context + { cConfig :: Config + , cDB :: DB + , cDetails :: RequestDetails + } instance MonadError ServantErr Guider where throwError :: ServantErr -> Guider a throwError = liftIO . throwIO catchError :: Guider a -> (ServantErr -> Guider a) -> Guider a - catchError (Guider m) f = Guider $ ReaderT $ \config -> - runReaderT m config `catch` (\err -> runReaderT (runGuider (f err)) config) + catchError (Guider m) f = Guider $ ReaderT $ \context -> + runReaderT m context `catch` (\err -> runReaderT (runGuider (f err)) context) -- | The custom type won't be accepted by servant server without this conventor used with 'hoistServer'. -guiderToHandler :: Config -> Guider a -> Handler a -guiderToHandler config (Guider m) = Handler $ ExceptT $ try $ runReaderT m config +guiderToHandler :: Context -> Guider a -> Handler a +guiderToHandler context (Guider m) = Handler $ ExceptT $ try $ runReaderT m context -- | 'GuiderServer' used to create 'Guider' api. type GuiderServer = AsServerT Guider diff --git a/back/src/Guide/Api/Methods.hs b/back/src/Guide/Api/Methods.hs index c89de19..aadf69b 100644 --- a/back/src/Guide/Api/Methods.hs +++ b/back/src/Guide/Api/Methods.hs @@ -15,7 +15,7 @@ import Data.Aeson (encode) import Data.Text (Text) import Servant -import Guide.Api.Guider (Guider) +import Guide.Api.Guider (Context (..), Guider) import Guide.Api.Types import Guide.Api.Utils import Guide.Config (Config (..)) @@ -34,25 +34,23 @@ import qualified Guide.Search as Search ---------------------------------------------------------------------------- -- | Get a list of available categories. -getCategories :: DB -> Guider [CCategoryInfo] -getCategories db = do - dbQuery db GetCategories <&> \xs -> - map toCCategoryInfo xs +getCategories :: Guider [CCategoryInfo] +getCategories = dbQuery GetCategories <&> map toCCategoryInfo -- | Get a single category and all of its items. -getCategory :: DB -> Uid Category -> Guider CCategoryFull -getCategory db catId = toCCategoryFull <$> getCategoryOrFail db catId +getCategory :: Uid Category -> Guider CCategoryFull +getCategory catId = toCCategoryFull <$> getCategoryOrFail catId -- | Create a new category, given the title and the grandparent (aka group). -- -- Returns the ID of the created category (or of the existing one if the -- category with this title exists already). -createCategory :: DB -> RequestDetails -> Text -> Text -> Guider (Uid Category) -createCategory db requestDetails title' group' = do +createCategory :: Text -> Text -> Guider (Uid Category) +createCategory title' group' = do when (T.null title') $ throwError err400{errBody = "Title not provided"} when (T.null group') $ throwError err400{errBody = "Group' not provided"} -- If the category exists already, don't create it - cats <- view categories <$> dbQuery db GetGlobalState + cats <- view categories <$> dbQuery GetGlobalState let isDuplicate cat = T.toCaseFold (cat^.title) == T.toCaseFold title' && T.toCaseFold (cat^.group_) == T.toCaseFold group' case find isDuplicate cats of @@ -60,41 +58,38 @@ createCategory db requestDetails title' group' = do Nothing -> do catId <- randomShortUid time <- liftIO getCurrentTime - (edit, _) <- dbUpdate db (AddCategory catId title' group' time) - addEdit db requestDetails edit + addEdit . fst =<< dbUpdate (AddCategory catId title' group' time) return catId -- | Edit categoty's note. -setCategoryNotes :: DB -> RequestDetails -> Uid Category -> CTextEdit -> Guider NoContent -setCategoryNotes db requestDetails catId CTextEdit{..} = do - serverModified <- markdownBlockMdSource . _categoryNotes <$> getCategoryOrFail db catId +setCategoryNotes :: Uid Category -> CTextEdit -> Guider NoContent +setCategoryNotes catId CTextEdit{..} = do + serverModified <- markdownBlockMdSource . _categoryNotes <$> getCategoryOrFail catId checkConflict CTextEdit{..} serverModified - (edit, _) <- dbUpdate db (SetCategoryNotes catId $ unH cteModified) - addEdit db requestDetails edit + addEdit . fst =<< dbUpdate (SetCategoryNotes catId $ unH cteModified) pure NoContent -- | Edit category's info (title, group, status, sections (pro/con, ecosystem, note)). -setCategoryInfo :: DB -> RequestDetails -> Uid Category -> CCategoryInfoEdit -> Guider NoContent -setCategoryInfo db requestDetails catId CCategoryInfoEdit{..} = do - category <- getCategoryOrFail db catId +setCategoryInfo :: Uid Category -> CCategoryInfoEdit -> Guider NoContent +setCategoryInfo catId CCategoryInfoEdit{..} = do + category <- getCategoryOrFail catId -- TODO diff and merge - (editTitle, _) <- dbUpdate db $ SetCategoryTitle catId $ unH ccieTitle - (editGroup, _) <- dbUpdate db $ SetCategoryGroup catId $ unH ccieGroup - (editStatus, _) <- dbUpdate db $ SetCategoryStatus catId ccieStatus + (editTitle, _) <- dbUpdate $ SetCategoryTitle catId $ unH ccieTitle + (editGroup, _) <- dbUpdate $ SetCategoryGroup catId $ unH ccieGroup + (editStatus, _) <- dbUpdate $ SetCategoryStatus catId ccieStatus let oldEnabledSections = category ^. enabledSections let newEnabledSections = unH ccieSections - (editSection, _) <- dbUpdate db $ ChangeCategoryEnabledSections catId + (editSection, _) <- dbUpdate $ ChangeCategoryEnabledSections catId (newEnabledSections S.\\ oldEnabledSections) (oldEnabledSections S.\\ newEnabledSections) - mapM_ (addEdit db requestDetails) [editTitle, editGroup, editStatus, editSection] + mapM_ addEdit [editTitle, editGroup, editStatus, editSection] pure NoContent -- | Delete a category. -deleteCategory :: DB -> RequestDetails -> Uid Category -> Guider NoContent -deleteCategory db requestDetails catId = do - _ <- getCategoryOrFail db catId - dbUpdate db (DeleteCategory catId) >>= (mapM_ $ \edit -> do - addEdit db requestDetails edit) +deleteCategory :: Uid Category -> Guider NoContent +deleteCategory catId = do + _ <- getCategoryOrFail catId + dbUpdate (DeleteCategory catId) >>= mapM_ addEdit pure NoContent ---------------------------------------------------------------------------- @@ -105,78 +100,68 @@ deleteCategory db requestDetails catId = do -- -- Returns the ID of the created item. Unlike 'createCategory', allows items -- with duplicated names. -createItem :: DB -> RequestDetails -> Uid Category -> Text -> Guider (Uid Item) -createItem db requestDetails catId name' = do - _ <- getCategoryOrFail db catId +createItem :: Uid Category -> Text -> Guider (Uid Item) +createItem catId name' = do + _ <- getCategoryOrFail catId when (T.null name') $ throwError err400{errBody = "Name not provided"} itemId <- randomShortUid time <- liftIO getCurrentTime - (edit, _) <- dbUpdate db (AddItem catId itemId name' time) - addEdit db requestDetails edit + addEdit . fst =<< dbUpdate (AddItem catId itemId name' time) pure itemId -- TODO: move an item -- | Modify item info. Fields that are not present ('Nothing') are not modified. -setItemInfo :: DB -> RequestDetails -> Uid Item -> CItemInfoEdit -> Guider NoContent -setItemInfo db requestDetails itemId CItemInfoEdit{..} = do - _ <- getItemOrFail db itemId +setItemInfo :: Uid Item -> CItemInfoEdit -> Guider NoContent +setItemInfo itemId CItemInfoEdit{..} = do + _ <- getItemOrFail itemId -- TODO diff and merge - whenJust (unH ciieName) $ \ciieName' -> do - (editName, _) <- dbUpdate db $ SetItemName itemId ciieName' - addEdit db requestDetails editName - whenJust (unH ciieGroup) $ \ciieGroup' -> do - (editGroup, _) <- dbUpdate db $ SetItemGroup itemId ciieGroup' - addEdit db requestDetails editGroup - whenJust (unH ciieHackage) $ \ciieHackage' -> do - (editHackage, _) <- dbUpdate db $ SetItemHackage itemId ciieHackage' - addEdit db requestDetails editHackage + whenJust (unH ciieName) $ \ciieName' -> + addEdit . fst =<< dbUpdate (SetItemName itemId ciieName') + whenJust (unH ciieGroup) $ \ciieGroup' -> + addEdit . fst =<< dbUpdate (SetItemGroup itemId ciieGroup') + whenJust (unH ciieHackage) $ \ciieHackage' -> + addEdit . fst =<< dbUpdate (SetItemHackage itemId ciieHackage') whenJust (unH ciieLink) $ \ciieLink' -> do - (editLink, _) <- dbUpdate db $ SetItemLink itemId ciieLink' - addEdit db requestDetails editLink + addEdit . fst =<< dbUpdate (SetItemLink itemId ciieLink') pure NoContent -- | Set item's summary. -setItemSummary :: DB -> RequestDetails -> Uid Item -> CTextEdit -> Guider NoContent -setItemSummary db requestDetails itemId CTextEdit{..} = do - serverModified <- markdownBlockMdSource . _itemSummary <$> getItemOrFail db itemId +setItemSummary :: Uid Item -> CTextEdit -> Guider NoContent +setItemSummary itemId CTextEdit{..} = do + serverModified <- markdownBlockMdSource . _itemSummary <$> getItemOrFail itemId checkConflict CTextEdit{..} serverModified - (edit, _) <- dbUpdate db (SetItemSummary itemId $ unH cteModified) - addEdit db requestDetails edit + addEdit . fst =<< dbUpdate (SetItemSummary itemId $ unH cteModified) pure NoContent -- | Set item's ecosystem. -setItemEcosystem :: DB -> RequestDetails -> Uid Item -> CTextEdit -> Guider NoContent -setItemEcosystem db requestDetails itemId CTextEdit{..} = do - serverModified <- markdownBlockMdSource . _itemEcosystem <$> getItemOrFail db itemId +setItemEcosystem :: Uid Item -> CTextEdit -> Guider NoContent +setItemEcosystem itemId CTextEdit{..} = do + serverModified <- markdownBlockMdSource . _itemEcosystem <$> getItemOrFail itemId checkConflict CTextEdit{..} serverModified - (edit, _) <- dbUpdate db (SetItemEcosystem itemId $ unH cteModified) - addEdit db requestDetails edit + addEdit . fst =<< dbUpdate (SetItemEcosystem itemId $ unH cteModified) pure NoContent -- | Set item's notes. -setItemNotes :: DB -> RequestDetails -> Uid Item -> CTextEdit -> Guider NoContent -setItemNotes db requestDetails itemId CTextEdit{..} = do - serverModified <- markdownTreeMdSource . _itemNotes <$> getItemOrFail db itemId +setItemNotes :: Uid Item -> CTextEdit -> Guider NoContent +setItemNotes itemId CTextEdit{..} = do + serverModified <- markdownTreeMdSource . _itemNotes <$> getItemOrFail itemId checkConflict CTextEdit{..} serverModified - (edit, _) <- dbUpdate db (SetItemNotes itemId $ unH cteModified) - addEdit db requestDetails edit + addEdit . fst =<< dbUpdate (SetItemNotes itemId $ unH cteModified) pure NoContent -- | Delete an item. -deleteItem :: DB -> RequestDetails -> Uid Item -> Guider NoContent -deleteItem db requestDetails itemId = do - _ <- getItemOrFail db itemId - dbUpdate db (DeleteItem itemId) >>= (mapM_ $ \edit -> do - addEdit db requestDetails edit) +deleteItem :: Uid Item -> Guider NoContent +deleteItem itemId = do + _ <- getItemOrFail itemId + dbUpdate (DeleteItem itemId) >>= mapM_ addEdit pure NoContent -- | Move item up or down -moveItem :: DB -> RequestDetails -> Uid Item -> CMove -> Guider NoContent -moveItem db requestDetails itemId CMove{..} = do - _ <- getItemOrFail db itemId - edit <- dbUpdate db (MoveItem itemId (cmDirection == DirectionUp)) - addEdit db requestDetails edit +moveItem :: Uid Item -> CMove -> Guider NoContent +moveItem itemId CMove{..} = do + _ <- getItemOrFail itemId + addEdit =<< dbUpdate (MoveItem itemId (cmDirection == DirectionUp)) pure NoContent ---------------------------------------------------------------------------- @@ -186,39 +171,35 @@ moveItem db requestDetails itemId CMove{..} = do -- TODO: move a trait -- | Create a trait (pro/con). -createTrait :: DB -> RequestDetails -> Uid Item -> CCreateTrait -> Guider (Uid Trait) -createTrait db requestDetails itemId CCreateTrait{..} = do +createTrait :: Uid Item -> CCreateTrait -> Guider (Uid Trait) +createTrait itemId CCreateTrait{..} = do when (T.null cctContent) $ throwError err400{errBody = "Trait text not provided"} traitId <- randomShortUid - (edit, _) <- case cctType of - Con -> dbUpdate db (AddCon itemId traitId cctContent) - Pro -> dbUpdate db (AddPro itemId traitId cctContent) - addEdit db requestDetails edit + addEdit . fst =<< case cctType of + Con -> dbUpdate (AddCon itemId traitId cctContent) + Pro -> dbUpdate (AddPro itemId traitId cctContent) pure traitId -- | Update the text of a trait (pro/con). -setTrait :: DB -> RequestDetails -> Uid Item -> Uid Trait -> CTextEdit -> Guider NoContent -setTrait db requestDetails itemId traitId CTextEdit{..} = do - serverModified <- markdownInlineMdSource . _traitContent <$> getTraitOrFail db itemId traitId +setTrait :: Uid Item -> Uid Trait -> CTextEdit -> Guider NoContent +setTrait itemId traitId CTextEdit{..} = do + serverModified <- markdownInlineMdSource . _traitContent <$> getTraitOrFail itemId traitId checkConflict CTextEdit{..} serverModified - (edit, _) <- dbUpdate db (SetTraitContent itemId traitId $ unH cteModified) - addEdit db requestDetails edit + addEdit . fst =<< dbUpdate (SetTraitContent itemId traitId $ unH cteModified) pure NoContent -- | Delete a trait (pro/con). -deleteTrait :: DB -> RequestDetails -> Uid Item -> Uid Trait -> Guider NoContent -deleteTrait db requestDetails itemId traitId = do - _ <- getTraitOrFail db itemId traitId - dbUpdate db (DeleteTrait itemId traitId) >>= (mapM_ $ \edit -> do - addEdit db requestDetails edit) +deleteTrait :: Uid Item -> Uid Trait -> Guider NoContent +deleteTrait itemId traitId = do + _ <- getTraitOrFail itemId traitId + dbUpdate (DeleteTrait itemId traitId) >>= mapM_ addEdit pure NoContent -- | Move trait up or down -moveTrait :: DB -> RequestDetails -> Uid Item -> Uid Trait -> CMove -> Guider NoContent -moveTrait db requestDetails itemId traitId CMove{..} = do - _ <- getTraitOrFail db itemId traitId - edit <- dbUpdate db (MoveTrait itemId traitId (cmDirection == DirectionUp)) - addEdit db requestDetails edit +moveTrait :: Uid Item -> Uid Trait -> CMove -> Guider NoContent +moveTrait itemId traitId CMove{..} = do + _ <- getTraitOrFail itemId traitId + addEdit =<< dbUpdate (MoveTrait itemId traitId (cmDirection == DirectionUp)) pure NoContent ---------------------------------------------------------------------------- @@ -228,9 +209,9 @@ moveTrait db requestDetails itemId traitId CMove{..} = do -- | Site-wide search. -- -- Returns at most 100 results. -search :: DB -> Text -> Guider [CSearchResult] -search db searchQuery = do - gs <- dbQuery db GetGlobalState +search :: Text -> Guider [CSearchResult] +search searchQuery = do + gs <- dbQuery GetGlobalState pure $ map toCSearchResult $ take 100 $ Search.search searchQuery gs ---------------------------------------------------------------------------- @@ -238,48 +219,51 @@ search db searchQuery = do ---------------------------------------------------------------------------- -- | Update something in the database. -dbUpdate :: (MonadIO m, EventState event ~ GlobalState, UpdateEvent event) - => DB -> event -> m (EventResult event) -dbUpdate db x = liftIO $ do - Acid.update db SetDirty - Acid.update db x +dbUpdate :: (EventState event ~ GlobalState, UpdateEvent event) + => event -> Guider (EventResult event) +dbUpdate x = do + Context{..} <- ask + liftIO $ do + Acid.update cDB SetDirty + Acid.update cDB x -- | Read something from the database. -dbQuery :: (MonadIO m, EventState event ~ GlobalState, QueryEvent event) - => DB -> event -> m (EventResult event) -dbQuery db x = liftIO $ - Acid.query db x +dbQuery :: (EventState event ~ GlobalState, QueryEvent event) + => event -> Guider (EventResult event) +dbQuery x = do + Context{..} <- ask + liftIO $ Acid.query cDB x -- Call this whenever any user-made change is applied to the database. -addEdit :: DB -> RequestDetails -> Edit -> Guider () -addEdit db RequestDetails{..} edit = unless (isVacuousEdit edit) $ do +addEdit :: Edit -> Guider () +addEdit edit = unless (isVacuousEdit edit) $ do time <- liftIO getCurrentTime - Config{..} <- ask - dbUpdate db $ RegisterEdit edit rdIp time - dbUpdate db $ RegisterAction (Action'Edit edit) rdIp time _baseUrl rdReferer rdUserAgent + Context Config{..} _ RequestDetails{..} <- ask + dbUpdate $ RegisterEdit edit rdIp time + dbUpdate $ RegisterAction (Action'Edit edit) rdIp time _baseUrl rdReferer rdUserAgent -- | Helper. Get a category from database and throw error 404 when it doesn't exist. -getCategoryOrFail :: DB -> Uid Category -> Guider Category -getCategoryOrFail db catId = do - dbQuery db (GetCategoryMaybe catId) >>= \case - Nothing -> throwError $ err404 {errBody = "Category not found"} +getCategoryOrFail :: Uid Category -> Guider Category +getCategoryOrFail catId = do + dbQuery (GetCategoryMaybe catId) >>= \case + Nothing -> throwError $ err404 {errBody = "Category not found"} Just cat -> pure cat -- | Helper. Get an item from database and throw error 404 when the item doesn't exist. -getItemOrFail :: DB -> Uid Item -> Guider Item -getItemOrFail db itemId = do - dbQuery db (GetItemMaybe itemId) >>= \case - Nothing -> throwError $ err404 {errBody = "Item not found"} +getItemOrFail :: Uid Item -> Guider Item +getItemOrFail itemId = do + dbQuery (GetItemMaybe itemId) >>= \case + Nothing -> throwError $ err404 {errBody = "Item not found"} Just item -> pure item -- | Helper. Get a trait from database and throw error 404 when -- either the item or the trait doesn't exist. -getTraitOrFail :: DB -> Uid Item -> Uid Trait -> Guider Trait -getTraitOrFail db itemId traitId = do - dbQuery db (GetItemMaybe itemId) >>= \case - Nothing -> throwError $ err404 {errBody = "Item not found"} +getTraitOrFail :: Uid Item -> Uid Trait -> Guider Trait +getTraitOrFail itemId traitId = do + dbQuery (GetItemMaybe itemId) >>= \case + Nothing -> throwError $ err404 {errBody = "Item not found"} Just _ -> do - dbQuery db (GetTraitMaybe itemId traitId) >>= \case + dbQuery (GetTraitMaybe itemId traitId) >>= \case Nothing -> throwError $ err404 {errBody = "Trait not found"} Just trait -> pure trait diff --git a/back/src/Guide/Api/Server.hs b/back/src/Guide/Api/Server.hs index c7b877e..5318491 100644 --- a/back/src/Guide/Api/Server.hs +++ b/back/src/Guide/Api/Server.hs @@ -26,46 +26,45 @@ import Servant.Swagger.UI -- putStrLn that works well with concurrency import Say (say) -import Guide.Api.Guider (GuiderServer, guiderToHandler) +import Guide.Api.Guider (Context (..), GuiderServer, guiderToHandler) import Guide.Api.Methods import Guide.Api.Types -import Guide.Api.Utils (RequestDetails (..)) import Guide.Config (Config (..)) import Guide.State import Data.Acid as Acid import qualified Data.ByteString.Char8 as BSC -guiderServer :: DB -> RequestDetails -> Site GuiderServer -guiderServer db requestDetails = Site +guiderServer :: Site GuiderServer +guiderServer = Site { _categorySite = toServant (CategorySite - { _getCategories = getCategories db - , _getCategory = getCategory db - , _createCategory = createCategory db requestDetails - , _setCategoryNotes = setCategoryNotes db requestDetails - , _setCategoryInfo = setCategoryInfo db requestDetails - , _deleteCategory = deleteCategory db requestDetails } + { _getCategories = getCategories + , _getCategory = getCategory + , _createCategory = createCategory + , _setCategoryNotes = setCategoryNotes + , _setCategoryInfo = setCategoryInfo + , _deleteCategory = deleteCategory } :: CategorySite GuiderServer) , _itemSite = toServant (ItemSite - { _createItem = createItem db requestDetails - , _setItemInfo = setItemInfo db requestDetails - , _setItemSummary = setItemSummary db requestDetails - , _setItemEcosystem = setItemEcosystem db requestDetails - , _setItemNotes = setItemNotes db requestDetails - , _deleteItem = deleteItem db requestDetails - , _moveItem = moveItem db requestDetails } + { _createItem = createItem + , _setItemInfo = setItemInfo + , _setItemSummary = setItemSummary + , _setItemEcosystem = setItemEcosystem + , _setItemNotes = setItemNotes + , _deleteItem = deleteItem + , _moveItem = moveItem } :: ItemSite GuiderServer) , _traitSite = toServant (TraitSite - { _createTrait = createTrait db requestDetails - , _setTrait = setTrait db requestDetails - , _deleteTrait = deleteTrait db requestDetails - , _moveTrait = moveTrait db requestDetails } + { _createTrait = createTrait + , _setTrait = setTrait + , _deleteTrait = deleteTrait + , _moveTrait = moveTrait } :: TraitSite GuiderServer) , _searchSite = toServant (SearchSite - { _search = search db } + { _search = search } :: SearchSite GuiderServer) } @@ -84,8 +83,10 @@ fullServer db config = -- | 'hoistServer' brings custom type server to 'Handler' type server. Custem types not consumed by servant. api :: DB -> Config -> Server Api -api db config = hoistServer (Proxy @Api) (guiderToHandler config) - (\requestDetails -> toServant $ guiderServer db requestDetails) +api db config = do + requestDetails <- ask + hoistServer (Proxy @Api) (guiderToHandler (Context config db requestDetails)) + (const $ toServant guiderServer) -- | Serve the API on port 4400. --