Skip to content

Commit 3cba1bd

Browse files
committed
add library to make requests against solga endpoints
1 parent bf91768 commit 3cba1bd

File tree

7 files changed

+392
-1
lines changed

7 files changed

+392
-1
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,4 @@ script:
174174
done
175175
;;
176176
esac
177-
set +ex
177+
set +ex

solga-client/LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright (c) 2017 Francesco Mazzoli
2+
3+
Permission is hereby granted, free of charge, to any person obtaining
4+
a copy of this software and associated documentation files (the
5+
"Software"), to deal in the Software without restriction, including
6+
without limitation the rights to use, copy, modify, merge, publish,
7+
distribute, sublicense, and/or sell copies of the Software, and to
8+
permit persons to whom the Software is furnished to do so, subject to
9+
the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included
12+
in all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

solga-client/Setup.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import Distribution.Simple
2+
main = defaultMain

solga-client/solga-client.cabal

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: solga-client
2+
version: 0.1.0.0
3+
synopsis: Simple typesafe web routing
4+
description: A library for easily making requests to solga endpoints
5+
license: MIT
6+
license-file: LICENSE
7+
author: Francesco Mazzoli
8+
maintainer: [email protected]
9+
copyright: Copyright (C) 2017 Francesco Mazzoli
10+
category: Web
11+
build-type: Simple
12+
homepage: https://github.com/chpatrick/solga
13+
bug-reports: https://github.com/chpatrick/solga/issues
14+
-- extra-source-files:
15+
cabal-version: >=1.10
16+
17+
library
18+
exposed-modules: Solga.Client
19+
build-depends: base >= 4.8 && < 5,
20+
solga-core,
21+
aeson,
22+
http-client,
23+
bytestring,
24+
text,
25+
blaze-builder,
26+
http-types
27+
hs-source-dirs: src
28+
default-language: Haskell2010
29+
ghc-options: -Wall
30+
31+
test-suite solga-client-tests
32+
type: exitcode-stdio-1.0
33+
hs-source-dirs: test
34+
main-is: Test.hs
35+
ghc-options: -Wall
36+
default-language: Haskell2010
37+
build-depends: base
38+
, solga-core
39+
, solga-client
40+
, solga-router
41+
, text
42+
, wai
43+
, aeson
44+
, hspec
45+
, http-types
46+
, unordered-containers
47+
, hashable
48+
, vector
49+
, scientific
50+
, QuickCheck
51+
, http-client
52+
, warp

