diff --git a/src/Vervis.hs b/src/Vervis.hs
index 42170c1..c2db79b 100644
--- a/src/Vervis.hs
+++ b/src/Vervis.hs
@@ -13,406 +13,7 @@
- .
-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE DeriveGeneric #-}
-
module Vervis
- ( UserID (..)
- , GroupID (..)
- , RepoID (..)
- , ProjID (..)
- , Username (..)
- , PasswordHash (..)
- , RealName (..)
- , EmailAddress (..)
- , GroupName (..)
- , RepoName (..)
- , ProjectName (..)
- , User (..)
- , Group (..)
- , IrcChannel (..)
- , Repository (..)
- , Project (..)
- , Vervis ()
- , runVervis
- , saveState
- , createUser
- , Server (..)--TODO remove this type later...
- , subdirs
- , lastChange
- , timeAgo
- , timesAgo
+ (
)
where
-
-import Control.Monad (join)
-import Control.Monad.Fix (MonadFix)
-import Control.Monad.IO.Class
-import Control.Monad.Trans.RWS (RWST (..))
-import Data.Aeson
-import Data.CaseInsensitive (CI)
-import Data.Foldable (toList)
-import Data.Git
-import Data.Git.Revision
-import Data.Git.Repository
-import Data.Hashable (Hashable)
-import Data.HashMap.Lazy (HashMap)
-import Data.HashSet (HashSet)
-import Data.Hourglass
-import Data.JsonState
-import Data.Maybe (fromMaybe, mapMaybe)
-import Data.Text (Text)
-import Data.Time.Units
-import GHC.Generics
-import System.Directory.Tree hiding (name, file, err)
-import System.FilePath ((>))
-import System.Hourglass (dateCurrent)
-
-import qualified Control.Monad.Trans.RWS as RWS
-import qualified Data.CaseInsensitive as CI
-import qualified Data.HashMap.Lazy as M
-import qualified Data.Text as T
-
--------------------------------------------------------------------------------
--- Types
--------------------------------------------------------------------------------
-
-newtype UserID = UserID { unUserID :: Int }
- deriving (Eq, Hashable, FromJSON, ToJSON, WrappedText)
-
-newtype GroupID = GroupID { unGroupID :: Int }
- deriving (Eq, Hashable, FromJSON, ToJSON, WrappedText)
-
-newtype RepoID = RepoID { unRepoID :: Int } deriving (Eq, Hashable, WrappedText)
-
-newtype ProjID = ProjID { unProjID :: Int }
- deriving (Eq, Hashable, FromJSON, ToJSON, WrappedText)
-
-newtype Username = Username { unUsername :: CI Text }
- deriving (FromJSON, ToJSON)
-
-newtype PasswordHash = PasswordHash { unPasswordHash :: Text }
- deriving (FromJSON, ToJSON)
-
-newtype RealName = RealName { unRealName :: Text } deriving (FromJSON, ToJSON)
-
-data EmailAddress = EmailAddress
- { emailUser :: Text
- , emailHost :: Text
- }
- deriving Generic
-
-instance FromJSON EmailAddress
-instance ToJSON EmailAddress
-
-newtype GroupName = GroupName { unGroupName :: CI Text }
- deriving (FromJSON, ToJSON)
-
-newtype RepoName = RepoName { unRepoName :: CI Text }
- deriving (FromJSON, ToJSON, WrappedText)
-
-newtype ProjectName = ProjectName { unProjectName :: CI Text }
- deriving (FromJSON, ToJSON, WrappedText)
-
-data User = User
- { userName :: Username
- , userPassHash :: Maybe PasswordHash -- to disable pass and use SSH only?
- , userRealName :: RealName
- , userEmail :: EmailAddress
- }
- deriving Generic
-
-instance FromJSON User
-instance ToJSON User
-
-data Group = Group
- { groupName :: GroupName
- , groupUsers :: HashSet UserID
- }
- deriving Generic
-
-instance FromJSON Group
-instance ToJSON Group
-
-data IrcChannel = IrcChannel
- { chanNetwork :: Text
- , chanName :: Text
- }
- deriving Generic
-
-instance FromJSON IrcChannel
-instance ToJSON IrcChannel
-
-data Repository = Repository
- { repoName :: RepoName
- , repoIRC :: Maybe IrcChannel
- , repoML :: Maybe Text
- }
- deriving Generic
-
-instance ToJSON Repository
-instance FromJSON Repository
-
-data Project = Project
- { projName :: ProjectName
- , projRepos :: HashMap RepoID Repository
- }
- deriving Generic
-
-instance ToJSON Project
-instance FromJSON Project
-
-data VervisEnv = VervisEnv
- { veName :: Text
- , veDir :: FilePath
- , veSave :: VervisState -> Vervis ()
- }
-
-data VervisState = VervisState
- { vsUsers :: HashMap UserID User
- , vsGroups :: HashMap GroupID Group
- , vsProjects :: HashMap (Either UserID GroupID) (HashMap ProjID Project)
- , vsNextUser :: UserID
- , vsNextGroup :: GroupID
- , vsNextProject :: ProjID
- }
- deriving Generic
-
-instance ToJSON VervisState
-instance FromJSON VervisState
-
-newtype Vervis a = Vervis { unVervis :: RWST VervisEnv () VervisState IO a }
- deriving (Functor, Applicative, Monad, MonadFix, MonadIO)
-
--------------------------------------------------------------------------------
--- Instances
--------------------------------------------------------------------------------
-
-instance (CI.FoldCase a, FromJSON a) => FromJSON (CI a) where
- parseJSON v = CI.mk <$> parseJSON v
-
-instance ToJSON a => ToJSON (CI a) where
- toJSON = toJSON . CI.original
-
-class WrappedText a where
- toText :: a -> Text
- fromText :: Text -> a
-
-instance WrappedText Text where
- toText = id
- fromText = id
-
-instance (WrappedText a, CI.FoldCase a) => WrappedText (CI a) where
- toText = toText . CI.original
- fromText = CI.mk . fromText
-
-instance WrappedText Int where
- toText = T.pack . show
- fromText = read . T.unpack
-
-mapFst :: (a -> c) -> [(a, b)] -> [(c, b)]
-mapFst f = map $ \ (x, y) -> (f x, y)
-
-instance (Eq k, Hashable k, WrappedText k, FromJSON v) => FromJSON (HashMap k v) where
- parseJSON v = M.fromList . mapFst fromText . M.toList <$> parseJSON v
-
-instance (WrappedText k, ToJSON v) => ToJSON (HashMap k v) where
- toJSON = toJSON . M.fromList . mapFst toText . M.toList
-
-instance (WrappedText a, WrappedText b) => WrappedText (Either a b) where
- toText (Left x) = 'l' `T.cons` toText x
- toText (Right y) = 'r' `T.cons` toText y
- fromText t =
- case T.uncons t of
- Nothing -> error "blank JSON field name???"
- Just ('l', r) -> Left $ fromText r
- Just ('r', r) -> Right $ fromText r
- _ -> error "what is dis ting"
-
--------------------------------------------------------------------------------
--- Monad
--------------------------------------------------------------------------------
-
--- | Fetch the value of the environment.
-ask :: Vervis (VervisEnv)
-ask = Vervis RWS.ask
-
--- | Retrieve a function of the current environment.
-asks :: (VervisEnv -> a) -> Vervis a
-asks = Vervis . RWS.asks
-
--- | Fetch the current value of the state within the monad.
-get :: Vervis (VervisState)
-get = Vervis RWS.get
-
--- | Get a specific component of the state, using a projection function
--- supplied.
-gets :: (VervisState -> a) -> Vervis a
-gets = Vervis . RWS.gets
-
--- | @'put' s@ sets the state within the monad to @s@.
-put :: VervisState -> Vervis ()
-put = Vervis . RWS.put
-
--- | @'modify' f@ is an action that updates the state to the result of
--- applying @f@ to the current state.
-modify :: (VervisState -> VervisState) -> Vervis ()
-modify = Vervis . RWS.modify
-
--------------------------------------------------------------------------------
--- Operations
--------------------------------------------------------------------------------
-
--- internal func, wrap with API func which hides env and state details
-runVervis' :: VervisEnv -> VervisState -> Vervis a -> IO (a, VervisState)
-runVervis' venv vstate computation = do
- let rwst = unVervis computation
- (a, s, _) <- runRWST rwst venv vstate
- return (a, s)
-
--- | Run a Vervis server computation.
-runVervis
- :: Text -- ^ Server name, e.g. @hub.vervis.org@
- -> FilePath -- ^ Path of database file, which is really JSON currently
- -> FilePath -- ^ Path to the directory containing the namespace/repo tree
- -> Vervis a -- ^ Computation to run
- -> IO a
-runVervis name file dir comp = do
- result <- loadState file
- case result of
- Left (False, err) -> error $ "Loading JSON state failed: " ++ err
- Left (True, err) -> error $ "Parsing JSON state failed: " ++ err
- Right vstate -> do
- save <- mkSaveState (3 :: Second) file
- let venv = VervisEnv
- { veName = name
- , veDir = dir
- , veSave = liftIO . save
- }
- (a, _s) <- runVervis' venv vstate comp
- return a
-
-saveState :: Vervis ()
-saveState = do
- save <- asks veSave
- vstate <- get
- save vstate
-
-createUser :: User -> Vervis ()
-createUser user = do
- vstate <- get
- let users = vsUsers vstate
- next = vsNextUser vstate
- users' = M.insert next user users
- next' = UserID $ unUserID next + 1
- put vstate { vsUsers = users', vsNextUser = next' }
- saveState
-
--------------------------------------------------------------------------------
--- Git Utils
--------------------------------------------------------------------------------
-
-data Server = Server
- { serverName :: Text
- , serverDir :: FilePath
- , serverUsers :: HashMap Int User
- , serverGroups :: HashMap Int Group
- , serverRepos :: HashMap (Either Int Int) [Repository]
- }
-
-subdirs :: FilePath -> IO [FilePath]
-subdirs dir = do
- _base :/ tree <- buildL dir
- return $ case tree of
- Dir _ cs ->
- let dirName (Dir n _) = Just n
- dirName _ = Nothing
- in mapMaybe dirName cs
- _ -> []
-
-lastBranchChange :: Git -> String -> IO GitTime
-lastBranchChange git branch = do
- mref <- resolveRevision git $ Revision branch []
- mco <- traverse (getCommitMaybe git) mref
- let mtime = fmap (personTime . commitCommitter) (join mco)
- return $ fromMaybe (error "mtime is Nothing") mtime
-
-lastChange :: FilePath -> IO DateTime
-lastChange path = withRepo (fromString path) $ \ git -> do
- --TODO add a better intro to json-state, the docs are bad there
-
- names <- branchList git
- times <- traverse (lastBranchChange git) $ map refNameRaw $ toList names
- let datetimes = map timeConvert times
- return $ maximum datetimes
-
-showPeriod :: Period -> String
-showPeriod (Period 0 0 d) = show d ++ " days"
-showPeriod (Period 0 m _) = show m ++ " months"
-showPeriod (Period y _ _) = show y ++ " years"
-
-showDuration :: Duration -> String
-showDuration (Duration (Hours h) (Minutes m) (Seconds s) _) =
- case (h, m, s) of
- (0, 0, 0) -> "now"
- (0, 0, _) -> show s ++ " seconds"
- (0, _, _) -> show m ++ " minutes"
- _ -> show h ++ " hours"
-
-showAgo :: Period -> Duration -> String
-showAgo (Period 0 0 0) d = showDuration d
-showAgo p _ = showPeriod p
-
-fromSec :: Seconds -> (Period, Duration)
-fromSec sec =
- let d = 3600 * 24
- m = 30 * d
- y = 365 * d
- fs (Seconds n) = fromIntegral n
- (years, yrest) = sec `divMod` Seconds y
- (months, mrest) = yrest `divMod` Seconds m
- (days, drest) = mrest `divMod` Seconds d
- in (Period (fs years) (fs months) (fs days), fst $ fromSeconds drest)
-
-timeAgo :: DateTime -> IO String
-timeAgo dt = do
- now <- dateCurrent
- let sec = timeDiff now dt
- (period, duration) = fromSec sec
- return $ showAgo period duration
-
-repoPaths :: Server -> Either Int Int -> [Repository] -> [FilePath]
-repoPaths server (Left uid) repos =
- case M.lookup uid $ serverUsers server of
- Nothing -> error "';..;'"
- Just user ->
- let dir = serverDir server
- ns = T.unpack $ CI.original $ unUsername $ userName user
- prefix = dir > ns
- repoNames =
- map (T.unpack . CI.original . unRepoName . repoName) repos
- in map (prefix >) repoNames
-repoPaths server (Right gid) repos =
- case M.lookup gid $ serverGroups server of
- Nothing -> error "';..;'"
- Just group ->
- let dir = serverDir server
- ns = T.unpack $ CI.original $ unGroupName $ groupName group
- prefix = dir > ns
- repoNames =
- map (T.unpack . CI.original . unRepoName . repoName) repos
- in map (prefix >) repoNames
-
-timesAgo :: Server -> IO [(Text, Text)]
-timesAgo server = do
- -- make list of file paths
- let paths = uncurry $ repoPaths server
- nsRepos = map paths $ M.toList $ serverRepos server
- repos = concat nsRepos
- -- run lastChange on each
- times <- traverse lastChange repos
- -- run timeAgo on each result
- agos <- traverse timeAgo times
- -- return
- return $ zip (map T.pack repos) (map T.pack agos)
diff --git a/src/Vervis/Git.hs b/src/Vervis/Git.hs
new file mode 100644
index 0000000..3cc4e7d
--- /dev/null
+++ b/src/Vervis/Git.hs
@@ -0,0 +1,160 @@
+{- This file is part of Vervis.
+ -
+ - Written in 2016 by fr33domlover .
+ -
+ - ♡ Copying is an act of love. Please copy, reuse and share.
+ -
+ - The author(s) have dedicated all copyright and related and neighboring
+ - rights to this software to the public domain worldwide. This software is
+ - distributed without any warranty.
+ -
+ - You should have received a copy of the CC0 Public Domain Dedication along
+ - with this software. If not, see
+ - .
+ -}
+
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE DeriveGeneric #-}
+
+module Vervis.Git
+ ( Server (..)--TODO remove this type later...
+ , subdirs
+ , lastChange
+ , timeAgo
+ , timesAgo
+ )
+where
+
+import Control.Monad (join)
+import Control.Monad.Fix (MonadFix)
+import Control.Monad.IO.Class
+import Control.Monad.Trans.RWS (RWST (..))
+import Data.Aeson
+import Data.CaseInsensitive (CI)
+import Data.Foldable (toList)
+import Data.Git
+import Data.Git.Revision
+import Data.Git.Repository
+import Data.Hashable (Hashable)
+import Data.HashMap.Lazy (HashMap)
+import Data.HashSet (HashSet)
+import Data.Hourglass
+import Data.JsonState
+import Data.Maybe (fromMaybe, mapMaybe)
+import Data.Text (Text)
+import Data.Time.Units
+import GHC.Generics
+import System.Directory.Tree hiding (name, file, err)
+import System.FilePath ((>))
+import System.Hourglass (dateCurrent)
+import Vervis.Types
+
+import qualified Control.Monad.Trans.RWS as RWS
+import qualified Data.CaseInsensitive as CI
+import qualified Data.HashMap.Lazy as M
+import qualified Data.Text as T
+
+data Server = Server
+ { serverName :: Text
+ , serverDir :: FilePath
+ , serverUsers :: HashMap Int User
+ , serverGroups :: HashMap Int Group
+ , serverRepos :: HashMap (Either Int Int) [Repository]
+ }
+
+subdirs :: FilePath -> IO [FilePath]
+subdirs dir = do
+ _base :/ tree <- buildL dir
+ return $ case tree of
+ Dir _ cs ->
+ let dirName (Dir n _) = Just n
+ dirName _ = Nothing
+ in mapMaybe dirName cs
+ _ -> []
+
+lastBranchChange :: Git -> String -> IO GitTime
+lastBranchChange git branch = do
+ mref <- resolveRevision git $ Revision branch []
+ mco <- traverse (getCommitMaybe git) mref
+ let mtime = fmap (personTime . commitCommitter) (join mco)
+ return $ fromMaybe (error "mtime is Nothing") mtime
+
+lastChange :: FilePath -> IO DateTime
+lastChange path = withRepo (fromString path) $ \ git -> do
+ --TODO add a better intro to json-state, the docs are bad there
+
+ names <- branchList git
+ times <- traverse (lastBranchChange git) $ map refNameRaw $ toList names
+ let datetimes = map timeConvert times
+ return $ maximum datetimes
+
+showPeriod :: Period -> String
+showPeriod (Period 0 0 d) = show d ++ " days"
+showPeriod (Period 0 m _) = show m ++ " months"
+showPeriod (Period y _ _) = show y ++ " years"
+
+showDuration :: Duration -> String
+showDuration (Duration (Hours h) (Minutes m) (Seconds s) _) =
+ case (h, m, s) of
+ (0, 0, 0) -> "now"
+ (0, 0, _) -> show s ++ " seconds"
+ (0, _, _) -> show m ++ " minutes"
+ _ -> show h ++ " hours"
+
+showAgo :: Period -> Duration -> String
+showAgo (Period 0 0 0) d = showDuration d
+showAgo p _ = showPeriod p
+
+fromSec :: Seconds -> (Period, Duration)
+fromSec sec =
+ let d = 3600 * 24
+ m = 30 * d
+ y = 365 * d
+ fs (Seconds n) = fromIntegral n
+ (years, yrest) = sec `divMod` Seconds y
+ (months, mrest) = yrest `divMod` Seconds m
+ (days, drest) = mrest `divMod` Seconds d
+ in (Period (fs years) (fs months) (fs days), fst $ fromSeconds drest)
+
+timeAgo :: DateTime -> IO String
+timeAgo dt = do
+ now <- dateCurrent
+ let sec = timeDiff now dt
+ (period, duration) = fromSec sec
+ return $ showAgo period duration
+
+repoPaths :: Server -> Either Int Int -> [Repository] -> [FilePath]
+repoPaths server (Left uid) repos =
+ case M.lookup uid $ serverUsers server of
+ Nothing -> error "';..;'"
+ Just user ->
+ let dir = serverDir server
+ ns = T.unpack $ CI.original $ unUsername $ userName user
+ prefix = dir > ns
+ repoNames =
+ map (T.unpack . CI.original . unRepoName . repoName) repos
+ in map (prefix >) repoNames
+repoPaths server (Right gid) repos =
+ case M.lookup gid $ serverGroups server of
+ Nothing -> error "';..;'"
+ Just group ->
+ let dir = serverDir server
+ ns = T.unpack $ CI.original $ unGroupName $ groupName group
+ prefix = dir > ns
+ repoNames =
+ map (T.unpack . CI.original . unRepoName . repoName) repos
+ in map (prefix >) repoNames
+
+timesAgo :: Server -> IO [(Text, Text)]
+timesAgo server = do
+ -- make list of file paths
+ let paths = uncurry $ repoPaths server
+ nsRepos = map paths $ M.toList $ serverRepos server
+ repos = concat nsRepos
+ -- run lastChange on each
+ times <- traverse lastChange repos
+ -- run timeAgo on each result
+ agos <- traverse timeAgo times
+ -- return
+ return $ zip (map T.pack repos) (map T.pack agos)
diff --git a/src/Vervis/Monad.hs b/src/Vervis/Monad.hs
new file mode 100644
index 0000000..8e0800d
--- /dev/null
+++ b/src/Vervis/Monad.hs
@@ -0,0 +1,138 @@
+{- This file is part of Vervis.
+ -
+ - Written in 2016 by fr33domlover .
+ -
+ - ♡ Copying is an act of love. Please copy, reuse and share.
+ -
+ - The author(s) have dedicated all copyright and related and neighboring
+ - rights to this software to the public domain worldwide. This software is
+ - distributed without any warranty.
+ -
+ - You should have received a copy of the CC0 Public Domain Dedication along
+ - with this software. If not, see
+ - .
+ -}
+
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE DeriveGeneric #-}
+
+module Vervis.Monad
+ ( VervisEnv (..)
+ , VervisState (..)
+ , Vervis ()
+ , runVervis
+ , ask
+ , asks
+ , get
+ , gets
+ , put
+ , modify
+ )
+where
+
+import Control.Monad (join)
+import Control.Monad.Fix (MonadFix)
+import Control.Monad.IO.Class
+import Control.Monad.Trans.RWS (RWST (..))
+import Data.Aeson
+import Data.CaseInsensitive (CI)
+import Data.Foldable (toList)
+import Data.Git
+import Data.Git.Revision
+import Data.Git.Repository
+import Data.Hashable (Hashable)
+import Data.HashMap.Lazy (HashMap)
+import Data.HashSet (HashSet)
+import Data.Hourglass
+import Data.JsonState
+import Data.Maybe (fromMaybe, mapMaybe)
+import Data.Text (Text)
+import Data.Time.Units
+import GHC.Generics
+import System.Directory.Tree hiding (name, file, err)
+import System.FilePath ((>))
+import System.Hourglass (dateCurrent)
+import Vervis.Types
+
+import qualified Control.Monad.Trans.RWS as RWS
+import qualified Data.CaseInsensitive as CI
+import qualified Data.HashMap.Lazy as M
+import qualified Data.Text as T
+
+data VervisEnv = VervisEnv
+ { veName :: Text
+ , veDir :: FilePath
+ , veSave :: VervisState -> Vervis ()
+ }
+
+data VervisState = VervisState
+ { vsUsers :: HashMap UserID User
+ , vsGroups :: HashMap GroupID Group
+ , vsProjects :: HashMap (Either UserID GroupID) (HashMap ProjID Project)
+ , vsNextUser :: UserID
+ , vsNextGroup :: GroupID
+ , vsNextProject :: ProjID
+ }
+ deriving Generic
+
+instance ToJSON VervisState
+instance FromJSON VervisState
+
+newtype Vervis a = Vervis { unVervis :: RWST VervisEnv () VervisState IO a }
+ deriving (Functor, Applicative, Monad, MonadFix, MonadIO)
+
+-- internal func, wrap with API func which hides env and state details
+runVervis' :: VervisEnv -> VervisState -> Vervis a -> IO (a, VervisState)
+runVervis' venv vstate computation = do
+ let rwst = unVervis computation
+ (a, s, _) <- runRWST rwst venv vstate
+ return (a, s)
+
+-- | Run a Vervis server computation.
+runVervis
+ :: Text -- ^ Server name, e.g. @hub.vervis.org@
+ -> FilePath -- ^ Path of database file, which is really JSON currently
+ -> FilePath -- ^ Path to the directory containing the namespace/repo tree
+ -> Vervis a -- ^ Computation to run
+ -> IO a
+runVervis name file dir comp = do
+ result <- loadState file
+ case result of
+ Left (False, err) -> error $ "Loading JSON state failed: " ++ err
+ Left (True, err) -> error $ "Parsing JSON state failed: " ++ err
+ Right vstate -> do
+ save <- mkSaveState (3 :: Second) file
+ let venv = VervisEnv
+ { veName = name
+ , veDir = dir
+ , veSave = liftIO . save
+ }
+ (a, _s) <- runVervis' venv vstate comp
+ return a
+
+-- | Fetch the value of the environment.
+ask :: Vervis VervisEnv
+ask = Vervis RWS.ask
+
+-- | Retrieve a function of the current environment.
+asks :: (VervisEnv -> a) -> Vervis a
+asks = Vervis . RWS.asks
+
+-- | Fetch the current value of the state within the monad.
+get :: Vervis VervisState
+get = Vervis RWS.get
+
+-- | Get a specific component of the state, using a projection function
+-- supplied.
+gets :: (VervisState -> a) -> Vervis a
+gets = Vervis . RWS.gets
+
+-- | @'put' s@ sets the state within the monad to @s@.
+put :: VervisState -> Vervis ()
+put = Vervis . RWS.put
+
+-- | @'modify' f@ is an action that updates the state to the result of
+-- applying @f@ to the current state.
+modify :: (VervisState -> VervisState) -> Vervis ()
+modify = Vervis . RWS.modify
diff --git a/src/Vervis/Ops.hs b/src/Vervis/Ops.hs
new file mode 100644
index 0000000..c18cd60
--- /dev/null
+++ b/src/Vervis/Ops.hs
@@ -0,0 +1,69 @@
+{- This file is part of Vervis.
+ -
+ - Written in 2016 by fr33domlover .
+ -
+ - ♡ Copying is an act of love. Please copy, reuse and share.
+ -
+ - The author(s) have dedicated all copyright and related and neighboring
+ - rights to this software to the public domain worldwide. This software is
+ - distributed without any warranty.
+ -
+ - You should have received a copy of the CC0 Public Domain Dedication along
+ - with this software. If not, see
+ - .
+ -}
+
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE DeriveGeneric #-}
+
+module Vervis.Ops
+ ( saveState
+ , createUser
+ )
+where
+
+import Control.Monad (join)
+import Control.Monad.Fix (MonadFix)
+import Control.Monad.IO.Class
+import Data.Aeson
+import Data.CaseInsensitive (CI)
+import Data.Foldable (toList)
+import Data.Git
+import Data.Git.Revision
+import Data.Git.Repository
+import Data.Hashable (Hashable)
+import Data.HashMap.Lazy (HashMap)
+import Data.HashSet (HashSet)
+import Data.Hourglass
+import Data.JsonState
+import Data.Maybe (fromMaybe, mapMaybe)
+import Data.Text (Text)
+import Data.Time.Units
+import GHC.Generics
+import System.Directory.Tree hiding (name, file, err)
+import System.FilePath ((>))
+import System.Hourglass (dateCurrent)
+import Vervis.Monad
+import Vervis.Types
+
+import qualified Control.Monad.Trans.RWS as RWS
+import qualified Data.CaseInsensitive as CI
+import qualified Data.HashMap.Lazy as M
+import qualified Data.Text as T
+
+saveState :: Vervis ()
+saveState = do
+ save <- asks veSave
+ vstate <- get
+ save vstate
+
+createUser :: User -> Vervis ()
+createUser user = do
+ vstate <- get
+ let users = vsUsers vstate
+ next = vsNextUser vstate
+ users' = M.insert next user users
+ next' = UserID $ unUserID next + 1
+ put vstate { vsUsers = users', vsNextUser = next' }
+ saveState
diff --git a/src/Vervis/Types.hs b/src/Vervis/Types.hs
new file mode 100644
index 0000000..bab26e7
--- /dev/null
+++ b/src/Vervis/Types.hs
@@ -0,0 +1,200 @@
+{- This file is part of Vervis.
+ -
+ - Written in 2016 by fr33domlover .
+ -
+ - ♡ Copying is an act of love. Please copy, reuse and share.
+ -
+ - The author(s) have dedicated all copyright and related and neighboring
+ - rights to this software to the public domain worldwide. This software is
+ - distributed without any warranty.
+ -
+ - You should have received a copy of the CC0 Public Domain Dedication along
+ - with this software. If not, see
+ - .
+ -}
+
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE DeriveGeneric #-}
+
+module Vervis.Types
+ ( UserID (..)
+ , GroupID (..)
+ , RepoID (..)
+ , ProjID (..)
+ , Username (..)
+ , PasswordHash (..)
+ , RealName (..)
+ , EmailAddress (..)
+ , GroupName (..)
+ , RepoName (..)
+ , ProjectName (..)
+ , User (..)
+ , Group (..)
+ , IrcChannel (..)
+ , Repository (..)
+ , Project (..)
+ )
+where
+
+import Control.Monad (join)
+import Control.Monad.Fix (MonadFix)
+import Control.Monad.IO.Class
+import Control.Monad.Trans.RWS (RWST (..))
+import Data.Aeson
+import Data.CaseInsensitive (CI)
+import Data.Foldable (toList)
+import Data.Git
+import Data.Git.Revision
+import Data.Git.Repository
+import Data.Hashable (Hashable)
+import Data.HashMap.Lazy (HashMap)
+import Data.HashSet (HashSet)
+import Data.Hourglass
+import Data.JsonState
+import Data.Maybe (fromMaybe, mapMaybe)
+import Data.Text (Text)
+import Data.Time.Units
+import GHC.Generics
+import System.Directory.Tree hiding (name, file, err)
+import System.FilePath ((>))
+import System.Hourglass (dateCurrent)
+
+import qualified Control.Monad.Trans.RWS as RWS
+import qualified Data.CaseInsensitive as CI
+import qualified Data.HashMap.Lazy as M
+import qualified Data.Text as T
+
+-------------------------------------------------------------------------------
+-- Types
+-------------------------------------------------------------------------------
+
+newtype UserID = UserID { unUserID :: Int }
+ deriving (Eq, Hashable, FromJSON, ToJSON, WrappedText)
+
+newtype GroupID = GroupID { unGroupID :: Int }
+ deriving (Eq, Hashable, FromJSON, ToJSON, WrappedText)
+
+newtype RepoID = RepoID { unRepoID :: Int } deriving (Eq, Hashable, WrappedText)
+
+newtype ProjID = ProjID { unProjID :: Int }
+ deriving (Eq, Hashable, FromJSON, ToJSON, WrappedText)
+
+newtype Username = Username { unUsername :: CI Text }
+ deriving (FromJSON, ToJSON)
+
+newtype PasswordHash = PasswordHash { unPasswordHash :: Text }
+ deriving (FromJSON, ToJSON)
+
+newtype RealName = RealName { unRealName :: Text } deriving (FromJSON, ToJSON)
+
+data EmailAddress = EmailAddress
+ { emailUser :: Text
+ , emailHost :: Text
+ }
+ deriving Generic
+
+instance FromJSON EmailAddress
+instance ToJSON EmailAddress
+
+newtype GroupName = GroupName { unGroupName :: CI Text }
+ deriving (FromJSON, ToJSON)
+
+newtype RepoName = RepoName { unRepoName :: CI Text }
+ deriving (FromJSON, ToJSON, WrappedText)
+
+newtype ProjectName = ProjectName { unProjectName :: CI Text }
+ deriving (FromJSON, ToJSON, WrappedText)
+
+data User = User
+ { userName :: Username
+ , userPassHash :: Maybe PasswordHash -- to disable pass and use SSH only?
+ , userRealName :: RealName
+ , userEmail :: EmailAddress
+ }
+ deriving Generic
+
+instance FromJSON User
+instance ToJSON User
+
+data Group = Group
+ { groupName :: GroupName
+ , groupUsers :: HashSet UserID
+ }
+ deriving Generic
+
+instance FromJSON Group
+instance ToJSON Group
+
+data IrcChannel = IrcChannel
+ { chanNetwork :: Text
+ , chanName :: Text
+ }
+ deriving Generic
+
+instance FromJSON IrcChannel
+instance ToJSON IrcChannel
+
+data Repository = Repository
+ { repoName :: RepoName
+ , repoIRC :: Maybe IrcChannel
+ , repoML :: Maybe Text
+ }
+ deriving Generic
+
+instance ToJSON Repository
+instance FromJSON Repository
+
+data Project = Project
+ { projName :: ProjectName
+ , projRepos :: HashMap RepoID Repository
+ }
+ deriving Generic
+
+instance ToJSON Project
+instance FromJSON Project
+
+-------------------------------------------------------------------------------
+-- Instances
+-------------------------------------------------------------------------------
+
+instance (CI.FoldCase a, FromJSON a) => FromJSON (CI a) where
+ parseJSON v = CI.mk <$> parseJSON v
+
+instance ToJSON a => ToJSON (CI a) where
+ toJSON = toJSON . CI.original
+
+class WrappedText a where
+ toText :: a -> Text
+ fromText :: Text -> a
+
+instance WrappedText Text where
+ toText = id
+ fromText = id
+
+instance (WrappedText a, CI.FoldCase a) => WrappedText (CI a) where
+ toText = toText . CI.original
+ fromText = CI.mk . fromText
+
+instance WrappedText Int where
+ toText = T.pack . show
+ fromText = read . T.unpack
+
+mapFst :: (a -> c) -> [(a, b)] -> [(c, b)]
+mapFst f = map $ \ (x, y) -> (f x, y)
+
+instance (Eq k, Hashable k, WrappedText k, FromJSON v) => FromJSON (HashMap k v) where
+ parseJSON v = M.fromList . mapFst fromText . M.toList <$> parseJSON v
+
+instance (WrappedText k, ToJSON v) => ToJSON (HashMap k v) where
+ toJSON = toJSON . M.fromList . mapFst toText . M.toList
+
+instance (WrappedText a, WrappedText b) => WrappedText (Either a b) where
+ toText (Left x) = 'l' `T.cons` toText x
+ toText (Right y) = 'r' `T.cons` toText y
+ fromText t =
+ case T.uncons t of
+ Nothing -> error "blank JSON field name???"
+ Just ('l', r) -> Left $ fromText r
+ Just ('r', r) -> Right $ fromText r
+ _ -> error "what is dis ting"
diff --git a/vervis.cabal b/vervis.cabal
index 85718c7..e5b516f 100644
--- a/vervis.cabal
+++ b/vervis.cabal
@@ -27,6 +27,10 @@ source-repository head
library
exposed-modules: Vervis
+ , Vervis.Git
+ , Vervis.Monad
+ , Vervis.Ops
+ , Vervis.Types
-- other-modules:
-- other-extensions:
build-depends: aeson