{-|
Module      : SimFin.Types.Common
Description : Types and functions common to the free and paid APIs.
Copyright   : (c) Owen Shepherd, 2022
License     : MIT
Maintainer  : owen@owen.cafe
-}

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}

module SimFin.Common
  ( ApiError(..)
  , ApiResult
  , fetchCompanyList
  , performRequest
  ) where

import Control.Monad.Catch
import Control.Monad.IO.Class
import Data.Aeson
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as LBS
import Data.Text (Text)
import qualified Data.Text as T

import SimFin.Internal
import SimFin.Types.CompanyListing

-- | Represents all the types of error the server returns, and that we can encounter
-- on our side.

data ApiError
  -- | Can't turn ByteString into JSON
  = DecodeError LBS.ByteString String
  -- | Can't turn JSON into result type
  | ParseError Value String
  -- | Server returned '{"error": "..."}' along with a non-200 status code.
  -- This could in theory be parsed into machine-readable format,
  -- with variants such as `InvalidApiKey | RateLimited | ...`, but 
  -- the API doesn't guarantee error message stability.
  | Other Text

instance Show ApiError where
  show :: ApiError -> String
show (DecodeError ByteString
_ String
err) = String
"Couldn't decode reponse body. Err: " String -> ShowS
forall a. Semigroup a => a -> a -> a
<> String
err
  show (ParseError Value
_ String
err) = String
"Couldn't parse JSON value. Err: " String -> ShowS
forall a. Semigroup a => a -> a -> a
<> String
err
  show (Other Text
err) = String
"Server returned error: " String -> ShowS
forall a. Semigroup a => a -> a -> a
<> Text -> String
T.unpack Text
err

-- | The result of calling fetch* is either an error or a successful result.

type ApiResult = Either ApiError

instance FromJSON ApiError where
  parseJSON :: Value -> Parser ApiError
parseJSON = String -> (Object -> Parser ApiError) -> Value -> Parser ApiError
forall a. String -> (Object -> Parser a) -> Value -> Parser a
withObject String
"Root" ((Object -> Parser ApiError) -> Value -> Parser ApiError)
-> (Object -> Parser ApiError) -> Value -> Parser ApiError
forall a b. (a -> b) -> a -> b
$ \Object
v -> Text -> ApiError
Other (Text -> ApiError) -> Parser Text -> Parser ApiError
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Object
v Object -> Text -> Parser Text
forall a. FromJSON a => Object -> Text -> Parser a
.: Text
"error"

-- | Make a request, all fetch* functions call this.

performRequest
  :: ( MonadIO m
     , FromJSON a
     )
  => SimFinContext
  -> ByteString
  -> [QueryParam]
  -> m (ApiResult a)
performRequest :: SimFinContext -> ByteString -> [QueryParam] -> m (ApiResult a)
performRequest = (ByteString -> String -> ApiError)
-> (Value -> String -> ApiError)
-> SimFinContext
-> ByteString
-> [QueryParam]
-> m (ApiResult a)
forall (m :: * -> *) a e.
(MonadIO m, FromJSON a, FromJSON e) =>
(ByteString -> String -> e)
-> (Value -> String -> e)
-> SimFinContext
-> ByteString
-> [QueryParam]
-> m (Either e a)
performGenericRequest ByteString -> String -> ApiError
DecodeError Value -> String -> ApiError
ParseError


------
-- List companies
------

-- | Fetch a list of company tickers and SimFin ids.
-- This is the only endpoint common to free and paid customers.

fetchCompanyList
  :: (MonadThrow m, MonadIO m)
  => SimFinContext
  -> m (Either ApiError [CompanyListingRow])
fetchCompanyList :: SimFinContext -> m (Either ApiError [CompanyListingRow])
fetchCompanyList SimFinContext
ctx =
  (CompanyListingKeyed -> [CompanyListingRow])
-> Either ApiError CompanyListingKeyed
-> Either ApiError [CompanyListingRow]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap CompanyListingKeyed -> [CompanyListingRow]
unKeyCompanyListing (Either ApiError CompanyListingKeyed
 -> Either ApiError [CompanyListingRow])
-> m (Either ApiError CompanyListingKeyed)
-> m (Either ApiError [CompanyListingRow])
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> SimFinContext
-> ByteString
-> [QueryParam]
-> m (Either ApiError CompanyListingKeyed)
forall (m :: * -> *) a.
(MonadIO m, FromJSON a) =>
SimFinContext -> ByteString -> [QueryParam] -> m (ApiResult a)
performRequest SimFinContext
ctx ByteString
"companies/list" []