git.iko.soy/typeable/servant-reflex
on master help sign in
Get compiling, add example client code, client instance for simplest API
commite302b6aa964b307e0b928f75fc32181d065081c6 authorGreg Hale <imalsogreg@gmail.com> dateSat 6 Feb 2016, 17:16 (11 years ago) parenta13bdbe
7 files changed+234−89
exec/API.hs+8 exec/Example.hs+28 exec/Server.hs+77 overrides.nix+5−2 servant-reflex.cabal+10 src/Servant/Common/Req.hs+60−58 src/Servant/Reflex.hs+46−29
exec/API.hsnew file+8
@@ -0,0 +1,8 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE TypeOperators #-}
3+
4+module API where
5+
6+import Servant.API
7+
8+type API = Get '[JSON] ()
exec/Example.hsnew file+28
@@ -0,0 +1,28 @@
1+{-# LANGUAGE ScopedTypeVariables #-}
2+{-# LANGUAGE RankNTypes #-}
3+
4+module Main where
5+
6+import Servant.Reflex
7+import API
8+import Data.Proxy
9+import Reflex.Dom
10+import Reflex.Dom.Contrib.Xhr
11+
12+api :: Proxy API
13+api = Proxy
14+
15+url :: BaseUrl
16+url = BaseUrl Http "localhost" 8000 ""
17+
18+main :: IO ()
19+main = mainWidget run
20+
21+run :: forall t m. MonadWidget t m => m ()
22+run = do
23+ let (getUnit :: Event t () -> m (Event t ((),()))) = client api url
24+ b :: Event t () <- button "Get unit"
25+ res :: Event t ((),()) <- getUnit b
26+ c <- foldDyn (\_ (n :: Int) -> succ n) 0 res
27+ display c
28+
exec/Server.hsnew file+77
@@ -0,0 +1,77 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE PolyKinds #-}
5+{-# LANGUAGE TypeFamilies #-}
6+{-# LANGUAGE TypeOperators #-}
7+
8+import Data.Aeson
9+import Data.Monoid
10+import Data.Proxy
11+import Data.Text
12+import GHC.Generics
13+import Network.Wai
14+import Network.Wai.Handler.Warp
15+
16+import Servant
17+import API
18+
19+-- * Example
20+
21+-- | A greet message data type
22+newtype Greet = Greet { _msg :: Text }
23+ deriving (Generic, Show)
24+
25+ instance FromJSON Greet
26+instance ToJSON Greet
27+
28+-- API specification
29+type TestApi =
30+ -- GET /hello/:name?capital={true, false} returns a Greet as JSON
31+ "hello" :> Capture "name" Text :> QueryParam "capital" Bool :> Get '[JSON] Greet
32+
33+ -- POST /greet with a Greet as JSON in the request body,
34+ -- returns a Greet as JSON
35+ :<|> "greet" :> ReqBody '[JSON] Greet :> Post '[JSON] Greet
36+
37+ -- DELETE /greet/:greetid
38+ :<|> "greet" :> Capture "greetid" Text :> Delete '[JSON] ()
39+
40+ testApi :: Proxy TestApi
41+ testApi = Proxy
42+
43+
44+server1 :: Server API
45+server1 = return $ Right ()
46+
47+ -- Server-side handlers.
48+--
49+-- There's one handler per endpoint, which, just like in the type
50+-- that represents the API, are glued together using :<|>.
51+--
52+-- Each handler runs in the 'ExceptT ServantErr IO' monad.
53+server :: Server TestApi
54+server = helloH :<|> postGreetH :<|> deleteGreetH
55+
56+ where helloH name Nothing = helloH name (Just False)
57+ helloH name (Just False) = return . Greet $ "Hello, " <> name
58+ helloH name (Just True) = return . Greet . toUpper $ "Hello, " <> name
59+
60+ postGreetH greet = return greet
61+
62+ deleteGreetH _ = return ()
63+
64+ -- Turn the server into a WAI app. 'serve' is provided by servant,
65+-- more precisely by the Servant.Server module.
66+test :: Application
67+test = serve testApi EmptyConfig server
68+
69+-- Run the server.
70+--
71+-- 'run' comes from Network.Wai.Handler.Warp
72+runTestServer :: Port -> IO ()
73+runTestServer port = run port test
74+
75+-- Put this all to work!
76+main :: IO ()
77+main = runTestServer 8000
overrides.nix+5−2
@@ -1,8 +1,11 @@
11{}:
2−let try-reflex = import deps/try-reflex {};
2+let
3+ pkgs = import <nixpkgs> {};
4+ try-reflex = import deps/try-reflex {};
35in try-reflex.ghcjs.override {
46 overrides = self: super: {
5− reflex-dom-contrib = self.callPackage deps/reflex-dom-contrib {};
7+ self.lens = pkgs.haskell.lib.dontCheck (self.lens);
8+ reflex-dom-contrib = pkgs.haskell.lib.dontCheck (self.callPackage deps/reflex-dom-contrib {});
69 servant = self.callPackage (try-reflex.cabal2nixResult deps/servant/servant) {};
710 };
811}
servant-reflex.cabal+10
@@ -24,9 +24,11 @@ library
2424 base,
2525 bytestring >= 0.10 && < 0.11,
2626 containers,
27+ data-default >= 0.5 && < 0.6,
2728 exceptions >= 0.8 && < 0.9,
2829 ghcjs-base,
2930 ghcjs-dom == 0.1.*,
31+ http-api-data >= 0.2 && < 0.3,
3032 http-media >= 0.6 && < 0.7,
3133 http-types >= 0.8 && < 0.9,
3234 mtl,
@@ -44,3 +46,11 @@ library
4446 build-depends: ghcjs-base
4547
4648 ghc-options: -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2
49+
50+executable example
51+ build-depends: reflex, servant-reflex, base, servant, reflex-dom, reflex-dom-contrib
52+ default-language: Haskell2010
53+ main-is: Example.hs
54+ other-modules: API
55+ hs-source-dirs: exec
56+
src/Servant/Common/Req.hs+60−58
@@ -2,6 +2,8 @@
22{-# LANGUAGE CPP #-}
33{-# LANGUAGE OverloadedStrings #-}
44{-# LANGUAGE ScopedTypeVariables #-}
5+{-# LANGUAGE RankNTypes #-}
6+
57module Servant.Common.Req where
68
79#if !MIN_VERSION_base(4,8,0)
@@ -65,60 +67,60 @@ data Req = Req
6567defReq :: Req
6668defReq = Req "" [] Nothing [] []
6769
68−appendToPath :: String -> Req -> Req
69−appendToPath p req =
70− req { reqPath = reqPath req ++ "/" ++ p }
71−
72−appendToQueryString :: Text -- ^ param name
73− -> Maybe Text -- ^ param value
74− -> Req
75− -> Req
76−appendToQueryString pname pvalue req =
77− req { qs = qs req ++ [(pname, pvalue)]
78− }
79−
80−addHeader :: ToHttpApiData a => String -> a -> Req -> Req
81−addHeader name val req = req { headers = headers req
82− ++ [(name, decodeUtf8 (toHeader val))]
83− }
84−
85−setRQBody :: ByteString -> MediaType -> Req -> Req
86−setRQBody b t req = req { reqBody = Just (b, t) }
87−
88−reqToRequest :: (Functor m, MonadThrow m) => Req -> BaseUrl -> m Request
89−reqToRequest req (BaseUrl reqScheme reqHost reqPort path) =
90− setheaders . setAccept . setrqb . setQS <$> parseUrl url
91−
92− where url = show $ nullURI { uriScheme = case reqScheme of
93− Http -> "http:"
94− Https -> "https:"
95− , uriAuthority = Just $
96− URIAuth { uriUserInfo = ""
97− , uriRegName = reqHost
98− , uriPort = ":" ++ show reqPort
99− }
100− , uriPath = path ++ reqPath req
101− }
102−
103− setrqb r = case reqBody req of
104− Nothing -> r
105− Just (b,t) -> r { requestBody = RequestBodyLBS b
106− , requestHeaders = requestHeaders r
107− ++ [(hContentType, cs . show $ t)] }
108− setQS = setQueryString $ queryTextToQuery (qs req)
109− setheaders r = r { requestHeaders = requestHeaders r
110− <> fmap toProperHeader (headers req) }
111− setAccept r = r { requestHeaders = filter ((/= "Accept") . fst) (requestHeaders r)
112− <> [("Accept", renderHeader $ reqAccept req)
113− | not . null . reqAccept $ req] }
114− toProperHeader (name, val) =
115− (fromString name, encodeUtf8 val)
116−
117−
118−-- * performing requests
119−
120−displayHttpRequest :: Method -> String
121−displayHttpRequest httpmethod = "HTTP " ++ cs httpmethod ++ " request"
70+-- appendToPath :: String -> Req -> Req
71+-- appendToPath p req =
72+-- req { reqPath = reqPath req ++ "/" ++ p }
73+
74+-- appendToQueryString :: Text -- ^ param name
75+-- -> Maybe Text -- ^ param value
76+-- -> Req
77+-- -> Req
78+-- appendToQueryString pname pvalue req =
79+-- req { qs = qs req ++ [(pname, pvalue)]
80+-- }
81+
82+-- addHeader :: ToHttpApiData a => String -> a -> Req -> Req
83+-- addHeader name val req = req { headers = headers req
84+-- ++ [(name, decodeUtf8 (toHeader val))]
85+-- }
86+
87+-- setRQBody :: ByteString -> MediaType -> Req -> Req
88+-- setRQBody b t req = req { reqBody = Just (b, t) }
89+
90+-- reqToRequest :: (Functor m, MonadThrow m) => Req -> BaseUrl -> m Request
91+-- reqToRequest req (BaseUrl reqScheme reqHost reqPort path) =
92+-- setheaders . setAccept . setrqb . setQS <$> parseUrl url
93+
94+-- where url = show $ nullURI { uriScheme = case reqScheme of
95+-- Http -> "http:"
96+-- Https -> "https:"
97+-- , uriAuthority = Just $
98+-- URIAuth { uriUserInfo = ""
99+-- , uriRegName = reqHost
100+-- , uriPort = ":" ++ show reqPort
101+-- }
102+-- , uriPath = path ++ reqPath req
103+-- }
104+
105+-- setrqb r = case reqBody req of
106+-- Nothing -> r
107+-- Just (b,t) -> r { requestBody = RequestBodyLBS b
108+-- , requestHeaders = requestHeaders r
109+-- ++ [(hContentType, cs . show $ t)] }
110+-- setQS = setQueryString $ queryTextToQuery (qs req)
111+-- setheaders r = r { requestHeaders = requestHeaders r
112+-- <> fmap toProperHeader (headers req) }
113+-- setAccept r = r { requestHeaders = filter ((/= "Accept") . fst) (requestHeaders r)
114+-- <> [("Accept", renderHeader $ reqAccept req)
115+-- | not . null . reqAccept $ req] }
116+-- toProperHeader (name, val) =
117+-- (fromString name, encodeUtf8 val)
118+
119+
120+-- -- * performing requests
121+
122+-- displayHttpRequest :: Method -> String
123+-- displayHttpRequest httpmethod = "HTTP " ++ cs httpmethod ++ " request"
122124
123125performRequest = undefined
124126performRequestCT = undefined
@@ -129,16 +131,16 @@ performRequestNoBody = undefined
129131-- , [HTTP.Header], Response ByteString)
130132-- performRequest reqMethod req reqHost manager = do
131133-- partialRequest <- liftIO $ reqToRequest req reqHost
132−--
134+
133135-- let request = partialRequest { Client.method = reqMethod
134136-- , checkStatus = \ _status _headers _cookies -> Nothing
135137-- }
136−--
138+
137139-- eResponse <- liftIO $ catchConnectionError $ Client.httpLbs request manager
138140-- case eResponse of
139141-- Left err ->
140142-- throwE . ConnectionError $ SomeException err
141−--
143+
142144-- Right response -> do
143145-- let status = Client.responseStatus response
144146-- body = Client.responseBody response
@@ -152,7 +154,7 @@ performRequestNoBody = undefined
152154-- unless (status_code >= 200 && status_code < 300) $
153155-- throwE $ FailureResponse status ct body
154156-- return (status_code, body, ct, hrds, response)
155−--
157+
156158--
157159-- performRequestCT :: MimeUnrender ct result =>
158160-- Proxy ct -> Method -> Req -> BaseUrl -> Manager -> ExceptT ServantError IO ([HTTP.Header], result)
src/Servant/Reflex.hs+46−29
@@ -8,6 +8,7 @@
88{-# LANGUAGE TypeFamilies #-}
99{-# LANGUAGE TypeOperators #-}
1010{-# LANGUAGE UndecidableInstances #-}
11+{-# LANGUAGE RankNTypes #-}
1112#if !MIN_VERSION_base(4,8,0)
1213{-# LANGUAGE OverlappingInstances #-}
1314#endif
@@ -27,6 +28,7 @@ import Control.Applicative ((<$>))
2728import Control.Monad
2829import Control.Monad.Trans.Except
2930import Data.ByteString.Lazy (ByteString)
31+import Data.Default
3032import Data.List
3133import Data.Proxy
3234import Data.String.Conversions
@@ -38,7 +40,11 @@ import qualified Network.HTTP.Types.Header as HTTP
3840import Servant.API
3941import Servant.Common.BaseUrl
4042import Servant.Common.Req
43+import Reflex
44+import Reflex.Dom
4145import Web.HttpApiData
46+import Reflex.Dom.Contrib.Xhr
47+import Reflex.Dom.Xhr
4248
4349-- * Accessing APIs as a Client
4450
@@ -54,37 +60,44 @@ import Web.HttpApiData
5460-- > postNewBook :: Book -> ExceptT String IO Book
5561-- > (getAllBooks :<|> postNewBook) = client myApi host
5662-- > where host = BaseUrl Http "localhost" 8080
57−client :: HasReflexClient layout => Proxy layout -> BaseUrl -> Input layout -> Client layout
63+client :: HasReflexClient layout
64+ => Proxy layout
65+ -> BaseUrl
66+ -- -> Input layout
67+ -> Client (Input layout) (Output layout)
5868client p baseurl = clientWithRoute p defReq baseurl
5969
60−type Final a = forall m. m a
61−
6270data a ::> b = a ::> b deriving (Eq,Ord,Show,Read)
6371
6472infixr 3 ::>
6573
74+
75+type Client ins outs = MonadWidget t m => Event t ins -> m (Event t (ins,outs))
76+
6677-- | This class lets us define how each API combinator
6778-- influences the creation of an HTTP request. It's mostly
6879-- an internal class, you can just use 'client'.
6980class HasReflexClient layout where
7081 type Input layout :: *
71− type Client layout :: *
72− clientWithRoute :: Proxy layout -> Req -> BaseUrl -> Input layout -> Client layout
82+ type Output layout :: *
83+ clientWithRoute :: Proxy layout -> Req -> BaseUrl -- -> Input layout
84+ -> Client (Input layout) (Output layout)
7385
7486
75−-- | If you have a 'Get' endpoint in your API, the client
76−-- side querying function that is created when calling 'client'
77−-- will just require an argument that specifies the scheme, host
78−-- and port to send the request to.
79−instance
80−#if MIN_VERSION_base(4,8,0)
81− {-# OVERLAPPABLE #-}
82−#endif
83− (MimeUnrender ct result) => HasReflexClient (Get (ct ': cts) result) where
84− type Input (Get (ct ': cts) result) = ()
85− type Client (Get (ct ': cts) result) = Final result
86− clientWithRoute Proxy req baseurl _ =
87− snd <$> performRequestCT (Proxy :: Proxy ct) H.methodGet req baseurl
87+-- -- | If you have a 'Get' endpoint in your API, the client
88+-- -- side querying function that is created when calling 'client'
89+-- -- will just require an argument that specifies the scheme, host
90+-- -- and port to send the request to.
91+-- instance
92+-- #if MIN_VERSION_base(4,8,0)
93+-- {-# OVERLAPPABLE #-}
94+-- #endif
95+-- (MimeUnrender ct result) => HasReflexClient (Get (ct ': cts) result) where
96+-- type Input (Get (ct ': cts) result) = ()
97+-- -- type Output (Get (ct' : cts) result) = result
98+-- type Client (Get (ct ': cts) result) = Final result
99+-- clientWithRoute Proxy req baseurl _ =
100+-- snd <$> performRequestCT (Proxy :: Proxy ct) H.methodGet req baseurl
88101
89102instance
90103#if MIN_VERSION_base(4,8,0)
@@ -92,19 +105,23 @@ instance
92105#endif
93106 HasReflexClient (Get (ct ': cts) ()) where
94107 type Input (Get (ct ': cts) ()) = ()
95− type Client (Get (ct ': cts) ()) = Final ()
96− clientWithRoute Proxy req baseurl _ =
97− performRequestNoBody H.methodGet req baseurl
108+ type Output (Get (ct ': cts) ()) = ()
109+ -- type Client (Get (ct ': cts) ()) = Final ()
110+ clientWithRoute Proxy req baseurl trigEvents =
111+ performAJAX requestBuilder responseParser trigEvents
112+ where
113+ requestBuilder _ = XhrRequest "GET" (showBaseUrl baseurl) def
114+ responseParser _ = ()
98115
99−-- | Pick a 'Method' and specify where the server you want to query is. You get
100−-- back the full `Response`.
101−instance HasReflexClient Raw where
102− type Input Raw = H.Method ::> ()
103− type Client Raw = Final (Int, ByteString, MediaType, [HTTP.Header], Response ByteString)
116+-- -- | Pick a 'Method' and specify where the server you want to query is. You get
117+-- -- back the full `Response`.
118+-- instance HasReflexClient Raw where
119+-- type Input Raw = H.Method ::> ()
120+-- type Client Raw = Final (Int, ByteString, MediaType, [HTTP.Header], Response ByteString)
104121
105− clientWithRoute :: Proxy Raw -> Req -> BaseUrl -> Input Raw -> Client Raw
106− clientWithRoute Proxy req baseurl (httpMethod ::> ()) = do
107− performRequest httpMethod req baseurl
122+-- clientWithRoute :: Proxy Raw -> Req -> BaseUrl -> Input Raw -> Client Raw
123+-- clientWithRoute Proxy req baseurl (httpMethod ::> ()) = do
124+-- performRequest httpMethod req baseurl
108125
109126-- -- | If you have a 'Get xs (Headers ls x)' endpoint, the client expects the
110127-- -- corresponding headers.