packages feed

simfin-1.0.0: src/SimFin/Internal.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}

module SimFin.Internal
  ( SimFinContext(..)
  , QueryParam
  , createKeyedRow
  , createKeyedRows
  , toCommaQueryParam
  , toBoolQueryParam
  , toTextCommaQueryParam
  , toShownCommaQueryParam
  , baseRequest
  , makeRequest
  , performGenericRequest
  ) where

import Control.Monad.IO.Class
import Data.Aeson
import Data.Aeson.Types (parse, Parser)

#if MIN_VERSION_aeson(2,0,0)
import qualified Data.Aeson.Key as K
import qualified Data.Aeson.KeyMap as KM
#else
import qualified Data.HashMap.Strict as HM
#endif

import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as LBS
import Data.Functor.Syntax
import Data.Text (Text)
import qualified Data.Text.Encoding as T
import Network.HTTP.Client
import Network.HTTP.Types.Status

-- | The context needed to call every fetch* function.

data SimFinContext = SimFinContext
  { simFinApiKey :: ByteString
  , simFinManager :: Manager
  }

baseRequest :: Request
baseRequest = defaultRequest
  { host = "simfin.com"
  , port = 443
  , secure = True
  , requestHeaders =
    [ ("Accept", "application/json")
    ]
  }

basePath :: ByteString
basePath = "/api/v2/"

makeRequest :: ByteString -> ByteString -> [QueryParam] -> Request
makeRequest apiKey path query =
  setQueryString (("api-key", Just apiKey) : query)
  $ baseRequest { path = basePath <> path }

performGenericRequest
  :: ( MonadIO m
     , FromJSON a
     , FromJSON e
     )
  => (LBS.ByteString -> String -> e)
  -> (Value -> String -> e)
  -> SimFinContext
  -> ByteString
  -> [QueryParam]
  -> m (Either e a)
performGenericRequest mkDecodeErr mkParseErr SimFinContext{..} path query = do
  let req = makeRequest simFinApiKey path query
  res <- liftIO $ httpLbs req simFinManager
  let body = responseBody res
  -- Try to parse body into generic JSON
  pure $ case eitherDecode $ responseBody res of
    Left err -> Left $ mkDecodeErr body err
    Right value -> case statusCode $ responseStatus res of
      200 -> case parse parseJSON value of
        Error err -> Left $ mkParseErr value err
        Success a -> Right a
      _ -> case parse parseJSON value of
        Error err -> Left $ mkParseErr value err
        Success a -> Left a

type QueryParam = (ByteString, Maybe ByteString)

#if MIN_VERSION_aeson(2,0,0)

toKey :: Text -> Key
toKey = K.fromText

toObject :: [(K.Key, Value)] -> Value
toObject = Object . KM.fromList

#else

toKey :: Text -> Text
toKey = id

toObject :: [(Text, Value)] -> Value
toObject = Object . HM.fromList 

#endif

createKeyedRow :: Value -> Parser Value
createKeyedRow = withObject "Root" $ \root -> do
  cols <- toKey <$$> root .: "columns"
  row <- root .: "data"
  pure $ toObject $ zip cols row

createKeyedRows :: Value -> Parser [Value]
createKeyedRows = withObject "Root" $ \root -> do
  cols <- toKey <$$> root .: "columns"
  rows <- root .: "data"
  pure $ toObject . zip cols <$> rows

toCommaQueryParam :: ByteString -> (a -> ByteString) -> [a] -> [QueryParam]
toCommaQueryParam key f as = case as of
  [] -> []
  _ -> [(key, Just $ BS8.intercalate "," $ f <$> as)]

-- Chars are truncated to 8-bits
toShownCommaQueryParam :: Show a => ByteString -> [a] -> [QueryParam]
toShownCommaQueryParam key = toCommaQueryParam key (BS8.pack . show)

toTextCommaQueryParam :: ByteString -> [Text] -> [QueryParam]
toTextCommaQueryParam key = toCommaQueryParam key T.encodeUtf8

toBoolQueryParam :: ByteString -> Bool -> [QueryParam]
toBoolQueryParam key b = case b of
  False -> []
  True -> [(key, Nothing)]