packages feed

flashblast-0.0.7.0: src/FlashBlast/ForvoClient.hs

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DuplicateRecordFields #-}
module FlashBlast.ForvoClient where

import Data.Aeson
import Polysemy
import Polysemy.Error
import Polysemy.Input
import RIO hiding (fromException, try)
import qualified RIO.ByteString.Lazy as LBS
import qualified RIO.ByteString as BS
import Polysemy.Http as Http

newtype Locale = Locale Text
  deriving (Eq, Show, Generic, Ord)

instance ToJSON Locale
instance FromJSON Locale

newtype ForvoStandardPronunciationResponseBody = ForvoStandardPronunciationResponseBody {
  items :: [ForvoPronunciationJson]
} deriving (Eq, Show, Generic)

instance FromJSON ForvoStandardPronunciationResponseBody
instance ToJSON ForvoStandardPronunciationResponseBody

data ForvoPronunciationJson = ForvoPronunciationJson {
  id :: Int
, word :: Text
, original :: Text
, hits :: Int
, username:: Text
, sex :: Text
, country:: Text
, code :: Text
, langname :: Text
, pathmp3 ::Text
, pathogg :: Text
, rate :: Int
, num_votes :: Int
, num_positive_votes :: Int
} deriving (Eq, Show, Generic)

instance FromJSON ForvoPronunciationJson
instance ToJSON ForvoPronunciationJson

data ForvoAttributeCount = ForvoAttributeCount {
  total :: Int
} deriving (Eq, Show, Generic)

instance FromJSON ForvoAttributeCount

data ForvoLanguageListResponseBody = ForvoLanguageListResponseBody {
  attributes :: ForvoAttributeCount
, items :: [ForvoLanguageCode]
} deriving (Eq, Show, Generic)

instance FromJSON ForvoLanguageListResponseBody

data ForvoLanguageCode = ForvoLanguageCode {
  code :: Text
, language :: Text
} deriving (Eq, Show, Generic)

instance FromJSON ForvoLanguageCode

newtype MP3Url = MP3Url Text
  deriving (Eq, Show, Generic)

class HasMP3Url x where
  mp3Url :: Lens' x MP3Url

instance HasMP3Url ForvoPronunciationJson where
  mp3Url = lens (MP3Url . pathmp3) undefined

newtype OggUrl = OggUrl Text
  deriving (Eq, Show, Generic)

class HasOggUrl x where
  oggUrl :: Lens' x OggUrl

instance HasOggUrl ForvoPronunciationJson where
  oggUrl = lens (OggUrl . pathogg) undefined

data ForvoClient m a where
  StandardPronunciation :: Locale -> Text -> ForvoClient m ForvoStandardPronunciationResponseBody
  LanguageList          :: ForvoClient m ForvoLanguageListResponseBody
  MP3For                :: HasMP3Url x => x -> ForvoClient m ByteString
  OggFor                :: HasOggUrl x => x -> ForvoClient m ByteString

makeSem ''ForvoClient

newtype ForvoAPIKey = ForvoAPIKey Text
  deriving (Eq, Show, Generic)

data ForvoLimitReachedException = ForvoLimitReachedException
  deriving (Eq, Show, Generic)

data ForvoLimitText = ForvoLimitText

instance FromJSON ForvoLimitText where
  parseJSON = withText "ForvoLimitText" $ \case
    "Limit/day reached." -> return ForvoLimitText
    _ -> fail "Unknown response."

instance Exception ForvoLimitReachedException where
  displayException = show

data ForvoLimitResponse = ForvoLimitResponse [ForvoLimitText]
  deriving Generic

instance FromJSON ForvoLimitResponse

data ForvoAPIKeyIncorrectException = ForvoAPIKeyIncorrectException
  deriving (Eq, Show, Generic)

instance Exception ForvoAPIKeyIncorrectException where
  displayException = show

data ForvoIncorrectDomainText = ForvoIncorrectDomainText
  deriving (Eq, Show, Generic)

instance FromJSON ForvoIncorrectDomainText where
  parseJSON = withText "ForvoIncorrectDomainText" $ \case
    "Calling from incorrect domain." -> return ForvoIncorrectDomainText
    _ -> fail "Unknown response."

data ForvoIncorrectDomainResponse = ForvoIncorrectDomainResponse [ForvoIncorrectDomainText]
  deriving Generic

instance FromJSON ForvoIncorrectDomainResponse

data ForvoResponseNotUnderstood = ForvoResponseNotUnderstood ByteString
  deriving (Show, Eq, Generic)

instance Exception ForvoResponseNotUnderstood where
  displayException (ForvoResponseNotUnderstood x) = show x

throwIfLimitReached :: Member (Error ForvoLimitReachedException) r => ByteString -> Sem r ByteString
throwIfLimitReached z = let (z' :: Either String ForvoLimitResponse) = eitherDecodeStrict z
                        in case z' of
                          Left _  -> return z
                          Right _ -> throw ForvoLimitReachedException

throwIfIncorrectDomain :: Member (Error ForvoAPIKeyIncorrectException) r => ByteString -> Sem r ByteString
throwIfIncorrectDomain z = let (z' :: Either String ForvoIncorrectDomainResponse) = eitherDecodeStrict z
                        in case z' of
                          Left _  -> return z
                          Right _ -> throw ForvoAPIKeyIncorrectException

validate :: forall r. Members [Error ForvoLimitReachedException
                             , Error ForvoAPIKeyIncorrectException
                             , Error HttpError] r
          => Either HttpError (Response LBS.ByteString) -> Sem r ByteString
validate z = do
        z' <- either throw (return . LBS.toStrict . body) z
        _ <- throwIfLimitReached z'
        _ <- throwIfIncorrectDomain z'
        return z'

analyse :: forall a r. (FromJSON a,
           Members '[Error ForvoResponseNotUnderstood] r)
        => BS.ByteString -> Sem r a
analyse z = do
        either
          (const $ throw @ForvoResponseNotUnderstood . ForvoResponseNotUnderstood $ z)
          return
          (eitherDecodeStrict z :: Either String a)

runForvoClient :: Members '[ Input ForvoAPIKey
                           , Error ForvoLimitReachedException
                           , Error ForvoAPIKeyIncorrectException
                           , Error HttpError
                           , Error ForvoResponseNotUnderstood
                           , Http (IO ByteString)] r => Sem (ForvoClient ': r) a -> Sem r a
runForvoClient = interpret \case
  StandardPronunciation (Locale l) t -> do
    ForvoAPIKey f <- input @ForvoAPIKey
    z <- Http.request (Http.get "apifree.forvo.com" $ Path $ "key/" <> f <> "/format/json/action/standard-pronunciation/word/" <> t <> "/language/" <> l)
    validate z >>= analyse
  LanguageList -> do
    ForvoAPIKey f <- input @ForvoAPIKey
    z <- Http.request (Http.get "apifree.forvo.com" $ Path $ "key/" <> f <> "/format/json/action/language-list/order/name")
    validate z >>= analyse
  MP3For x -> let (MP3Url x') = view mp3Url x
              in do
                let Right k = getUrl x'
                request k >>= validate
  OggFor x -> let (OggUrl x') = view oggUrl x
              in do
                let Right k = getUrl x'
                request k >>= validate