Skip to content

Commit cdf584a

Browse files
authored
[TSD-71] Generalize HTTP functions to a layer. (#36)
1 parent 003411c commit cdf584a

File tree

5 files changed

+215
-104
lines changed

5 files changed

+215
-104
lines changed

log-classifier.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ library
3131
DataSource.DB
3232
DataSource.Types
3333
DataSource.Http
34+
HttpLayer
3435
ghc-options: -Wall
3536
build-depends: aeson
3637
, array

src/DataSource.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Universum
1212
import DataSource.DB
1313
import DataSource.Http
1414
import DataSource.Types
15+
import HttpLayer (basicHTTPNetworkLayer)
1516

1617

1718
-- | The default configuration.
@@ -24,8 +25,9 @@ defaultConfig = Config
2425
, cfgAssignTo = 0
2526
, cfgKnowledgebase = []
2627
, cfgNumOfLogsToAnalyze = 5
27-
, cfgIsCommentPublic = True -- TODO(ks): For now, we need this in CLI.
28+
, cfgIsCommentPublic = False -- TODO(ks): For now, we need this in CLI.
2829
, cfgZendeskLayer = basicZendeskLayer
30+
, cfgHTTPNetworkLayer = basicHTTPNetworkLayer
2931
, cfgIOLayer = basicIOLayer
3032
, cfgDBLayer = connDBLayer
3133
}

src/DataSource/Http.hs

Lines changed: 75 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,27 @@ import Universum
1313
import Control.Concurrent (threadDelay)
1414
import Control.Monad.Reader (ask)
1515

16-
import Data.Aeson (FromJSON, ToJSON, Value, encode, parseJSON)
16+
import Data.Aeson (parseJSON)
1717
import Data.Aeson.Text (encodeToLazyText)
18-
import Data.Aeson.Types (Parser, parseEither)
1918
import Data.List (nub)
20-
import Network.HTTP.Simple (Request, addRequestHeader, getResponseBody, httpJSON, httpLBS,
21-
parseRequest_, setRequestBasicAuth, setRequestBodyJSON,
22-
setRequestMethod, setRequestPath)
19+
import Network.HTTP.Simple (Request, getResponseBody, httpLBS, parseRequest_)
20+
21+
import HttpLayer
2322

2423
import DataSource.Types (Attachment (..), AttachmentContent (..), Comment (..),
2524
CommentBody (..), CommentId (..), Config (..),
2625
DeletedTicket (..), ExportFromTime (..), FromPageResultList (..),
2726
PageResultList (..), Ticket (..), TicketId (..), TicketInfo (..),
2827
TicketTag (..), TicketTags (..), User, UserId (..),
2928
ZendeskAPIUrl (..), ZendeskLayer (..), ZendeskResponse (..),
30-
parseComments, parseTicket, renderTicketStatus, showURL)
29+
asksHTTPNetworkLayer, parseComments, renderTicketStatus, showURL)
3130

3231
-- ./mitmproxy --mode reverse:https://iohk.zendesk.com -p 4001
3332

33+
------------------------------------------------------------
34+
-- Zendesk layer
35+
------------------------------------------------------------
36+
3437
-- | The basic Zendesk layer.
3538
-- The convention:
3639
-- - get returns a single result (wrapped in @Maybe@)
@@ -65,6 +68,10 @@ emptyZendeskLayer = ZendeskLayer
6568
, zlExportTickets = \_ -> error "Not implemented zlExportTickets!"
6669
}
6770

71+
------------------------------------------------------------
72+
-- Zendesk functions
73+
------------------------------------------------------------
74+
6875
-- | Get single ticket info.
6976
getTicketInfo
7077
:: (MonadIO m, MonadReader Config m)
@@ -75,7 +82,10 @@ getTicketInfo ticketId = do
7582

7683
let url = showURL $ TicketsURL ticketId
7784
let req = apiRequest cfg url
78-
liftIO $ Just <$> apiCall parseTicket req
85+
86+
apiCall <- asksHTTPNetworkLayer hnlApiCall
87+
88+
Just <$> apiCall parseJSON req
7989

8090
-- | Return list of deleted tickets.
8191
listDeletedTickets
@@ -146,23 +156,28 @@ getExportedTickets
146156
-> m [TicketInfo]
147157
getExportedTickets time = do
148158
cfg <- ask
159+
160+
apiCall <- asksHTTPNetworkLayer hnlApiCall
161+
149162
let url = showURL $ ExportDataByTimestamp time
150163
let req = apiRequestAbsolute cfg url
151-
iterateExportedTicketsWithDelay req
164+
165+
iterateExportedTicketsWithDelay req (apiCall parseJSON)
152166
where
153167

