packages feed

hurl-workbench-cli-0.1.0.0: test/fixture-server/FixtureServer.hs

module FixtureServer
  ( application,
    runFixtureServer,
    runFixtureRole,
  )
where

import Control.Concurrent (forkIO, killThread, myThreadId, threadDelay, throwTo)
import Control.Exception (AsyncException (UserInterrupt), bracket, bracket_)
import Control.Monad (forever, when)
import Data.ByteString qualified as ByteString
import Data.ByteString.Char8 qualified as Char8
import Data.ByteString.Lazy qualified as LazyByteString
import Data.IORef (atomicModifyIORef', newIORef)
import Network.HTTP.Types
  ( hContentType,
    methodDelete,
    methodGet,
    methodPost,
    status200,
    status201,
    status404,
    status405,
    status409,
    status503,
  )
import Network.Wai
  ( Application,
    pathInfo,
    queryString,
    requestMethod,
    responseLBS,
    strictRequestBody,
  )
import Network.Wai.Handler.Warp (defaultSettings, runSettings, setHost, setPort)
import System.Directory (doesFileExist, renameFile)
import System.FilePath ((</>))
import System.Posix.Process (getProcessID)
import System.Posix.Signals (Handler (Catch), installHandler, sigTERM)

runFixtureServer :: Int -> IO ()
runFixtureServer port =
  runSettings (setPort port (setHost "127.0.0.1" defaultSettings)) application

-- | Deterministic roles for the public orchestration example. Only the worker
-- writes visibility; the API accepts one command and exposes read-only polling.
runFixtureRole :: String -> Int -> FilePath -> IO ()
runFixtureRole role port root = do
  mainThread <- myThreadId
  _ <- installHandler sigTERM (Catch (throwTo mainThread UserInterrupt)) Nothing
  pid <- getProcessID
  appendFile (root </> "pids") (show pid <> "\n")
  generation <-
    if role == "worker"
      then do
        exists <- doesFileExist (root </> "generation")
        previous <- if exists then read . Char8.unpack <$> ByteString.readFile (root </> "generation") else pure (0 :: Int)
        writeFile (root </> "generation") (show (previous + 1))
        pure (previous + 1)
      else pure 0
  firstHealth <- newIORef True
  let event label = appendFile (root </> "events") (role <> "-" <> label <> "\n")
      project = forever $ do
        accepted <- doesFileExist (root </> "accepted")
        when accepted $ do
          writeFile (root </> "visible.tmp") ("visible:" <> show generation)
          renameFile (root </> "visible.tmp") (root </> "visible")
        threadDelay 10000
      serve request respond = case (requestMethod request, pathInfo request) of
        (method, ["health"]) | method == methodGet -> do
          first <- atomicModifyIORef' firstHealth (\value -> (False, value))
          when first (event "ready")
          respond (responseLBS status200 [] "ok")
        (method, ["command"])
          | role == "api",
            method == methodPost -> do
              exists <- doesFileExist (root </> "accepted")
              if exists
                then respond (responseLBS status409 [] "duplicate")
                else do
                  _ <- strictRequestBody request
                  writeFile (root </> "accepted") "accepted"
                  event "mutation"
                  respond (responseLBS status201 [] "accepted")
        (method, ["read"])
          | role == "api",
            method == methodGet -> do
              event "read"
              exists <- doesFileExist (root </> "visible")
              if exists
                then do
                  visible <- ByteString.readFile (root </> "visible")
                  respond (responseLBS status200 [] (LazyByteString.fromStrict visible))
                else respond (responseLBS status503 [] "pending")
        _ -> respond (responseLBS status404 [] "not found")
      run = runSettings (setPort port (setHost "127.0.0.1" defaultSettings)) serve
  bracket_ (event "start") (event "stop") $
    if role == "worker" then bracket (forkIO project) killThread (const run) else run

application :: Application
application request respond = case (requestMethod request, pathInfo request) of
  (method, ["health"])
    | method == methodGet ->
        respond (text status200 "ok\n")
  (method, ["oauth", "token"])
    | method == methodPost ->
        respond (json status200 "{\"access_token\":\"fixture-token\",\"token_type\":\"Bearer\"}\n")
  (method, ["odata", "echo"])
    | method == methodGet ->
        respond (json status200 (LazyByteString.fromStrict (showQuery (queryString request))))
  (method, ["capture"])
    | method == methodGet ->
        respond (responseLBS status200 [("Content-Type", "text/plain"), ("X-Fixture-Capture", "captured-value")] "capture-body\n")
  (method, ["raw-response"])
    | method == methodGet ->
        respond (responseLBS status200 [("Content-Type", "application/octet-stream"), ("X-Raw", "yes")] "raw\NULbytes\n")
  (method, ["mutating"]) | method == methodPost || method == methodDelete -> do
    body <- strictRequestBody request
    respond (responseLBS status201 [(hContentType, "application/octet-stream")] body)
  (_method, ["mutating"]) ->
    respond (text status405 "method not allowed\n")
  _ -> respond (text status404 "not found\n")
  where
    text status = responseLBS status [(hContentType, "text/plain; charset=utf-8")]
    json status = responseLBS status [(hContentType, "application/json")]

showQuery :: [(ByteString.ByteString, Maybe ByteString.ByteString)] -> ByteString.ByteString
showQuery query =
  "{\"query\":\""
    <> ByteString.intercalate
      "&"
      [name <> maybe "" ("=" <>) value | (name, value) <- query]
    <> "\"}\n"