@@ -0,0 +1,410 @@
1+module PureStack.Server
11+ , internalServerError
13+ -------------------------------------------------------------------------------------------------
21+ , class ParsePathPiece
30+import PureStack.Route
32+import Bun.Request (Request)
33+import Bun.Request as Bun
34+import Bun.Request as Request
35+import Bun.Response as Bun.Response
36+import Control.Alternative (class Plus, empty)
37+import Control.Monad.Except (ExceptT, runExceptT)
38+import Data.Argonaut (class DecodeJson, class EncodeJson, Json, decodeJson, encodeJson)
39+import Data.Array as Array
40+import Data.Either (Either(..), either)
41+import Data.Int as Int
43+import Data.Map as Map
44+import Data.Maybe (Maybe(..))
45+import Data.Number as Number
46+import Data.String as String
47+import Data.Symbol (class IsSymbol, reflectSymbol)
48+import Data.Traversable (traverse)
49+import Data.URL as URL
50+import Effect.Aff (Aff)
51+import Effect.Aff.Class (liftAff)
52+import Foreign.Object (Object)
53+import Foreign.Object as Object
54+import Prim.Row as Row
55+import Prim.RowList (class RowToList, RowList)
56+import Prim.RowList as RowList
57+import PureStack.Cookie (Cookie)
58+import PureStack.Cookie as Cookie
59+import Record as Record
60+import Record.Builder (Builder)
61+import Record.Builder as Builder
62+import Type.Proxy (Proxy(..))
65+ { path :: Array String
67+ , query :: Map String (Array String)
68+ , originalRequest :: Bun.Request
71+routeFromRequest :: Request -> Route
72+routeFromRequest req =
74+ url = URL.fromString $ Request.url req
76+ { verb: Request.method req
79+ Just u -> case URL.path u of
81+ URL.PathAbsolute p -> p
82+ URL.PathRelative p -> p
85+ Just u -> URL.query u
86+ , originalRequest: req
89+type Server a = ExceptT Response Aff a
91+newtype Nt m = Nt (forall x. m x -> Server x)
93+class ServeRoute :: forall k. k -> Type -> Row Type -> (Type -> Type) -> Constraint
94+class ServeRoute route handler middlewares m | route middlewares -> handler, handler -> m where
95+ serveRoute :: Route -> Maybe (Nt m -> handler -> Record middlewares -> Request -> Server Response)
97+instance (ServeRoute rest handler middlewares m, IsSymbol path) => ServeRoute (path / rest) handler middlewares m where
98+ serveRoute route@{ path } = do
99+ { head: h, tail } <- Array.uncons path
100+ assert $ h == (reflectSymbol @path Proxy)
101+ serveRoute @rest route { path = tail }
104+ ( ServeRoute rest handler middlewares m
105+ , ServeQuery row list
106+ , RowToList row list
108+ ServeRoute (Record row / rest) (Record row -> handler) middlewares m where
109+ serveRoute route@{ query } = do
110+ builder <- serveQuery @row @list query
111+ serveRoute @rest route <#> (\f nt handler req -> f nt (handler $ Builder.buildFromScratch builder) req)
114+ ( ServeRoute rest handler middlewares m
116+ , Row.Cons field (Bun.Request -> m x) tail middlewares
118+ ServeRoute (Middleware field / rest) (x -> handler) middlewares m where
119+ serveRoute route@{ originalRequest } = do
120+ serveRoute @rest route <#>
121+ ( \f nt@(Nt ntf) handler middlewares req -> do
122+ x <- ntf $ Record.get (Proxy @field) middlewares originalRequest
123+ f nt (handler x) middlewares req
126+else instance (ServeRoute rest handler middlewares m, ParsePathPiece t) => ServeRoute (t / rest) (t -> handler) middlewares m where
127+ serveRoute route@{ path } = do
128+ { head: h, tail } <- Array.uncons path
129+ t <- parsePathPiece @t h
130+ serveRoute @rest route { path = tail } <#> (\f nt handler mws req -> f nt (handler t) mws req)
132+else instance MethodName (meth Unit Unit) => ServeRoute (meth Unit Unit) (m Unit) middlewares m where
133+ serveRoute { path, verb } = do
134+ assert $ verb == methodName @(meth Unit Unit)
136+ pure $ \(Nt nt) handler _middlewares _req -> do
139+else instance (ToResponse resp, MethodName (meth Unit resp)) => ServeRoute (meth Unit resp) (m resp) middlewares m where
140+ serveRoute { path, verb } = do
141+ assert $ verb == methodName @(meth Unit resp)
143+ pure $ \(Nt nt) handler _middlewares _req -> do
145+ pure $ toResponse resp
146+else instance (FromRequest req, MethodName (meth resp Unit)) => ServeRoute (meth resp Unit) (req -> m Unit) middlewares m where
147+ serveRoute { path, verb } = do
148+ assert $ verb == methodName @(meth resp Unit)
150+ pure $ \(Nt nt) handler _middlewares req -> do
151+ liftAff (fromRequest req) >>= case _ of
152+ Nothing -> pure $ badRequest
156+else instance (FromRequest req, ToResponse resp, MethodName (meth req resp)) => ServeRoute (meth req resp) (req -> m resp) middlewares m where
157+ serveRoute { path, verb } = do
158+ assert $ verb == methodName @(meth req resp)
160+ pure $ \(Nt nt) handler _middlewares req -> do
161+ liftAff (fromRequest req) >>= case _ of
162+ Nothing -> pure $ badRequest
164+ resp <- nt $ handler r
165+ pure $ toResponse resp
168+ ( ServeAPI routes list handlers middlewares m
169+ , RowToList routes list
171+ ServeRoute routes (Record handlers) middlewares m where
172+ serveRoute route = pure $ \nt handlers middlewares req ->
173+ serveAPI @routes @list nt handlers middlewares route req
175+class ServeQuery :: Row Type -> RowList Type -> Constraint
176+class ServeQuery row list | list -> row where
177+ serveQuery :: Map String (Array String) -> Maybe (Builder (Record ()) (Record row))
179+instance ServeQuery () RowList.Nil where
180+ serveQuery _ = Just $ identity
184+ , ServeQuery tail rest
186+ , Row.Cons field (Array t) tail row
187+ , Row.Lacks field tail
189+ ServeQuery row (RowList.Cons field (Array t) rest) where
191+ r <- serveQuery @tail @rest m
192+ case Map.lookup (reflectSymbol @field Proxy) m of
193+ Nothing -> pure $ Builder.insert (Proxy @field) [] <<< r
195+ v <- parsePathPiece @t `traverse` a
196+ pure $ Builder.insert (Proxy @field) v <<< r
200+ , ServeQuery tail rest
202+ , Row.Cons field (Maybe t) tail row
203+ , Row.Lacks field tail
205+ ServeQuery row (RowList.Cons field (Maybe t) rest) where
207+ r <- serveQuery @tail @rest m
208+ case Map.lookup (reflectSymbol @field Proxy) m of
209+ Nothing -> pure $ Builder.insert (Proxy @field) Nothing <<< r
211+ { head, tail } <- Array.uncons a
213+ v <- parsePathPiece @t head
214+ pure $ Builder.insert (Proxy @field) (Just v) <<< r
218+ , ServeQuery tail rest
220+ , Row.Cons field t tail row
221+ , Row.Lacks field tail
223+ ServeQuery row (RowList.Cons field t rest) where
225+ { head, tail } <- Map.lookup (reflectSymbol @field Proxy) m >>= Array.uncons
227+ v <- parsePathPiece @t head
228+ r <- serveQuery @tail @rest m
229+ pure $ Builder.insert (Proxy @field) v <<< r
232+ = JsonResponseBody Json
234+ | StringResponseBody String
236+newtype Response = Response { body :: ResponseBody, status :: Int, statusText :: String, headers :: Object String }
238+class ToResponse r where
239+ toResponse :: r -> Response
241+instance EncodeJson (Record row) => ToResponse (Record row) where
242+ toResponse rec = Response { body: JsonResponseBody $ encodeJson rec, status: 200, statusText: "OK", headers: Object.empty }
244+instance ToResponse Response where
245+ toResponse resp = resp
247+class FromRequest r where
248+ fromRequest :: Request -> Aff (Maybe r)
250+instance DecodeJson (Record row) => FromRequest (Record row) where
251+ fromRequest req = Request.json req <#> \j -> case decodeJson j of
255+instance FromRequest Request where
256+ fromRequest req = pure $ Just req
258+instance FromRequest Unit where
259+ fromRequest _ = pure $ Just unit
265+ FromRequest (Headers (Object header) req) where
266+ fromRequest req = do
267+ r <- fromRequest req
269+ headers <- traverse fromHeader (Request.headers req)
271+ pure $ Headers headers r'
274+ ( RowToList headers headersList
276+ , FromHeaders headers headersList
278+ FromRequest (Headers (Record headers) req) where
279+ fromRequest req = do
280+ r <- fromRequest req
282+ headers <- fromHeaders @headers @headersList (Request.headers req)
284+ pure $ Headers (Builder.buildFromScratch headers) r'
286+class FromHeader t where
287+ fromHeader :: String -> Maybe t
289+instance FromHeader Cookie where
290+ fromHeader = Cookie.parse >>> case _ of
294+instance FromHeader String where
297+instance FromHeader Int where
298+ fromHeader = Int.fromString
300+class FromHeaders :: Row Type -> RowList Type -> Constraint
301+class FromHeaders row list | list -> row where
302+ fromHeaders :: Object String -> Maybe (Builder {} (Record row))
304+instance FromHeaders () RowList.Nil where
305+ fromHeaders _ = Just $ identity
310+ , FromHeaders tail rest
311+ , Row.Cons field (Maybe t) tail row
312+ , Row.Lacks field tail
314+ FromHeaders row (RowList.Cons field (Maybe t) rest) where
315+ fromHeaders headers = do
316+ r <- fromHeaders @tail @rest headers
317+ case Object.lookup (reflectSymbol @field Proxy) headers of
318+ Nothing -> pure $ Builder.insert (Proxy @field) Nothing <<< r
321+ pure $ Builder.insert (Proxy @field) (Just v) <<< r
326+ , FromHeaders tail rest
327+ , Row.Cons field t tail row
328+ , Row.Lacks field tail
330+ FromHeaders row (RowList.Cons field t rest) where
331+ fromHeaders headers = do
332+ t <- Object.lookup (reflectSymbol @field Proxy) headers >>= fromHeader
333+ r <- fromHeaders @tail @rest headers
334+ pure $ Builder.insert (Proxy @field) t <<< r
336+class ParsePathPiece t where
337+ parsePathPiece :: String -> Maybe t
339+instance ParsePathPiece String where
340+ parsePathPiece = Just
342+instance ParsePathPiece Int where
343+ parsePathPiece = Int.fromString
345+instance ParsePathPiece Number where
346+ parsePathPiece = Number.fromString
348+instance ParsePathPiece Boolean where
349+ parsePathPiece s = case String.toLower s of
352+ "false" -> Just false
356+assert :: forall m. Applicative m => Plus m => Boolean -> m Unit
357+assert true = pure unit
360+class ServeAPI :: Row Type -> RowList Type -> Row Type -> Row Type -> (Type -> Type) -> Constraint
361+class ServeAPI row list handlers middlewares m | row -> handlers, handlers -> m where
362+ serveAPI :: Nt m -> Record handlers -> Record middlewares -> Route -> Request -> Server Response
364+instance ServeAPI row RowList.Nil () middlewares m where
365+ serveAPI _ _ _ _ _ = pure notFound
368+ ( ServeRoute t handler middlewares m
369+ , Row.Cons field t tail row
370+ , ServeAPI row rest restHandlers middlewares m
372+ , Row.Cons field handler restHandlers handlers
373+ , Row.Lacks field restHandlers
375+ ServeAPI row (RowList.Cons field t rest) handlers middlewares m where
376+ serveAPI nt handlers middlewares route req =
377+ case serveRoute @t route of
378+ Nothing -> serveAPI @row @rest @restHandlers @middlewares nt (Record.delete (Proxy @field) handlers) middlewares route req
379+ Just f -> f nt (Record.get (Proxy @field) handlers) middlewares req
382+notFound = Response { body: EmptyResponseBody, status: 404, statusText: "Not Found", headers: Object.empty }
384+internalServerError :: Response
385+internalServerError = Response { body: EmptyResponseBody, status: 500, statusText: "Internal Server Error", headers: Object.empty }
387+badRequest :: Response
388+badRequest = Response { body: EmptyResponseBody, status: 400, statusText: "Bad Request", headers: Object.empty }
391+ok = Response { body: EmptyResponseBody, status: 200, statusText: "OK", headers: Object.empty }
393+-- | The first argument is used to run an arbitrary monad in handlers. To use the 'Server' monad
394+-- | instead you can just pass `identity` as the first argument.
396+ :: forall @row list handlers m middlewares
397+ . RowToList row list
398+ => ServeAPI row list handlers middlewares m
399+ => (forall x. m x -> Server x)
400+ -> { handlers :: Record handlers, middlewares :: Record middlewares }
402+ -> Aff Bun.Response.Response
403+run nt { handlers, middlewares } req = do
404+ Response { body, headers, status, statusText } <-
405+ map (either identity identity) $ runExceptT $ serveAPI @row @list @handlers (Nt nt) handlers middlewares (routeFromRequest req) req
406+ let opts = { headers, status, statusText }
408+ EmptyResponseBody -> Bun.Response.string "" opts
409+ JsonResponseBody json -> Bun.Response.json json opts
410+ StringResponseBody string -> Bun.Response.string string opts