hsdev-0.1.0.1: tools/Update.hs
{-# LANGUAGE FlexibleContexts, GeneralizedNewtypeDeriving, OverloadedStrings, MultiParamTypeClasses #-}
module Update (
Status(..), isStatus,
Settings(..),
UpdateDB,
updateDB,
setStatus, postStatus, waiter, updater, loadCache, runTask, runTasks,
readDB,
scanModule, scanModules, scanFile, scanCabal, scanProject, scanDirectory,
-- * Helpers
liftErrorT
) where
import Control.Applicative
import Control.Monad.CatchIO
import Control.Monad.Error
import Control.Monad.Reader
import Data.Aeson
import Data.Aeson.Types
import qualified Data.HashMap.Strict as HM
import Data.Map (Map)
import qualified Data.Map as M
import Data.Maybe (mapMaybe, isJust)
import Data.Monoid
import Data.Traversable (traverse)
import System.Directory (canonicalizePath)
import qualified HsDev.Cache.Structured as Cache
import HsDev.Database
import HsDev.Database.Async
import HsDev.Project
import HsDev.Symbols
import HsDev.Tools.HDocs
import qualified HsDev.Scan as S
import HsDev.Scan.Browse
import HsDev.Util ((.::))
data Status = Status {
status :: Value,
statusDetails :: [Pair] }
instance ToJSON Status where
toJSON (Status s ds) = object $ ("status" .= s) : ds
instance FromJSON Status where
parseJSON = withObject "status" $ \v -> Status <$> (v .:: "status") <*> pure (HM.toList $ HM.delete "status" v)
isStatus :: Value -> Bool
isStatus = isJust . parseMaybe (parseJSON :: Value -> Parser Status)
data Settings = Settings {
database :: Async Database,
databaseCacheReader :: (FilePath -> ErrorT String IO Structured) -> IO (Maybe Database),
onStatus :: Status -> IO (),
ghcOptions :: [String] }
newtype UpdateDB m a = UpdateDB { runUpdateDB :: ReaderT Settings m a }
deriving (Applicative, Monad, MonadIO, MonadCatchIO, Functor, MonadReader Settings)
-- | Run `UpdateDB` monad
updateDB :: Monad m => Settings -> ErrorT String (UpdateDB m) () -> m ()
updateDB sets act = runUpdateDB (runErrorT act >> return ()) `runReaderT` sets
-- | Set status details
setStatus :: MonadReader Settings m => [Pair] -> m a -> m a
setStatus ps act = local alter act where
alter st = st {
onStatus = \(Status s ds) -> onStatus st (Status s (ds ++ ps)) }
-- | Post status
postStatus :: (MonadIO m, MonadReader Settings m) => Value -> m ()
postStatus s = do
on' <- asks onStatus
liftIO $ on' $ Status s []
-- | Wait DB to complete actions
waiter :: (MonadIO m, MonadReader Settings m) => m () -> m ()
waiter act = do
db <- asks database
act
wait db
-- | Update task result to database
updater :: (MonadIO m, MonadReader Settings m) => m Database -> m ()
updater act = do
db <- asks database
act >>= update db . return
-- | Load data from cache and wait
loadCache :: (MonadIO m, MonadReader Settings m) => (FilePath -> ErrorT String IO Structured) -> m ()
loadCache act = do
cacheReader <- asks databaseCacheReader
mdat <- liftIO $ cacheReader act
case mdat of
Nothing -> return ()
Just dat -> waiter (updater (return dat))
-- | Run one task
runTask :: MonadIO m => Value -> ErrorT String (UpdateDB m) a -> ErrorT String (UpdateDB m) a
runTask v act = ["task" .= v] `setStatus` wrapAct act where
wrapAct :: MonadIO m => ErrorT String (UpdateDB m) a -> ErrorT String (UpdateDB m) a
wrapAct a = do
postStatus $ toJSON ("working" :: String)
x <- a
postStatus taskOk
return x
`catchError`
(\e -> postStatus (taskErr e) >> throwError e)
taskOk = toJSON ("ok" :: String)
taskErr e = object ["error" .= e]
-- | Run many tasks with numeration
runTasks :: Monad m => [ErrorT String (UpdateDB m) ()] -> ErrorT String (UpdateDB m) ()
runTasks ts = zipWithM_ taskNum [1..] (map noErr ts) where
total = length ts
taskNum n t = progress `setStatus` t where
progress = ["progress" .= object [
"current" .= (n :: Integer), "total" .= total]]
noErr v = v `mplus` return ()
-- | Get database value
readDB :: (MonadIO m, MonadReader Settings m) => m Database
readDB = asks database >>= liftIO . readAsync
-- | Scan module
scanModule :: MonadCatchIO m => [String] -> ModuleLocation -> ErrorT String (UpdateDB m) ()
scanModule opts mloc = runTask task' $ do
im <- liftErrorT $ S.scanModule opts mloc
updater $ return $ fromModule im
ErrorT $ return $ inspectionResult im
return ()
where
task' = object ["scanning" .= mloc]
-- | Scan modules
scanModules :: MonadCatchIO m => [String] -> [S.ModuleToScan] -> ErrorT String (UpdateDB m) ()
scanModules opts ms = do
db <- asks database
dbval <- readDB
runTask (toJSON ("updating projects files" :: String)) $ updater $ do
projects <- mapM (liftErrorT . S.scanProjectFile opts) ps
return $ mconcat $ map fromProject projects
ms' <- liftErrorT $ filterM (S.changedModule dbval opts . fst) ms
runTasks [scanModule (opts ++ snd m) (fst m) | m <- ms']
where
ps = mapMaybe (toProj . fst) ms
toProj (FileModule _ p) = fmap projectCabal p
toProj _ = Nothing
-- | Scan source file
scanFile :: MonadCatchIO m => [String] -> FilePath -> ErrorT String (UpdateDB m) ()
scanFile opts fpath = do
fpath' <- liftIO $ canonicalizePath fpath
mproj <- liftIO $ locateProject fpath'
let
mtarget = mproj >>= (`fileTarget` fpath')
fileExts = maybe [] (extensionsOpts . infoExtensions) mtarget
scanModule (opts ++ fileExts) (FileModule fpath' mproj)
-- | Scan cabal modules
scanCabal :: MonadCatchIO m => [String] -> Cabal -> ErrorT String (UpdateDB m) ()
scanCabal opts sandbox = do
loadCache $ Cache.loadCabal sandbox
dbval <- readDB
ms <- runTask
(object ["action" .= ("loading modules" :: String), "sandbox" .= sandbox]) $
liftErrorT $ browseFilter opts sandbox (S.changedModule dbval opts)
docs <- runTask
(object ["action" .= ("loading docs" :: String), "sandbox" .= sandbox]) $
liftErrorT $ hdocsCabal sandbox opts
updater $ return $ mconcat $ map (fromModule . fmap (setDocs' docs)) ms
where
setDocs' :: Map String (Map String String) -> Module -> Module
setDocs' docs m = maybe m (`setDocs` m) $ M.lookup (moduleName m) docs
-- | Scan project
scanProject :: MonadCatchIO m => [String] -> FilePath -> ErrorT String (UpdateDB m) ()
scanProject opts cabal = do
proj <- liftErrorT $ S.scanProjectFile opts cabal
loadCache $ Cache.loadProject $ projectCabal proj
(_, sources) <- liftErrorT $ S.enumProject proj
scanModules opts sources
-- | Scan directory for source files and projects
scanDirectory :: MonadCatchIO m => [String] -> FilePath -> ErrorT String (UpdateDB m) ()
scanDirectory opts dir = do
(projSrcs, standSrcs) <- runTask (toJSON ("getting list of sources" :: String)) $ liftErrorT $ S.enumDirectory dir
forM_ projSrcs $ \(p, _) -> loadCache (Cache.loadProject $ projectCabal p)
loadCache $ Cache.loadFiles $ mapMaybe (moduleSource . fst) standSrcs
scanModules opts (concatMap snd projSrcs ++ standSrcs)
-- | Lift errors
liftErrorT :: MonadIO m => ErrorT String IO a -> ErrorT String m a
liftErrorT = mapErrorT liftIO
-- | Merge two JSON object
union :: Value -> Value -> Value
union (Object l) (Object r) = Object $ HM.union l r
union _ _ = error "Commands.union: impossible happened"