#!/usr/bin/env stack {- stack --nix --resolver lts-10.3 --install-ghc runghc --package http-types --package yesod --package yesod-core --package text --package aeson --package acid-state --package cassava --package ixset -} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE RecordWildCards #-} import Data.Acid import Data.Acid.Advanced (update', query') import Data.Acid.Local (createCheckpointAndClose) import qualified Data.ByteString.Lazy as BSL import qualified Data.Csv as Csv import Data.IxSet (Indexable(..), IxSet(..), (@=), Proxy(..), getOne, ixFun, ixSet) import qualified Data.IxSet as IxSet import Data.SafeCopy import Data.Semigroup import qualified Data.Vector as Vector import Data.Data (Data, Typeable) import GHC.Generics import Control.Exception (bracket) import Control.Monad import Control.Monad.Reader (ask) import Control.Monad.State (get, put) import Data.Maybe (isJust) import Data.Text (Text, pack, unpack) import Yesod hiding (Number, Update, update, get) import Network.HTTP.Types.Status (status400, status200) import Data.Aeson hiding (Number) -------------------------------------------------------------------- -- | Here be the state and data model stuff. -- | Wrap a regular Int for CallerId, so we can track unique callers. newtype CallerId = CallerId { unCallerId :: Int } deriving (Show, Eq, Ord, Data, Enum, Typeable, Generic) $(deriveSafeCopy 0 'base ''CallerId) instance ToJSON CallerId where toJSON CallerId{..} = toJSON unCallerId instance FromJSON CallerId -- | A type to describe the shape of our core model data Caller = Caller { callerId :: CallerId , name :: Text , number :: Text , context :: Text } deriving (Show, Eq, Ord, Generic, ToJSON, FromJSON) $(deriveSafeCopy 0 'base ''Caller) -- | Create indexes for each field we want to query newtype Name = Name Text deriving (Eq, Ord, Data, Typeable) newtype PhoneNumber = PhoneNumber Text deriving (Eq, Ord, Data, Typeable) newtype Context = Context Text deriving (Eq, Ord, Data, Typeable) $(deriveSafeCopy 0 'base ''Name) $(deriveSafeCopy 0 'base ''PhoneNumber) $(deriveSafeCopy 0 'base ''Context) instance Indexable Caller where empty = ixSet [ ixFun $ (:[]) . callerId , ixFun $ \c -> [ Name $ name c ] , ixFun $ \c -> [ PhoneNumber $ number c ] , ixFun $ \c -> [ Context $ context c ] ] -- | The database is a set of @Caller@ records, plus a record counter, so we -- know the next CallerId to use. data Database = Database { nextCallerId :: CallerId , callers :: IxSet Caller } deriving (Typeable) instance Data Database $(deriveSafeCopy 0 'base ''Database) initDatabase :: Database initDatabase = Database { nextCallerId = CallerId 1 -- ^ Index starting a 1 , callers = empty } -- | Insert the caller into database. addCaller :: Text -> Text -> Text -> Update Database Caller addCaller name number context = do db@Database{..} <- get let caller = Caller { callerId = nextCallerId , name = name , number = number , context = context } put $ db { nextCallerId = succ nextCallerId , callers = IxSet.insert caller callers } return caller viewCallers :: Int -> Query Database [Caller] viewCallers limit = do Database{..} <- ask return $ take limit $ IxSet.toList callers -- | Update a single caller record updateCaller :: Caller -> Update Database () updateCaller updatedCaller = do db@Database{..} <- get put $ db { callers = IxSet.updateIx (callerId updatedCaller) updatedCaller callers } -- | Lookup caller by CallerId callerById :: CallerId -> Query Database (Maybe Caller) callerById cid = do Database{..} <- ask return $ getOne $ callers @= cid callerByNumber :: PhoneNumber -> Query Database [Caller] callerByNumber num = do Database{..} <- ask return $ IxSet.toList $ callers @= num $(makeAcidic ''Database ['addCaller, 'updateCaller, 'callerById, 'callerByNumber, 'viewCallers]) ----------------------------------------------------------------------- -- | Here be the HTTP stuff data App = App { appState :: AcidState Database } mkYesod "App" [parseRoutes| /query QueryR GET /number NumberR POST |] -- | Initiate Yesod. The default method instances are fine for a prototype or -- demo app. instance Yesod App data ApiError = ApiError { msg :: Text } deriving (Show, Eq, Generic, ToJSON, FromJSON) getQueryR :: Handler RepJson getQueryR = do qm <- lookupGetParam "number" case qm of Nothing -> do app <- getYesod let db = appState app callers <- query' db $ ViewCallers 20 sendStatusJSON status200 $ object [ "results" .= callers ] Just q -> do app <- getYesod let db = appState app caller <- query' db $ CallerByNumber $ PhoneNumber q $logInfo $ pack $ show caller sendStatusJSON status200 $ object [ "results" .= caller ] data PostRequest = PostRequest { _name :: Text , _number :: Text , _context :: Text } deriving (Show, Eq, Generic) instance FromJSON PostRequest where parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = drop 1 } instance ToJSON PostRequest where toJSON = genericToJSON defaultOptions { fieldLabelModifier = drop 1 } postNumberR :: Handler RepJson postNumberR = do (obj :: Result PostRequest) <- parseJsonBody case obj of Error err -> sendStatusJSON status400 $ ApiError $ "Invalid request. Could not parse JSON body: " <> pack err Success PostRequest{..} -> do app <- getYesod let db = appState app caller <- liftIO $ update db $ AddCaller _name _number _context sendStatusJSON status200 $ caller mapIndexed f l = zipWith f l [0..] -- | Start a simple warp server on 3000 main :: IO () main = do bracket (openLocalState initDatabase) (createCheckpointAndClose) (\db -> do seedData <- BSL.readFile "interview-callerid-data.csv" case Csv.decode Csv.NoHeader seedData of Left err -> fail err Right v -> Vector.forM_ (Vector.indexed v) $ \(callerId, record) -> do let (number, context, name) = record update' db $ UpdateCaller $ Caller (CallerId callerId) name number context putStrLn "Ready" warp 3000 (App db))