mirror of
https://github.com/monadicsystems/okapi.git
synced 2024-11-23 01:13:38 +03:00
Reorg modules again; finalize imports & exports
This commit is contained in:
parent
00625b84fc
commit
342ee25ebc
@ -12,7 +12,7 @@ import GHC.Generics (Generic)
|
||||
import Okapi
|
||||
|
||||
main :: IO ()
|
||||
main = runOkapi id notFound 3000 calc
|
||||
main = runOkapi id _404 3000 calc
|
||||
|
||||
type Okapi a = OkapiT IO a
|
||||
|
||||
@ -27,20 +27,20 @@ addOp = do
|
||||
pathSeg "add"
|
||||
(x, y) <- getArgs
|
||||
respond $
|
||||
aeson (x + y) ok
|
||||
json (x + y) _200
|
||||
|
||||
subOp :: Okapi Response
|
||||
subOp = do
|
||||
pathSeg "sub" <|> pathSeg "minus"
|
||||
(x, y) <- getArgs
|
||||
respond $
|
||||
ok & aeson (x - y)
|
||||
_200 & json (x - y)
|
||||
|
||||
mulOp :: Okapi Response
|
||||
mulOp = do
|
||||
pathSeg "mul"
|
||||
(x, y) <- getArgs
|
||||
ok & aeson (x * y) & respond
|
||||
_200 & json (x * y) & respond
|
||||
|
||||
data DivResult = DivResult
|
||||
{ answer :: Int,
|
||||
@ -53,10 +53,10 @@ divOp = do
|
||||
pathSeg "div"
|
||||
(x, y) <- getArgs
|
||||
if y == 0
|
||||
then throw forbidden
|
||||
then throw _403
|
||||
else
|
||||
ok
|
||||
& aeson DivResult {answer = x `div` y, remainder = x `mod` y}
|
||||
_200
|
||||
& json DivResult {answer = x `div` y, remainder = x `mod` y}
|
||||
& respond
|
||||
|
||||
getArgs :: Okapi (Int, Int)
|
||||
|
@ -12,7 +12,7 @@ import Okapi
|
||||
import Template
|
||||
|
||||
okapiApplication :: Application
|
||||
okapiApplication = makeOkapiApp id okapiAPI
|
||||
okapiApplication = okapiApp id okapiAPI
|
||||
|
||||
type Okapi a = OkapiT IO a
|
||||
|
||||
|
@ -108,7 +108,7 @@ main :: IO ()
|
||||
main = do
|
||||
conn <- open "todo.db"
|
||||
execute_ conn "CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY, name TEXT, status TEXT)"
|
||||
runOkapi id notFound 3000 (todoAPI conn)
|
||||
runOkapi id _404 3000 (todoAPI conn)
|
||||
close conn
|
||||
|
||||
-- SERVER FUNCTIONS
|
||||
@ -126,7 +126,7 @@ healthCheck :: Okapi Response
|
||||
healthCheck = do
|
||||
get
|
||||
optional $ pathSeg ""
|
||||
respond ok
|
||||
respond _200
|
||||
|
||||
getTodo :: Connection -> Okapi Response
|
||||
getTodo conn = do
|
||||
@ -135,8 +135,8 @@ getTodo conn = do
|
||||
todoID <- pathParam @Int
|
||||
maybeTodo <- liftIO $ selectTodo conn todoID
|
||||
case maybeTodo of
|
||||
Nothing -> throw internalServerError
|
||||
Just todo -> ok & aeson todo & respond
|
||||
Nothing -> throw _500
|
||||
Just todo -> _200 & json todo & respond
|
||||
|
||||
getAllTodos :: Connection -> Okapi Response
|
||||
getAllTodos conn = do
|
||||
@ -144,7 +144,7 @@ getAllTodos conn = do
|
||||
pathSeg "todos"
|
||||
status <- optional $ queryParam @Status "status"
|
||||
todos <- liftIO $ selectAllTodos conn status
|
||||
ok & aeson todos & respond
|
||||
_200 & json todos & respond
|
||||
|
||||
createTodo :: Connection -> Okapi Response
|
||||
createTodo conn = do
|
||||
@ -152,7 +152,7 @@ createTodo conn = do
|
||||
pathSeg "todos"
|
||||
todoForm <- bodyForm
|
||||
liftIO $ insertTodoForm conn todoForm
|
||||
respond ok
|
||||
respond _200
|
||||
|
||||
editTodo :: Connection -> Okapi Response
|
||||
editTodo conn = do
|
||||
@ -161,7 +161,7 @@ editTodo conn = do
|
||||
todoID <- pathParam @Int
|
||||
todoForm <- bodyForm @TodoForm
|
||||
liftIO $ updateTodo conn todoID todoForm
|
||||
respond ok
|
||||
respond _200
|
||||
|
||||
forgetTodo :: Connection -> Okapi Response
|
||||
forgetTodo conn = do
|
||||
@ -169,7 +169,7 @@ forgetTodo conn = do
|
||||
pathSeg "todos"
|
||||
todoID <- pathParam @Int
|
||||
liftIO $ deleteTodo conn todoID
|
||||
respond ok
|
||||
respond _200
|
||||
|
||||
-- DATABASE FUNCTIONS
|
||||
|
||||
|
10
okapi.cabal
10
okapi.cabal
@ -28,19 +28,13 @@ source-repository head
|
||||
library
|
||||
exposed-modules:
|
||||
Okapi
|
||||
Okapi.Application
|
||||
Okapi.Event
|
||||
Okapi.Failure
|
||||
Okapi.Internal.Functions.Application
|
||||
Okapi.Internal.Functions.Event
|
||||
Okapi.Internal.Functions.Failure
|
||||
Okapi.Internal.Functions.Parser
|
||||
Okapi.Internal.Functions.Response
|
||||
Okapi.Internal.Functions.Route
|
||||
Okapi.Internal.Types
|
||||
Okapi.Parser
|
||||
Okapi.Response
|
||||
Okapi.Route
|
||||
Okapi.Test
|
||||
Okapi.Types
|
||||
other-modules:
|
||||
Paths_okapi
|
||||
hs-source-dirs:
|
||||
|
67
src/Okapi.hs
67
src/Okapi.hs
@ -11,16 +11,44 @@
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
|
||||
module Okapi
|
||||
( module Okapi.Internal.Types,
|
||||
module Okapi.Event,
|
||||
module Okapi.Failure,
|
||||
module Okapi.Parser,
|
||||
module Okapi.Response,
|
||||
module Okapi.Route,
|
||||
module Okapi.Test,
|
||||
( -- TOP LEVEL
|
||||
runOkapi,
|
||||
runOkapiTLS,
|
||||
runOkapiWebsockets
|
||||
runOkapiWebsockets,
|
||||
runOkapiWebsocketsTLS,
|
||||
-- APPLICATION
|
||||
module Okapi.Application,
|
||||
-- SERVER SENT EVENTS
|
||||
newEventSource,
|
||||
sendValue,
|
||||
sendEvent,
|
||||
-- PARSER
|
||||
module Okapi.Parser,
|
||||
-- RESPONSE
|
||||
module Okapi.Response,
|
||||
-- ROUTE
|
||||
module Okapi.Route,
|
||||
-- TEST
|
||||
module Okapi.Test,
|
||||
-- TYPES
|
||||
MonadOkapi,
|
||||
OkapiT,
|
||||
State,
|
||||
Request,
|
||||
Response,
|
||||
ResponseBody,
|
||||
Failure,
|
||||
Path,
|
||||
Headers,
|
||||
QueryItem,
|
||||
Query,
|
||||
Cookie,
|
||||
Cookies,
|
||||
ToSSE (..),
|
||||
Event (..),
|
||||
Chan,
|
||||
EventSource,
|
||||
URL (..),
|
||||
)
|
||||
where
|
||||
|
||||
@ -60,15 +88,14 @@ import qualified Network.Wai.Handler.WarpTLS as Warp
|
||||
import qualified Network.Wai.Internal as Wai
|
||||
import Network.Wai.Middleware.Gzip (def, gzip)
|
||||
import qualified Network.WebSockets as WS
|
||||
import Okapi.Application
|
||||
import Okapi.Event
|
||||
import qualified Okapi.Event as Event
|
||||
import Okapi.Failure
|
||||
import Okapi.Internal.Functions.Application
|
||||
import Okapi.Internal.Types
|
||||
import Okapi.Parser
|
||||
import Okapi.Response
|
||||
import Okapi.Route
|
||||
import Okapi.Test
|
||||
import Okapi.Types
|
||||
import qualified Web.Cookie as Cookie
|
||||
import qualified Web.FormUrlEncoded as Web
|
||||
import qualified Web.HttpApiData as Web
|
||||
@ -76,14 +103,18 @@ import qualified Web.HttpApiData as Web
|
||||
runOkapi :: Monad m => (forall a. m a -> IO a) -> Response -> Int -> OkapiT m Response -> IO ()
|
||||
runOkapi hoister defaultResponse port okapiT = do
|
||||
print $ "Running Okapi App on port " <> show port
|
||||
Warp.run port $ makeOkapiApp hoister defaultResponse okapiT
|
||||
|
||||
runOkapiWebsockets :: Monad m => (forall a. m a -> IO a) -> Response -> Int -> OkapiT m Response -> WS.ConnectionOptions -> WS.ServerApp -> IO ()
|
||||
runOkapiWebsockets hoister defaultResponse port okapiT connSettings serverApp = do
|
||||
print $ "Running Okapi App on port " <> show port
|
||||
Warp.run port $ makeOkapiAppWebsockets hoister defaultResponse okapiT connSettings serverApp
|
||||
Warp.run port $ okapiApp hoister defaultResponse okapiT
|
||||
|
||||
runOkapiTLS :: Monad m => (forall a. m a -> IO a) -> Response -> Warp.TLSSettings -> Warp.Settings -> OkapiT m Response -> IO ()
|
||||
runOkapiTLS hoister defaultResponse tlsSettings settings okapiT = do
|
||||
print "Running servo on port 43"
|
||||
Warp.runTLS tlsSettings settings $ makeOkapiApp hoister defaultResponse okapiT
|
||||
Warp.runTLS tlsSettings settings $ okapiApp hoister defaultResponse okapiT
|
||||
|
||||
runOkapiWebsockets :: Monad m => (forall a. m a -> IO a) -> Response -> Int -> OkapiT m Response -> WS.ConnectionOptions -> WS.ServerApp -> IO ()
|
||||
runOkapiWebsockets hoister defaultResponse port okapiT connSettings serverApp = do
|
||||
print $ "Running Okapi App on port " <> show port
|
||||
Warp.run port $ okapiAppWebsockets hoister defaultResponse okapiT connSettings serverApp
|
||||
|
||||
runOkapiWebsocketsTLS :: Monad m => (forall a. m a -> IO a) -> Response -> Warp.TLSSettings -> Warp.Settings -> OkapiT m Response -> WS.ConnectionOptions -> WS.ServerApp -> IO ()
|
||||
runOkapiWebsocketsTLS hoister defaultResponse tlsSettings settings okapiT connSettings serverApp = do
|
||||
Warp.runTLS tlsSettings settings $ okapiAppWebsockets hoister defaultResponse okapiT connSettings serverApp
|
||||
|
55
src/Okapi/Application.hs
Normal file
55
src/Okapi/Application.hs
Normal file
@ -0,0 +1,55 @@
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module Okapi.Application
|
||||
( okapiApp,
|
||||
okapiAppWebsockets,
|
||||
)
|
||||
where
|
||||
|
||||
import qualified Control.Monad.Except as Except
|
||||
import qualified Control.Monad.Morph as Morph
|
||||
import qualified Control.Monad.State.Strict as State
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Network.Wai as Wai
|
||||
import Network.Wai.Handler.WebSockets
|
||||
import qualified Network.Wai.Handler.WebSockets as WS
|
||||
import qualified Network.Wai.Middleware.Gzip as Middleware
|
||||
import qualified Network.WebSockets as WS
|
||||
import Okapi.Event
|
||||
import Okapi.Types
|
||||
|
||||
okapiApp :: Monad m => (forall a. m a -> IO a) -> Response -> OkapiT m Response -> Wai.Application
|
||||
okapiApp hoister defaultResponse okapiT waiRequest respond = do
|
||||
(eitherFailureOrResponse, _state) <- (State.runStateT . Except.runExceptT . unOkapiT $ Morph.hoist hoister okapiT) (waiRequestToState waiRequest)
|
||||
let response =
|
||||
case eitherFailureOrResponse of
|
||||
Left Skip -> defaultResponse
|
||||
Left (Error errorResponse) -> errorResponse
|
||||
Right succesfulResponse -> succesfulResponse
|
||||
responseToWaiApp response waiRequest respond
|
||||
where
|
||||
responseToWaiApp :: Response -> Wai.Application
|
||||
responseToWaiApp (Response {..}) waiRequest respond = case responseBody of
|
||||
ResponseBodyRaw body -> respond $ Wai.responseLBS (toEnum $ fromEnum responseStatus) responseHeaders body
|
||||
ResponseBodyFile filePath -> respond $ Wai.responseFile (toEnum $ fromEnum responseStatus) responseHeaders filePath Nothing
|
||||
ResponseBodyEventSource eventSource -> (Middleware.gzip Middleware.def $ eventSourceAppUnagiChan eventSource) waiRequest respond
|
||||
|
||||
waiRequestToState :: Wai.Request -> State
|
||||
waiRequestToState waiRequest =
|
||||
let requestMethod = Wai.requestMethod waiRequest
|
||||
requestPath = Wai.pathInfo waiRequest
|
||||
requestQuery = HTTP.queryToQueryText $ Wai.queryString waiRequest
|
||||
requestBody = Wai.strictRequestBody waiRequest
|
||||
requestHeaders = Wai.requestHeaders waiRequest
|
||||
requestVault = Wai.vault waiRequest
|
||||
stateRequest = Request {..}
|
||||
stateRequestMethodParsed = False
|
||||
stateRequestBodyParsed = False
|
||||
stateResponded = False
|
||||
in State {..}
|
||||
|
||||
okapiAppWebsockets :: Monad m => (forall a. m a -> IO a) -> Response -> OkapiT m Response -> WS.ConnectionOptions -> WS.ServerApp -> Wai.Application
|
||||
okapiAppWebsockets hoister defaultResponse okapiT connSettings serverApp =
|
||||
let backup = okapiApp hoister defaultResponse okapiT
|
||||
in WS.websocketsOr connSettings serverApp backup
|
@ -1,6 +1,25 @@
|
||||
module Okapi.Event where
|
||||
import Okapi.Internal.Types
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module Okapi.Event
|
||||
( newEventSource,
|
||||
sendValue,
|
||||
sendEvent,
|
||||
eventSourceAppUnagiChan,
|
||||
)
|
||||
where
|
||||
|
||||
import qualified Control.Concurrent.Chan.Unagi as Unagi
|
||||
import qualified Control.Monad.IO.Class as IO
|
||||
import qualified Data.ByteString.Builder as Builder
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import qualified Data.Function as Function
|
||||
import qualified Data.Text.Encoding as Text
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Network.Wai as Wai
|
||||
import qualified Network.Wai.EventSource as Wai
|
||||
import Network.WebSockets (send)
|
||||
import Okapi.Types
|
||||
|
||||
-- TODO: Use MonadOkapi
|
||||
newEventSource :: IO EventSource
|
||||
@ -11,3 +30,61 @@ sendValue (inChan, _outChan) = Unagi.writeChan inChan . toSSE
|
||||
|
||||
sendEvent :: EventSource -> Event -> IO ()
|
||||
sendEvent (inChan, _outChan) = Unagi.writeChan inChan
|
||||
|
||||
eventSourceAppUnagiChan :: EventSource -> Wai.Application
|
||||
eventSourceAppUnagiChan (inChan, _outChan) req sendResponse = do
|
||||
outChan <- IO.liftIO $ Unagi.dupChan inChan
|
||||
eventSourceAppIO (eventToServerEvent <$> Unagi.readChan outChan) req sendResponse
|
||||
|
||||
-- BELOW IS INTERNAL
|
||||
|
||||
eventSourceAppIO :: IO Wai.ServerEvent -> Wai.Application
|
||||
eventSourceAppIO src _ sendResponse =
|
||||
sendResponse $
|
||||
Wai.responseStream
|
||||
HTTP.status200
|
||||
[(HTTP.hContentType, "text/event-stream")]
|
||||
$ \sendChunk flush -> do
|
||||
flush
|
||||
Function.fix $ \loop -> do
|
||||
se <- src
|
||||
case eventToBuilder se of
|
||||
Nothing -> return ()
|
||||
Just b -> sendChunk b >> flush >> loop
|
||||
|
||||
eventToBuilder :: Wai.ServerEvent -> Maybe Builder.Builder
|
||||
eventToBuilder (Wai.CommentEvent txt) = Just $ field commentField txt
|
||||
eventToBuilder (Wai.RetryEvent n) = Just $ field retryField (Builder.string8 . show $ n)
|
||||
eventToBuilder Wai.CloseEvent = Nothing
|
||||
eventToBuilder (Wai.ServerEvent n i d) =
|
||||
Just $
|
||||
mappend (name n (evid i $ evdata (mconcat d) nl)) nl
|
||||
where
|
||||
name Nothing = id
|
||||
name (Just n') = mappend (field nameField n')
|
||||
evid Nothing = id
|
||||
evid (Just i') = mappend (field idField i')
|
||||
evdata d' = mappend (field dataField d')
|
||||
|
||||
nl :: Builder.Builder
|
||||
nl = Builder.char7 '\n'
|
||||
|
||||
nameField, idField, dataField, retryField, commentField :: Builder.Builder
|
||||
nameField = Builder.string7 "event:"
|
||||
idField = Builder.string7 "id:"
|
||||
dataField = Builder.string7 "data:"
|
||||
retryField = Builder.string7 "retry:"
|
||||
commentField = Builder.char7 ':'
|
||||
|
||||
-- | Wraps the text as a labeled field of an event stream.
|
||||
field :: Builder.Builder -> Builder.Builder -> Builder.Builder
|
||||
field l b = l `mappend` b `mappend` nl
|
||||
|
||||
eventToServerEvent :: Event -> Wai.ServerEvent
|
||||
eventToServerEvent Event {..} =
|
||||
Wai.ServerEvent
|
||||
(Builder.byteString . Text.encodeUtf8 <$> eventName)
|
||||
(Builder.byteString . Text.encodeUtf8 <$> eventID)
|
||||
(Builder.word8 <$> LBS.unpack eventData)
|
||||
eventToServerEvent (CommentEvent comment) = Wai.CommentEvent $ Builder.lazyByteString comment
|
||||
eventToServerEvent CloseEvent = Wai.CloseEvent
|
||||
|
@ -1,30 +0,0 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
|
||||
module Okapi.Failure
|
||||
( module Okapi.Failure,
|
||||
module Okapi.Internal.Functions.Failure,
|
||||
)
|
||||
where
|
||||
|
||||
import qualified Control.Monad.Except as Except
|
||||
import Okapi.Internal.Functions.Failure
|
||||
import Okapi.Internal.Types
|
||||
|
||||
-- | Execute the next parser even if the first one throws an Error failure
|
||||
(<!>) :: forall a m. MonadOkapi m => m a -> m a -> m a
|
||||
parser1 <!> parser2 = Except.catchError parser1 (const parser2)
|
||||
|
||||
guardE :: forall a m. MonadOkapi m => Response -> Bool -> m ()
|
||||
guardE _ True = pure ()
|
||||
guardE response False = throw response
|
||||
|
||||
optionalE :: forall a m. MonadOkapi m => m a -> m (Maybe a)
|
||||
optionalE parser = (Just <$> parser) <!> pure Nothing
|
||||
|
||||
optionE :: forall a m. MonadOkapi m => a -> m a -> m a
|
||||
optionE value parser = do
|
||||
mbValue <- optionalE parser
|
||||
case mbValue of
|
||||
Nothing -> pure value
|
||||
Just value' -> pure value'
|
@ -1,51 +0,0 @@
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module Okapi.Internal.Functions.Application where
|
||||
|
||||
import qualified Control.Monad.Except as Except
|
||||
import qualified Control.Monad.Morph as Morph
|
||||
import qualified Control.Monad.State.Strict as State
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Network.Wai as Wai
|
||||
import Network.Wai.Handler.WebSockets
|
||||
import qualified Network.Wai.Handler.WebSockets as WS
|
||||
import qualified Network.Wai.Middleware.Gzip as Middleware
|
||||
import qualified Network.WebSockets as WS
|
||||
import Okapi.Internal.Functions.Event
|
||||
import Okapi.Internal.Types
|
||||
|
||||
makeOkapiApp :: Monad m => (forall a. m a -> IO a) -> Response -> OkapiT m Response -> Wai.Application
|
||||
makeOkapiApp hoister defaultResponse okapiT waiRequest respond = do
|
||||
(eitherFailureOrResponse, _state) <- (State.runStateT . Except.runExceptT . unOkapiT $ Morph.hoist hoister okapiT) (waiRequestToState waiRequest)
|
||||
let response =
|
||||
case eitherFailureOrResponse of
|
||||
Left Skip -> defaultResponse
|
||||
Left (Error errorResponse) -> errorResponse
|
||||
Right succesfulResponse -> succesfulResponse
|
||||
responseToWaiApp response waiRequest respond
|
||||
|
||||
makeOkapiAppWebsockets :: Monad m => (forall a. m a -> IO a) -> Response -> OkapiT m Response -> WS.ConnectionOptions -> WS.ServerApp -> Wai.Application
|
||||
makeOkapiAppWebsockets hoister defaultResponse okapiT connSettings serverApp =
|
||||
let backup = makeOkapiApp hoister defaultResponse okapiT
|
||||
in WS.websocketsOr connSettings serverApp backup
|
||||
|
||||
responseToWaiApp :: Response -> Wai.Application
|
||||
responseToWaiApp (Response {..}) waiRequest respond = case responseBody of
|
||||
ResponseBodyRaw body -> respond $ Wai.responseLBS (toEnum $ fromEnum responseStatus) responseHeaders body
|
||||
ResponseBodyFile filePath -> respond $ Wai.responseFile (toEnum $ fromEnum responseStatus) responseHeaders filePath Nothing
|
||||
ResponseBodyEventSource eventSource -> (Middleware.gzip Middleware.def $ eventSourceAppUnagiChan eventSource) waiRequest respond
|
||||
|
||||
waiRequestToState :: Wai.Request -> State
|
||||
waiRequestToState waiRequest =
|
||||
let requestMethod = Wai.requestMethod waiRequest
|
||||
requestPath = Wai.pathInfo waiRequest
|
||||
requestQuery = HTTP.queryToQueryText $ Wai.queryString waiRequest
|
||||
requestBody = Wai.strictRequestBody waiRequest
|
||||
requestHeaders = Wai.requestHeaders waiRequest
|
||||
requestVault = Wai.vault waiRequest
|
||||
stateRequest = Request {..}
|
||||
stateRequestMethodParsed = False
|
||||
stateRequestBodyParsed = False
|
||||
stateResponded = False
|
||||
in State {..}
|
@ -1,80 +0,0 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE StrictData #-}
|
||||
|
||||
module Okapi.Internal.Functions.Event where
|
||||
|
||||
import qualified Control.Concurrent.Chan.Unagi as Unagi
|
||||
import qualified Control.Monad.IO.Class as IO
|
||||
import qualified Data.ByteString.Builder as Builder
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import qualified Data.Function as Function
|
||||
import qualified Data.Map.Strict as Map
|
||||
import qualified Data.Text as Text
|
||||
import qualified Data.Text.Encoding as Text
|
||||
import qualified Data.UUID as UUID
|
||||
import qualified Data.UUID.V4 as UUID
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Network.HTTP.Types.Status as HTTP
|
||||
import qualified Network.Wai as Wai
|
||||
import qualified Network.Wai.EventSource as Wai
|
||||
import Okapi.Internal.Types
|
||||
|
||||
-- import Network.Wai.EventSource
|
||||
|
||||
-- TODO: Below this point, put in Interface module
|
||||
eventSourceAppUnagiChan :: EventSource -> Wai.Application
|
||||
eventSourceAppUnagiChan (inChan, _outChan) req sendResponse = do
|
||||
outChan <- IO.liftIO $ Unagi.dupChan inChan
|
||||
eventSourceAppIO (eventToServerEvent <$> Unagi.readChan outChan) req sendResponse
|
||||
|
||||
eventSourceAppIO :: IO Wai.ServerEvent -> Wai.Application
|
||||
eventSourceAppIO src _ sendResponse =
|
||||
sendResponse $
|
||||
Wai.responseStream
|
||||
HTTP.status200
|
||||
[(HTTP.hContentType, "text/event-stream")]
|
||||
$ \sendChunk flush -> do
|
||||
flush
|
||||
Function.fix $ \loop -> do
|
||||
se <- src
|
||||
case eventToBuilder se of
|
||||
Nothing -> return ()
|
||||
Just b -> sendChunk b >> flush >> loop
|
||||
|
||||
eventToBuilder :: Wai.ServerEvent -> Maybe Builder.Builder
|
||||
eventToBuilder (Wai.CommentEvent txt) = Just $ field commentField txt
|
||||
eventToBuilder (Wai.RetryEvent n) = Just $ field retryField (Builder.string8 . show $ n)
|
||||
eventToBuilder Wai.CloseEvent = Nothing
|
||||
eventToBuilder (Wai.ServerEvent n i d) =
|
||||
Just $
|
||||
mappend (name n (evid i $ evdata (mconcat d) nl)) nl
|
||||
where
|
||||
name Nothing = id
|
||||
name (Just n') = mappend (field nameField n')
|
||||
evid Nothing = id
|
||||
evid (Just i') = mappend (field idField i')
|
||||
evdata d' = mappend (field dataField d')
|
||||
|
||||
nl :: Builder.Builder
|
||||
nl = Builder.char7 '\n'
|
||||
|
||||
nameField, idField, dataField, retryField, commentField :: Builder.Builder
|
||||
nameField = Builder.string7 "event:"
|
||||
idField = Builder.string7 "id:"
|
||||
dataField = Builder.string7 "data:"
|
||||
retryField = Builder.string7 "retry:"
|
||||
commentField = Builder.char7 ':'
|
||||
|
||||
-- | Wraps the text as a labeled field of an event stream.
|
||||
field :: Builder.Builder -> Builder.Builder -> Builder.Builder
|
||||
field l b = l `mappend` b `mappend` nl
|
||||
|
||||
eventToServerEvent :: Event -> Wai.ServerEvent
|
||||
eventToServerEvent Event {..} =
|
||||
Wai.ServerEvent
|
||||
(Builder.byteString . Text.encodeUtf8 <$> eventName)
|
||||
(Builder.byteString . Text.encodeUtf8 <$> eventID)
|
||||
(Builder.word8 <$> LBS.unpack eventData)
|
||||
eventToServerEvent (CommentEvent comment) = Wai.CommentEvent $ Builder.lazyByteString comment
|
||||
eventToServerEvent CloseEvent = Wai.CloseEvent
|
@ -1,15 +0,0 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
|
||||
module Okapi.Internal.Functions.Failure where
|
||||
|
||||
import qualified Control.Monad.Except as Except
|
||||
import Okapi.Internal.Types
|
||||
|
||||
-- ERROR FUNCTIONS
|
||||
|
||||
skip :: forall a m. MonadOkapi m => m a
|
||||
skip = Except.throwError Skip
|
||||
|
||||
throw :: forall a m. MonadOkapi m => Response -> m a
|
||||
throw = Except.throwError . Error
|
@ -1,133 +0,0 @@
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE DerivingStrategies #-}
|
||||
{-# LANGUAGE ExistentialQuantification #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE InstanceSigs #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StrictData #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
|
||||
module Okapi.Internal.Functions.Parser where
|
||||
|
||||
import qualified Control.Applicative as Applicative
|
||||
import qualified Control.Concurrent.Chan as Chan
|
||||
import qualified Control.Concurrent.STM.TVar as TVar
|
||||
import qualified Control.Monad as Monad
|
||||
import qualified Control.Monad.Except as Except
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import qualified Control.Monad.IO.Class as IO
|
||||
import qualified Control.Monad.Morph as Morph
|
||||
import qualified Control.Monad.Reader.Class as Reader
|
||||
import qualified Control.Monad.State as State
|
||||
import qualified Control.Monad.State.Class as State
|
||||
import qualified Control.Monad.Trans.Except as ExceptT
|
||||
import qualified Control.Monad.Trans.State.Strict as StateT
|
||||
import qualified Data.ByteString.Lazy as LazyByteString
|
||||
import qualified Data.Foldable as Foldable
|
||||
import qualified Data.List as List
|
||||
import qualified Data.Text as Text
|
||||
import qualified Data.Vault.Lazy as Vault
|
||||
import qualified GHC.Natural as Natural
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Okapi.Event as Event
|
||||
import Okapi.Internal.Functions.Failure
|
||||
import Okapi.Internal.Types
|
||||
import Okapi.Response
|
||||
|
||||
-- PRIMITIVE PARSERS
|
||||
|
||||
parseMethod :: MonadOkapi m => m HTTP.Method
|
||||
parseMethod = do
|
||||
isMethodParsed <- methodParsed
|
||||
if isMethodParsed
|
||||
then skip
|
||||
else do
|
||||
method <- State.gets (requestMethod . stateRequest)
|
||||
State.modify (\state -> state {stateRequestMethodParsed = True})
|
||||
pure method
|
||||
|
||||
parsePathSeg :: MonadOkapi m => m Text.Text
|
||||
parsePathSeg = do
|
||||
maybePathSeg <- State.gets (safeHead . requestPath . stateRequest)
|
||||
case maybePathSeg of
|
||||
Nothing -> skip
|
||||
Just pathSeg -> do
|
||||
State.modify (\state -> state {stateRequest = (stateRequest state) {requestPath = Prelude.drop 1 $ requestPath $ stateRequest state}})
|
||||
pure pathSeg
|
||||
where
|
||||
safeHead :: [a] -> Maybe a
|
||||
safeHead [] = Nothing
|
||||
safeHead (x : _) = Just x
|
||||
|
||||
parseQueryItem :: MonadOkapi m => Text.Text -> m QueryItem
|
||||
parseQueryItem queryItemName = do
|
||||
maybeQueryItem <- State.gets (Foldable.find (\(queryItemName', _) -> queryItemName == queryItemName') . requestQuery . stateRequest)
|
||||
case maybeQueryItem of
|
||||
Nothing -> skip
|
||||
Just queryItem -> do
|
||||
State.modify (\state -> state {stateRequest = (stateRequest state) {requestQuery = List.delete queryItem $ requestQuery $ stateRequest state}})
|
||||
pure queryItem
|
||||
|
||||
parseHeader :: MonadOkapi m => HTTP.HeaderName -> m HTTP.Header
|
||||
parseHeader headerName = do
|
||||
maybeHeader <- State.gets (Foldable.find (\(headerName', _) -> headerName == headerName') . requestHeaders . stateRequest)
|
||||
case maybeHeader of
|
||||
Nothing -> skip
|
||||
Just header -> do
|
||||
State.modify (\state -> state {stateRequest = (stateRequest state) {requestHeaders = List.delete header $ requestHeaders $ stateRequest state}})
|
||||
pure header
|
||||
|
||||
parseBody :: forall m. MonadOkapi m => m LazyByteString.ByteString
|
||||
parseBody = do
|
||||
isBodyParsed <- bodyParsed
|
||||
if isBodyParsed
|
||||
then skip
|
||||
else do
|
||||
bodyRef <- State.gets (requestBody . stateRequest)
|
||||
body <- liftIO bodyRef
|
||||
State.modify (\state -> state {stateRequestBodyParsed = True})
|
||||
pure body
|
||||
|
||||
-- State Checks
|
||||
|
||||
methodParsed :: MonadOkapi m => m Bool
|
||||
methodParsed = State.gets stateRequestMethodParsed
|
||||
|
||||
pathParsed :: MonadOkapi m => m Bool
|
||||
pathParsed = State.gets (Prelude.null . requestPath . stateRequest)
|
||||
|
||||
queryParsed :: MonadOkapi m => m Bool
|
||||
queryParsed = State.gets (Prelude.null . requestQuery . stateRequest)
|
||||
|
||||
headersParsed :: MonadOkapi m => m Bool
|
||||
headersParsed = State.gets (Prelude.null . requestHeaders . stateRequest)
|
||||
|
||||
bodyParsed :: MonadOkapi m => m Bool
|
||||
bodyParsed = State.gets stateRequestBodyParsed
|
||||
|
||||
{-
|
||||
TODO: HTTPDataStore? Not really needed because you can just pass data normally or store in own monad.
|
||||
One benefit is that the data is available to all sub-branches without explicitly passing them to every sub-branch.
|
||||
|
||||
-- This data structure should be hidden from user
|
||||
data HTTPDataStore = HTTPDataStore
|
||||
{ pathStore :: (Map Text Text)
|
||||
, queryStore :: (Map Text Text)
|
||||
, headerStore :: (Map Text Text)
|
||||
}
|
||||
|
||||
-- Can only store parsed information
|
||||
storePathParam :: forall a. FromHttpApiData a => Text -> Okapi a
|
||||
storePathParam = ...
|
||||
|
||||
storeQueryParam :: ... => Text -> Okapi a
|
||||
|
||||
storeHeader :: ... => Text -> Okapi a
|
||||
|
||||
-- Can fail on Map lookup and data conversion
|
||||
findPathParam :: forall a. FromHttpApiData a => Okapi a
|
||||
-}
|
@ -1,30 +0,0 @@
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module Okapi.Internal.Functions.Response where
|
||||
|
||||
import qualified GHC.Natural as Natural
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import Okapi.Internal.Types
|
||||
|
||||
setResponseStatus :: Natural.Natural -> Response -> Response
|
||||
setResponseStatus status response = response {responseStatus = status}
|
||||
|
||||
setResponseHeaders :: Headers -> Response -> Response
|
||||
setResponseHeaders headers response = response {responseHeaders = headers}
|
||||
|
||||
-- TODO: setResponseCookie
|
||||
|
||||
setResponseHeader :: HTTP.Header -> Response -> Response
|
||||
setResponseHeader header response@Response {..} =
|
||||
response {responseHeaders = update header responseHeaders}
|
||||
where
|
||||
update :: forall a b. Eq a => (a, b) -> [(a, b)] -> [(a, b)]
|
||||
update pair [] = [pair]
|
||||
update pair@(key, value) (pair'@(key', value') : ps) =
|
||||
if key == key'
|
||||
then pair : ps
|
||||
else pair' : update pair ps
|
||||
|
||||
setResponseBody :: ResponseBody -> Response -> Response
|
||||
setResponseBody body response = response {responseBody = body}
|
@ -1,170 +0,0 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Okapi.Internal.Functions.Route where
|
||||
|
||||
import Control.Monad.Combinators
|
||||
import qualified Data.Attoparsec.Text as Atto
|
||||
import Data.Char
|
||||
import Data.Maybe
|
||||
import Data.Text
|
||||
import Language.Haskell.TH
|
||||
import Okapi.Internal.Types
|
||||
import System.Random
|
||||
|
||||
parseCurlyExpr :: Atto.Parser CurlyExpr
|
||||
parseCurlyExpr = between (Atto.char '{') (Atto.char '}') $ do
|
||||
typeName <- Atto.takeWhile (\char -> isAlphaNum char || char == '[' || char == ']' || char == '(' || char == ')')
|
||||
transformFunctionNames <- many $ do
|
||||
Atto.string "->"
|
||||
Atto.takeWhile isAlphaNum
|
||||
filterFunctionName <- optional $ do
|
||||
Atto.char '|'
|
||||
Atto.takeWhile isAlphaNum
|
||||
pure $ CurlyExpr typeName transformFunctionNames filterFunctionName
|
||||
|
||||
routeParser :: Atto.Parser [RoutePart]
|
||||
routeParser = many $ do
|
||||
Atto.skipSpace
|
||||
parseMethod <|> parsePathSegMatch <|> parseAnonPathSeg <|> parseAnonQueryParam <|> parseBind
|
||||
|
||||
parseMethod :: Atto.Parser RoutePart
|
||||
parseMethod = do
|
||||
method <- Atto.takeWhile isUpper
|
||||
case method of
|
||||
"GET" -> pure $ Method "GET"
|
||||
"HEAD" -> pure $ Method "HEAD"
|
||||
"POST" -> pure $ Method "POST"
|
||||
"DELETE" -> pure $ Method "DELETE"
|
||||
"PUT" -> pure $ Method "PUT"
|
||||
"PATCH" -> pure $ Method "PATCH"
|
||||
_ -> fail "Couldn't parse method"
|
||||
|
||||
parsePathSegMatch :: Atto.Parser RoutePart
|
||||
parsePathSegMatch = do
|
||||
Atto.char '/'
|
||||
match <- Atto.takeWhile1 isAlpha
|
||||
pure $ PathSegMatch match
|
||||
|
||||
parseAnonPathSeg :: Atto.Parser RoutePart
|
||||
parseAnonPathSeg = do
|
||||
Atto.char '/'
|
||||
AnonPathSeg <$> parseCurlyExpr
|
||||
|
||||
parseAnonQueryParam :: Atto.Parser RoutePart
|
||||
parseAnonQueryParam = do
|
||||
Atto.char '?'
|
||||
queryParamName <- Atto.takeWhile isAlphaNum
|
||||
AnonQueryParam queryParamName <$> parseCurlyExpr
|
||||
|
||||
parseBind :: Atto.Parser RoutePart
|
||||
parseBind = do
|
||||
Atto.string ">>="
|
||||
Atto.skipSpace
|
||||
functionName <- Atto.takeWhile1 (\char -> isAlphaNum char || char == '.')
|
||||
pure $ Bind functionName
|
||||
|
||||
routePartsToExp :: [RoutePart] -> Q Exp
|
||||
routePartsToExp [] =
|
||||
pure $
|
||||
RecConE
|
||||
(mkName "Route")
|
||||
[ (mkName "parser", VarE (mkName "Okapi.skip")),
|
||||
(mkName "url", LamE [VarP $ mkName "unit"] (AppE (ConE $ mkName "Okapi.URL") (LitE $ StringL "")))
|
||||
]
|
||||
routePartsToExp routeParts = do
|
||||
let binds = Prelude.dropWhile (not . isBind) routeParts
|
||||
notBinds = Prelude.takeWhile (not . isBind) routeParts
|
||||
routePartStmtsAndBindings <- mapM routePartStmtAndBinding notBinds
|
||||
let routePartStmts = Prelude.map (\(_, _, stmts) -> stmts) routePartStmtsAndBindings
|
||||
bindingsAndTypes = mapMaybe (\(bandTs, _, _) -> bandTs) routePartStmtsAndBindings
|
||||
bAndTHelper :: (Maybe (Name, Type), Maybe HTTPDataType, Stmt) -> Maybe (Maybe Name, HTTPDataType) = \case
|
||||
(Nothing, Just pathSegType@(PathSegType seg), _) -> Just (Nothing, pathSegType)
|
||||
(Just (name, _), Just httpDataType, _) -> Just (Just name, httpDataType)
|
||||
_ -> Nothing
|
||||
bindingsAndHTTPDataTypes :: [(Maybe Name, HTTPDataType)] = mapMaybe bAndTHelper routePartStmtsAndBindings
|
||||
bindings = Prelude.map fst bindingsAndTypes
|
||||
-- types = map snd bindingsAndTypes
|
||||
returnStmt :: Stmt =
|
||||
case bindings of
|
||||
[] -> NoBindS (AppE (VarE $ mkName "pure") (ConE $ mkName "()"))
|
||||
[binding] -> NoBindS (AppE (VarE $ mkName "pure") (VarE binding))
|
||||
_ -> NoBindS (AppE (VarE $ mkName "pure") (TupE (Prelude.map (Just . VarE) bindings)))
|
||||
leftSide = ParensE (DoE Nothing $ routePartStmts <> [returnStmt])
|
||||
(middle, rightSide) =
|
||||
case binds of
|
||||
[] -> (VarE $ mkName ">>=", LamE [VarP $ mkName "params"] (AppE (VarE $ mkName "pure") (VarE $ mkName "params")))
|
||||
[Bind functionName] -> (VarE $ mkName ">>=", VarE $ mkName $ unpack functionName)
|
||||
_ -> (VarE $ mkName ">>", AppE (VarE $ mkName "Okapi.throw") (VarE $ mkName "Okapi.internalServerError"))
|
||||
pure $
|
||||
RecConE
|
||||
(mkName "Okapi.Route")
|
||||
[ (mkName "parser", UInfixE leftSide middle rightSide),
|
||||
(mkName "url", LamE [lambdaPattern bindingsAndTypes] (lambdaBody True bindingsAndHTTPDataTypes))
|
||||
]
|
||||
|
||||
isBind :: RoutePart -> Bool
|
||||
isBind (Bind _) = True
|
||||
isBind _ = False
|
||||
|
||||
-- bindsExp :: NonEmpty RoutePart -> Exp
|
||||
-- bindsExp (Bind functionName) = VarE $ mkName $ unpack functionName
|
||||
-- bindsExp ((Bind functionName) :| rps) = UInfixE (VarE $ mkName $ unpack functionName) (VarE $ mkName ">>=") (loop rps)
|
||||
-- where
|
||||
-- loop :: [RoutePart] -> Exp
|
||||
-- loop [] = LamE [WildP] (VarE $ mkName "Okapi.skip")
|
||||
-- loop ((Bind functionName) : rps) = undefined
|
||||
|
||||
lambdaPattern :: [(Name, Type)] -> Pat
|
||||
lambdaPattern [] = WildP
|
||||
lambdaPattern [(n, t)] = SigP (VarP n) t
|
||||
lambdaPattern nAndTs = TupP $ Prelude.map (\(n, t) -> SigP (VarP n) t) nAndTs
|
||||
|
||||
isQueryParamType :: HTTPDataType -> Bool
|
||||
isQueryParamType (AnonQueryParamType _) = True
|
||||
isQueryParamType _ = False
|
||||
|
||||
lambdaBody :: Bool -> [(Maybe Name, HTTPDataType)] -> Exp
|
||||
lambdaBody _ [] = AppE (ConE (mkName "Okapi.URL")) (LitE $ StringL "")
|
||||
lambdaBody isFirstQueryParam (combo@(_, httpDataType) : combos) =
|
||||
UInfixE
|
||||
(helper isFirstQueryParam combo)
|
||||
(VarE $ mkName "<>")
|
||||
( lambdaBody
|
||||
( not (isQueryParamType httpDataType && isFirstQueryParam) && isFirstQueryParam
|
||||
)
|
||||
combos
|
||||
)
|
||||
where
|
||||
helper :: Bool -> (Maybe Name, HTTPDataType) -> Exp
|
||||
helper _ (Nothing, PathSegType match) = AppE (ConE (mkName "Okapi.URL")) (LitE $ StringL $ "/" <> unpack match)
|
||||
helper _ (Just name, AnonPathParamType) = AppE (ConE (mkName "Okapi.URL")) (UInfixE (LitE $ StringL "/") (VarE $ mkName "<>") (ParensE $ AppE (VarE $ mkName "toUrlPiece") (VarE name)))
|
||||
helper isFirstQueryParam' (Just name, AnonQueryParamType queryParamName) = AppE (ConE (mkName "Okapi.URL")) (UInfixE (LitE $ StringL $ unpack $ (if isFirstQueryParam' then "?" else "&") <> queryParamName <> "=") (VarE $ mkName "<>") (ParensE $ AppE (VarE $ mkName "toQueryParam") (VarE name)))
|
||||
helper _ _ = AppE (ConE (mkName "Okapi.URL")) (LitE $ StringL "")
|
||||
|
||||
routePartStmtAndBinding :: RoutePart -> Q (Maybe (Name, Type), Maybe HTTPDataType, Stmt)
|
||||
routePartStmtAndBinding rp = case rp of
|
||||
Method m -> case m of
|
||||
"GET" -> pure (Nothing, Nothing, NoBindS (VarE $ mkName "Okapi.get"))
|
||||
"HEAD" -> pure (Nothing, Nothing, NoBindS (VarE $ mkName "Okapi.head"))
|
||||
"POST" -> pure (Nothing, Nothing, NoBindS (VarE $ mkName "Okapi.post"))
|
||||
"DELETE" -> pure (Nothing, Nothing, NoBindS (VarE $ mkName "Okapi.delete"))
|
||||
"PUT" -> pure (Nothing, Nothing, NoBindS (VarE $ mkName "Okapi.put"))
|
||||
"PATCH" -> pure (Nothing, Nothing, NoBindS (VarE $ mkName "Okapi.patch"))
|
||||
_ -> pure (Nothing, Nothing, NoBindS (VarE $ mkName "Okapi.skip"))
|
||||
PathSegMatch txt -> pure (Nothing, Just $ PathSegType txt, NoBindS (AppE (VarE $ mkName "Okapi.pathSeg") (LitE $ StringL $ unpack txt)))
|
||||
AnonPathSeg (CurlyExpr typeName functionNamesToApply maybeGuardFunction) -> do
|
||||
stmtBinding <- runIO randName
|
||||
let stmt = BindS (SigP (VarP stmtBinding) (ConT $ mkName $ unpack typeName)) (VarE (mkName "Okapi.pathParam"))
|
||||
pure (Just (stmtBinding, ConT $ mkName $ unpack typeName), Just AnonPathParamType, stmt)
|
||||
AnonQueryParam queryParamName (CurlyExpr typeName functionNamesToApply maybeGuardFunction) -> do
|
||||
stmtBinding <- runIO randName
|
||||
let stmt = BindS (SigP (VarP stmtBinding) (ConT $ mkName $ unpack typeName)) (AppE (VarE (mkName "Okapi.queryParam")) (LitE $ StringL $ unpack queryParamName))
|
||||
pure (Just (stmtBinding, ConT $ mkName $ unpack typeName), Just $ AnonQueryParamType queryParamName, stmt)
|
||||
Bind functionName -> pure (Nothing, Nothing, NoBindS $ AppE (VarE $ mkName "Okapi.throw") (VarE $ mkName "Okapi.internalServerError"))
|
||||
|
||||
randName :: IO Name
|
||||
randName = do
|
||||
str <- fmap (Prelude.take 10 . randomRs ('a', 'z')) newStdGen
|
||||
pure $ mkName str
|
@ -2,23 +2,72 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
|
||||
module Okapi.Parser where
|
||||
module Okapi.Parser
|
||||
( -- Method parsers
|
||||
get,
|
||||
post,
|
||||
head,
|
||||
put,
|
||||
delete,
|
||||
trace,
|
||||
connect,
|
||||
options,
|
||||
anyMethod,
|
||||
method,
|
||||
-- Path parsers
|
||||
pathSeg,
|
||||
path,
|
||||
pathParam,
|
||||
pathParamRaw,
|
||||
pathSegWith,
|
||||
-- Query parsers
|
||||
queryParam,
|
||||
queryParamRaw,
|
||||
queryFlag,
|
||||
-- Header parsers
|
||||
basicAuth,
|
||||
cookies,
|
||||
header,
|
||||
-- Body parsers
|
||||
bodyJSON,
|
||||
bodyForm,
|
||||
bodyRaw,
|
||||
-- Response Helpers
|
||||
respond,
|
||||
-- Error Helpers
|
||||
skip,
|
||||
throw,
|
||||
(<!>),
|
||||
guardThrow,
|
||||
optionalThrow,
|
||||
optionThrow,
|
||||
-- State Checkers
|
||||
methodParsed,
|
||||
pathParsed,
|
||||
queryParsed,
|
||||
headersParsed,
|
||||
bodyParsed,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad.Combinators
|
||||
import qualified Control.Monad.Except as Except
|
||||
import qualified Control.Monad.IO.Class as IO
|
||||
import qualified Control.Monad.State.Strict as State
|
||||
import qualified Data.Aeson as Aeson
|
||||
import qualified Data.ByteString.Char8 as Char8
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import qualified Data.Foldable as Foldable
|
||||
import qualified Data.List as List
|
||||
import qualified Data.Text as Text
|
||||
import qualified Data.Text.Encoding as Text
|
||||
import Data.Text.Encoding.Base64
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import Okapi.Internal.Functions.Failure
|
||||
import Okapi.Internal.Functions.Parser
|
||||
import Okapi.Internal.Types
|
||||
import Okapi.Types
|
||||
import qualified Web.Cookie as Cookie
|
||||
import qualified Web.FormUrlEncoded as Web
|
||||
import qualified Web.HttpApiData as Web
|
||||
import Prelude hiding (head)
|
||||
|
||||
-- METHOD HELPERS
|
||||
|
||||
@ -155,9 +204,130 @@ bodyForm = do
|
||||
bodyRaw :: forall m. MonadOkapi m => m LBS.ByteString
|
||||
bodyRaw = parseBody
|
||||
|
||||
-- Response helpers
|
||||
|
||||
respond :: forall m. MonadOkapi m => Response -> m Response
|
||||
respond response = do
|
||||
check1 <- methodParsed
|
||||
check2 <- pathParsed
|
||||
check3 <- queryParsed
|
||||
if check1 && check2 && check3 then return response else skip
|
||||
|
||||
-- Error HELPERS
|
||||
|
||||
skip :: forall a m. MonadOkapi m => m a
|
||||
skip = Except.throwError Skip
|
||||
|
||||
throw :: forall a m. MonadOkapi m => Response -> m a
|
||||
throw = Except.throwError . Error
|
||||
|
||||
(<!>) :: forall a m. MonadOkapi m => m a -> m a -> m a
|
||||
parser1 <!> parser2 = Except.catchError parser1 (const parser2)
|
||||
|
||||
guardThrow :: forall a m. MonadOkapi m => Response -> Bool -> m ()
|
||||
guardThrow _ True = pure ()
|
||||
guardThrow response False = throw response
|
||||
|
||||
optionalThrow :: forall a m. MonadOkapi m => m a -> m (Maybe a)
|
||||
optionalThrow parser = (Just <$> parser) <!> pure Nothing
|
||||
|
||||
optionThrow :: forall a m. MonadOkapi m => a -> m a -> m a
|
||||
optionThrow value parser = do
|
||||
mbValue <- optionalThrow parser
|
||||
case mbValue of
|
||||
Nothing -> pure value
|
||||
Just value' -> pure value'
|
||||
|
||||
-- State Checks
|
||||
|
||||
methodParsed :: MonadOkapi m => m Bool
|
||||
methodParsed = State.gets stateRequestMethodParsed
|
||||
|
||||
pathParsed :: MonadOkapi m => m Bool
|
||||
pathParsed = State.gets (Prelude.null . requestPath . stateRequest)
|
||||
|
||||
queryParsed :: MonadOkapi m => m Bool
|
||||
queryParsed = State.gets (Prelude.null . requestQuery . stateRequest)
|
||||
|
||||
headersParsed :: MonadOkapi m => m Bool
|
||||
headersParsed = State.gets (Prelude.null . requestHeaders . stateRequest)
|
||||
|
||||
bodyParsed :: MonadOkapi m => m Bool
|
||||
bodyParsed = State.gets stateRequestBodyParsed
|
||||
|
||||
-- PRIMITIVE PARSERS (BELOW IS INTERNAL)
|
||||
|
||||
parseMethod :: MonadOkapi m => m HTTP.Method
|
||||
parseMethod = do
|
||||
isMethodParsed <- methodParsed
|
||||
if isMethodParsed
|
||||
then skip
|
||||
else do
|
||||
method <- State.gets (requestMethod . stateRequest)
|
||||
State.modify (\state -> state {stateRequestMethodParsed = True})
|
||||
pure method
|
||||
|
||||
parsePathSeg :: MonadOkapi m => m Text.Text
|
||||
parsePathSeg = do
|
||||
maybePathSeg <- State.gets (safeHead . requestPath . stateRequest)
|
||||
case maybePathSeg of
|
||||
Nothing -> skip
|
||||
Just pathSeg -> do
|
||||
State.modify (\state -> state {stateRequest = (stateRequest state) {requestPath = Prelude.drop 1 $ requestPath $ stateRequest state}})
|
||||
pure pathSeg
|
||||
where
|
||||
safeHead :: [a] -> Maybe a
|
||||
safeHead [] = Nothing
|
||||
safeHead (x : _) = Just x
|
||||
|
||||
parseQueryItem :: MonadOkapi m => Text.Text -> m QueryItem
|
||||
parseQueryItem queryItemName = do
|
||||
maybeQueryItem <- State.gets (Foldable.find (\(queryItemName', _) -> queryItemName == queryItemName') . requestQuery . stateRequest)
|
||||
case maybeQueryItem of
|
||||
Nothing -> skip
|
||||
Just queryItem -> do
|
||||
State.modify (\state -> state {stateRequest = (stateRequest state) {requestQuery = List.delete queryItem $ requestQuery $ stateRequest state}})
|
||||
pure queryItem
|
||||
|
||||
parseHeader :: MonadOkapi m => HTTP.HeaderName -> m HTTP.Header
|
||||
parseHeader headerName = do
|
||||
maybeHeader <- State.gets (Foldable.find (\(headerName', _) -> headerName == headerName') . requestHeaders . stateRequest)
|
||||
case maybeHeader of
|
||||
Nothing -> skip
|
||||
Just header -> do
|
||||
State.modify (\state -> state {stateRequest = (stateRequest state) {requestHeaders = List.delete header $ requestHeaders $ stateRequest state}})
|
||||
pure header
|
||||
|
||||
parseBody :: forall m. MonadOkapi m => m LBS.ByteString
|
||||
parseBody = do
|
||||
isBodyParsed <- bodyParsed
|
||||
if isBodyParsed
|
||||
then skip
|
||||
else do
|
||||
bodyRef <- State.gets (requestBody . stateRequest)
|
||||
body <- IO.liftIO bodyRef
|
||||
State.modify (\state -> state {stateRequestBodyParsed = True})
|
||||
pure body
|
||||
|
||||
{-
|
||||
TODO: HTTPDataStore? Not really needed because you can just pass data normally or store in own monad.
|
||||
One benefit is that the data is available to all sub-branches without explicitly passing them to every sub-branch.
|
||||
|
||||
-- This data structure should be hidden from user
|
||||
data HTTPDataStore = HTTPDataStore
|
||||
{ pathStore :: (Map Text Text)
|
||||
, queryStore :: (Map Text Text)
|
||||
, headerStore :: (Map Text Text)
|
||||
}
|
||||
|
||||
-- Can only store parsed information
|
||||
storePathParam :: forall a. FromHttpApiData a => Text -> Okapi a
|
||||
storePathParam = ...
|
||||
|
||||
storeQueryParam :: ... => Text -> Okapi a
|
||||
|
||||
storeHeader :: ... => Text -> Okapi a
|
||||
|
||||
-- Can fail on Map lookup and data conversion
|
||||
findPathParam :: forall a. FromHttpApiData a => Okapi a
|
||||
-}
|
||||
|
@ -2,70 +2,90 @@
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module Okapi.Response where
|
||||
module Okapi.Response
|
||||
( _200,
|
||||
_204,
|
||||
_401,
|
||||
_403,
|
||||
_404,
|
||||
_500,
|
||||
redirect,
|
||||
-- RESPONSE BODY MODIFIERS
|
||||
plaintext,
|
||||
html,
|
||||
json,
|
||||
file,
|
||||
eventSource,
|
||||
lucid,
|
||||
-- RESPONSE SETTERS
|
||||
setResponseStatus,
|
||||
setResponseBody,
|
||||
setResponseHeaders,
|
||||
setResponseHeader,
|
||||
)
|
||||
where
|
||||
|
||||
import qualified Data.Aeson as Aeson
|
||||
import qualified Data.ByteString.Char8 as Char8
|
||||
import qualified Data.ByteString.Lazy as LazyByteString
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import Data.Function ((&))
|
||||
import qualified Data.Text as Text
|
||||
import Data.Text.Encoding
|
||||
import qualified Data.Text.Encoding as Text
|
||||
import qualified GHC.Natural as Natural
|
||||
import qualified Lucid
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Okapi.Event as Event
|
||||
import Okapi.Internal.Types
|
||||
import Okapi.Internal.Functions.Response
|
||||
import Okapi.Types
|
||||
|
||||
-- BASE RESPONSES
|
||||
|
||||
ok :: Response
|
||||
ok =
|
||||
_200 :: Response
|
||||
_200 =
|
||||
let responseStatus = 200
|
||||
responseHeaders = []
|
||||
responseBody = ResponseBodyRaw "OK"
|
||||
in Response {..}
|
||||
|
||||
noContent :: Response
|
||||
noContent =
|
||||
_204 :: Response
|
||||
_204 =
|
||||
let responseStatus = 204
|
||||
responseHeaders = []
|
||||
responseBody = ResponseBodyRaw "No Content"
|
||||
in Response {..}
|
||||
|
||||
forbidden :: Response
|
||||
forbidden =
|
||||
_403 :: Response
|
||||
_403 =
|
||||
let responseStatus = 403
|
||||
responseHeaders = []
|
||||
responseBody = ResponseBodyRaw "Forbidden"
|
||||
in Response {..}
|
||||
|
||||
notFound :: Response
|
||||
notFound =
|
||||
_404 :: Response
|
||||
_404 =
|
||||
let responseStatus = 404
|
||||
responseHeaders = []
|
||||
responseBody = ResponseBodyRaw "Not Found"
|
||||
in Response {..}
|
||||
|
||||
unauthorized :: Response
|
||||
unauthorized =
|
||||
_401 :: Response
|
||||
_401 =
|
||||
let responseStatus = 401
|
||||
responseHeaders = []
|
||||
responseBody = ResponseBodyRaw "Unauthorized"
|
||||
in Response {..}
|
||||
|
||||
internalServerError :: Response
|
||||
internalServerError =
|
||||
_500 :: Response
|
||||
_500 =
|
||||
let responseStatus = 500
|
||||
responseHeaders = []
|
||||
responseBody = ResponseBodyRaw "Internal Server Error"
|
||||
in Response {..}
|
||||
|
||||
-- TODO: Change type of URL?
|
||||
redirectTo :: Char8.ByteString -> Response
|
||||
redirectTo url =
|
||||
redirect :: URL -> Response
|
||||
redirect (URL url) =
|
||||
let responseStatus = 302
|
||||
responseHeaders = [("Location", url)]
|
||||
responseHeaders = [("Location", encodeUtf8 url)]
|
||||
responseBody = ResponseBodyRaw ""
|
||||
in Response {..}
|
||||
|
||||
@ -75,19 +95,19 @@ plaintext :: Text.Text -> Response -> Response
|
||||
plaintext text response =
|
||||
response
|
||||
& setResponseHeader ("Content-Type", "text/plain")
|
||||
& setResponseBody (ResponseBodyRaw $ LazyByteString.fromStrict . Text.encodeUtf8 $ text)
|
||||
& setResponseBody (ResponseBodyRaw $ LBS.fromStrict . Text.encodeUtf8 $ text)
|
||||
|
||||
html :: LazyByteString.ByteString -> Response -> Response
|
||||
html :: LBS.ByteString -> Response -> Response
|
||||
html htmlRaw response =
|
||||
response
|
||||
& setResponseBody (ResponseBodyRaw htmlRaw)
|
||||
& setResponseHeader ("Content-Type", "text/html")
|
||||
|
||||
json :: LazyByteString.ByteString -> Response -> Response
|
||||
json bytes response =
|
||||
json :: forall a. Aeson.ToJSON a => a -> Response -> Response
|
||||
json value response =
|
||||
response
|
||||
& setResponseHeader ("Content-Type", "application/json")
|
||||
& setResponseBody (ResponseBodyRaw bytes)
|
||||
& setResponseBody (ResponseBodyRaw $ Aeson.encode value)
|
||||
|
||||
file :: FilePath -> Response -> Response
|
||||
file path = setResponseBody (ResponseBodyFile path) -- TODO: setHeader???
|
||||
@ -97,8 +117,29 @@ eventSource source response =
|
||||
response
|
||||
& setResponseBody (ResponseBodyEventSource source)
|
||||
|
||||
aeson :: forall a. Aeson.ToJSON a => a -> Response -> Response
|
||||
aeson = json . Aeson.encode
|
||||
lucid :: Lucid.Html a -> Response -> Response
|
||||
lucid = html . Lucid.renderBS
|
||||
|
||||
lucid :: forall a. Lucid.ToHtml a => a -> Response -> Response
|
||||
lucid = html . Lucid.renderBS . Lucid.toHtml
|
||||
-- RESPONSE SETTERS
|
||||
|
||||
setResponseStatus :: Natural.Natural -> Response -> Response
|
||||
setResponseStatus status response = response {responseStatus = status}
|
||||
|
||||
setResponseHeaders :: Headers -> Response -> Response
|
||||
setResponseHeaders headers response = response {responseHeaders = headers}
|
||||
|
||||
-- TODO: setResponseCookie
|
||||
|
||||
setResponseHeader :: HTTP.Header -> Response -> Response
|
||||
setResponseHeader header response@Response {..} =
|
||||
response {responseHeaders = update header responseHeaders}
|
||||
where
|
||||
update :: forall a b. Eq a => (a, b) -> [(a, b)] -> [(a, b)]
|
||||
update pair [] = [pair]
|
||||
update pair@(key, value) (pair'@(key', value') : ps) =
|
||||
if key == key'
|
||||
then pair : ps
|
||||
else pair' : update pair ps
|
||||
|
||||
setResponseBody :: ResponseBody -> Response -> Response
|
||||
setResponseBody body response = response {responseBody = body}
|
||||
|
@ -8,13 +8,17 @@
|
||||
|
||||
--- !!!!!!TODO: Change Name of Okapi to HTTParser??? :TODO!!!!!!
|
||||
|
||||
module Okapi.Route where
|
||||
module Okapi.Route
|
||||
( Route (..),
|
||||
route,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative
|
||||
import Control.Applicative.Combinators
|
||||
import Control.Monad (forM)
|
||||
import Control.Monad.IO.Class
|
||||
import Data.Attoparsec.Text
|
||||
import qualified Data.Attoparsec.Text as Atto
|
||||
import Data.Char (isAlpha, isAlphaNum, isUpper)
|
||||
import Data.List.NonEmpty
|
||||
import Data.Maybe (catMaybes, mapMaybe)
|
||||
@ -24,9 +28,26 @@ import GHC.ExecutionStack (Location (functionName))
|
||||
import GHC.Unicode (isAscii)
|
||||
import Language.Haskell.TH
|
||||
import Language.Haskell.TH.Quote
|
||||
import Okapi.Internal.Functions.Route
|
||||
import Okapi.Types
|
||||
import System.Random
|
||||
|
||||
data Route m i o = Route
|
||||
{ parser :: OkapiT m o,
|
||||
url :: i -> URL
|
||||
}
|
||||
|
||||
data RoutePart = Method Text | PathSegMatch Text | AnonPathSeg CurlyExpr | AnonQueryParam Text CurlyExpr | Bind Text
|
||||
deriving (Eq, Show)
|
||||
|
||||
data CurlyExpr
|
||||
= CurlyExpr
|
||||
Text -- type name
|
||||
[Text] -- transform function names
|
||||
(Maybe Text) -- filter function name
|
||||
deriving (Eq, Show)
|
||||
|
||||
data HTTPDataType = PathSegType Text | AnonPathParamType | AnonQueryParamType Text
|
||||
|
||||
route :: QuasiQuoter
|
||||
route =
|
||||
QuasiQuoter
|
||||
@ -38,11 +59,167 @@ route =
|
||||
where
|
||||
genRouteExp :: Text -> Q Exp
|
||||
genRouteExp txt = do
|
||||
let parserResult = parseOnly routeParser txt
|
||||
let parserResult = Atto.parseOnly routeParser txt
|
||||
case parserResult of
|
||||
Left _ -> routePartsToExp []
|
||||
Right routeParts -> routePartsToExp routeParts
|
||||
|
||||
parseCurlyExpr :: Atto.Parser CurlyExpr
|
||||
parseCurlyExpr = between (Atto.char '{') (Atto.char '}') $ do
|
||||
typeName <- Atto.takeWhile (\char -> isAlphaNum char || char == '[' || char == ']' || char == '(' || char == ')')
|
||||
transformFunctionNames <- many $ do
|
||||
Atto.string "->"
|
||||
Atto.takeWhile isAlphaNum
|
||||
filterFunctionName <- optional $ do
|
||||
Atto.char '|'
|
||||
Atto.takeWhile isAlphaNum
|
||||
pure $ CurlyExpr typeName transformFunctionNames filterFunctionName
|
||||
|
||||
routeParser :: Atto.Parser [RoutePart]
|
||||
routeParser = many $ do
|
||||
Atto.skipSpace
|
||||
parseMethod <|> parsePathSegMatch <|> parseAnonPathSeg <|> parseAnonQueryParam <|> parseBind
|
||||
|
||||
parseMethod :: Atto.Parser RoutePart
|
||||
parseMethod = do
|
||||
method <- Atto.takeWhile isUpper
|
||||
case method of
|
||||
"GET" -> pure $ Method "GET"
|
||||
"HEAD" -> pure $ Method "HEAD"
|
||||
"POST" -> pure $ Method "POST"
|
||||
"DELETE" -> pure $ Method "DELETE"
|
||||
"PUT" -> pure $ Method "PUT"
|
||||
"PATCH" -> pure $ Method "PATCH"
|
||||
_ -> fail "Couldn't parse method"
|
||||
|
||||
parsePathSegMatch :: Atto.Parser RoutePart
|
||||
parsePathSegMatch = do
|
||||
Atto.char '/'
|
||||
match <- Atto.takeWhile1 isAlpha
|
||||
pure $ PathSegMatch match
|
||||
|
||||
parseAnonPathSeg :: Atto.Parser RoutePart
|
||||
parseAnonPathSeg = do
|
||||
Atto.char '/'
|
||||
AnonPathSeg <$> parseCurlyExpr
|
||||
|
||||
parseAnonQueryParam :: Atto.Parser RoutePart
|
||||
parseAnonQueryParam = do
|
||||
Atto.char '?'
|
||||
queryParamName <- Atto.takeWhile isAlphaNum
|
||||
AnonQueryParam queryParamName <$> parseCurlyExpr
|
||||
|
||||
parseBind :: Atto.Parser RoutePart
|
||||
parseBind = do
|
||||
Atto.string ">>="
|
||||
Atto.skipSpace
|
||||
functionName <- Atto.takeWhile1 (\char -> isAlphaNum char || char == '.')
|
||||
pure $ Bind functionName
|
||||
|
||||
routePartsToExp :: [RoutePart] -> Q Exp
|
||||
routePartsToExp [] =
|
||||
pure $
|
||||
RecConE
|
||||
(mkName "Route")
|
||||
[ (mkName "parser", VarE (mkName "Okapi.skip")),
|
||||
(mkName "url", LamE [VarP $ mkName "unit"] (AppE (ConE $ mkName "Okapi.URL") (LitE $ StringL "")))
|
||||
]
|
||||
routePartsToExp routeParts = do
|
||||
let binds = Prelude.dropWhile (not . isBind) routeParts
|
||||
notBinds = Prelude.takeWhile (not . isBind) routeParts
|
||||
routePartStmtsAndBindings <- mapM routePartStmtAndBinding notBinds
|
||||
let routePartStmts = Prelude.map (\(_, _, stmts) -> stmts) routePartStmtsAndBindings
|
||||
bindingsAndTypes = mapMaybe (\(bandTs, _, _) -> bandTs) routePartStmtsAndBindings
|
||||
bAndTHelper :: (Maybe (Name, Type), Maybe HTTPDataType, Stmt) -> Maybe (Maybe Name, HTTPDataType) = \case
|
||||
(Nothing, Just pathSegType@(PathSegType seg), _) -> Just (Nothing, pathSegType)
|
||||
(Just (name, _), Just httpDataType, _) -> Just (Just name, httpDataType)
|
||||
_ -> Nothing
|
||||
bindingsAndHTTPDataTypes :: [(Maybe Name, HTTPDataType)] = mapMaybe bAndTHelper routePartStmtsAndBindings
|
||||
bindings = Prelude.map fst bindingsAndTypes
|
||||
-- types = map snd bindingsAndTypes
|
||||
returnStmt :: Stmt =
|
||||
case bindings of
|
||||
[] -> NoBindS (AppE (VarE $ mkName "pure") (ConE $ mkName "()"))
|
||||
[binding] -> NoBindS (AppE (VarE $ mkName "pure") (VarE binding))
|
||||
_ -> NoBindS (AppE (VarE $ mkName "pure") (TupE (Prelude.map (Just . VarE) bindings)))
|
||||
leftSide = ParensE (DoE Nothing $ routePartStmts <> [returnStmt])
|
||||
(middle, rightSide) =
|
||||
case binds of
|
||||
[] -> (VarE $ mkName ">>=", LamE [VarP $ mkName "params"] (AppE (VarE $ mkName "pure") (VarE $ mkName "params")))
|
||||
[Bind functionName] -> (VarE $ mkName ">>=", VarE $ mkName $ unpack functionName)
|
||||
_ -> (VarE $ mkName ">>", AppE (VarE $ mkName "Okapi.throw") (VarE $ mkName "Okapi.internalServerError"))
|
||||
pure $
|
||||
RecConE
|
||||
(mkName "Okapi.Route")
|
||||
[ (mkName "parser", UInfixE leftSide middle rightSide),
|
||||
(mkName "url", LamE [lambdaPattern bindingsAndTypes] (lambdaBody True bindingsAndHTTPDataTypes))
|
||||
]
|
||||
|
||||
isBind :: RoutePart -> Bool
|
||||
isBind (Bind _) = True
|
||||
isBind _ = False
|
||||
|
||||
-- bindsExp :: NonEmpty RoutePart -> Exp
|
||||
-- bindsExp (Bind functionName) = VarE $ mkName $ unpack functionName
|
||||
-- bindsExp ((Bind functionName) :| rps) = UInfixE (VarE $ mkName $ unpack functionName) (VarE $ mkName ">>=") (loop rps)
|
||||
-- where
|
||||
-- loop :: [RoutePart] -> Exp
|
||||
-- loop [] = LamE [WildP] (VarE $ mkName "Okapi.skip")
|
||||
-- loop ((Bind functionName) : rps) = undefined
|
||||
|
||||
lambdaPattern :: [(Name, Type)] -> Pat
|
||||
lambdaPattern [] = WildP
|
||||
lambdaPattern [(n, t)] = SigP (VarP n) t
|
||||
lambdaPattern nAndTs = TupP $ Prelude.map (\(n, t) -> SigP (VarP n) t) nAndTs
|
||||
|
||||
isQueryParamType :: HTTPDataType -> Bool
|
||||
isQueryParamType (AnonQueryParamType _) = True
|
||||
isQueryParamType _ = False
|
||||
|
||||
lambdaBody :: Bool -> [(Maybe Name, HTTPDataType)] -> Exp
|
||||
lambdaBody _ [] = AppE (ConE (mkName "Okapi.URL")) (LitE $ StringL "")
|
||||
lambdaBody isFirstQueryParam (combo@(_, httpDataType) : combos) =
|
||||
UInfixE
|
||||
(helper isFirstQueryParam combo)
|
||||
(VarE $ mkName "<>")
|
||||
( lambdaBody
|
||||
( not (isQueryParamType httpDataType && isFirstQueryParam) && isFirstQueryParam
|
||||
)
|
||||
combos
|
||||
)
|
||||
where
|
||||
helper :: Bool -> (Maybe Name, HTTPDataType) -> Exp
|
||||
helper _ (Nothing, PathSegType match) = AppE (ConE (mkName "Okapi.URL")) (LitE $ StringL $ "/" <> unpack match)
|
||||
helper _ (Just name, AnonPathParamType) = AppE (ConE (mkName "Okapi.URL")) (UInfixE (LitE $ StringL "/") (VarE $ mkName "<>") (ParensE $ AppE (VarE $ mkName "toUrlPiece") (VarE name)))
|
||||
helper isFirstQueryParam' (Just name, AnonQueryParamType queryParamName) = AppE (ConE (mkName "Okapi.URL")) (UInfixE (LitE $ StringL $ unpack $ (if isFirstQueryParam' then "?" else "&") <> queryParamName <> "=") (VarE $ mkName "<>") (ParensE $ AppE (VarE $ mkName "toQueryParam") (VarE name)))
|
||||
helper _ _ = AppE (ConE (mkName "Okapi.URL")) (LitE $ StringL "")
|
||||
|
||||
routePartStmtAndBinding :: RoutePart -> Q (Maybe (Name, Type), Maybe HTTPDataType, Stmt)
|
||||
routePartStmtAndBinding rp = case rp of
|
||||
Method m -> case m of
|
||||
"GET" -> pure (Nothing, Nothing, NoBindS (VarE $ mkName "Okapi.get"))
|
||||
"HEAD" -> pure (Nothing, Nothing, NoBindS (VarE $ mkName "Okapi.head"))
|
||||
"POST" -> pure (Nothing, Nothing, NoBindS (VarE $ mkName "Okapi.post"))
|
||||
"DELETE" -> pure (Nothing, Nothing, NoBindS (VarE $ mkName "Okapi.delete"))
|
||||
"PUT" -> pure (Nothing, Nothing, NoBindS (VarE $ mkName "Okapi.put"))
|
||||
"PATCH" -> pure (Nothing, Nothing, NoBindS (VarE $ mkName "Okapi.patch"))
|
||||
_ -> pure (Nothing, Nothing, NoBindS (VarE $ mkName "Okapi.skip"))
|
||||
PathSegMatch txt -> pure (Nothing, Just $ PathSegType txt, NoBindS (AppE (VarE $ mkName "Okapi.pathSeg") (LitE $ StringL $ unpack txt)))
|
||||
AnonPathSeg (CurlyExpr typeName functionNamesToApply maybeGuardFunction) -> do
|
||||
stmtBinding <- runIO randName
|
||||
let stmt = BindS (SigP (VarP stmtBinding) (ConT $ mkName $ unpack typeName)) (VarE (mkName "Okapi.pathParam"))
|
||||
pure (Just (stmtBinding, ConT $ mkName $ unpack typeName), Just AnonPathParamType, stmt)
|
||||
AnonQueryParam queryParamName (CurlyExpr typeName functionNamesToApply maybeGuardFunction) -> do
|
||||
stmtBinding <- runIO randName
|
||||
let stmt = BindS (SigP (VarP stmtBinding) (ConT $ mkName $ unpack typeName)) (AppE (VarE (mkName "Okapi.queryParam")) (LitE $ StringL $ unpack queryParamName))
|
||||
pure (Just (stmtBinding, ConT $ mkName $ unpack typeName), Just $ AnonQueryParamType queryParamName, stmt)
|
||||
Bind functionName -> pure (Nothing, Nothing, NoBindS $ AppE (VarE $ mkName "Okapi.throw") (VarE $ mkName "Okapi.internalServerError"))
|
||||
|
||||
randName :: IO Name
|
||||
randName = do
|
||||
str <- fmap (Prelude.take 10 . randomRs ('a', 'z')) newStdGen
|
||||
pure $ mkName str
|
||||
|
||||
{-
|
||||
newtype URL = URL Text
|
||||
|
||||
|
@ -1,7 +1,19 @@
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module Okapi.Test where
|
||||
module Okapi.Test
|
||||
( TestRequest (..),
|
||||
testParser,
|
||||
testParserIO,
|
||||
assertFailure,
|
||||
assertState,
|
||||
assertResponse,
|
||||
-- For use with Wai.Test
|
||||
runSession,
|
||||
withSession,
|
||||
testRequest,
|
||||
)
|
||||
where
|
||||
|
||||
import qualified Control.Monad.Except as Except
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
@ -17,9 +29,16 @@ import Network.Wai (defaultRequest)
|
||||
import qualified Network.Wai as Wai
|
||||
import Network.Wai.Test (SRequest (..), setRawPathInfo)
|
||||
import qualified Network.Wai.Test as Wai.Test
|
||||
import Okapi.Internal.Functions.Application
|
||||
import Okapi.Internal.Types
|
||||
import Okapi.Application
|
||||
import Okapi.Response
|
||||
import Okapi.Types
|
||||
|
||||
data TestRequest = TestRequest
|
||||
{ testRequestMethod :: HTTP.Method,
|
||||
testRequestHeaders :: HTTP.RequestHeaders,
|
||||
testRequestRawPath :: BS.ByteString,
|
||||
testRequestBody :: LBS.ByteString
|
||||
}
|
||||
|
||||
testParser ::
|
||||
Monad m =>
|
||||
@ -50,7 +69,7 @@ testRequestToState (TestRequest method headers rawPath body) =
|
||||
stateResponded = False
|
||||
in State {..}
|
||||
|
||||
-- ASSERTION FUNCTIONS
|
||||
-- ASSERTION FUNCTIONS TODO: Add common assertion helpers
|
||||
|
||||
assertFailure ::
|
||||
(Failure -> Bool) ->
|
||||
@ -83,7 +102,7 @@ runSession ::
|
||||
OkapiT m Response ->
|
||||
IO a
|
||||
runSession session hoister okapiT = do
|
||||
let app = makeOkapiApp hoister notFound okapiT
|
||||
let app = okapiApp hoister _404 okapiT
|
||||
Wai.Test.runSession session app
|
||||
|
||||
withSession ::
|
||||
@ -94,8 +113,8 @@ withSession ::
|
||||
IO a
|
||||
withSession hoister okapiT session = runSession session hoister okapiT
|
||||
|
||||
send :: TestRequest -> Wai.Test.Session Wai.Test.SResponse
|
||||
send TestRequest {..} =
|
||||
testRequest :: TestRequest -> Wai.Test.Session Wai.Test.SResponse
|
||||
testRequest TestRequest {..} =
|
||||
let request =
|
||||
Wai.defaultRequest
|
||||
{ Wai.requestMethod = testRequestMethod,
|
||||
|
@ -6,9 +6,10 @@
|
||||
{-# LANGUAGE InstanceSigs #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE StrictData #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
|
||||
module Okapi.Internal.Types where
|
||||
module Okapi.Types where
|
||||
|
||||
import qualified Control.Applicative as Applicative
|
||||
import qualified Control.Concurrent.Chan.Unagi as Unagi
|
||||
@ -202,29 +203,5 @@ type Chan a = (Unagi.InChan a, Unagi.OutChan a)
|
||||
|
||||
type EventSource = Chan Event
|
||||
|
||||
data Route m i o = Route
|
||||
{ parser :: OkapiT m o,
|
||||
url :: i -> URL
|
||||
}
|
||||
|
||||
newtype URL = URL {unURL :: Text}
|
||||
deriving newtype (IsString, Semigroup, Monoid, Eq, Ord, Show)
|
||||
|
||||
data RoutePart = Method Text | PathSegMatch Text | AnonPathSeg CurlyExpr | AnonQueryParam Text CurlyExpr | Bind Text
|
||||
deriving (Eq, Show)
|
||||
|
||||
data CurlyExpr
|
||||
= CurlyExpr
|
||||
Text -- type name
|
||||
[Text] -- transform function names
|
||||
(Maybe Text) -- filter function name
|
||||
deriving (Eq, Show)
|
||||
|
||||
data HTTPDataType = PathSegType Text | AnonPathParamType | AnonQueryParamType Text
|
||||
|
||||
data TestRequest = TestRequest
|
||||
{ testRequestMethod :: HTTP.Method,
|
||||
testRequestHeaders :: HTTP.RequestHeaders,
|
||||
testRequestRawPath :: BS.ByteString,
|
||||
testRequestBody :: LBS.ByteString
|
||||
}
|
76
test/Spec.hs
76
test/Spec.hs
@ -16,18 +16,17 @@ import Network.Wai
|
||||
import Network.Wai.EventSource (ServerEvent (RetryEvent))
|
||||
import Network.Wai.Test
|
||||
import Okapi
|
||||
import qualified Okapi
|
||||
import Web.HttpApiData
|
||||
|
||||
import Test.DocTest (mainFromCabal)
|
||||
import System.Environment (getArgs)
|
||||
import Test.DocTest (mainFromCabal)
|
||||
import Web.HttpApiData
|
||||
|
||||
runDoctests :: IO ()
|
||||
runDoctests = mainFromCabal "okapi" =<< getArgs
|
||||
|
||||
type Okapi = OkapiT IO
|
||||
|
||||
someRoute = [route|
|
||||
someRoute =
|
||||
[route|
|
||||
GET
|
||||
HEAD
|
||||
/movies
|
||||
@ -39,7 +38,7 @@ someRoute = [route|
|
||||
|]
|
||||
|
||||
someRouteHandler :: (Int, Text, Text, Text) -> Okapi Okapi.Response
|
||||
someRouteHandler (_, _, _, _) = respond ok
|
||||
someRouteHandler (_, _, _, _) = respond _200
|
||||
|
||||
someRoute2 =
|
||||
[route|
|
||||
@ -54,7 +53,7 @@ someRoute2 =
|
||||
|]
|
||||
|
||||
someRoute2Handler :: (Int, Text, Text, Text) -> Okapi Okapi.Response
|
||||
someRoute2Handler (_, _, _, _) = respond ok
|
||||
someRoute2Handler (_, _, _, _) = respond _200
|
||||
|
||||
someRoute3 =
|
||||
[route|
|
||||
@ -65,17 +64,17 @@ someRoute3 =
|
||||
|]
|
||||
|
||||
verifyTodo :: Int -> Okapi Okapi.Response
|
||||
verifyTodo id_ = if id_ > 0 then pure ok else Okapi.throw noContent
|
||||
verifyTodo id_ = if id_ > 0 then pure _200 else Okapi.throw _204
|
||||
|
||||
someRoute3TestSession :: Session ()
|
||||
someRoute3TestSession = do
|
||||
send (TestRequest methodGet [] "/todos/20" "")
|
||||
testRequest (TestRequest methodGet [] "/todos/20" "")
|
||||
>>= assertStatus 200
|
||||
|
||||
send (TestRequest methodGet [] "/todos" "")
|
||||
testRequest (TestRequest methodGet [] "/todos" "")
|
||||
>>= assertStatus 404
|
||||
|
||||
send (TestRequest methodGet [] "/todos/-1" "")
|
||||
testRequest (TestRequest methodGet [] "/todos/-1" "")
|
||||
>>= assertStatus 204
|
||||
|
||||
testSomeRoute3 :: IO ()
|
||||
@ -93,36 +92,36 @@ testServer = do
|
||||
let parser1 = do
|
||||
get
|
||||
pathSeg "todos"
|
||||
respond ok
|
||||
respond _200
|
||||
|
||||
parser2 = do
|
||||
get
|
||||
path ["todos", "completed"]
|
||||
respond ok
|
||||
respond _200
|
||||
|
||||
parser3 = do
|
||||
get
|
||||
pathSeg "todos"
|
||||
status <- queryParam "status"
|
||||
ok
|
||||
_200
|
||||
& plaintext status
|
||||
& respond
|
||||
|
||||
parser4 = do
|
||||
get
|
||||
pathSeg "a"
|
||||
respond ok
|
||||
respond _200
|
||||
|
||||
parser5 = do
|
||||
get
|
||||
pathSeg "todos"
|
||||
queryFlag "progress"
|
||||
respond ok
|
||||
respond _200
|
||||
|
||||
parser6 = do
|
||||
get
|
||||
pathSeg ""
|
||||
respond ok
|
||||
respond _200
|
||||
|
||||
choice
|
||||
[ parser1,
|
||||
@ -134,49 +133,50 @@ testServer = do
|
||||
]
|
||||
|
||||
testServerQuasi :: Okapi Okapi.Response
|
||||
testServerQuasi = choice
|
||||
[ parser1
|
||||
, parser2
|
||||
, parser3
|
||||
, parser4
|
||||
]
|
||||
where
|
||||
parser1 = parser [route|GET /todos|] >> respond ok
|
||||
parser2 = parser [route|GET /todos /completed|] >> respond ok
|
||||
parser3 = parser [route|GET /todos ?status{Text}|] >>= (\status -> ok & plaintext status & respond)
|
||||
parser4 = parser [route|GET /a|] >> respond ok
|
||||
testServerQuasi =
|
||||
choice
|
||||
[ parser1,
|
||||
parser2,
|
||||
parser3,
|
||||
parser4
|
||||
]
|
||||
where
|
||||
parser1 = parser [route|GET /todos|] >> respond _200
|
||||
parser2 = parser [route|GET /todos /completed|] >> respond _200
|
||||
parser3 = parser [route|GET /todos ?status{Text}|] >>= (\status -> _200 & plaintext status & respond)
|
||||
parser4 = parser [route|GET /a|] >> respond _200
|
||||
|
||||
testSession :: Session ()
|
||||
testSession = do
|
||||
send (TestRequest methodGet [] "/todos" "")
|
||||
testRequest (TestRequest methodGet [] "/todos" "")
|
||||
>>= assertStatus 200
|
||||
|
||||
send (TestRequest methodPost [] "/todos" "")
|
||||
testRequest (TestRequest methodPost [] "/todos" "")
|
||||
>>= assertStatus 404
|
||||
|
||||
send (TestRequest methodGet [] "/todos/completed" "")
|
||||
testRequest (TestRequest methodGet [] "/todos/completed" "")
|
||||
>>= assertStatus 200
|
||||
|
||||
res3 <- send $ TestRequest methodGet [] "/todos?status=done" ""
|
||||
res3 <- testRequest $ TestRequest methodGet [] "/todos?status=done" ""
|
||||
assertStatus 200 res3
|
||||
assertBody "done" res3
|
||||
|
||||
-- send (TestRequest methodGet [] "/todos?progress" "")
|
||||
-- testRequest (TestRequest methodGet [] "/todos?progress" "")
|
||||
-- >>= assertStatus 200
|
||||
|
||||
send (TestRequest methodGet [] "/todos?what" "")
|
||||
testRequest (TestRequest methodGet [] "/todos?what" "")
|
||||
>>= assertStatus 404
|
||||
|
||||
send (TestRequest methodGet [] "/what" "")
|
||||
testRequest (TestRequest methodGet [] "/what" "")
|
||||
>>= assertStatus 404
|
||||
|
||||
send (TestRequest methodGet [] "/a" "")
|
||||
testRequest (TestRequest methodGet [] "/a" "")
|
||||
>>= assertStatus 200
|
||||
|
||||
-- testSession2 = do
|
||||
-- send (TestRequest methodGet [] "/")
|
||||
-- testRequest (TestRequest methodGet [] "/")
|
||||
|
||||
-- send (TestRequest methodGet [] "/" "") ?? Maybe because of how path is stored in srequest
|
||||
-- testRequest (TestRequest methodGet [] "/" "") ?? Maybe because of how path is stored in srequest
|
||||
-- >>= assertStatus 200
|
||||
{-
|
||||
test1 :: IO ()
|
||||
|
Loading…
Reference in New Issue
Block a user