git.iko.soy/typeable/servant
on master help sign in
reorganize everything into a sane(r) module structure
commitd838191ec8890d9dd0ef7bab8af067be3ffbf7fc authorAlp Mestanogullari <alpmestan@gmail.com> dateSat 25 Oct 2014, 01:27 (12 years ago) parent50f5c36
15 files changed+557−420
example/greet.hs+74 servant.cabal+32−5 src/Servant.hs+11−202 src/Servant/API.hs+17 src/Servant/API/Capture.hs+50 src/Servant/API/Get.hs+45 src/Servant/API/GetParam.hs+54 src/Servant/API/Post.hs+51 src/Servant/API/RQBody.hs+40 src/Servant/API/Sub.hs+39 src/Servant/API/Union.hs+26 src/Servant/Client.hs+66 src/Servant/Server.hs+27 src/Servant/Text.hs+25 src/Soenke.hs−213
example/greet.hsnew file+74
@@ -0,0 +1,74 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE PolyKinds #-}
3+{-# LANGUAGE TypeFamilies #-}
4+{-# LANGUAGE DeriveGeneric #-}
5+{-# LANGUAGE TypeOperators #-}
6+{-# LANGUAGE OverloadedStrings #-}
7+
8+import Control.Concurrent (forkIO, killThread)
9+import Control.Monad.Trans.Either
10+import Data.Aeson
11+import Data.Monoid
12+import Data.Proxy
13+import Data.Text
14+import GHC.Generics
15+import Network.URI
16+import Network.Wai
17+import Network.Wai.Handler.Warp
18+
19+import Servant.API
20+import Servant.Client
21+import Servant.Server
22+
23+-- * Example
24+
25+data Greet = Greet { msg :: Text }
26+ deriving (Generic, Show)
27+
28+instance FromJSON Greet
29+instance ToJSON Greet
30+
31+-- API specification
32+type TestApi =
33+ "hello" :> Capture "name" Text :> GetParam "capital" Bool :> Get Greet
34+ :<|> "greet" :> RQBody Greet :> Post Greet
35+
36+testApi :: Proxy TestApi
37+testApi = Proxy
38+
39+-- Server-side handlers
40+server :: Server TestApi
41+server = hello :<|> greet
42+
43+ where hello name Nothing = hello name (Just False)
44+ hello name (Just False) = return . Greet $ "Hello, " <> name
45+ hello name (Just True) = return . Greet . toUpper $ "Hello, " <> name
46+
47+ greet = return
48+
49+-- Client-side query functions
50+clientApi :: Client TestApi
51+clientApi = client testApi
52+
53+getGreet :: Text -> Maybe Bool -> URI -> EitherT String IO Greet
54+postGreet :: Greet -> URI -> EitherT String IO Greet
55+getGreet :<|> postGreet = clientApi
56+
57+-- Turn the server into a WAI app
58+test :: Application
59+test = serve testApi server
60+
61+-- Run the server
62+runTestServer :: Port -> IO ()
63+runTestServer port = run port test
64+
65+-- Run some queries against the server
66+main :: IO ()
67+main = do
68+ tid <- forkIO $ runTestServer 8001
69+ let Just uri = parseURI "http://localhost:8001"
70+ print =<< runEitherT (getGreet "alp" (Just True) uri)
71+ print =<< runEitherT (getGreet "alp" (Just False) uri)
72+ let g = Greet "yo"
73+ print =<< runEitherT (postGreet g uri)
74+ killThread tid
servant.cabal+32−5
@@ -4,7 +4,7 @@ version: 0.2
44-- description:
55license: BSD3
66license-file: LICENSE
7−author: Alp Mestanogullari
7+author: Alp Mestanogullari, Soenke Hahn
88maintainer: alpmestan@gmail.com
99-- copyright:
1010category: Web
@@ -13,7 +13,19 @@ build-type: Simple
1313cabal-version: >=1.10
1414
1515library
16− exposed-modules: Servant Soenke
16+ exposed-modules:
17+ Servant
18+ Servant.Client
19+ Servant.Server
20+ Servant.Text
21+ Servant.API
22+ Servant.API.Capture
23+ Servant.API.Get
24+ Servant.API.GetParam
25+ Servant.API.Post
26+ Servant.API.RQBody
27+ Servant.API.Sub
28+ Servant.API.Union
1729 -- other-modules:
1830 -- other-extensions:
1931 build-depends:
@@ -30,6 +42,21 @@ library
3042 , warp
3143 , transformers
3244 , text
33− hs-source-dirs: src
34− default-language: Haskell2010
35− ghc-options: -O2
45+ hs-source-dirs: src
46+ default-language: Haskell2010
47+ ghc-options: -O0 -Wall
48+
49+executable greet
50+ main-is: greet.hs
51+ hs-source-dirs: example
52+ ghc-options: -O0 -Wall
53+ default-language: Haskell2010
54+ build-depends:
55+ base
56+ , servant
57+ , aeson
58+ , warp
59+ , wai
60+ , either
61+ , text
62+ , network-uri
src/Servant.hs+11−202
@@ -1,202 +1,11 @@
1−{-# LANGUAGE DataKinds #-}
2−{-# LANGUAGE PolyKinds #-}
3−{-# LANGUAGE TypeFamilies #-}
4−{-# LANGUAGE DeriveGeneric #-}
5−{-# LANGUAGE TypeOperators #-}
6−{-# LANGUAGE FlexibleContexts #-}
7−{-# LANGUAGE FlexibleInstances #-}
8−{-# LANGUAGE OverloadedStrings #-}
9−{-# LANGUAGE ScopedTypeVariables #-}
10−module Servant where
11−
12−import Control.Applicative
13−import Control.Concurrent (forkIO, killThread)
14−import Control.Monad
15−import Control.Monad.IO.Class
16−import Control.Monad.Trans.Either
17−import Data.Aeson
18−import Data.ByteString.Lazy (ByteString)
19−import Data.Monoid
20−import Data.Proxy
21−import Data.String.Conversions
22−import Data.Text
23−import GHC.Generics
24−import GHC.TypeLits
25−import Network.HTTP.Types
26−import Network.URI
27−import Network.Wai
28−import Network.Wai.Handler.Warp
29−import Soenke
30−
31−import qualified Network.HTTP.Client as Http.Client
32−
33−class FromText a where
34− fromText :: Text -> Maybe a
35−
36−class ToText a where
37− toText :: a -> Text
38−
39−instance FromText Text where
40− fromText = Just
41−
42−instance ToText Text where
43− toText = id
44−
45−instance FromText Bool where
46− fromText "true" = Just True
47− fromText "false" = Just False
48− fromText _ = Nothing
49−
50−instance ToText Bool where
51− toText True = "true"
52− toText False = "false"
53−
54−-- * Captures
55−data Capture sym a
56−
57−captured :: FromText a => proxy (Capture sym a) -> Text -> Maybe a
58−captured _ = fromText
59−
60−instance (KnownSymbol capture, FromText a, HasServer sublayout)
61− => HasServer (Capture capture a :> sublayout) where
62−
63− type Server (Capture capture a :> sublayout) =
64− a -> Server sublayout
65−
66− route Proxy subserver request = case pathInfo request of
67− (first : rest)
68− -> case captured captureProxy first of
69− Nothing -> return Nothing
70− Just v -> route (Proxy :: Proxy sublayout) (subserver v) request{
71− pathInfo = rest
72− }
73− _ -> return Nothing
74−
75− where captureProxy = Proxy :: Proxy (Capture capture a)
76−
77−instance (KnownSymbol capture, ToText a, HasClient sublayout)
78− => HasClient (Capture capture a :> sublayout) where
79−
80− type Client (Capture capture a :> sublayout) =
81− a -> Client sublayout
82−
83− clientWithRoute Proxy req val =
84− clientWithRoute (Proxy :: Proxy sublayout) $
85− appendToPath p req
86−
87− where p = unpack (toText val)
88−
89−-- * Request Body support
90−data RQBody a
91−
92−instance (FromJSON a, HasServer sublayout)
93− => HasServer (RQBody a :> sublayout) where
94−
95− type Server (RQBody a :> sublayout) =
96− a -> Server sublayout
97−
98− route Proxy subserver request = do
99− mrqbody <- decode' <$> lazyRequestBody request
100− case mrqbody of
101− Nothing -> return Nothing
102− Just v -> route (Proxy :: Proxy sublayout) (subserver v) request
103−
104−instance (ToJSON a, HasClient sublayout)
105− => HasClient (RQBody a :> sublayout) where
106−
107− type Client (RQBody a :> sublayout) =
108− a -> Client sublayout
109−
110− clientWithRoute Proxy req body =
111− clientWithRoute (Proxy :: Proxy sublayout) $
112− setRQBody (encode body) req
113−
114−-- * GET params support (i.e query string arguments)
115−data GetParam sym a
116−
117−instance (KnownSymbol sym, FromText a, HasServer sublayout)
118− => HasServer (GetParam sym a :> sublayout) where
119−
120− type Server (GetParam sym a :> sublayout) =
121− Maybe a -> Server sublayout
122−
123− route Proxy subserver request = do
124− let querytext = parseQueryText $ rawQueryString request
125− param =
126− case lookup paramName querytext of
127− Nothing -> Nothing -- param absent from the query string
128− Just Nothing -> Nothing -- param present with no value -> Nothing
129− Just (Just v) -> fromText v -- if present, we try to convert to
130− -- the right type
131−
132− route (Proxy :: Proxy sublayout) (subserver param) request
133−
134− where paramName = cs $ symbolVal (Proxy :: Proxy sym)
135−
136−instance (KnownSymbol sym, ToText a, HasClient sublayout)
137− => HasClient (GetParam sym a :> sublayout) where
138−
139− type Client (GetParam sym a :> sublayout) =
140− Maybe a -> Client sublayout
141−
142− -- if mparam = Nothing, we don't add it to the query string
143− clientWithRoute Proxy req mparam =
144− clientWithRoute (Proxy :: Proxy sublayout) $
145− appendToQueryString pname mparamText req
146−
147− where pname = pack pname'
148− pname' = symbolVal (Proxy :: Proxy sym)
149− mparamText = fmap toText mparam
150−
151−-- * Example
152−
153−data Greet = Greet { msg :: Text }
154− deriving (Generic, Show)
155−
156−instance FromJSON Greet
157−instance ToJSON Greet
158−
159−-- API specification
160−type TestApi =
161− "hello" :> Capture "name" Text :> GetParam "capital" Bool :> Get Greet
162− :<|> "greet" :> RQBody Greet :> Post Greet
163−
164−testApi :: Proxy TestApi
165−testApi = Proxy
166−
167−-- Server-side handlers
168−server :: Server TestApi
169−server = hello :<|> greet
170−
171− where hello name Nothing = hello name (Just False)
172− hello name (Just False) = return . Greet $ "Hello, " <> name
173− hello name (Just True) = return . Greet . toUpper $ "Hello, " <> name
174−
175− greet = return
176−
177−-- Client-side query functions
178−clientApi :: Client TestApi
179−clientApi = client testApi
180−
181−getGreet :: Text -> Maybe Bool -> URI -> EitherT String IO Greet
182−postGreet :: Greet -> URI -> EitherT String IO Greet
183−getGreet :<|> postGreet = clientApi
184−
185−-- Turn the server into a WAI app
186−test :: Application
187−test = serve testApi server
188−
189−-- Run the server
190−runTestServer :: Port -> IO ()
191−runTestServer port = run port test
192−
193−-- Run some queries against the server
194−runTest :: IO ()
195−runTest = do
196− tid <- forkIO $ runTestServer 8001
197− let Just uri = parseURI "http://localhost:8001"
198− print =<< runEitherT (getGreet "alp" (Just True) uri)
199− print =<< runEitherT (getGreet "alp" (Just False) uri)
200− let g = Greet "yo"
201− print =<< runEitherT (postGreet g uri)
202− killThread tid
1+module Servant
2+ ( module Servant.API
3+ , module Servant.Client
4+ , module Servant.Server
5+ , module Servant.Text
6+ ) where
7+
8+import Servant.API
9+import Servant.Client
10+import Servant.Server
11+import Servant.Text
src/Servant/API.hsnew file+17
@@ -0,0 +1,17 @@
1+module Servant.API
2+ ( module Servant.API.Capture
3+ , module Servant.API.Get
4+ , module Servant.API.GetParam
5+ , module Servant.API.Post
6+ , module Servant.API.RQBody
7+ , module Servant.API.Sub
8+ , module Servant.API.Union
9+ ) where
10+
11+import Servant.API.Capture
12+import Servant.API.Get
13+import Servant.API.GetParam
14+import Servant.API.Post
15+import Servant.API.RQBody
16+import Servant.API.Sub
17+import Servant.API.Union
src/Servant/API/Capture.hsnew file+50
@@ -0,0 +1,50 @@
1+{-# LANGUAGE PolyKinds #-}
2+{-# LANGUAGE TypeFamilies #-}
3+{-# LANGUAGE TypeOperators #-}
4+{-# LANGUAGE FlexibleInstances #-}
5+{-# LANGUAGE ScopedTypeVariables #-}
6+module Servant.API.Capture where
7+
8+import Data.Proxy
9+import Data.Text
10+import GHC.TypeLits
11+import Network.Wai
12+import Servant.API.Sub
13+import Servant.Client
14+import Servant.Server
15+import Servant.Text
16+
17+-- * Captures
18+data Capture sym a
19+
20+captured :: FromText a => proxy (Capture sym a) -> Text -> Maybe a
21+captured _ = fromText
22+
23+instance (KnownSymbol capture, FromText a, HasServer sublayout)
24+ => HasServer (Capture capture a :> sublayout) where
25+
26+ type Server (Capture capture a :> sublayout) =
27+ a -> Server sublayout
28+
29+ route Proxy subserver request = case pathInfo request of
30+ (first : rest)
31+ -> case captured captureProxy first of
32+ Nothing -> return Nothing
33+ Just v -> route (Proxy :: Proxy sublayout) (subserver v) request{
34+ pathInfo = rest
35+ }
36+ _ -> return Nothing
37+
38+ where captureProxy = Proxy :: Proxy (Capture capture a)
39+
40+instance (KnownSymbol capture, ToText a, HasClient sublayout)
41+ => HasClient (Capture capture a :> sublayout) where
42+
43+ type Client (Capture capture a :> sublayout) =
44+ a -> Client sublayout
45+
46+ clientWithRoute Proxy req val =
47+ clientWithRoute (Proxy :: Proxy sublayout) $
48+ appendToPath p req
49+
50+ where p = unpack (toText val)
src/Servant/API/Get.hsnew file+45
@@ -0,0 +1,45 @@
1+{-# LANGUAGE TypeFamilies #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Servant.API.Get where
4+
5+import Control.Monad
6+import Control.Monad.IO.Class
7+import Control.Monad.Trans.Either
8+import Data.Aeson
9+import Data.Proxy
10+import Data.String.Conversions
11+import Network.HTTP.Types
12+import Network.URI
13+import Network.Wai
14+import Servant.Client
15+import Servant.Server
16+
17+import qualified Network.HTTP.Client as Client
18+
19+-- | Endpoint for simple GET requests. The server doesn't receive any arguments
20+-- and serves the contained type as JSON.
21+data Get a
22+
23+instance ToJSON result => HasServer (Get result) where
24+ type Server (Get result) = EitherT (Int, String) IO result
25+ route Proxy action request
26+ | null (pathInfo request) && requestMethod request == methodGet = do
27+ e <- runEitherT action
28+ return $ Just $ case e of
29+ Right output ->
30+ responseLBS ok200 [("Content-Type", "application/json")] (encode output)
31+ Left (status, message) ->
32+ responseLBS (mkStatus status (cs message)) [] (cs message)
33+ | otherwise = return Nothing
34+
35+instance FromJSON result => HasClient (Get result) where
36+ type Client (Get result) = URI -> EitherT String IO result
37+ clientWithRoute Proxy req uri = do
38+ innerRequest <- liftIO $ reqToRequest req uri
39+
40+ innerResponse <- liftIO $ __withGlobalManager $ \ manager ->
41+ Client.httpLbs innerRequest manager
42+ when (Client.responseStatus innerResponse /= ok200) $
43+ left ("HTTP GET request failed with status: " ++ show (Client.responseStatus innerResponse))
44+ maybe (left "HTTP GET request returned invalid json") return $
45+ decode' (Client.responseBody innerResponse)
src/Servant/API/GetParam.hsnew file+54
@@ -0,0 +1,54 @@
1+{-# LANGUAGE PolyKinds #-}
2+{-# LANGUAGE TypeFamilies #-}
3+{-# LANGUAGE TypeOperators #-}
4+{-# LANGUAGE FlexibleInstances #-}
5+{-# LANGUAGE ScopedTypeVariables #-}
6+module Servant.API.GetParam where
7+
8+import Data.Proxy
9+import Data.String.Conversions
10+import Data.Text
11+import GHC.TypeLits
12+import Network.HTTP.Types
13+import Network.Wai
14+import Servant.API.Sub
15+import Servant.Client
16+import Servant.Server
17+import Servant.Text
18+
19+-- * GET params support (i.e query string arguments)
20+data GetParam sym a
21+
22+instance (KnownSymbol sym, FromText a, HasServer sublayout)
23+ => HasServer (GetParam sym a :> sublayout) where
24+
25+ type Server (GetParam sym a :> sublayout) =
26+ Maybe a -> Server sublayout
27+
28+ route Proxy subserver request = do
29+ let querytext = parseQueryText $ rawQueryString request
30+ param =
31+ case lookup paramName querytext of
32+ Nothing -> Nothing -- param absent from the query string
33+ Just Nothing -> Nothing -- param present with no value -> Nothing
34+ Just (Just v) -> fromText v -- if present, we try to convert to
35+ -- the right type
36+
37+ route (Proxy :: Proxy sublayout) (subserver param) request
38+
39+ where paramName = cs $ symbolVal (Proxy :: Proxy sym)
40+
41+instance (KnownSymbol sym, ToText a, HasClient sublayout)
42+ => HasClient (GetParam sym a :> sublayout) where
43+
44+ type Client (GetParam sym a :> sublayout) =
45+ Maybe a -> Client sublayout
46+
47+ -- if mparam = Nothing, we don't add it to the query string
48+ clientWithRoute Proxy req mparam =
49+ clientWithRoute (Proxy :: Proxy sublayout) $
50+ appendToQueryString pname mparamText req
51+
52+ where pname = pack pname'
53+ pname' = symbolVal (Proxy :: Proxy sym)
54+ mparamText = fmap toText mparam
src/Servant/API/Post.hsnew file+51
@@ -0,0 +1,51 @@
1+{-# LANGUAGE TypeFamilies #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Servant.API.Post where
4+
5+import Control.Monad
6+import Control.Monad.IO.Class
7+import Control.Monad.Trans.Either
8+import Data.Aeson
9+import Data.Proxy
10+import Data.String.Conversions
11+import Network.HTTP.Types
12+import Network.URI
13+import Network.Wai
14+import Servant.Client
15+import Servant.Server
16+
17+import qualified Network.HTTP.Client as Client
18+
19+-- | Endpoint for POST requests.
20+data Post a
21+
22+instance ToJSON a => HasServer (Post a) where
23+ type Server (Post a) = EitherT (Int, String) IO a
24+
25+ route Proxy action request
26+ | null (pathInfo request) && requestMethod request == methodPost = do
27+ e <- runEitherT action
28+ return $ Just $ case e of
29+ Right out ->
30+ responseLBS status201 [("Content-Type", "application/json")] (encode out)
31+ Left (status, message) ->
32+ responseLBS (mkStatus status (cs message)) [] (cs message)
33+ | otherwise = return Nothing
34+
35+instance FromJSON a => HasClient (Post a) where
36+ type Client (Post a) = URI -> EitherT String IO a
37+
38+ clientWithRoute Proxy req uri = do
39+ partialRequest <- liftIO $ reqToRequest req uri
40+
41+ let request = partialRequest { Client.method = methodPost
42+ }
43+
44+ innerResponse <- liftIO . __withGlobalManager $ \ manager ->
45+ Client.httpLbs request manager
46+
47+ when (Client.responseStatus innerResponse /= status201) $
48+ left ("HTTP POST request failed with status: " ++ show (Client.responseStatus innerResponse))
49+
50+ maybe (left "HTTP POST request returned invalid json") return $
51+ decode' (Client.responseBody innerResponse)
src/Servant/API/RQBody.hsnew file+40
@@ -0,0 +1,40 @@
1+{-# LANGUAGE PolyKinds #-}
2+{-# LANGUAGE TypeFamilies #-}
3+{-# LANGUAGE TypeOperators #-}
4+{-# LANGUAGE FlexibleInstances #-}
5+{-# LANGUAGE ScopedTypeVariables #-}
6+module Servant.API.RQBody where
7+
8+import Control.Applicative
9+import Data.Aeson
10+import Data.Proxy
11+import Data.Text
12+import Network.Wai
13+import Servant.API.Sub
14+import Servant.Client
15+import Servant.Server
16+
17+-- * Request Body support
18+data RQBody a
19+
20+instance (FromJSON a, HasServer sublayout)
21+ => HasServer (RQBody a :> sublayout) where
22+
23+ type Server (RQBody a :> sublayout) =
24+ a -> Server sublayout
25+
26+ route Proxy subserver request = do
27+ mrqbody <- decode' <$> lazyRequestBody request
28+ case mrqbody of
29+ Nothing -> return Nothing
30+ Just v -> route (Proxy :: Proxy sublayout) (subserver v) request
31+
32+instance (ToJSON a, HasClient sublayout)
33+ => HasClient (RQBody a :> sublayout) where
34+
35+ type Client (RQBody a :> sublayout) =
36+ a -> Client sublayout
37+
38+ clientWithRoute Proxy req body =
39+ clientWithRoute (Proxy :: Proxy sublayout) $
40+ setRQBody (encode body) req
src/Servant/API/Sub.hsnew file+39
@@ -0,0 +1,39 @@
1+{-# LANGUAGE PolyKinds #-}
2+{-# LANGUAGE TypeFamilies #-}
3+{-# LANGUAGE TypeOperators #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
5+module Servant.API.Sub where
6+
7+import Data.Proxy
8+import Data.String.Conversions
9+import GHC.TypeLits
10+import Network.Wai
11+import Servant.Client
12+import Servant.Server
13+
14+-- | The contained API (second argument) can be found under @("/" ++ path)@
15+-- (path being the first argument).
16+data (path :: k) :> a = Proxy path :> a
17+infixr 9 :>
18+
19+instance (KnownSymbol path, HasServer sublayout) => HasServer (path :> sublayout) where
20+ type Server (path :> sublayout) = Server sublayout
21+ route Proxy subserver request = case pathInfo request of
22+ (first : rest)
23+ | first == cs (symbolVal proxyPath)
24+ -> route (Proxy :: Proxy sublayout) subserver request{
25+ pathInfo = rest
26+ }
27+ _ -> return Nothing
28+
29+ where proxyPath = Proxy :: Proxy path
30+
31+instance (KnownSymbol path, HasClient sublayout) => HasClient (path :> sublayout) where
32+ type Client (path :> sublayout) = Client sublayout
33+
34+ clientWithRoute Proxy req =
35+ clientWithRoute (Proxy :: Proxy sublayout) $
36+ appendToPath p req
37+
38+ where p = symbolVal (Proxy :: Proxy path)
39+
src/Servant/API/Union.hsnew file+26
@@ -0,0 +1,26 @@
1+{-# LANGUAGE TypeFamilies #-}
2+{-# LANGUAGE TypeOperators #-}
3+{-# LANGUAGE ScopedTypeVariables #-}
4+module Servant.API.Union where
5+
6+import Data.Proxy
7+import Servant.Client
8+import Servant.Server
9+
10+-- | Union of two APIs, first takes precedence in case of overlap.
11+data a :<|> b = a :<|> b
12+infixr 8 :<|>
13+
14+instance (HasServer a, HasServer b) => HasServer (a :<|> b) where
15+ type Server (a :<|> b) = Server a :<|> Server b
16+ route Proxy (a :<|> b) request = do
17+ m <- route (Proxy :: Proxy a) a request
18+ case m of
19+ Nothing -> route (Proxy :: Proxy b) b request
20+ Just response -> return $ Just response
21+
22+instance (HasClient a, HasClient b) => HasClient (a :<|> b) where
23+ type Client (a :<|> b) = Client a :<|> Client b
24+ clientWithRoute Proxy req =
25+ clientWithRoute (Proxy :: Proxy a) req :<|>
26+ clientWithRoute (Proxy :: Proxy b) req
src/Servant/Client.hsnew file+66
@@ -0,0 +1,66 @@
1+{-# LANGUAGE TypeFamilies #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Servant.Client where
4+
5+import Control.Concurrent
6+import Control.Monad.Catch
7+import Data.ByteString.Lazy
8+import Data.Proxy
9+import Data.Text
10+import Network.HTTP.Client hiding (Proxy)
11+import Network.HTTP.Types
12+import Network.URI
13+import System.IO.Unsafe
14+
15+-- * Accessing APIs as a Client
16+
17+-- | 'client' allows you to produce operations to query an API from a client.
18+client :: HasClient layout => Proxy layout -> Client layout
19+client p = clientWithRoute p defReq
20+
21+class HasClient layout where
22+ type Client layout :: *
23+ clientWithRoute :: Proxy layout -> Req -> Client layout
24+
25+data Req = Req
26+ { reqPath :: String
27+ , qs :: QueryText
28+ , reqBody :: ByteString
29+ }
30+
31+defReq :: Req
32+defReq = Req "" [] ""
33+
34+appendToPath :: String -> Req -> Req
35+appendToPath p req =
36+ req { reqPath = reqPath req ++ "/" ++ p }
37+
38+appendToQueryString :: Text -- ^ param name
39+ -> Maybe Text -- ^ param value
40+ -> Req
41+ -> Req
42+appendToQueryString pname pvalue req
43+ | pvalue == Nothing = req
44+ | otherwise = req { qs = qs req ++ [(pname, pvalue)]
45+ }
46+
47+setRQBody :: ByteString -> Req -> Req
48+setRQBody b req = req { reqBody = b }
49+
50+reqToRequest :: (Functor m, MonadThrow m) => Req -> URI -> m Request
51+reqToRequest req uri = fmap (setrqb . setQS ) $ parseUrl url
52+
53+ where url = show $ nullURI { uriPath = reqPath req }
54+ `relativeTo` uri
55+
56+ setrqb r = r { requestBody = RequestBodyLBS (reqBody req) }
57+ setQS = setQueryString $ queryTextToQuery (qs req)
58+
59+{-# NOINLINE __manager #-}
60+__manager :: MVar Manager
61+__manager = unsafePerformIO (newManager defaultManagerSettings >>= newMVar)
62+
63+__withGlobalManager :: (Manager -> IO a) -> IO a
64+__withGlobalManager action = modifyMVar __manager $ \ manager -> do
65+ result <- action manager
66+ return (manager, result)
src/Servant/Server.hsnew file+27
@@ -0,0 +1,27 @@
1+{-# LANGUAGE TypeFamilies #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Servant.Server where
4+
5+import Data.Proxy
6+import Network.HTTP.Types
7+import Network.Wai
8+
9+-- * Implementing Servers
10+
11+-- | 'serve' allows you to implement an API and produce a wai 'Application'.
12+serve :: HasServer layout => Proxy layout -> Server layout -> Application
13+serve p server = toApplication (route p server)
14+
15+toApplication :: RoutingApplication -> Application
16+toApplication ra = \ request respond -> do
17+ m <- ra request
18+ case m of
19+ Nothing -> respond $ responseLBS notFound404 [] "not found"
20+ Just response -> respond response
21+
22+type RoutingApplication =
23+ Request -> IO (Maybe Response)
24+
25+class HasServer layout where
26+ type Server layout :: *
27+ route :: Proxy layout -> Server layout -> RoutingApplication
src/Servant/Text.hsnew file+25
@@ -0,0 +1,25 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Servant.Text where
3+
4+import Data.Text
5+
6+class FromText a where
7+ fromText :: Text -> Maybe a
8+
9+class ToText a where
10+ toText :: a -> Text
11+
12+instance FromText Text where
13+ fromText = Just
14+
15+instance ToText Text where
16+ toText = id
17+
18+instance FromText Bool where
19+ fromText "true" = Just True
20+ fromText "false" = Just False
21+ fromText _ = Nothing
22+
23+instance ToText Bool where
24+ toText True = "true"
25+ toText False = "false"
src/Soenke.hsdeleted−213
@@ -1,213 +0,0 @@
1−{-# LANGUAGE DataKinds, FlexibleInstances, OverloadedStrings, PolyKinds,
2− ScopedTypeVariables, TypeFamilies, TypeOperators #-}
3−module Soenke where
4−
5−import Control.Concurrent
6−import Control.Monad
7−import Control.Monad.Catch
8−import Control.Monad.IO.Class
9−import Control.Monad.Trans.Either
10−import Data.Aeson
11−import Data.ByteString.Lazy (ByteString)
12−import Data.Proxy
13−import Data.String.Conversions
14−import Data.Text (Text)
15−import GHC.TypeLits
16−import Network.HTTP.Client (Manager, defaultManagerSettings,
17− newManager)
18−import qualified Network.HTTP.Client as Http.Client
19−import Network.HTTP.Types
20−import Network.URI
21−import Network.Wai
22−import System.IO.Unsafe (unsafePerformIO)
23−
24−
25−-- * Url Layout
26−
27−-- $url_layout
28−-- These types allow you to specify a url layout for a REST API using a type
29−-- alias. The defined API doesn't really have value. It is convenient to define
30−-- a Proxy for every API.
31−--
32−-- The provided constructors are used in 'HasServer' and 'HasClient'.
33−
34−-- | Endpoint for simple GET requests. The server doesn't receive any arguments
35−-- and serves the contained type as JSON.
36−data Get a
37−
38−-- | Endpoint for POST requests.
39−data Post a
40−
41−-- | The contained API (second argument) can be found under @("/" ++ path)@
42−-- (path being the first argument).
43−data (path :: k) :> a = Proxy path :> a
44−infixr 9 :>
45−
46−-- | Union of two APIs, first takes precedence in case of overlap.
47−data a :<|> b = a :<|> b
48−infixr 8 :<|>
49−
50−
51−-- * Implementing Servers
52−
53−-- | 'serve' allows you to implement an API and produce a wai 'Application'.
54−serve :: HasServer layout => Proxy layout -> Server layout -> Application
55−serve p server = toApplication (route p server)
56−
57−toApplication :: RoutingApplication -> Application
58−toApplication ra = \ request respond -> do
59− m <- ra request
60− case m of
61− Nothing -> respond $ responseLBS notFound404 [] "not found"
62− Just response -> respond response
63−
64−type RoutingApplication =
65− Request -> IO (Maybe Response)
66−
67−class HasServer layout where
68− type Server layout :: *
69− route :: Proxy layout -> Server layout -> RoutingApplication
70−
71−instance ToJSON result => HasServer (Get result) where
72− type Server (Get result) = EitherT (Int, String) IO result
73− route Proxy action request
74− | null (pathInfo request) && requestMethod request == methodGet = do
75− e <- runEitherT action
76− return $ Just $ case e of
77− Right output ->
78− responseLBS ok200 [("Content-Type", "application/json")] (encode output)
79− Left (status, message) ->
80− responseLBS (mkStatus status (cs message)) [] (cs message)
81− | otherwise = return Nothing
82−
83−instance ToJSON a => HasServer (Post a) where
84− type Server (Post a) = EitherT (Int, String) IO a
85−
86− route Proxy action request
87− | null (pathInfo request) && requestMethod request == methodPost = do
88− e <- runEitherT action
89− return $ Just $ case e of
90− Right out ->
91− responseLBS status201 [("Content-Type", "application/json")] (encode out)
92− Left (status, message) ->
93− responseLBS (mkStatus status (cs message)) [] (cs message)
94− | otherwise = return Nothing
95−
96−instance (KnownSymbol path, HasServer sublayout) => HasServer (path :> sublayout) where
97− type Server (path :> sublayout) = Server sublayout
98− route Proxy subserver request = case pathInfo request of
99− (first : rest)
100− | first == cs (symbolVal proxyPath)
101− -> route (Proxy :: Proxy sublayout) subserver request{
102− pathInfo = rest
103− }
104− _ -> return Nothing
105−
106− where proxyPath = Proxy :: Proxy path
107−
108−instance (HasServer a, HasServer b) => HasServer (a :<|> b) where
109− type Server (a :<|> b) = Server a :<|> Server b
110− route Proxy (a :<|> b) request = do
111− m <- route (Proxy :: Proxy a) a request
112− case m of
113− Nothing -> route (Proxy :: Proxy b) b request
114− Just response -> return $ Just response
115−
116−
117−-- * Accessing APIs as a Client
118−
119−-- | 'client' allows you to produce operations to query an API from a client.
120−client :: forall layout . HasClient layout => Proxy layout -> Client layout
121−client Proxy = clientWithRoute (Proxy :: Proxy layout) defReq
122−
123−class HasClient layout where
124− type Client layout :: *
125− clientWithRoute :: Proxy layout -> Req -> Client layout
126−
127−data Req = Req
128− { reqPath :: String
129− , qs :: QueryText
130− , reqBody :: ByteString
131− }
132−
133−defReq :: Req
134−defReq = Req "" [] ""
135−
136−appendToPath :: String -> Req -> Req
137−appendToPath p req =
138− req { reqPath = reqPath req ++ "/" ++ p }
139−
140−appendToQueryString :: Text -- ^ param name
141− -> Maybe Text -- ^ param value
142− -> Req
143− -> Req
144−appendToQueryString pname pvalue req
145− | pvalue == Nothing = req
146− | otherwise = req { qs = qs req ++ [(pname, pvalue)]
147− }
148−
149−setRQBody :: ByteString -> Req -> Req
150−setRQBody b req = req { reqBody = b }
151−
152−reqToRequest :: (Functor m, MonadThrow m) => Req -> URI -> m Http.Client.Request
153−reqToRequest req uri = fmap (setrqb . setQS ) $ Http.Client.parseUrl url
154−
155− where url = show $ nullURI { uriPath = reqPath req }
156− `relativeTo` uri
157−
158− setrqb r = r { Http.Client.requestBody = Http.Client.RequestBodyLBS (reqBody req) }
159− setQS = Http.Client.setQueryString $ queryTextToQuery (qs req)
160−
161−{-# NOINLINE __manager #-}
162−__manager :: MVar Manager
163−__manager = unsafePerformIO (newManager defaultManagerSettings >>= newMVar)
164−
165−__withGlobalManager :: (Manager -> IO a) -> IO a
166−__withGlobalManager action = modifyMVar __manager $ \ manager -> do
167− result <- action manager
168− return (manager, result)
169−
170−instance FromJSON result => HasClient (Get result) where
171− type Client (Get result) = URI -> EitherT String IO result
172− clientWithRoute Proxy req uri = do
173− innerRequest <- liftIO $ reqToRequest req uri
174−
175− innerResponse <- liftIO $ __withGlobalManager $ \ manager ->
176− Http.Client.httpLbs innerRequest manager
177− when (Http.Client.responseStatus innerResponse /= ok200) $
178− left ("HTTP GET request failed with status: " ++ show (Http.Client.responseStatus innerResponse))
179− maybe (left "HTTP GET request returned invalid json") return $
180− decode' (Http.Client.responseBody innerResponse)
181−
182−instance FromJSON a => HasClient (Post a) where
183− type Client (Post a) = URI -> EitherT String IO a
184−
185− clientWithRoute Proxy req uri = do
186− partialRequest <- liftIO $ reqToRequest req uri
187−
188− let request = partialRequest { Http.Client.method = methodPost
189− }
190−
191− innerResponse <- liftIO . __withGlobalManager $ \ manager ->
192− Http.Client.httpLbs request manager
193−
194− when (Http.Client.responseStatus innerResponse /= status201) $
195− left ("HTTP POST request failed with status: " ++ show (Http.Client.responseStatus innerResponse))
196−
197− maybe (left "HTTP POST request returned invalid json") return $
198− decode' (Http.Client.responseBody innerResponse)
199−
200−instance (KnownSymbol path, HasClient sublayout) => HasClient (path :> sublayout) where
201− type Client (path :> sublayout) = Client sublayout
202−
203− clientWithRoute Proxy req =
204− clientWithRoute (Proxy :: Proxy sublayout) $
205− appendToPath p req
206−
207− where p = symbolVal (Proxy :: Proxy path)
208−
209−instance (HasClient a, HasClient b) => HasClient (a :<|> b) where
210− type Client (a :<|> b) = Client a :<|> Client b
211− clientWithRoute Proxy req =
212− clientWithRoute (Proxy :: Proxy a) req :<|>
213− clientWithRoute (Proxy :: Proxy b) req