packages feed

couch-hs-0.1.3: source/Database/CouchDB/ViewServer/Main/Server.hs

{-# LANGUAGE OverloadedStrings #-}

module Database.CouchDB.ViewServer.Main.Server
    ( runServer
    )
    where

import System.IO
import System.Exit

import Control.Applicative
import Control.Monad (unless)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Reader (ReaderT, runReaderT, ask)
import Control.Monad.Trans.State (StateT, evalStateT, get, put, modify)

import Data.Either (Either(..), lefts, rights)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy as L
import qualified Data.Text as T
import qualified Data.Text.Encoding as T.E
import Data.Text (Text)
import Data.List (intercalate)

import Data.Attoparsec (parseOnly)
import qualified Data.Aeson as J
import Data.Aeson ((.=), json, toJSON, fromJSON, Result(..))
import qualified Language.Haskell.Interpreter as H

import Database.CouchDB.ViewServer.Internal
import Database.CouchDB.ViewServer.Map
import Database.CouchDB.ViewServer.Reduce
import Database.CouchDB.ViewServer.Main.Context
import Database.CouchDB.ViewServer.Main.Server.Command


data ServerState = ServerState
 { stateContext :: Context
 , stateMapFuncs :: [MapFunc]
 , stateReduceFuncs :: [(Text, ReduceFunc)]
 }

initialServerState context = ServerState
 { stateContext = context
 , stateMapFuncs = []
 , stateReduceFuncs = []
 }

type LineProcessor a = StateT ServerState IO a

type ResponseValue = J.Value


runServer :: Context -> IO ExitCode
runServer context = do hSetBuffering stdout LineBuffering
                       evalStateT processLines $ initialServerState context
                       return ExitSuccess


processLines :: LineProcessor ()
processLines = do
    eof <- liftIO isEOF
    unless eof $ do processNextLine
                    processLines


{-
    Line processing
-}
processNextLine :: LineProcessor ()
processNextLine = do
    line <- liftIO B.getLine
    logInputLine line
    case line of
        "" -> return ()
        _  -> do result <- processLine line
                 liftIO $ L.putStrLn $ J.encode result


{- Log the input line to a file, if requested in the command-line arguments. -}
logInputLine :: B.ByteString -> LineProcessor ()
logInputLine line = do
    commandLog <- getInputLog
    case commandLog of
        Just handle -> liftIO $ B.hPutStrLn handle line
        Nothing     -> return ()


processLine :: B.ByteString -> LineProcessor ResponseValue
processLine line =
    case parseOnly json line of
        Left err     -> return $ parseErrorValue err
        Right value  -> case fromJSON value of
                            Error string    -> return $ parseErrorValue string
                            Success command -> processCommand command


processCommand :: ViewCommand -> LineProcessor ResponseValue
processCommand command =
    case command of
        Reset                 -> processReset
        AddFun code           -> processAddFun code
        MapDoc doc            -> processMapDoc doc
        Reduce codes args     -> processReduce codes args
        Rereduce codes values -> processRereduce codes values


{-
    Command handlers
-}
processReset :: LineProcessor ResponseValue
processReset = do
    putMapFuncs []
    return $ J.Bool True


processAddFun :: Text -> LineProcessor ResponseValue
processAddFun code = do
    mapInterpreter <- getMapFuncInterpreter
    eitherFunc <- liftIO $ H.runInterpreter $ mapInterpreter (B.unpack $ T.E.encodeUtf8 code)
    case eitherFunc of
        Left err   -> return $ compileErrorValue $ "Map: " ++ show err
        Right func -> do modifyMapFuncs (++ [func])
                         return $ J.Bool True


processMapDoc :: J.Object -> LineProcessor ResponseValue
processMapDoc doc = do outputs <- map (`execMapFunc` doc) <$> getMapFuncs
                       mapM_ (liftIO . L.putStrLn . J.encode . toJSON) (concatMap logs outputs)
                       return $ toJSON $ map emits outputs


processReduce :: [Text] -> [ReduceArg] -> LineProcessor ResponseValue
processReduce codes args = do
    eitherFuncs <- eitherReduceFuncs codes
    case eitherFuncs of
        Left err    -> return $ compileErrorValue err
        Right funcs -> let keys = map reduceKey args
                           values =  map reduceValue args
                       in  reduceResponse $ map (\f -> execReduceFunc f keys values False) funcs


processRereduce :: [Text] -> [J.Value] -> LineProcessor ResponseValue
processRereduce codes values = do
    eitherFuncs <- eitherReduceFuncs codes
    case eitherFuncs of
        Left err    -> return $ compileErrorValue err
        Right funcs -> reduceResponse $ map (\f -> execReduceFunc f [] values True) funcs


reduceResponse :: [ReduceOutput] -> LineProcessor ResponseValue
reduceResponse outputs =
    do mapM_ (liftIO . L.putStrLn . J.encode . toJSON) logMessages
       return $ toJSON (True, results)
    where logMessages = concatMap snd outputs
          results = map fst outputs

{-
    Monadic utils
-}
eitherReduceFuncs :: [Text] -> LineProcessor (Either String [ReduceFunc])
eitherReduceFuncs codes = do
    reduceInterpreter <- getReduceFuncInterpreter
    eitherFuncs <- mapM getReduceFunc codes
    let errors = lefts eitherFuncs
    if null errors
        then return $ Right $ rights eitherFuncs
        else return $ Left $ "Reduce: " ++ intercalate "; " (map show errors)


getReduceFunc :: Text -> LineProcessor (Either H.InterpreterError ReduceFunc)
getReduceFunc code = do
    maybeFunc <- lookup code <$> getReduceFuncs
    case maybeFunc of
        Just func -> return $ Right func
        Nothing   -> loadReduceFunc code


loadReduceFunc :: Text -> LineProcessor (Either H.InterpreterError ReduceFunc)
loadReduceFunc code = do
    interpreter <- getReduceFuncInterpreter
    eitherFunc <- liftIO $ H.runInterpreter $ interpreter (B.unpack $ T.E.encodeUtf8 code)
    case eitherFunc of
        Left _     -> return eitherFunc
        Right func -> do modifyReduceFuncs $ ((code, func) :) . take 4  -- Cache up to 5 reduce functions
                         return $ Right func


couchLog :: String -> IO ()
couchLog message = L.putStrLn $ J.encode $ toJSON $ LogMessage message


{-
    Monad transformer wrappers.
-}
getMapFuncInterpreter = ctxMapFuncInterpreter <$> getContext
getReduceFuncInterpreter = ctxReduceFuncInterpreter <$> getContext
getInputLog = ctxInputLog <$> getContext

getContext :: LineProcessor Context
getContext = stateContext <$> get

getMapFuncs :: LineProcessor [MapFunc]
getMapFuncs = stateMapFuncs <$> get
putMapFuncs funcs = get >>= \state -> put state { stateMapFuncs = funcs }
modifyMapFuncs f = getMapFuncs >>= \funcs -> putMapFuncs $ f funcs

getReduceFuncs :: LineProcessor [(Text, ReduceFunc)]
getReduceFuncs = stateReduceFuncs <$> get
putReduceFuncs funcs = get >>= \state -> put state { stateReduceFuncs = funcs }
modifyReduceFuncs f = getReduceFuncs >>= \funcs -> putReduceFuncs $ f funcs


{-
    Errors
-}
parseErrorValue = errorValue "parse"
compileErrorValue = errorValue "compile"

errorValue :: String -> String -> J.Value
errorValue code reason = J.object ["error" .= code, "reason" .= reason]