packages feed

cqrs-example-0.7.0: src/CQRSExample/Json.hs

module CQRSExample.Json
       ( jsonServerPart
       ) where

import           Control.Applicative (optional)
import           Control.Monad (liftM, void)
import           Control.Monad.Trans.Class (lift)
import           CQRSExample.Command (logWork, createProject, createTask, starTask, unstarTask)
import           CQRSExample.Events (Event)
import           CQRSExample.Query
import           Data.Aeson (ToJSON, encode, toJSON)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import           Data.CQRS (EventStore, runTransactionT)
import           Data.CQRS.GUID (GUID, hexEncode, hexDecode)
import           Data.List (groupBy)
import qualified Data.Map as M
import           Data.Maybe (fromJust)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Encoding as TE
import           Data.Time.Calendar (Day)
import           Data.Time.Format (parseTime)
import           Database.HDBC (IConnection)
import           Happstack.Lite
import           Happstack.Server.RqData (lookRead)
import           System.Locale (defaultTimeLocale)

-- Instances.
instance ToJSON Day where
  toJSON d = toJSON $ show d

-- Parse request parameter to date. Fails the request
-- if the parameter is absent.
lookDay :: String -> ServerPart Day
lookDay pn = do
  p <- lookText pn
  case parseTime defaultTimeLocale "%F" $ TL.unpack $ p of
    Just d -> return d
    Nothing -> fail $ "Missing parameter '" ++ pn ++ "'"

-- Retrieve a GUID parameter.
lookGUID :: String -> ServerPart (Maybe GUID)
lookGUID name = do
  bs <- optional $ lookBS name
  case bs of
    Nothing -> return Nothing
    Just bs' -> return $ hexDecode $ BS.concat $ BSL.toChunks $ bs'

-- Serve JSON ok response.
okJson :: ToJSON a => a -> ServerPart Response
okJson a = ok $ toResponseBS "text/json" $ encode a

-- Serve a list generated by a monadic action as a JSON response.
listJson :: (IConnection c, ToJSON b) => c -> (c -> IO [a]) -> (a -> b) -> ServerPart Response
listJson connection lister f = do
  p <- liftM (map f) $ lift $ lister connection
  okJson $ p

-- Serve JSON project list.
projectsJson :: IConnection c => c -> EventStore Event -> ServerPart Response
projectsJson connection eventStore =
  msum [ method GET >> getProjects
       , method POST >> postProjects
       ]
  where
    getProjects :: ServerPart Response
    getProjects = do
      (listJson connection qProjectList
       (\(g,pn,psd) ->
         M.fromList [ (T.pack "id"        , TE.decodeUtf8 $ hexEncode g)
                    , (T.pack "name"      , pn)
                    , (T.pack "short_desc", psd)
                    ]))
    postProjects = do
      pn <- fmap TL.toStrict $ lookText "name"
      pd <- fmap TL.toStrict $ lookText "shortDescription"
      pid <- lift $ runTransactionT eventStore $ do
        createProject pn pd
      ok $ toResponse $ hexEncode pid

-- Serve JSON task list.
tasksJson :: IConnection c => c -> EventStore Event -> String -> ServerPart Response
tasksJson connection eventStore userName =
  msum [ method GET >> getTasks
       , method POST >> postTasks
       ]
  where
    getTasks :: ServerPart Response
    getTasks = do
      projectId <- lookGUID "projectId"
      (listJson connection (qTaskList userName projectId)
       (\(g,tsd,starred) ->
         M.fromList [ (T.pack "id"               , toJSON $ TE.decodeUtf8 $ hexEncode g)
                    , (T.pack "short_description", toJSON $ tsd)
                    , (T.pack "starred"          , toJSON $ starred)
                    ]))

    postTasks :: ServerPart Response
    postTasks = do
      mpid <- fmap (hexDecode . BS.concat . BSL.toChunks) $ lookBS "projectId"
      case mpid of
        Nothing -> fail "Invalid project ID"
        Just pid -> do
          tsd <- fmap TL.toStrict $ lookText "shortDescription"
          tid <- lift $ runTransactionT eventStore $ do
            createTask pid tsd
          ok $ toResponse $ hexEncode tid

-- Serve JSON time sheet.
timeSheetJson :: IConnection c => c -> String -> ServerPart Response
timeSheetJson connection userName = do
  fromDate <- lookDay "fromDate"
  toDate <- lookDay "toDate"
  -- Generate the time sheet data.
  timeSheet <- lift $ qTimeSheet fromDate toDate userName connection
  -- Group by tasks to make the front end manipulation a little easier.
  let groupedTimeSheet =
        groupBy (\(tid1,_,_,_) (tid2,_,_,_) -> (tid1==tid2)) timeSheet
  -- Generate the JSON data.
  okJson $ map
    (\entries ->
      case entries of
        [] -> M.fromList []
        ((tid,tsd,_,_):_) ->
          M.fromList [ (T.pack "task_id", toJSON $ hexEncode tid)
                     , (T.pack "task_short_description", toJSON tsd)
                     , (T.pack "timeSheet",
                        toJSON $ map (\(_,_,day,total) ->
                                       M.fromList [ (T.pack "date", toJSON $ day)
                                                  , (T.pack "total", toJSON total) ] ) entries) ])
    groupedTimeSheet

-- Stars server part.
starsServerPart :: IConnection c => c -> EventStore Event -> String -> ServerPart Response
starsServerPart connection eventStore userName = do
  msum [ method POST >> postStars
       ]
  where
    postStars :: ServerPart Response
    postStars = do
      mTaskId <- fmap (hexDecode . BS.concat . BSL.toChunks) $ lookBS "taskId"
      case mTaskId of
        Nothing -> fail "Invalid task ID"
        Just taskId -> do
          mUserId <- lift $ qUserIdFromUserName connection (T.pack userName)        -- TODO: Move this lookup to the authentication server part.
          case mUserId of
            Nothing -> fail "Invalid user name"
            Just userId -> do
              starredS <- lookBS "starred"
              case starredS of
                "true" -> lift $ runTransactionT eventStore $ void $ starTask taskId userId
                "false" -> lift $ runTransactionT eventStore $ void $ unstarTask taskId userId
                _ -> fail "Invalid input"
              ok $ toResponse ("OK" :: String)

-- Log work.
logWorkPost :: EventStore Event -> ServerPart Response
logWorkPost eventStore = do
  -- FIXME: Avoid fromJust + lookRead! This is extremely crashy.
  taskId <- liftM (fromJust . hexDecode . BS.concat . BSL.toChunks) $ lookBS "taskId"
  duration <- lookRead "duration"
  comment <- lookText "comment"
  date <- lookDay "date"
  let userId = fromJust $ hexDecode ""    -- FIXME: Use USER ID of authenticated user!
  lift $ runTransactionT eventStore $ do
    void $ logWork taskId userId date duration (TL.toStrict comment)
  ok $ toResponse ("" :: String)

-- Serve JSON data.
jsonServerPart :: IConnection c => c -> EventStore Event -> String -> ServerPart Response
jsonServerPart conn eventStore userName =
  msum
  [ dir "projects" $ projectsJson conn eventStore
  , dir "tasks" $ tasksJson conn eventStore userName
  , dir "stars" $ starsServerPart conn eventStore userName
  , dir "time-sheet" $ timeSheetJson conn userName
  , dir "do-log-work" $ logWorkPost eventStore
  ]