giphy-api-0.5.2.0: src/Web/Giphy.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE KindSignatures #-}
-- |
-- Provides a Giphy monad that can be used to issue selected API calls under a
-- selected API key.
--
-- @
-- import qualified Web.Giphy as Giphy
--
-- let apiKey = Giphy.'Key' "dc6zaTOxFJmzC"
-- let config = Giphy.'GiphyConfig' apiKey
-- resp <- Giphy.'runGiphy' (Giphy.'search' $ Giphy.'query' "puppies") config
-- let fstUrl = resp ^? _Right
-- . Giphy.'searchItems'
-- . _head
-- . Giphy.'gifImages'
-- . at "original"
-- . traverse
-- . Giphy.'imageUrl'
-- . traverse
-- print fstUrl
-- @
module Web.Giphy
(
-- * Request Data Types
-- $request
Key(..)
, Query(..)
, Phrase(..)
, Tag(..)
, Pagination(..)
, GifId(..)
-- * Response Data Types
-- $response
, Gif(..)
, Image(..)
, ImageMap()
, PaginationOffset(..)
, SearchResponse(..)
, SingleGifResponse(..)
, TranslateResponse(..)
, RandomResponse(..)
-- * Giphy Monad
-- $giphy
, GiphyConfig(..)
, Giphy()
, runGiphy
, runGiphy'
-- * Lenses
-- $lenses
, gifId
, gifImages
, gifSlug
, gifUrl
, imageHeight
, imageUrl
, imageSize
, imageWidth
, imageMp4Url
, imageMp4Size
, imageWebpUrl
, imageWebpSize
, paginationCount
, paginationOffset
, paginationTotalCount
, searchItems
, searchPagination
, singleGifItem
, translateItem
, randomGifItem
-- * API calls
-- $api
, search
, searchOffset
, translate
, gif
, random
) where
import Control.Monad (MonadPlus (), forM, join, mzero)
import Control.Monad.Trans (MonadIO (), lift, liftIO)
import Data.Aeson ((.:), (.:?))
import Data.Functor ((<$>))
import Data.Monoid ((<>))
import GHC.Generics (Generic ())
import Network.HTTP.Client.TLS (tlsManagerSettings)
import Servant.API ((:<|>) (..), (:>))
import qualified Control.Monad.Reader as Reader
import qualified Data.Aeson.Types as Aeson
import qualified Data.Map.Strict as Map
import qualified Data.Proxy as Proxy
import qualified Data.Text as T
import qualified Lens.Micro.TH as Lens
import qualified Network.HTTP.Client as HTTP
import qualified Network.URI as URI
import qualified Servant.API as Servant
import qualified Servant.Client as Servant
import qualified Text.Read as Read
import qualified Web.HttpApiData as Data
maybeParse :: MonadPlus m => (a -> Maybe b) -> m a -> m b
maybeParse f = (maybe mzero return . f =<<)
fromURI :: MonadPlus m => m String -> m Servant.URI
fromURI = maybeParse URI.parseURI
fromInt :: MonadPlus m => m String -> m Int
fromInt = maybeParse Read.readMaybe
-- $request
--
-- These data types are used to encapsulate otherwise weakly
-- typed arguments.
-- | The API Key. See https://github.com/Giphy/GiphyAPI
newtype Key = Key T.Text
deriving (Data.ToHttpApiData, Data.FromHttpApiData, Show, Eq)
-- | A search query.
newtype Query = Query T.Text
deriving (Data.ToHttpApiData, Data.FromHttpApiData, Show, Eq)
-- | A phrase or term used for translation.
newtype Phrase = Phrase T.Text
deriving (Data.ToHttpApiData, Data.FromHttpApiData, Show, Eq)
-- | A tag to retrieve a random GIF for.
newtype Tag = Tag T.Text
deriving (Data.ToHttpApiData, Data.FromHttpApiData, Show, Eq)
-- | A unique gif identifier.
newtype GifId = GifId T.Text
deriving (Data.ToHttpApiData, Data.FromHttpApiData, Show, Eq)
-- | Offset for paginated requests.
newtype PaginationOffset = PaginationOffset Int
deriving (Data.ToHttpApiData, Data.FromHttpApiData, Show, Eq)
-- $response
--
-- These data types contain are the parsed JSON responses from
-- the Giphy API.
-- | An image contained in a Giphy response.
data Image = Image {
_imageUrl :: Maybe URI.URI
, _imageSize :: Maybe Int
, _imageMp4Url :: Maybe URI.URI
, _imageMp4Size :: Maybe Int
, _imageWebpUrl :: Maybe URI.URI
, _imageWebpSize :: Maybe Int
, _imageWidth :: Maybe Int
, _imageHeight :: Maybe Int
} deriving (Show, Eq, Ord, Generic)
Lens.makeLenses ''Image
parseImageJSON :: T.Text -> Aeson.Object -> Aeson.Parser Image
parseImageJSON prefix o =
let p = if T.null prefix then "" else prefix <> "_"
in Image <$> (fromURI <$> (o .:? (p <> "url")))
<*> (fromInt <$> (o .:? (p <> "size")))
<*> (fromURI <$> (o .:? (p <> "mp4")))
<*> (fromInt <$> (o .:? (p <> "mp4_size")))
<*> (fromURI <$> (o .:? (p <> "webp")))
<*> (fromInt <$> (o .:? (p <> "webp_size")))
<*> (fromInt <$> (o .:? (p <> "width")))
<*> (fromInt <$> (o .:? (p <> "height")))
instance Aeson.FromJSON Image where
parseJSON (Aeson.Object o) = parseImageJSON "" o
parseJSON _ = error "Invalid image response."
-- | Mapping from a 'T.Text' identifier to an 'Image'.
type ImageMap = Map.Map T.Text Image
-- | A search response item.
data Gif = Gif {
_gifId :: T.Text
, _gifSlug :: T.Text
, _gifUrl :: URI.URI
, _gifImages :: ImageMap
} deriving (Show, Eq, Ord, Generic)
Lens.makeLenses ''Gif
instance Aeson.FromJSON Gif where
parseJSON (Aeson.Object o) =
Gif <$> o .: "id"
<*> o .: "slug"
<*> fromURI (o .: "url")
<*> o .: "images"
parseJSON _ = error "Invalid GIF response."
-- | Metadata about pagination in a response.
data Pagination = Pagination {
_paginationTotalCount :: Int
, _paginationCount :: Int
, _paginationOffset :: Int
} deriving (Show, Eq, Ord, Generic)
Lens.makeLenses ''Pagination
instance Aeson.FromJSON Pagination where
parseJSON (Aeson.Object o) =
Pagination <$> o .: "total_count"
<*> o .: "count"
<*> o .: "offset"
parseJSON _ = error "Invalid pagination data."
-- | A collection of GIFs as part of a search response.
data SearchResponse = SearchResponse {
_searchItems :: [Gif]
, _searchPagination :: Pagination
} deriving (Show, Eq, Ord, Generic)
Lens.makeLenses ''SearchResponse
instance Aeson.FromJSON SearchResponse where
parseJSON (Aeson.Object o) =
SearchResponse <$> o .: "data"
<*> o .: "pagination"
parseJSON _ = error "Invalid search response."
-- | A single GIF as part of a translate response.
newtype TranslateResponse = TranslateResponse {
_translateItem :: Gif
} deriving (Show, Eq, Ord, Generic)
Lens.makeLenses ''TranslateResponse
instance Aeson.FromJSON TranslateResponse where
parseJSON (Aeson.Object o) =
TranslateResponse <$> o .: "data"
parseJSON _ = error "Invalid translate response."
-- | A single gif as part of a response.
newtype SingleGifResponse = SingleGifResponse {
_singleGifItem :: Gif
} deriving (Show, Eq, Ord, Generic)
Lens.makeLenses ''SingleGifResponse
instance Aeson.FromJSON SingleGifResponse where
parseJSON (Aeson.Object o) =
SingleGifResponse <$> o .: "data"
parseJSON _ = error "Invalid GIF response."
-- | A single gif as part of a response.
newtype RandomResponse = RandomResponse {
_randomGifItem :: Gif
} deriving (Show, Eq, Ord, Generic)
Lens.makeLenses ''RandomResponse
randomImageKeys :: [T.Text]
randomImageKeys = [ "fixed_height_downsampled"
, "fixed_height_small"
, "fixed_width_downsampled"
, "fixed_width_small"
, "fixed_width"
]
instance Aeson.FromJSON RandomResponse where
parseJSON (Aeson.Object o) =
RandomResponse <$> (mkGif =<< (o .: "data"))
where
mkGif :: Aeson.Object -> Aeson.Parser Gif
mkGif d =
Gif <$> d .: "id"
<*> pure ""
<*> fromURI (d .: "url")
<*> mkImageMap d
mkImageMap :: Aeson.Object -> Aeson.Parser ImageMap
mkImageMap = (Map.fromList <$>) . forM keys . uncurry . extractImage
where
dup = join (,)
keys = ("image", "original") : (dup <$> randomImageKeys)
extractImage :: Aeson.Object -> T.Text -> T.Text -> Aeson.Parser (T.Text, Image)
extractImage d key tag = (,) <$> pure tag <*> parseImageJSON key d
parseJSON _ = error "Invalid GIF response."
-- | The Giphy API
type GiphyAPI = "search"
:> Servant.QueryParam "api_key" Key
:> Servant.QueryParam "offset" PaginationOffset
:> Servant.QueryParam "q" Query
:> Servant.Get '[Servant.JSON] SearchResponse
:<|> "translate"
:> Servant.QueryParam "api_key" Key
:> Servant.QueryParam "s" Phrase
:> Servant.Get '[Servant.JSON] TranslateResponse
:<|> Servant.Capture "gif_id" GifId
:> Servant.QueryParam "api_key" Key
:> Servant.Get '[Servant.JSON] SingleGifResponse
:<|> "random"
:> Servant.QueryParam "api_key" Key
:> Servant.QueryParam "tag" Tag
:> Servant.Get '[Servant.JSON] RandomResponse
api :: Proxy.Proxy GiphyAPI
api = Proxy.Proxy
baseUrl :: Servant.BaseUrl
baseUrl = Servant.BaseUrl Servant.Https "api.giphy.com" 443 "/v1/gifs/"
search'
:: Maybe Key
-> Maybe PaginationOffset
-> Maybe Query
-> Servant.ClientM SearchResponse
translate'
:: Maybe Key
-> Maybe Phrase
-> Servant.ClientM TranslateResponse
gif'
:: GifId
-> Maybe Key
-> Servant.ClientM SingleGifResponse
random'
:: Maybe Key
-> Maybe Tag
-> Servant.ClientM RandomResponse
search' :<|> translate' :<|> gif' :<|> random' = Servant.client api
-- $api
--
-- Functions that directly access the Giphy API. All these functions run in
-- the 'Giphy' monad.
--
-- | Issue a search request for the given query without specifying an offset.
-- E.g. <https://api.giphy.com/v1/gifs/search?q=funny+cat&api_key=dc6zaTOxFJmzC>
search
:: Query
-> Giphy SearchResponse
search query = do
key <- Reader.asks (configApiKey . ctxConfig)
lift $ search' (pure key) (pure $ PaginationOffset 0) (pure query)
-- | Issue a search request for the given query by specifying a
-- pagination offset.
-- E.g. <https://api.giphy.com/v1/gifs/search?q=funny+cat&api_key=dc6zaTOxFJmzC&offset=25>
searchOffset
:: Query
-> PaginationOffset -- ^ Offset as a number of items you want to skip.
-> Giphy SearchResponse
searchOffset query offset = do
key <- Reader.asks (configApiKey . ctxConfig)
lift $ search' (pure key) (pure offset) (pure query)
-- | Issue a request for a single GIF identified by its 'GifId'.
-- E.g. <https://api.giphy.com/v1/gifs/feqkVgjJpYtjy?api_key=dc6zaTOxFJmzC>
gif
:: GifId
-> Giphy SingleGifResponse
gif gifid = do
key <- Reader.asks (configApiKey . ctxConfig)
lift $ gif' gifid (pure key)
-- | Issue a translate request for a given phrase or term.
-- E.g. <https://api.giphy.com/v1/gifs/translate?s=superman&api_key=dc6zaTOxFJmzC>
translate
:: Phrase
-> Giphy TranslateResponse
translate phrase = do
key <- Reader.asks (configApiKey . ctxConfig)
lift $ translate' (pure key) (pure phrase)
-- | Issue a request for a random GIF for the given (optional) tag.
-- E.g. <https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=american+psycho>
random
:: Maybe Tag
-> Giphy RandomResponse
random tag = do
key <- Reader.asks (configApiKey . ctxConfig)
lift $ random' (pure key) tag
-- $giphy
--
-- Use 'runGiphy' to lift the 'Giphy' monad into IO.
--
-- | Contains the key to access the API.
newtype GiphyConfig = GiphyConfig { configApiKey :: Key }
deriving (Show, Eq)
-- | Internal data structure holding the config.
newtype GiphyContext = GiphyContext { ctxConfig :: GiphyConfig }
-- | The Giphy monad contains the execution context.
type Giphy = Reader.ReaderT GiphyContext Servant.ClientM
-- | You need to provide a 'GiphyConfig' to lift a 'Giphy' computation
-- into 'MonadIO'.
runGiphy :: MonadIO m => Giphy a -> GiphyConfig -> m (Either Servant.ServantError a)
runGiphy g conf = do
manager <- liftIO $ HTTP.newManager tlsManagerSettings
runGiphy' manager g conf
-- | Same as 'runGiphy' but accepts an explicit 'HTTP.Manager' instead of
-- implicitly creating one for you.
runGiphy'
:: MonadIO m
=> HTTP.Manager -- ^ This must be a TLS-enabled Manager
-> Giphy a
-> GiphyConfig
-> m (Either Servant.ServantError a)
runGiphy' manager giphy conf =
let env = Servant.ClientEnv manager baseUrl
runClientM' = flip Servant.runClientM
in
liftIO . runClientM' env . Reader.runReaderT giphy $ GiphyContext conf
-- $lenses
--
-- You can use these lenses if you prefer them to manually accessing
-- record fields.