packages feed

simfin-1.0.0: src/SimFin/Common.hs

{-|
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 (DecodeError _ err) = "Couldn't decode reponse body. Err: " <> err
  show (ParseError _ err) = "Couldn't parse JSON value. Err: " <> err
  show (Other err) = "Server returned error: " <> T.unpack err

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

type ApiResult = Either ApiError

instance FromJSON ApiError where
  parseJSON = withObject "Root" $ \v -> Other <$> v .: "error"

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

performRequest
  :: ( MonadIO m
     , FromJSON a
     )
  => SimFinContext
  -> ByteString
  -> [QueryParam]
  -> m (ApiResult a)
performRequest = performGenericRequest DecodeError 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 ctx =
  fmap unKeyCompanyListing <$> performRequest ctx "companies/list" []