{-# 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. 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. 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. 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. 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. 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.ClientError 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.ClientError a) runGiphy' manager giphy conf = let env = Servant.ClientEnv manager baseUrl Nothing 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.