{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeOperators #-} module Network.Hoggl.Types (TimeEntryId(..) ,Token(..) ,HogglError(..) ,TimeEntryStart(..) ,TimeEntry(..) ,ISO6801(..) ,ISO6801Date(..) ,Workspace(..) ,WorkspaceId(..) ,Project(..) ,ProjectId(..) ,DetailedReport(..) ,TogglApi ,ToggleReportApi ,parseTimeStamp) where import Codec.Binary.Base64.String (encode) import Control.Applicative ((<|>)) import Control.Monad (mzero) import Data.Aeson (FromJSON(..), Value (..), (.:), (.:?), ToJSON(..), object, (.=), (.!=)) import Data.Aeson.Types (Parser) import qualified Data.HashMap.Strict as H import Data.Hashable (Hashable) import Data.Monoid ((<>)) import Data.String (IsString) import Data.Text (Text) import qualified Data.Text as T import Data.Time.Calendar (Day) import Data.Time.Clock (UTCTime, NominalDiffTime) import Data.Time.Format (defaultTimeLocale, formatTime, parseTimeM, iso8601DateFormat) import Servant.API import Servant.Client newtype TimeEntryId = TID Integer deriving (Show,Eq,FromJSON,ToHttpApiData) newtype WorkspaceId = WID Integer deriving (Show,Eq,FromJSON,ToHttpApiData) newtype ProjectId = PID Integer deriving (Show,Eq,FromJSON,ToHttpApiData) newtype ApiToken = ApiToken String deriving (IsString) newtype ISO6801 = ISO6801 UTCTime deriving (Show,Eq,Ord) newtype ISO6801Date = ISO6801Date Day deriving (Show,Eq,Ord) instance ToHttpApiData ISO6801 where toUrlPiece (ISO6801 t) = toUrlPiece . addColon $ formatTime defaultTimeLocale "%Y-%m-%dT%H%::%M:%S%z" t addColon :: String -> String addColon s = reverse (p2 <> ":" <> su) where sr = reverse s p2 = take 2 sr su = drop 2 sr instance FromJSON ISO6801 where parseJSON (String s) = ISO6801 <$> parseTimeStamp s parseJSON _ = mzero parseTimeStamp :: Monad m => Text -> m UTCTime parseTimeStamp ts = parseTimeM True defaultTimeLocale (iso8601DateFormat (Just "%H:%M:%S%z")) (removeColon (T.unpack ts)) where removeColon s = reverse (dropWhile (/= '+') (reverse s)) ++ reverse (filter (/= ':') (takeWhile (/= '+') (reverse s))) instance ToHttpApiData ISO6801Date where toUrlPiece (ISO6801Date day) = toUrlPiece (formatTime defaultTimeLocale "%Y-%m-%d" day) data Token = Api String | UserPass String String deriving (Show,Eq) instance ToHttpApiData Token where toUrlPiece (Api token) = toUrlPiece $ "Basic " ++ encode (token ++ ":api_token") toUrlPiece (UserPass user pass) = toUrlPiece $ "Basic " ++ encode (user ++ ":" ++ pass) data HogglError = ServantError ServantError | HogglError String deriving Show data TimeEntryStart = TES { tesDescription :: Maybe Text , tesTags :: [Text] , tesPid :: Maybe Integer , tesCreatedWith :: Text } deriving (Show,Eq) instance ToJSON TimeEntryStart where toJSON (TES desc tags pid createdWith) = object ["time_entry" .= object ["description" .= desc ,"tags" .= tags ,"pid" .= pid ,"created_with" .= createdWith]] data TimeEntry = TimeEntry {teId :: TimeEntryId ,teProjectId :: Maybe ProjectId ,teProject :: Maybe Text ,teClient :: Maybe Text ,teStart :: ISO6801 ,teStop :: Maybe ISO6801 ,teDuration :: NominalDiffTime ,teDescription :: Maybe Text }deriving (Show,Eq) instance FromJSON TimeEntry where parseJSON v@(Object o) = ((o .: "data") >>= p) <|> p v where p (Object d) = TimeEntry <$> d .: "id" <*> d .:?? "pid" <*> d .:?? "project" <*> d .:?? "client" <*> d .: "start" <*> (d .: "stop" <|> d.:?? "end") <*> (convert <$> ((d .: "duration") <|> ((`div` 1000) <$> (d .: "dur")))) <*> d .:? "description" p _ = mzero convert :: Integer -> NominalDiffTime convert = fromIntegral parseJSON _ = mzero (.:??) :: (FromJSON a, Hashable k, Eq k) => H.HashMap k Value -> k -> Parser (Maybe a) obj .:?? key = case H.lookup key obj of Nothing -> pure Nothing Just Null -> pure Nothing Just v -> Just <$> parseJSON v {-# INLINE (.:??) #-} data Workspace = Workspace {wsId :: WorkspaceId ,wsName :: Text ,wsPremium :: Bool ,wsAdmin :: Bool ,wsDefaultHourlyRate :: Double ,wsDefaultCurrency :: Text ,wsRounding :: Int ,wsRoundingMinutes :: Int ,wsAt :: ISO6801 } deriving (Show,Eq) instance FromJSON Workspace where parseJSON (Object o) = Workspace <$> o .: "id" <*> o .: "name" <*> o .: "premium" <*> o .: "admin" <*> o .: "default_hourly_rate" <*> o .: "default_currency" <*> o .: "rounding" <*> o .: "rounding_minutes" <*> o .: "at" parseJSON _ = mzero data Project = Project { prId :: ProjectId , prWsId :: WorkspaceId , prName :: Text , prBillable :: Bool , prPrivate :: Bool , prActive :: Bool , prAt :: ISO6801 } instance FromJSON Project where parseJSON (Object o) = Project <$> o .: "id" <*> o .: "wid" <*> o .: "name" <*> o .: "billable" <*> o .: "is_private" <*> o .: "active" <*> o .: "at" parseJSON _ = mzero data DetailedReport = DetailedReport {drPerPage :: Int ,drTotalCount :: Int ,drTotalBillable :: Double ,drTotalGrand :: Integer ,drData :: [TimeEntry] } deriving (Show,Eq) instance FromJSON DetailedReport where parseJSON (Object o) = DetailedReport <$> o .: "per_page" <*> o .: "total_count" <*> o .:? "total_billable" .!= 0 <*> ((`div` 1000 ) <$> (o .: "total_grand")) <*> o .: "data" parseJSON _ = mzero type WithAuth a = "api" :> "v8" :> Header "Authorization" Token :> a type Current = WithAuth ("time_entries" :> "current" :> Get '[JSON] TimeEntry) type Stop = WithAuth ("time_entries" :> Capture "time_entry_id" TimeEntryId :> "stop" :> Put '[JSON] TimeEntry) type Start = WithAuth ("time_entries" :> "start" :> ReqBody '[JSON] TimeEntryStart :> Post '[JSON] TimeEntry) type Details = WithAuth ("time_entries" :> Capture "time_entry_id" TimeEntryId :> Get '[JSON] TimeEntry) type GetEntries = WithAuth ("time_entries" :> QueryParam "start_date" ISO6801 :> QueryParam "end_date" ISO6801 :> Get '[JSON] [TimeEntry]) type ListWorkspaces = WithAuth ("workspaces" :> Get '[JSON] [Workspace]) type ListProjects = WithAuth ("workspaces" :> Capture "workspace_id" WorkspaceId :> "projects" :> Get '[JSON] [Project]) type TogglApi = Current :<|> Stop :<|> Start :<|> Details :<|> GetEntries :<|> ListWorkspaces :<|> ListProjects type GetDetailedReport = "reports" :> "api" :> "v2" :> "details" :> Header "Authorization" Token :> QueryParam "workspace_id" WorkspaceId :> QueryParam "since" ISO6801Date :> QueryParam "until" ISO6801Date :> QueryParam "user_agent" Text :> Get '[JSON] DetailedReport type ToggleReportApi = GetDetailedReport