Cleaning up and implementing helpers; moving on to playing with Roda/Cuba-like routing system

This commit is contained in:
Rashad Gover 2023-10-15 06:13:04 -07:00
parent 24a15646ef
commit 2ca3855f5d
3 changed files with 41 additions and 474 deletions

View File

@ -1,4 +1,4 @@
cabal-version: 2.0 cabal-version: 3.6
-- This file has been generated from package.yaml by hpack version 0.34.4. -- This file has been generated from package.yaml by hpack version 0.34.4.
-- --
@ -14,7 +14,7 @@ bug-reports: https://github.com/monadicsystems/okapi/issues
author: Monadic Systems LLC author: Monadic Systems LLC
maintainer: tech@monadic.systems maintainer: tech@monadic.systems
copyright: 2022 Monadic Systems LLC copyright: 2022 Monadic Systems LLC
license: BSD3 license: BSD-3-Clause
license-file: LICENSE license-file: LICENSE
build-type: Simple build-type: Simple
extra-source-files: extra-source-files:
@ -46,7 +46,7 @@ library
hs-source-dirs: hs-source-dirs:
src src
build-depends: build-depends:
aeson >=1.4.7 aeson
, base >=4.7 && <5 , base >=4.7 && <5
, base64 , base64
, bytestring , bytestring
@ -62,7 +62,7 @@ library
, text , text
, vault , vault
, wai , wai
, wai-extra -- , wai-extra
, warp , warp
default-language: Haskell2010 default-language: Haskell2010

View File