154168
iterateExportedTicketsWithDelay
155169
:: Request
170+
-> (Request -> m (PageResultList TicketInfo))
156171
-> m [TicketInfo]
157-
iterateExportedTicketsWithDelay req = do
172+
iterateExportedTicketsWithDelay req apiCall = do
158173
cfg <- ask
159174

160-
let go :: [TicketInfo] -> Text -> IO [TicketInfo]
175+
let go :: [TicketInfo] -> Text -> m [TicketInfo]
161176
go list' nextPage' = do
162-
threadDelay $ 10 * 1000000 -- Wait, Zendesk allows for 10 per minute.
177+
liftIO $ threadDelay $ 10 * 1000000 -- Wait, Zendesk allows for 10 per minute.
163178

164179
let req' = apiRequestAbsolute cfg nextPage'
165-
(PageResultList pagen nextPagen count) <- apiCall parseJSON req'
180+
(PageResultList pagen nextPagen count) <- apiCall req'
166181
case nextPagen of
167182
Just nextUrl -> if maybe False (>= 1000) count
168183
then go (list' <> pagen) nextUrl
@@ -171,45 +186,11 @@ getExportedTickets time = do
171186
Nothing -> pure (list' <> pagen)
172187

173188

174-
(PageResultList page0 nextPage _) <- liftIO $ apiCall parseJSON req
189+
(PageResultList page0 nextPage _) <- apiCall req
175190
case nextPage of
176-
Just nextUrl -> liftIO $ go page0 nextUrl
191+
Just nextUrl -> go page0 nextUrl
177192
Nothing -> pure page0
178193

179-
-- | Iterate all the ticket pages and combine into a result.
180-
iteratePages
181-
:: forall m a. (MonadIO m, MonadReader Config m, FromPageResultList a)
182-
=> Request
183-
-> m [a]
184-
iteratePages req = iteratePagesWithDelay 0 req
185-
186-
-- | Iterate all the ticket pages and combine into a result. Wait for
187-
-- some time in-between the requests.
188-
iteratePagesWithDelay
189-
:: forall m a. (MonadIO m, MonadReader Config m, FromPageResultList a)
190-
=> Int
191-
-> Request
192-
-> m [a]
193-
iteratePagesWithDelay seconds req = do
194-
cfg <- ask
195-
196-
let go :: [a] -> Text -> IO [a]
197-
go list' nextPage' = do
198-
-- Wait for @Int@ seconds.
199-
threadDelay $ seconds * 1000000
200-
201-
let req' = apiRequestAbsolute cfg nextPage'
202-
(PageResultList pagen nextPagen _) <- apiCall parseJSON req'
203-
case nextPagen of
204-
Just nextUrl -> go (list' <> pagen) nextUrl
205-
Nothing -> pure (list' <> pagen)
206-
207-
(PageResultList page0 nextPage _) <- liftIO $ apiCall parseJSON req
208-
case nextPage of
209-
Just nextUrl -> liftIO $ go page0 nextUrl
210-
Nothing -> pure page0
211-
212-
213194
-- | Send API request to post comment
214195
postTicketComment
215196
:: (MonadIO m, MonadReader Config m)
@@ -218,10 +199,14 @@ postTicketComment
218199
-> m ()
219200
postTicketComment ticketInfo zendeskResponse = do
220201
cfg <- ask
202+
203+
addJsonBody <- asksHTTPNetworkLayer hnlAddJsonBody
204+
apiCall <- asksHTTPNetworkLayer hnlApiCall
205+
221206
let responseTicket = createResponseTicket (cfgAgentId cfg) ticketInfo zendeskResponse
222207
let url = showURL $ TicketsURL (zrTicketId zendeskResponse)
223208
let req = addJsonBody responseTicket (apiRequest cfg url)
224-
void $ liftIO $ apiCall (pure . encodeToLazyText) req
209+
void $ apiCall (pure . encodeToLazyText) req
225210

226211
-- | Create response ticket
227212
createResponseTicket :: Integer -> TicketInfo -> ZendeskResponse -> Ticket
@@ -248,10 +233,12 @@ _getUser
248233
_getUser = do
249234
cfg <- ask
250235

236+
apiCall <- asksHTTPNetworkLayer hnlApiCall
237+
251238
let url = showURL UserInfoURL
252239
let req = apiRequest cfg url
253240

254-
liftIO $ apiCall parseJSON req
241+
apiCall parseJSON req
255242

256243
-- | Given attachmentUrl, return attachment in bytestring
257244
getAttachment
@@ -271,10 +258,12 @@ getTicketComments
271258
getTicketComments tId = do
272259
cfg <- ask
273260

261+
apiCallSafe <- asksHTTPNetworkLayer hnlApiCallSafe
262+
274263
let url = showURL $ TicketCommentsURL tId
275264
let req = apiRequest cfg url
276265

277-
result <- apiCallSafe parseComments req
266+
result <- apiCallSafe parseComments req
278267

279268
-- TODO(ks): For now return empty if there is an exception.
280269
-- After we have exception handling, we propagate this up.
@@ -283,51 +272,41 @@ getTicketComments tId = do
283272
Right r -> pure r
284273

285274
------------------------------------------------------------
286-
-- HTTP utility
275+
-- Utility
287276
------------------------------------------------------------
288277

289-
-- | Request PUT
290-
addJsonBody :: ToJSON a => a -> Request -> Request
291-
addJsonBody body req = setRequestBodyJSON body $ setRequestMethod "PUT" req
292-
293-
-- | Make an api call
294-
-- TODO(ks): Switch to @Either@.
295-
apiCall :: FromJSON a => (Value -> Parser a) -> Request -> IO a
296-
apiCall parser req = do
297-
putTextLn $ show req
298-
v <- getResponseBody <$> httpJSON req
299-
case parseEither parser v of
300-
Right o -> pure o
301-
Left e -> error $ "couldn't parse response "
302-
<> toText e <> "\n" <> decodeUtf8 (encode v)
303-
304-
-- | Make a safe api call.
305-
apiCallSafe
306-
:: forall m a. (MonadIO m, FromJSON a)
307-
=> (Value -> Parser a)
278+
-- | Iterate all the ticket pages and combine into a result.
279+
iteratePages
280+
:: forall m a. (MonadIO m, MonadReader Config m, FromPageResultList a)
281+
=> Request
282+
-> m [a]
283+
iteratePages req = iteratePagesWithDelay 0 req
284+
285+
-- | Iterate all the ticket pages and combine into a result. Wait for
286+
-- some time in-between the requests.
287+
iteratePagesWithDelay
288+
:: forall m a. (MonadIO m, MonadReader Config m, FromPageResultList a)
289+
=> Int
308290
-> Request
309-
-> m (Either String a)
310-
apiCallSafe parser req = do
311-
putTextLn $ show req
312-
v <- getResponseBody <$> httpJSON req
313-
pure $ parseEither parser v
314-
315-
-- | General api request function
316-
apiRequest :: Config -> Text -> Request
317-
apiRequest Config{..} u = setRequestPath (encodeUtf8 path) $
318-
addRequestHeader "Content-Type" "application/json" $
319-
setRequestBasicAuth
320-
(encodeUtf8 cfgEmail <> "/token")
321-
(encodeUtf8 cfgToken) $
322-
parseRequest_ (toString (cfgZendesk <> path))
323-
where
324-
path :: Text
325-
path = "/api/v2" <> u
326-
327-
-- | Api request but use absolute path
328-
apiRequestAbsolute :: Config -> Text -> Request
329-
apiRequestAbsolute Config{..} u = addRequestHeader "Content-Type" "application/json" $
330-
setRequestBasicAuth
331-
(encodeUtf8 cfgEmail <> "/token")
332-
(encodeUtf8 cfgToken) $
333-
parseRequest_ (toString u)
291+
-> m [a]
292+
iteratePagesWithDelay seconds req = do
293+
cfg <- ask
294+
295+
apiCall <- asksHTTPNetworkLayer hnlApiCall
296+
297+
let go :: [a] -> Text -> IO [a]
298+
go list' nextPage' = do
299+
-- Wait for @Int@ seconds.
300+
threadDelay $ seconds * 1000000
301+
302+
let req' = apiRequestAbsolute cfg nextPage'
303+
(PageResultList pagen nextPagen _) <- apiCall parseJSON req'
304+
case nextPagen of
305+
Just nextUrl -> go (list' <> pagen) nextUrl
306+
Nothing -> pure (list' <> pagen)
307+
308+
(PageResultList page0 nextPage _) <- liftIO $ apiCall parseJSON req
309+
case nextPage of
310+
Just nextUrl -> liftIO $ go page0 nextUrl
311+
Nothing -> pure page0
312+

0 commit comments

Comments
 (0)