solga-client/src/Solga/Client.hs

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
{-# LANGUAGE TypeFamilies #-}
2+
{-# LANGUAGE MultiParamTypeClasses #-}
3+
{-# LANGUAGE FlexibleInstances #-}
4+
{-# LANGUAGE ExistentialQuantification #-}
5+
{-# LANGUAGE FunctionalDependencies #-}
6+
{-# LANGUAGE ScopedTypeVariables #-}
7+
{-# LANGUAGE GADTs #-}
8+
{-# LANGUAGE DataKinds #-}
9+
{-# LANGUAGE TypeOperators #-}
10+
{-# LANGUAGE ScopedTypeVariables #-}
11+
{-# LANGUAGE RankNTypes #-}
12+
{-# LANGUAGE TypeInType #-}
13+
{-# LANGUAGE TypeApplications #-}
14+
{-# LANGUAGE LambdaCase #-}
15+
{-# LANGUAGE OverloadedStrings #-}
16+
{-# LANGUAGE DeriveGeneric #-}
17+
{-# LANGUAGE DefaultSignatures #-}
18+
{-# LANGUAGE FlexibleContexts #-}
19+
{-# LANGUAGE UndecidableInstances #-}
20+
module Solga.Client
21+
( Client(..)
22+
, SomeRequestData(..)
23+
, choose
24+
, RawRequest(..)
25+
, ToSegment(..)
26+
, WithData(..)
27+
, GetResponse(..)
28+
) where
29+
30+
import Data.Kind
31+
import Data.Proxy
32+
import qualified Data.Aeson as Aeson
33+
import qualified Network.HTTP.Client as Http
34+
import qualified Network.HTTP.Client.MultipartFormData as Http
35+
import GHC.Generics
36+
import qualified Data.ByteString.Char8 as BSC8
37+
import GHC.TypeLits (symbolVal, KnownSymbol, Symbol)
38+
import Data.ByteString (ByteString)
39+
import qualified Data.ByteString as BS
40+
import Data.Monoid ((<>))
41+
import qualified Data.Text.Encoding as T
42+
import Data.Text (Text)
43+
import Network.HTTP.Types (urlEncodeBuilder)
44+
import qualified Blaze.ByteString.Builder as Blaze
45+
import qualified Data.ByteString.Lazy as BSL
46+
import qualified Data.Text as T
47+
48+
import Solga.Core
49+
50+
data SomeRequestData out a = forall in_. (Client in_) => SomeRequestData (Proxy in_) (RequestData in_ a)
51+
52+
class Client r where
53+
type RequestData r :: * -> *
54+
type RequestData r = SomeRequestData r
55+
performRequest :: proxy r -> Http.Request -> Http.Manager -> RequestData r a -> IO a
56+
default
57+
performRequest :: forall (proxy :: * -> *) a.
58+
(RequestData r ~ SomeRequestData r)
59+
=> proxy r -> Http.Request -> Http.Manager -> RequestData r a -> IO a
60+
performRequest _p req mgr (SomeRequestData p perf) = performRequest p req mgr perf
61+
62+
choose :: forall in_ out a.
63+
(Client in_, RequestData out ~ SomeRequestData out)
64+
=> (out -> in_) -> RequestData in_ a -> RequestData out a
65+
choose _f perf = SomeRequestData (Proxy @in_) perf
66+
67+
newtype RawRequest a = RawRequest {unRequestDataRaw :: Http.Request -> Http.Manager -> IO a}
68+
69+
instance Client (Raw a) where
70+
type RequestData (Raw a) = RawRequest
71+
performRequest _p mgr req (RawRequest f) = f mgr req
72+
73+
instance Client (RawResponse a) where
74+
type RequestData (RawResponse a) = RawRequest
75+
performRequest _p mgr req (RawRequest f) = f mgr req
76+
77+
instance (Client next) => Client (End next) where
78+
type RequestData (End next) = RequestData next
79+
performRequest _p mgr req perf = performRequest (Proxy @next) mgr req perf
80+
81+
addSegment :: Http.Request -> Text -> Http.Request
82+
addSegment req segtxt = req
83+
{ Http.path = if BS.null (Http.path req) || BSC8.last (Http.path req) == '/'
84+
then Http.path req <> seg
85+
else Http.path req <> "/" <> seg
86+
}
87+
where
88+
seg = BSL.toStrict (Blaze.toLazyByteString (urlEncodeBuilder False (T.encodeUtf8 segtxt)))
89+
90+
instance (Client next, KnownSymbol seg) => Client (Seg seg next) where
91+
type RequestData (Seg seg next) = RequestData next
92+
performRequest _p req mgr perf =
93+
performRequest (Proxy @next) (addSegment req (T.pack (symbolVal (Proxy @seg)))) mgr perf
94+
95+
instance (Client left, Client right) => Client (left :<|> right) where
96+
type RequestData (left :<|> right) = RequestData left :+: RequestData right
97+
performRequest _p mgr req = \case
98+
L1 perf -> performRequest (Proxy @left) mgr req perf
99+
R1 perf -> performRequest (Proxy @right) mgr req perf
100+
101+
data WhichSeg (segs :: [Symbol]) where
102+
ThisSeg :: KnownSymbol seg => WhichSeg (seg ': segs)
103+
ThatSeg :: WhichSeg segs -> WhichSeg (seg ': segs)
104+
105+
thisSeg :: forall seg segs. KnownSymbol seg => WhichSeg (seg ': segs) -> String
106+
thisSeg _ = symbolVal (Proxy @seg)
107+
108+
whichSeg :: WhichSeg segs -> String
109+
whichSeg ts@ThisSeg = thisSeg ts
110+
whichSeg (ThatSeg ws) = whichSeg ws
111+
112+
instance (Client next) => Client (OneOfSegs segs next) where
113+
type RequestData (OneOfSegs segs next) = WithData (WhichSeg segs) (RequestData next)
114+
performRequest _p req mgr (WithData ws perf) =
115+
performRequest (Proxy @next) (addSegment req (T.pack (whichSeg ws))) mgr perf
116+
117+
class ToSegment a where
118+
toSegment :: a -> Text
119+
120+
instance ToSegment Text where
121+
toSegment = id
122+
123+
data WithData a next b = WithData
124+
{ ardData :: a
125+
, ardNext :: next b
126+
}
127+
128+
instance (Client next, ToSegment a) => Client (Capture a next) where
129+
type RequestData (Capture a next) = WithData a (RequestData next)
130+
performRequest _p req mgr (WithData x perf) =
131+
performRequest (Proxy @next) (addSegment req (toSegment x)) mgr perf
132+
133+
instance (Client next, KnownSymbol method) => Client (Method method next) where
134+
type RequestData (Method seg next) = RequestData next
135+
performRequest _p req mgr perf = performRequest
136+
(Proxy @next) req{Http.method = BSC8.pack (symbolVal (Proxy @method))} mgr perf
137+
138+
newtype GetResponse resp a b = GetResponse {unGetResponse :: Http.Response resp -> a -> IO b}
139+
140+
instance (Aeson.FromJSON a) => Client (JSON a) where
141+
-- note that we do not decode eagerly because it's often the case that the body
142+
-- cannot be decoded since web servers return invalid json on errors
143+
-- (e.g. "Internal server error" on a 500 rather than a json encoded error)
144+
type RequestData (JSON a) = GetResponse BSL.ByteString (Either String a)
145+
performRequest _p req mgr (GetResponse f) = do
146+
resp <- Http.httpLbs req mgr
147+
let decode = Aeson.eitherDecode' (Http.responseBody resp)
148+
f resp decode
149+
150+
instance (Client next) => Client (ExtraHeaders next) where
151+
type RequestData (ExtraHeaders next) = RequestData next
152+
performRequest _p req mgr perf = performRequest (Proxy @next) req mgr perf
153+
154+
instance (Client next) => Client (NoCache next) where
155+
type RequestData (NoCache next) = RequestData next
156+
performRequest _p req mgr perf = performRequest (Proxy @next) req mgr perf
157+
158+
instance (Client next, Aeson.ToJSON a) => Client (ReqBodyJSON a next) where
159+
type RequestData (ReqBodyJSON a next) = WithData a (RequestData next)
160+
performRequest _p req mgr (WithData x perf) = performRequest
161+
(Proxy @next) req{Http.requestBody = Http.RequestBodyLBS (Aeson.encode x)} mgr perf
162+
163+
instance (Client next) => Client (WithIO next) where
164+
type RequestData (WithIO next) = RequestData next
165+
performRequest _p req mgr perf = performRequest (Proxy @next) req mgr perf
166+
167+
instance (Client next) => Client (ReqBodyMultipart fp a next) where
168+
type
169+
RequestData (ReqBodyMultipart fp a next) =
170+
WithData ([Http.Part], Maybe ByteString) (RequestData next)
171+
performRequest _p req mgr (WithData (parts, mbBoundary) perf) = do
172+
req' <- case mbBoundary of
173+
Nothing -> Http.formDataBody parts req
174+
Just x -> Http.formDataBodyWithBoundary x parts req
175+
performRequest (Proxy @next) req' mgr perf
176+

0 commit comments

Comments
 (0)