packages feed

spire-wai-0.1.0.0: src/Spire/Wai.hs

-- | WAI/warp backend adapter for spire.
--
-- This is the __only package that imports WAI__. Everything above
-- (spire, spire-http, acolyte-server) uses http-core types
-- and is backend-agnostic.
--
-- @
-- import Spire.Wai (runWarp)
-- runWarp 3000 myService
-- @
module Spire.Wai
  ( -- * Running on warp
    runWarp
  , runWarpSettings

    -- * Core WAI conversion
  , toWaiApp
  , fromWaiRequest
  , toWaiResponse

    -- * WAI middleware interop (migration bridge)
  , fromWaiMiddleware
  ) where

import Control.Concurrent.MVar
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import Data.Text (Text)

import qualified Network.Wai as Wai
import qualified Network.Wai.Internal as Wai (ResponseReceived (..))
import qualified Network.Wai.Handler.Warp as Warp

import Spire (Middleware, middleware)
import Spire.Service (Service (..))
import Spire.Layer (Layer (..))
import Http.Core
  ( Request (..), Response (..)
  , emptyExtensions
  )


-- ===================================================================
-- Running on warp
-- ===================================================================

-- | Run a spire Service on warp at the given port.
runWarp :: Int -> Service IO (Request ByteString) (Response ByteString) -> IO ()
runWarp port svc = Warp.run port (toWaiApp svc)

-- | Run with full warp settings.
runWarpSettings
  :: Warp.Settings
  -> Service IO (Request ByteString) (Response ByteString)
  -> IO ()
runWarpSettings settings svc = Warp.runSettings settings (toWaiApp svc)


-- ===================================================================
-- Core WAI conversion
-- ===================================================================

-- | Convert a spire Service to a WAI Application.
toWaiApp :: Service IO (Request ByteString) (Response ByteString) -> Wai.Application
toWaiApp (Service f) waiReq respond = do
  req <- fromWaiRequest waiReq
  resp <- f req
  respond (toWaiResponse resp)

-- | Convert a WAI Request to our Request (collects body as strict bytes).
fromWaiRequest :: Wai.Request -> IO (Request ByteString)
fromWaiRequest waiReq = do
  body <- LBS.toStrict <$> Wai.consumeRequestBodyStrict waiReq
  exts <- emptyExtensions
  pure Request
    { requestMethod     = Wai.requestMethod waiReq
    , requestPathRaw    = Wai.rawPathInfo waiReq
    , requestPath       = filter (/= "") (Wai.pathInfo waiReq)
    , requestQuery      = Wai.queryString waiReq
    , requestHeaders    = Wai.requestHeaders waiReq
    , requestBody       = body
    , requestExtensions = exts
    }

-- | Convert our Response to a WAI Response.
toWaiResponse :: Response ByteString -> Wai.Response
toWaiResponse resp =
  Wai.responseLBS
    (responseStatus resp)
    (responseHeaders resp)
    (LBS.fromStrict (responseBody resp))


-- ===================================================================
-- WAI middleware interop
-- ===================================================================

-- | Wrap existing WAI Middleware as a spire Middleware.
--
-- Migration bridge for @wai-extra@, @wai-cors@, etc. The WAI
-- middleware wraps the service at the WAI level.
--
-- @
-- import qualified Network.Wai.Middleware.Gzip as Wai
-- myService |> fromWaiMiddleware Wai.gzip
-- @
fromWaiMiddleware
  :: Wai.Middleware
  -> Middleware IO (Request ByteString) (Response ByteString)
fromWaiMiddleware waiMw = middleware $ \innerSvc ->
  let innerWaiApp = toWaiApp innerSvc
      wrappedWaiApp = waiMw innerWaiApp
  in  waiAppToService wrappedWaiApp


-- | Convert a WAI Application to a spire Service.
-- Bridges WAI's CPS response style via MVar.
waiAppToService :: Wai.Application -> Service IO (Request ByteString) (Response ByteString)
waiAppToService waiApp = Service $ \req -> do
  let waiReq = toWaiRequest req
  resultVar <- newEmptyMVar
  _ <- waiApp waiReq $ \waiResp -> do
    putMVar resultVar (fromWaiResponse waiResp)
    pure Wai.ResponseReceived
  takeMVar resultVar


-- | Reconstruct a minimal WAI Request from our Request.
-- Lossy — vault, socket info, HTTP version not preserved.
toWaiRequest :: Request ByteString -> Wai.Request
toWaiRequest req = Wai.defaultRequest
  { Wai.requestMethod     = requestMethod req
  , Wai.rawPathInfo       = requestPathRaw req
  , Wai.pathInfo          = requestPath req
  , Wai.queryString       = requestQuery req
  , Wai.requestHeaders    = requestHeaders req
  , Wai.requestBodyLength = Wai.KnownLength (fromIntegral $ BS.length $ requestBody req)
  }


-- | Extract status and headers from a WAI Response.
-- Body is empty — for header-only middleware (CORS, security headers)
-- the real body passes through the inner toWaiApp/toWaiResponse path.
fromWaiResponse :: Wai.Response -> Response ByteString
fromWaiResponse waiResp = Response
  { responseStatus  = Wai.responseStatus waiResp
  , responseHeaders = Wai.responseHeaders waiResp
  , responseBody    = ""
  }