@@ -1,9 +1,23 @@
1module PureStack.Server
1module PureStack.Server
3− , serve
3+ , class ToResponse
11+ -------------------------------------------------------------------------------------------------
4 , class ServeAPI
12 , class ServeAPI
5 , serveAPI
13 , serveAPI
19+ , class ParsePathPiece
9import Prelude
23import Prelude
@@ -14,35 +28,46 @@ import Bun.Request as Request
14import Bun.Response (Response)
28import Bun.Response (Response)
15import Bun.Response as Response
29import Bun.Response as Response
16import Control.Alternative (class Plus, empty)
30import Control.Alternative (class Plus, empty)
17−import Control.Monad.Except (ExceptT, runExceptT, throwError)
31+import Control.Monad.Except (ExceptT, runExceptT)
18import Data.Argonaut (class DecodeJson, class EncodeJson, decodeJson, encodeJson)
32import Data.Argonaut (class DecodeJson, class EncodeJson, decodeJson, encodeJson)
19import Data.Array as Array
33import Data.Array as Array
20import Data.Either (Either(..))
34import Data.Either (Either(..))
35+import Data.Int as Int
37+import Data.Map as Map
21import Data.Maybe (Maybe(..))
38import Data.Maybe (Maybe(..))
39+import Data.Number as Number
22import Data.Symbol (class IsSymbol, reflectSymbol)
40import Data.Symbol (class IsSymbol, reflectSymbol)
41+import Data.Traversable (traverse)
23import Data.URL as URL
42import Data.URL as URL
24−import Effect (Effect)
25import Effect.Aff (Aff)
43import Effect.Aff (Aff)
26import Effect.Aff.Class (liftAff)
44import Effect.Aff.Class (liftAff)
27−import Prim.Row (class Lacks)
28import Prim.Row as Row
45import Prim.Row as Row
29import Prim.RowList (class RowToList, RowList)
46import Prim.RowList (class RowToList, RowList)
30import Prim.RowList as RowList
47import Prim.RowList as RowList
31import Record as Record
48import Record as Record
49+import Record.Builder (Builder)
50+import Record.Builder as Builder
32import Type.Proxy (Proxy(..))
51import Type.Proxy (Proxy(..))
34−type Route = { path :: Array String, verb :: String }
53+type Route = { path :: Array String, verb :: String, query :: Map String (Array String) }
36routeFromRequest :: Request -> Route
55routeFromRequest :: Request -> Route
37routeFromRequest req =
56routeFromRequest req =
58+ url = URL.fromString $ Request.url req
38− { verb: Request.method req
60+ { verb: Request.method req
39− , path: case URL.fromString (Request.url req) of
61+ , path: case url of
40− Nothing -> []
62+ Nothing -> []
41− Just url -> case URL.path url of
63+ Just u -> case URL.path u of
42− URL.PathEmpty -> []
64+ URL.PathEmpty -> []
43− URL.PathAbsolute p -> p
65+ URL.PathAbsolute p -> p
44− URL.PathRelative p -> p
66+ URL.PathRelative p -> p
69+ Just u -> URL.query u
47type Server a = ExceptT Response Aff a
72type Server a = ExceptT Response Aff a
@@ -53,16 +78,26 @@ class ServeRoute route handler m | route -> handler, handler -> m where
53 serveRoute :: Route -> Maybe (Nt m -> handler -> Request -> Server Response)
78 serveRoute :: Route -> Maybe (Nt m -> handler -> Request -> Server Response)
55instance (ServeRoute rest handler m, IsSymbol path) => ServeRoute (path / rest) handler m where
80instance (ServeRoute rest handler m, IsSymbol path) => ServeRoute (path / rest) handler m where
56− serveRoute { path, verb } = do
81+ serveRoute route@{ path } = do
57 { head: h, tail } <- Array.uncons path
82 { head: h, tail } <- Array.uncons path
58 assert $ h == (reflectSymbol @path Proxy)
83 assert $ h == (reflectSymbol @path Proxy)
59− serveRoute @rest { path: tail, verb: verb }
84+ serveRoute @rest route { path = tail }
87+ ( ServeRoute rest handler m
88+ , ServeQuery row list
91+ ServeRoute (Record row / rest) (Record row -> handler) m where
92+ serveRoute route@{ query } = do
93+ builder <- serveQuery @row @list query
94+ serveRoute @rest route <#> (\f nt handler req -> f nt (handler $ Builder.buildFromScratch builder) req)
61else instance (ServeRoute rest handler m, ParsePathPiece t) => ServeRoute (t / rest) (t -> handler) m where
96else instance (ServeRoute rest handler m, ParsePathPiece t) => ServeRoute (t / rest) (t -> handler) m where
62− serveRoute { path, verb } = do
97+ serveRoute route@{ path } = do
63 { head: h, tail } <- Array.uncons path
98 { head: h, tail } <- Array.uncons path
64 t <- parsePathPiece @t h
99 t <- parsePathPiece @t h
65− serveRoute @rest { path: tail, verb } <#> (\f nt handler req -> f nt (handler t) req)
100+ serveRoute @rest route { path = tail } <#> (\f nt handler req -> f nt (handler t) req)
67else instance ToResponse resp => ServeRoute (GET resp) (m resp) m where
102else instance ToResponse resp => ServeRoute (GET resp) (m resp) m where
68 serveRoute { path, verb } = do
103 serveRoute { path, verb } = do
@@ -91,6 +126,62 @@ else instance (FromRequest req, ToResponse resp) => ServeRoute (POST req resp) (
91 resp <- nt $ handler r
126 resp <- nt $ handler r
92 pure $ toResponse resp
127 pure $ toResponse resp
129+class ServeQuery :: Row Type -> RowList Type -> Constraint
130+class ServeQuery row list | list -> row where
131+ serveQuery :: Map String (Array String) -> Maybe (Builder (Record ()) (Record row))
133+instance ServeQuery () RowList.Nil where
134+ serveQuery _ = Just $ identity
138+ , ServeQuery tail rest
140+ , Row.Cons field (Array t) tail row
141+ , Row.Lacks field tail
143+ ServeQuery row (RowList.Cons field (Array t) rest) where
145+ r <- serveQuery @tail @rest m
146+ case Map.lookup (reflectSymbol @field Proxy) m of
147+ Nothing -> pure $ Builder.insert (Proxy @field) [] <<< r
149+ v <- parsePathPiece @t `traverse` a
150+ pure $ Builder.insert (Proxy @field) v <<< r
154+ , ServeQuery tail rest
156+ , Row.Cons field (Maybe t) tail row
157+ , Row.Lacks field tail
159+ ServeQuery row (RowList.Cons field (Maybe t) rest) where
161+ r <- serveQuery @tail @rest m
162+ case Map.lookup (reflectSymbol @field Proxy) m of
163+ Nothing -> pure $ Builder.insert (Proxy @field) Nothing <<< r
165+ { head, tail } <- Array.uncons a
167+ v <- parsePathPiece @t head
168+ pure $ Builder.insert (Proxy @field) (Just v) <<< r
172+ , ServeQuery tail rest
174+ , Row.Cons field t tail row
175+ , Row.Lacks field tail
177+ ServeQuery row (RowList.Cons field t rest) where
179+ { head, tail } <- Map.lookup (reflectSymbol @field Proxy) m >>= Array.uncons
181+ v <- parsePathPiece @t head
182+ r <- serveQuery @tail @rest m
183+ pure $ Builder.insert (Proxy @field) v <<< r
94class ToResponse r where
185class ToResponse r where
95 toResponse :: r -> Response
186 toResponse :: r -> Response
@@ -114,6 +205,15 @@ instance FromRequest Request where
114class ParsePathPiece t where
205class ParsePathPiece t where
115 parsePathPiece :: String -> Maybe t
206 parsePathPiece :: String -> Maybe t
208+instance ParsePathPiece String where
209+ parsePathPiece = Just
211+instance ParsePathPiece Int where
212+ parsePathPiece = Int.fromString
214+instance ParsePathPiece Number where
215+ parsePathPiece = Number.fromString
117assert :: forall m. Applicative m => Plus m => Boolean -> m Unit
217assert :: forall m. Applicative m => Plus m => Boolean -> m Unit
118assert true = pure unit
218assert true = pure unit
119assert false = empty
219assert false = empty
@@ -134,8 +234,8 @@ instance
134 , Row.Lacks field restHandlers
234 , Row.Lacks field restHandlers
136 ServeAPI row (RowList.Cons field t rest) handlers m where
236 ServeAPI row (RowList.Cons field t rest) handlers m where
137− serveAPI nt handlers route@{ path, verb } req =
237+ serveAPI nt handlers route req =
138− case serveRoute @t { path, verb } of
238+ case serveRoute @t route of
139 Nothing -> serveAPI @row @rest @restHandlers nt (Record.delete (Proxy @field) handlers) route req
239 Nothing -> serveAPI @row @rest @restHandlers nt (Record.delete (Proxy @field) handlers) route req
140 Just f -> f nt (Record.get (Proxy @field) handlers) req
240 Just f -> f nt (Record.get (Proxy @field) handlers) req
@@ -148,7 +248,9 @@ internalServerError = Response.string "" { status: 500, statusText: "Internal Se
148badRequest :: Response
248badRequest :: Response
149badRequest = Response.string "" { status: 400, statusText: "Bad Request", headers: [] }
249badRequest = Response.string "" { status: 400, statusText: "Bad Request", headers: [] }
151−serve
251+-- | The first argument is used to run an arbitrary monad in handlers. To use the 'Server' monad
252+-- | instead you can just pass `identity` as the first argument.
152 :: forall @row list handlers m
254 :: forall @row list handlers m
153 . RowToList row list
255 . RowToList row list
154 => ServeAPI row list handlers m
256 => ServeAPI row list handlers m
@@ -156,61 +258,8 @@ serve
156 -> Record handlers
258 -> Record handlers
157 -> Request
259 -> Request
158 -> Aff Response
260 -> Aff Response
159−serve nt handlers req = do
261+run nt handlers req = do
160 res <- runExceptT $ serveAPI @row @list @handlers (Nt nt) handlers (routeFromRequest req) req
262 res <- runExceptT $ serveAPI @row @list @handlers (Nt nt) handlers (routeFromRequest req) req
161 pure case res of
263 pure case res of
162 Left resp -> resp
264 Left resp -> resp
163 Right resp -> resp
265 Right resp -> resp
165−-- class RecordNaturalTransformation
174−-- RecordNaturalTransformation from to row list row'
175−-- | list from -> row
177−-- , row' from -> row
178−-- , row to -> row where
179−-- nt' :: (forall a. from a -> to a) -> Record row -> Record row'
181−-- instance RecordNaturalTransformation from to () RowList.Nil () where
186−-- , Row.Cons field t tail row
187−-- , Row.Cons field t' tail' row'
188−-- , FunctionNaturalTransformation from to t t'
189−-- , RecordNaturalTransformation from to tail rest tail'
190−-- , Lacks field tail
191−-- , Lacks field tail'
193−-- RecordNaturalTransformation from to row (RowList.Cons field t rest) row' where
194−-- nt' f rec = Record.insert (Proxy @field) (ntFunction f $ Record.get (Proxy @field) rec)
195−-- $ nt' @from @to @tail @rest @tail' f (Record.delete (Proxy @field) rec)
197−-- class FunctionNaturalTransformation :: forall k. (k -> Type) -> (k -> Type) -> Type -> Type -> Constraint
198−-- class FunctionNaturalTransformation from to func func' | to func -> func' where
199−-- ntFunction :: (forall a. from a -> to a) -> func -> func'
201−-- instance FunctionNaturalTransformation from to (from x) (to x) where
202−-- ntFunction f g = f g
205−-- FunctionNaturalTransformation from to func func' =>
206−-- FunctionNaturalTransformation from to (a -> func) (a -> func') where
207−-- ntFunction f g = \a -> ntFunction f (g a)
210−-- :: forall @from row row' list
211−-- . RowToList row list
212−-- => RecordNaturalTransformation from Aff row list row'
213−-- => (forall a. from a -> Aff a)
216−-- nt f rec = nt' @from @Aff @row @list @row' f rec