@ -34,137 +34,32 @@ import Okapi.NewDSL
import Okapi.Route qualified as Route import Okapi.Route qualified as Route
import Web.HttpApiData qualified as Web import Web.HttpApiData qualified as Web
data Expr a where type Handler env = Wai.Request -> env Wai.Response
Static :: Web.ToHttpApiData a => a -> Expr ()
Param :: Web.FromHttpApiData a => Expr a
-- Optional :: Expr a -> Expr (Maybe a)
-- Macro :: Context Expr state error => DSL Expr state error a -> Expr a
End :: Expr ()
type State = [Text]
data Error where
Error :: Text -> Error
instance Context Expr State Error where
eval state expr = case expr of
Static @t x -> undefined
Param @t -> undefined
-- Optional expr' -> undefined
-- Macro dsl -> undefined
End -> undefined
-- embed :: Expr a -> DSL Expr State Error a
-- embed = Eval interpreter
static :: Web.ToHttpApiData a => a -> DSL Expr State Error ()
static = Expr . Static
param :: Web.FromHttpApiData a => DSL Expr State Error a
param = Expr Param
-- optional :: Expr a -> DSL Expr State Error (Maybe a)
-- optional = Expr . Optional
end :: DSL Expr State Error ()
end = Expr End
-- instance DSL Expr [Text] Error where
-- eval :: Expr -> [Text] -> (Either Error Result, [Text])
-- eval (Static @t x) input = (Right $ StaticResult (), [])
-- eval (Param @t) input = undefined
-- eval End [] = (Right $ EndResult (), [])
-- Compile Time - while code is compiling and typechecking. Type level errors occur here
-- Run Time - code is running. It has typechecked, but runtime errors like IO errors can still occur.
-- App Time - the Okapi app is running. Before running the app, certain errors can be caught
class FromPath a where
-- pattern Path :: a -> [Text]
route :: DSL Expr State Error a
{-# MINIMAL route #-}
fromPath :: FromPath a => [Text] -> Either Error a
fromPath path = fst (exec path route)
class ToPath a where
toPath :: a -> [Text]
{-# MINIMAL toPath #-}
class (Eq a, FromPath a, ToPath a) => Route a where
roundtrip :: a -> Bool
roundtrip x = case fromPath (toPath x) of
Left _ -> False
Right x' -> x == x'
-- instance (Eq a, Typeable a) => Eq (DSL Expr State Error a) where
{-
(^==) :: DSL Expr State Error a -> DSL Expr State Error b -> Bool
FMap f dsl ^== FMap f' dsl' = dsl ^== dsl'
Pure x ^== Pure x' = Just x == cast x'
Apply dslF dslX ^== Apply dslF' dslX' = dslX ^== dslX'
Expr expr ^== Expr expr' = case (expr, expr') of
(Static s, Static s') -> Just s == cast s'
(Param @p, Param @p') -> convert @p == convert @p'
(End, End) -> True
(_, _) -> False
_ ^== _ = False
-}
data API where data API where
Router :: forall a env. (env ~> IO) -> DSL Expr State Error a -> (Wai.Request -> a -> env Wai.Response) -> API Router :: forall a env. (env ~> IO) -> Route.Route a -> (a -> Handler env) -> API
Endpoint :: forall a env. (env ~> IO) -> (HTTP.StdMethod -> Bool) -> DSL Expr State Error a -> (Wai.Request -> a -> env Wai.Response) -> API Endpoint :: forall a env. (env ~> IO) -> (HTTP.StdMethod -> Bool) -> Route.Route a -> (a -> Handler env) -> API
MethodMap :: forall a env. (env ~> IO) -> DSL Expr State Error a -> Map HTTP.StdMethod (Wai.Request -> a -> env Wai.Response) -> API MethodMap :: forall a env. (env ~> IO) -> Route.Route a -> Map HTTP.StdMethod (a -> Handler env) -> API
Scope :: [Text] -> Wai.Middleware -> [API] -> API Scope :: Wai.Middleware -> [Text] -> [API] -> API
DynScope :: Wai.Middleware -> Route.Route a -> (a -> [API]) -> API
-- ?? Scope :: [Text] -> ((Wai.Request -> env Wai.Response) -> Wai.Request -> env Wai.Response) -> [API] -> API router :: forall a env. (env ~> IO) -> Route.Route a -> (a -> Handler env) -> API
router :: forall a env. (env ~> IO) -> DSL Expr State Error a -> (Wai.Request -> a -> env Wai.Response) -> API
router = Router router = Router
endpoint :: forall a env. (env ~> IO) -> (HTTP.StdMethod -> Bool) -> DSL Expr State Error a -> (Wai.Request -> a -> env Wai.Response) -> API endpoint :: forall a env. (env ~> IO) -> (HTTP.StdMethod -> Bool) -> Route.Route a -> (a -> Handler env) -> API
endpoint = Endpoint @a @env endpoint = Endpoint @a @env
methodMap :: forall a env. (env ~> IO) -> DSL Expr State Error a -> Map HTTP.StdMethod (Wai.Request -> a -> env Wai.Response) -> API get_ :: Int -> Int -> Int
get_ = undefined
methodMap :: forall a env. (env ~> IO) -> Route.Route a -> Map HTTP.StdMethod (a -> Handler env) -> API
methodMap = MethodMap @a @env methodMap = MethodMap @a @env
scope :: [Text] -> Wai.Middleware -> [API] -> API scope :: Wai.Middleware -> [Text] -> [API] -> API
scope = Scope scope = Scope
myAPI :: API dynScope :: Wai.Middleware -> Route.Route a -> (a -> [API]) -> API
myAPI = router id (route @Person) \req -> \Person {..} -> do dynScope = DynScope
undefined
myAPI' :: API
myAPI' = endpoint id (HTTP.GET==) (route @Person) \req Person {..} -> do
undefined
myAPI'' :: API
myAPI'' = endpoint id (`Prelude.elem` [HTTP.GET, HTTP.HEAD, HTTP.POST]) (route @Person) \req Person {..} -> do
undefined
myAPI''' :: API
myAPI''' = endpoint id (const True) (route @Person) \req Person {..} -> do
undefined
(|>) = (&)
anotherAPI :: API
anotherAPI =
methodMap id (route @Person) $
Data.Map.empty
|> insert HTTP.GET (\req Person {..} -> do undefined)
|> insert HTTP.PUT (\req Person {..} -> do undefined)
fullAPI :: API
fullAPI =
scope
["hello", "world"]
id
[ myAPI,
myAPI',
anotherAPI
]
helloWorld :: Route.Route (Text, Int) helloWorld :: Route.Route (Text, Int)
helloWorld = do helloWorld = do
@ -192,26 +87,6 @@ helloWorld'' = do
data Person = Person {name :: Text, age :: Int, salary :: Float} data Person = Person {name :: Text, age :: Int, salary :: Float}
-- subRoute :: Route.Route Person
-- -- subRoute :: Route.Route (Text, Int) -> Route.Route Person
-- subRoute = do
-- salary <- Route.param @Float
-- (name, age) <- helloWorld
-- pure Person {..}
{-
do
(name, age) <- helloW
float <- Route.param @Float
pure (name, age, float)
-}
-- bigRoute :: Route.Route ((Text, Int), (Text, Int))
-- bigRoute = do
-- -- (name, age) <- helloWorld
-- (name', age') <- helloWorld'
-- pure ((name, age), (name', age'))
xRoute :: Route.Route Int xRoute :: Route.Route Int
xRoute = do xRoute = do
_ <- Route.static "x" _ <- Route.static "x"
@ -240,269 +115,5 @@ xyRoute = do
data Datum = Datum {foo :: Int, bar :: Int, baz :: Int} data Datum = Datum {foo :: Int, bar :: Int, baz :: Int}
-- xyzRoute :: Route.Route (Text, Int) -> Route.Route Datum
-- xyzRoute alphaR = do
-- _ <- Route.static "alphay"
-- (_, foo) <- alphaR
-- (bar, baz) <- xyRoute
-- pure Datum {..}
-- resource :: forall a env. Route a => (env ~> IO) -> API
-- resource t = (Resource @a @env) { transform = t, gets = Nothing, posts = Nothing, puts = Nothing }
-- makeEndpoints :: forall a env. Route a => (env ~> IO) -> [(HTTP.Method, a -> Wai.Request -> env Wai.Response)] -> API
-- makeEndpoints =
{-
data API where
Endpoint :: Path a => (m ~> IO) -> HTTP.Method -> (a -> Wai.Request -> m Wai.Response) -> API
Route :: Path a => (m ~> IO) -> [(HTTP.Method, a -> Wai.Request -> m Wai.Response)] -> API
-- Route :: Path a => (m ~> IO) -> (HTTP.Method -> a -> Wai.Request -> m Wai.Response) -> API
Scope :: { prefix :: [Text], middleware :: Wai.Middleware, children :: [API] } -> API
-- Dir :: [Text] -> API TODO: For serving files
-}
{-
appify :: API -> Wai.Application -> Wai.Application
appify api catchAll request respond = case api of
Endpoint @isPath transformation method handler -> do
if method == Wai.requestMethod request
then case fromPath @isPath $ Wai.pathInfo request of
Left _ -> catchAll request respond
Right pathData -> do
response <- transformation $ handler pathData request
respond response
else catchAll request respond
Route @isPath transformation handlerMap -> do
case lookup (Wai.requestMethod request) handlerMap of
Nothing -> catchAll request respond
Just handler -> case fromPath @isPath $ Wai.pathInfo request of
Left _ -> catchAll request respond
Right pathData -> do
response <- transformation $ handler pathData request
respond response
Scope _ _ [] -> catchAll request respond
Scope prefix middleware apis -> do
let maybeNewPath = stripPrefix prefix $ Wai.pathInfo request
case maybeNewPath of
Nothing -> catchAll request respond
Just newPath -> do
let newRequest = request {Wai.pathInfo = newPath}
chooseAPI apis catchAll
_ -> catchAll request respond
-}
-- chooseAPI :: [API] -> Wai.Application -> Wai.Application
-- chooseAPI catchAll request respond
convert :: forall a. Typeable a => String convert :: forall a. Typeable a => String
convert = show . typeRep $ Proxy @a convert = show . typeRep $ Proxy @a
instance FromPath Person where
route = undefined
{-
class Path a where
-- pattern Path :: a -> [Text]
path :: DSL Expr State Error a
-- data METHOD where
-- HTTP.GET :: METHOD
-- HTTP.POST :: METHOD
-- HTTP.PUT :: METHOD
data Method :: HTTP.StdMethod -> Type where
GET :: Method HTTP.GET
POST :: Method HTTP.POST
PUT :: Method HTTP.PUT
-- cmpMethod :: Method a -> Method b -> Bool
-- cmpMethod Get Get = True
-- cmpMethod Post Post = True
-- cmpMethod Put Put = True
-- cmpMethod Get Post = False
-- cmpMethod _ _ = False
class IsStdMethod (m :: HTTP.StdMethod) where
toStdMethod :: HTTP.StdMethod
instance IsStdMethod HTTP.GET where
toStdMethod = HTTP.GET
instance IsStdMethod HTTP.POST where
toStdMethod = HTTP.POST
instance IsStdMethod HTTP.PUT where
toStdMethod = HTTP.PUT
-- class (IsStdMethod m, Path a) => IsEndpoint (m :: METHOD) a where
class (Monad env, IsStdMethod m, Path a) => IsEndpoint env (m :: HTTP.StdMethod) a | a -> env where
endpointHandler :: Wai.Request -> a -> env Wai.Response
class (Monad env, Path a) => IsRoute env a | a -> env where
routeHandler :: Wai.Request -> HTTP.StdMethod -> a -> env Wai.Response
instance IsRoute IO Animal where
routeHandler = undefined
type Env = Int
-- data API where
-- Router :: forall env a. IsRoute env a => { transformation :: env ~> IO } -> API
-- Endpointer :: forall env method a. IsEndpoint env method a => { transformation :: env ~> IO } -> API
-- Scope :: { path :: [Text], middleware :: Wai.Middleware, children :: [API] } -> API
data API where
Route :: forall env a. IsRoute env a => (env ~> IO) -> API
Endpoint :: forall env method a. IsEndpoint env method a => (env ~> IO) -> API
Scope :: { prefix :: [Text], middleware :: Wai.Middleware, children :: [API] } -> API
route :: forall env a. IsRoute env a => (env ~> IO) -> API
route = Route @env @a
endpoint :: forall env method a. IsEndpoint env method a => (env ~> IO) -> API
endpoint = Endpoint @env @method @a
get :: forall env a. IsEndpoint env HTTP.GET a => (env ~> IO) -> API
get = Endpoint @env @HTTP.GET @a
post :: forall env a. IsEndpoint env HTTP.POST a => (env ~> IO) -> API
post = Endpoint @env @HTTP.POST @a
put :: forall env a. IsEndpoint env HTTP.PUT a => (env ~> IO) -> API
put = Endpoint @env @HTTP.PUT @a
scope :: [Text] -> Wai.Middleware -> [API] -> API
scope = Scope
data Person = Person Text Int
instance Path Person where
path = undefined
data Animal = Animal
instance Path Animal where
path = undefined
instance IsEndpoint IO HTTP.GET Person where
endpointHandler = undefined
instance IsEndpoint IO HTTP.POST Person where
endpointHandler = undefined
instance IsEndpoint IO HTTP.POST Animal where
endpointHandler = undefined
instance IsEndpoint IO HTTP.PUT Animal where
endpointHandler = undefined
{-
findHandler :: Method m -> [Text] -> [Handler] -> Maybe Handler
findHandler m p [] = Nothing
findHandler m p ((Handler @env @method @route t f):hs) = case method @method of
Get -> findHandler m p hs
_ -> Nothing
-}
myAPI :: API
myAPI = scope [] id
[ get @IO @Person id
, post @IO @Animal id
, put @IO @Animal id
, route @IO @Animal id
, scope ["api"] id
[ get @IO @Person id
, post @IO @Animal id
, scope ["v2"] id
[ post @IO @Animal id
]
]
]
{-
data API where
Route :: forall env a. IsRoute env a => (env ~> IO) -> API
Endpoint :: forall env method a. IsEndpoint env method a => (env ~> IO) -> API
Scope :: { prefix :: [Text], middleware :: Wai.Middleware, children :: [API] } -> API
-}
appify :: API -> Wai.Application -> Wai.Application
appify (Route @env @a transformation) defaultApp request respond = do
case HTTP.parseMethod $ Wai.requestMethod request of
Left _ -> defaultApp request respond
Right stdMethod -> do
let pathParser = path @a
pathResult = fst $ exec (Wai.pathInfo request) pathParser
case pathResult of
Left _ -> defaultApp request respond
Right x -> do
response <- transformation $ routeHandler @env @a request stdMethod x
respond response
appify (Endpoint @env @method @a transformation) defaultApp request respond = do
case HTTP.parseMethod $ Wai.requestMethod request of
Left _ -> defaultApp request respond
Right stdMethod ->
if toStdMethod @method == stdMethod
then do
let pathParser = path @a
pathResult = fst $ exec (Wai.pathInfo request) pathParser
case pathResult of
Left _ -> defaultApp request respond
Right x -> do
response <- transformation $ endpointHandler @env @method @a request x
respond response
else defaultApp request respond
appify (Scope prefix middleware []) defaultApp request respond = defaultApp request respond
appify (Scope prefix middleware children) defaultApp request respond = do
let reqPath = Wai.pathInfo request
case matchPrefix prefix reqPath of
Nothing -> defaultApp request respond
Just prefix -> do
let trimmedReqPath = remove prefix reqPath
candidates = filterAPIsByMethod children
case candidates of
[] -> defaultApp request respond
(c:cs) -> case c of
Route @env @a transformation -> do
let pathParser = path @a
pathResult = fst $ exec (Wai.pathInfo request) pathParser
case pathResult of
Left _ -> undefined
Right res -> case HTTP.parseMethod $ Wai.requestMethod request of
Left _ -> defaultApp request respond -- TODO: This shouldn't really happen I think? If so, not good.
Right stdMethod -> middleware (\request' respond' -> do
response <- transformation $ routeHandler @env @a request' stdMethod res
respond' response) $ (request {Wai.pathInfo = trimmedReqPath}) respond
Endpoint @env @method @a transformation -> do
let pathParser = path @a
pathResult = fst $ exec (Wai.pathInfo request) pathParser
case pathResult of
Left _ -> undefined
Right res -> middleware (\request' respond' -> do
response <- transformation $ endpointHandler @env @method @a request' res
respond' response) $ (request {Wai.pathInfo = trimmedReqPath}) respond
scoper@(Scope prefix' middleware' children') -> do
appify _ _ _ _ = undefined
filterAPIsByMethod :: HTTP.StdMethod -> [API] -> [API]
filterAPIsByMethod method apis = apis
remove :: [Text] -> [Text] -> [Text]
remove [] [] = []
remove [] path = path
remove prefix [] = [] -- TODO: Shouldn't be allowed
remove (p:ps) (p':ps') = p' : remove ps ps'
matchPrefix :: [Text] -> [Text] -> Maybe [Text]
matchPrefix prefix path = if Prelude.length prefix > Prelude.length path
then Nothing
else case (prefix, path) of
(p:ps, p':ps') -> if p == p'
then case matchPrefix ps ps' of
Nothing -> Nothing
Just pp -> Just (p : pp)
else Nothing
(ps, []) -> Nothing
([], ps') -> Just []
([], []) -> Just []
-}

View File

@ -1,21 +1,16 @@
{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE GADTs #-} {-# LANGUAGE GADTs #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- {-# LANGUAGE RebindableSyntax #-} -- {-# LANGUAGE RebindableSyntax #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ImportQualifiedPost #-}
module Okapi.Route where module Okapi.Route where
import Data.Text import Data.Text
import Data.Typeable import Data.Typeable
import Web.HttpApiData qualified as Web import Web.HttpApiData qualified as Web
-- import Prelude hiding (fmap, (<$>), pure, return, (<*>))
-- import Distribution.Simple.Setup (testOptions')
-- TODO!!!!! DO NOT NEED constraints on Applicative methods, so make it Applicative!!!!
data Route a where data Route a where
FMap :: (a -> b) -> Route a -> Route b FMap :: (a -> b) -> Route a -> Route b
@ -23,26 +18,6 @@ data Route a where
Apply :: Route (a -> b) -> Route a -> Route b Apply :: Route (a -> b) -> Route a -> Route b
Static :: Text -> Route () Static :: Text -> Route ()
Param :: (Typeable a, Web.FromHttpApiData a) => Route a Param :: (Typeable a, Web.FromHttpApiData a) => Route a
-- Assert :: (a -> Bool) -> Route a ???? Do we need this.
-- fmap :: (a -> b) -> Route a -> Route b
-- fmap = FMap
-- pure :: a -> Route a
-- pure = Pure
-- return :: a -> Route a
-- return = Pure
-- (<$>) :: (a -> b) -> Route a -> Route b
-- (<$>) = FMap
-- (<*>) :: Route (a -> b) -> Route a -> Route b
-- (<*>) = Apply
-- (>>=) = undefined
-- (>>) = undefined
param :: (Typeable a, Web.FromHttpApiData a) => Route a param :: (Typeable a, Web.FromHttpApiData a) => Route a
param = Param param = Param
@ -57,43 +32,24 @@ instance Applicative Route where
pure = Pure pure = Pure
(<*>) = Apply (<*>) = Apply
heq :: Route a -> Route b -> Bool -- equals :: Route a -> Route b -> Bool
heq (FMap _ r) (FMap _ r') = heq r r' -- equals (FMap _ r) (FMap _ r') = equals r r'
heq (Pure _) (Pure _) = True -- equals (Pure _) (Pure _) = True
heq (Apply af ap) (Apply af' ap') = heq af af' && heq ap ap' -- equals (Apply af ap) (Apply af' ap') = equals af af' && equals ap ap'
heq (Static t) (Static t') = t == t' -- equals (Static t) (Static t') = t == t'
heq (Param @a) (Param @b) = case eqT @a @b of -- equals (Param @a) (Param @b) = case heqT @a @b of
Nothing -> False -- Nothing -> False
Just Refl -> True -- Just HRefl -> True
heq _ _ = False -- equals _ _ = False
showRoute :: forall a. Route a -> Text rep :: Route a -> Text
showRoute (FMap _ dsl) = showRoute dsl rep (FMap _ dsl) = rep dsl
showRoute (Pure x) = "" rep (Pure x) = ""
showRoute (Apply aF aX) = showRoute aF <> showRoute aX rep (Apply aF aX) = rep aF <> rep aX
showRoute (Static t) = "/" <> t rep (Static t) = "/" <> t
showRoute (Param @p) = "/:" <> pack (convert @p) rep (Param @p) = "/{:" <> pack (show . typeRep $ Proxy @p) <> "}"
-- instance (Show a, Typeable a) => Show (Route a) where data Error = Error
-- show (FMap f dsl) = "FMap <function> (" <> show dsl <> ")"
-- show (Pure x) = "Pure " <> show x
-- show (Apply aF aX) = "Apply <apFunction> (" <> show aX <> ")"
-- show (Static t) = "Static " <> unpack t
-- show (Param @p) = "Param " <> convert @p
convert :: forall a. Typeable a => String exec :: Route a -> [Text] -> (Either Error a, [Text])
convert = show . typeRep $ Proxy @a exec = undefined
{-
(===) :: (Eq a, Typeable a, Eq b, Typeable b) => Route a -> Route b -> Bool
FMap f dsl === FMap f' dsl' = dsl === dsl'
Pure x === Pure x' = Just x == cast x'
Apply fR xR === Apply fR' xR' = (===) xR xR'
Static t === Static t' = t == t'
Param @a === Param @a' = case eqT @a @a' of
Nothing -> False
Just Refl -> True
Assert _ === _ = False
_ === Assert _ = False
_ === _ = False
-}