giphy-api (empty) → 0.1.0.0
raw patch · 7 files changed
+770/−0 lines, 7 filesdep +aesondep +basedep +basic-preludesetup-changed
Dependencies added: aeson, base, basic-prelude, bytestring, containers, directory, either, giphy-api, hspec, lens, microlens, microlens-th, mtl, network-uri, optparse-applicative, servant, servant-client, text
Files
- LICENSE +30/−0
- Setup.hs +2/−0
- app/Main.hs +98/−0
- app/Sample.hs +21/−0
- giphy-api.cabal +68/−0
- src/Web/Giphy.hs +414/−0
- test/Spec.hs +137/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Pascal Hartig (c) 2016++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Pascal Hartig nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,98 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import qualified Data.Text as T+import qualified Options.Applicative as Opt+import qualified Options.Applicative.Types as Opt+import qualified Web.Giphy as Giphy++import Control.Applicative (optional, (<**>), (<|>))+import Control.Lens.At (at)+import Control.Lens.Cons (_head)+import Control.Lens.Operators+import Control.Lens.Prism (_Right)+import Data.Monoid ((<>))+import Data.Version (Version (), showVersion)+import Paths_giphy_api (version)+import System.Environment (getProgName)++data Options = OptSearch T.Text | OptTranslate T.Text | OptRandom (Maybe T.Text)++apiKey :: Giphy.Key+apiKey = Giphy.Key "dc6zaTOxFJmzC"++options :: Opt.Parser Options+options = ( OptSearch <$> textOption+ ( Opt.long "search"+ <> Opt.short 's'+ <> Opt.help "Use search to find a matching GIF." ) )+ <|> ( OptTranslate <$> textOption+ ( Opt.long "translate"+ <> Opt.short 't'+ <> Opt.help "Use translate to find a matching GIF." ) )+ <|> ( OptRandom <$> optional ( textArgument ( Opt.metavar "RANDOM_TAG" ) ) )+ where+ -- TODO: This seems quite useful. Maybe publish as Options.Applicative.Text?+ text :: Opt.ReadM T.Text+ text = T.pack <$> Opt.readerAsk++ textOption :: Opt.Mod Opt.OptionFields T.Text -> Opt.Parser T.Text+ textOption = Opt.option text++ textArgument :: Opt.Mod Opt.ArgumentFields T.Text -> Opt.Parser T.Text+ textArgument = Opt.argument text++cliParser :: String -> Version -> Opt.ParserInfo Options+cliParser progName ver =+ Opt.info ( Opt.helper <*> options <**> versionInfo )+ ( Opt.fullDesc+ <> Opt.progDesc "Find GIFs on the command line."+ <> Opt.header progName )+ where+ versionInfo = Opt.infoOption ( unwords [progName, showVersion ver] )+ ( Opt.short 'V'+ <> Opt.long "version"+ <> Opt.hidden+ <> Opt.help "Show version information" )++main :: IO ()+main = do+ progName <- getProgName+ Opt.execParser (cliParser progName version) >>= run+ where+ run :: Options -> IO ()+ run opts = do+ let config = Giphy.GiphyConfig apiKey+ let app = getApp opts+ resp <- Giphy.runGiphy app config+ let fstUrl = resp ^? _Right+ . _head+ . Giphy.gifImages+ . at "original"+ . traverse+ . Giphy.imageUrl+ . traverse+ print fstUrl++ getApp :: Options -> Giphy.Giphy [Giphy.Gif]+ getApp opts =+ case opts of+ OptSearch s -> searchApp s+ OptTranslate t -> translateApp t+ OptRandom r -> randomApp r++translateApp :: T.Text -> Giphy.Giphy [Giphy.Gif]+translateApp q = do+ resp <- Giphy.translate $ Giphy.Phrase q+ return . pure $ resp ^. Giphy.translateItem++searchApp :: T.Text -> Giphy.Giphy [Giphy.Gif]+searchApp q = do+ resp <- Giphy.search $ Giphy.Query q+ return $ resp ^. Giphy.searchItems++randomApp :: Maybe T.Text -> Giphy.Giphy [Giphy.Gif]+randomApp q = do+ resp <- Giphy.random $ Giphy.Tag <$> q+ return . pure $ resp ^. Giphy.randomGifItem
+ app/Sample.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE OverloadedStrings #-}++module Sample where++import qualified Data.Text as T+import qualified Web.Giphy as Giphy++apiKey :: Giphy.Key+apiKey = Giphy.Key "dc6zaTOxFJmzC"++sample :: IO ()+sample = do+ let config = Giphy.GiphyConfig apiKey+ resp <- Giphy.runGiphy (app "puppies") config+ print resp++ where+ app :: T.Text -> Giphy.Giphy [Giphy.Gif]+ app q = do+ resp <- Giphy.search $ Giphy.Query q+ return $ Giphy._searchItems resp
+ giphy-api.cabal view
@@ -0,0 +1,68 @@+name: giphy-api+version: 0.1.0.0+cabal-version: >=1.10+build-type: Simple+license: BSD3+license-file: LICENSE+maintainer: Pascal Hartig <phartig@rdrei.net>+homepage: http://github.com/passy/giphy-api#readme+synopsis: Giphy HTTP API wrapper and CLI search tool.+description:+ Please see README.md+category: Web+author: Pascal Hartig++library+ exposed-modules:+ Web.Giphy+ build-depends:+ base >=4.7 && <5,+ text <1.3,+ network-uri <2.7,+ aeson <0.10,+ containers <0.6,+ either <4.5,+ microlens <0.5,+ microlens-th <0.4,+ mtl <2.3,+ servant <0.5,+ servant-client <0.5+ default-language: Haskell2010+ hs-source-dirs: src+ other-modules:+ Paths_giphy_api+ ghc-options: -Wall -fwarn-tabs -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unused-do-bind++executable giphy-search+ main-is: Main.hs+ build-depends:+ base >=4.7 && <5,+ text <1.3,+ network-uri <2.7,+ giphy-api <0.2,+ lens <4.14,+ optparse-applicative <0.13+ default-language: Haskell2010+ hs-source-dirs: app+ other-modules:+ Sample+ ghc-options: -Wall -fwarn-tabs -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unused-do-bind -threaded -rtsopts -with-rtsopts=-N++test-suite spec+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ build-depends:+ base >=4.7 && <5,+ text <1.3,+ network-uri <2.7,+ giphy-api <0.2,+ aeson <0.10,+ basic-prelude <0.6,+ bytestring <0.11,+ containers <0.6,+ directory <1.3,+ hspec <2.3,+ lens <4.14+ default-language: Haskell2010+ hs-source-dirs: test+ ghc-options: -Wall -fwarn-tabs -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unused-do-bind -threaded -rtsopts -with-rtsopts=-N
+ src/Web/Giphy.hs view
@@ -0,0 +1,414 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeOperators #-}++-- |+-- 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(..)+ -- * Response Data Types+ -- $response+ , Gif(..)+ , Image(..)+ , ImageMap()+ , PaginationOffset(..)+ , SearchResponse(..)+ , SingleGifResponse(..)+ , TranslateResponse(..)+ -- * Giphy Monad+ -- $giphy+ , GiphyConfig(..)+ , Giphy()+ , 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 qualified Control.Monad.Reader as Reader+import Control.Monad.Trans (MonadIO (), lift, liftIO)+import Control.Monad.Trans.Either (EitherT, runEitherT)+import Data.Aeson ((.:), (.:?))+import qualified Data.Aeson.Types as Aeson+import qualified Data.Map.Strict as Map+import Data.Monoid ((<>))+import qualified Data.Proxy as Proxy+import qualified Data.Text as T+import GHC.Generics (Generic ())+import qualified Lens.Micro.TH as Lens+import qualified Network.URI as URI+import Servant.API ((:<|>) (..), (:>))+import qualified Servant.API as Servant+import qualified Servant.Client as Servant+import qualified Text.Read as Read++maybeParse :: (Monad m, MonadPlus m) => (a -> Maybe b) -> m a -> m b+maybeParse f = (maybe mzero return . f =<<)++fromURI :: (Monad m, MonadPlus m) => m String -> m Servant.URI+fromURI = maybeParse URI.parseURI++fromInt :: (Monad m, 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 (Servant.ToText, Servant.FromText, Show, Eq)++-- | A search query.+newtype Query = Query T.Text+ deriving (Servant.ToText, Servant.FromText, Show, Eq)++-- | A phrase or term used for translation.+newtype Phrase = Phrase T.Text+ deriving (Servant.ToText, Servant.FromText, Show, Eq)++-- | A tag to retrieve a random GIF for.+newtype Tag = Tag T.Text+ deriving (Servant.ToText, Servant.FromText, Show, Eq)++-- | A unique gif identifier.+newtype GifId = GifId T.Text+ deriving (Servant.ToText, Servant.FromText, Show, Eq)++-- | Offset for paginated requests.+newtype PaginationOffset = PaginationOffset Int+ deriving (Servant.ToText, Servant.FromText, 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 = "v1"+ :> "gifs"+ :> "search"+ :> Servant.QueryParam "api_key" Key+ :> Servant.QueryParam "offset" PaginationOffset+ :> Servant.QueryParam "q" Query+ :> Servant.Get '[Servant.JSON] SearchResponse+ :<|> "v1"+ :> "gifs"+ :> "translate"+ :> Servant.QueryParam "api_key" Key+ :> Servant.QueryParam "s" Phrase+ :> Servant.Get '[Servant.JSON] TranslateResponse+ :<|> "v1"+ :> "gifs"+ :> Servant.Capture "gif_id" GifId+ :> Servant.QueryParam "api_key" Key+ :> Servant.Get '[Servant.JSON] SingleGifResponse+ :<|> "v1"+ :> "gifs"+ :> "random"+ :> Servant.QueryParam "api_key" Key+ :> Servant.QueryParam "tag" Tag+ :> Servant.Get '[Servant.JSON] RandomResponse++api :: Proxy.Proxy GiphyAPI+api = Proxy.Proxy++search'+ :: Maybe Key+ -> Maybe PaginationOffset+ -> Maybe Query+ -> EitherT Servant.ServantError IO SearchResponse++translate'+ :: Maybe Key+ -> Maybe Phrase+ -> EitherT Servant.ServantError IO TranslateResponse++gif'+ :: GifId+ -> Maybe Key+ -> EitherT Servant.ServantError IO SingleGifResponse++random'+ :: Maybe Key+ -> Maybe Tag+ -> EitherT Servant.ServantError IO RandomResponse++search' :<|> translate' :<|> gif' :<|> random' = Servant.client api host+ where host = Servant.BaseUrl Servant.Https "api.giphy.com" 443++-- $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. <http://api.giphy.com/v1/gifs/search?q=funny+cat&api_key=dc6zaTOxFJmzC>+search+ :: Query+ -> Giphy SearchResponse+search query = do+ key <- Reader.asks configApiKey+ lift $ search' (pure key) (pure $ PaginationOffset 0) (pure query)++-- | Issue a search request for the given query by specifying a+-- pagination offset.+-- E.g. <http://api.giphy.com/v1/gifs/search?q=funny+cat&api_key=dc6zaTOxFJmzC&offset=25>+searchOffset+ :: Query+ -> PaginationOffset+ -> Giphy SearchResponse+searchOffset query offset = do+ key <- Reader.asks configApiKey+ lift $ search' (pure key) (pure offset) (pure query)++-- | Issue a request for a single GIF identified by its 'GifId'.+-- E.g. <http://api.giphy.com/v1/gifs/feqkVgjJpYtjy?api_key=dc6zaTOxFJmzC>+gif+ :: GifId+ -> Giphy SingleGifResponse+gif gifid = do+ key <- Reader.asks configApiKey+ lift $ gif' gifid (pure key)++-- | Issue a translate request for a given phrase or term.+-- E.g. <http://api.giphy.com/v1/gifs/translate?s=superman&api_key=dc6zaTOxFJmzC>+translate+ :: Phrase+ -> Giphy TranslateResponse+translate phrase = do+ key <- Reader.asks configApiKey+ lift $ translate' (pure key) (pure phrase)++-- | Issue a request for a random GIF for the given (optional) tag.+-- E.g. <http://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=american+psycho>+random+ :: Maybe Tag+ -> Giphy RandomResponse+random tag = do+ key <- Reader.asks configApiKey+ lift $ random' (pure key) tag++-- $giphy+--+-- Use 'runGiphy' to lift the 'Giphy' monad into IO.+--++-- | Contains the key to access the API.+data GiphyConfig = GiphyConfig { configApiKey :: Key }+ deriving (Show, Eq)++-- | The Giphy monad contains the execution context.+type Giphy = Reader.ReaderT GiphyConfig (EitherT Servant.ServantError IO)++-- | 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 = ((liftIO . runEitherT) .) . Reader.runReaderT++-- $lenses+--+-- You can use these lenses if you prefer them to manually accessing+-- record fields.
+ test/Spec.hs view
@@ -0,0 +1,137 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}++import BasicPrelude+import Data.Maybe (fromJust)+import System.Directory (getCurrentDirectory)++import Control.Lens.At (at)+import Control.Lens.Cons (_head)+import qualified Data.Aeson as Aeson+import qualified Data.ByteString.Lazy as BS+import qualified Data.Map.Strict as Map+import qualified Data.Text.Encoding as TE+import qualified Data.Text.IO as TIO+import Network.URI (parseURI)++import Control.Lens.Operators+import Test.Hspec++import qualified Web.Giphy as Giphy++readFixture :: FilePath -> IO BS.ByteString+readFixture path = do+ dir <- getCurrentDirectory+ BS.fromStrict . TE.encodeUtf8 <$> TIO.readFile (dir </> "test" </> "fixtures" </> path)++main :: IO ()+main = hspec $ do+ describe "Giphy" $ do+ describe "JSON Parsing" $ do++ it "parses a search response" $ do+ resp <- readFixture "search_response.json"+ let res = either error id (Aeson.eitherDecode resp)+ let item = res ^?! Giphy.searchItems . _head++ item ^. Giphy.gifId `shouldBe` "QgcQLZa6glP2w"+ item ^. Giphy.gifSlug `shouldBe` "cat-funny-QgcQLZa6glP2w"+ item ^. Giphy.gifUrl `shouldBe` fromJust (parseURI "https://giphy.com/gifs/cat-funny-QgcQLZa6glP2w")+ res ^. Giphy.searchPagination . Giphy.paginationCount `shouldBe` 25+ res ^. Giphy.searchPagination . Giphy.paginationTotalCount `shouldBe` 2161++ let images = item ^. Giphy.gifImages++ Map.keys images `shouldBe` [+ "downsized"+ , "downsized_large"+ , "downsized_medium"+ , "downsized_still"+ , "fixed_height"+ , "fixed_height_downsampled"+ , "fixed_height_small"+ , "fixed_height_small_still"+ , "fixed_height_still"+ , "fixed_width"+ , "fixed_width_downsampled"+ , "fixed_width_small"+ , "fixed_width_small_still"+ , "fixed_width_still"+ , "looping"+ , "original"+ , "original_still"+ ]++ images ^? at "fixed_height" . traverse . Giphy.imageUrl . traverse `shouldBe`+ parseURI "https://media0.giphy.com/media/QgcQLZa6glP2w/200.gif"++ images ^? at "looping" . traverse . Giphy.imageMp4Url . traverse `shouldBe`+ parseURI "https://media.giphy.com/media/QgcQLZa6glP2w/giphy-loop.mp4"++ images ^? at "fixed_width_small" . traverse . Giphy.imageMp4Size . traverse `shouldBe` Just 39646++ it "parses a translate response" $ do+ resp <- readFixture "translate_response.json"+ let item = case Aeson.eitherDecode resp of+ Left err -> error err+ Right i -> i ^?! Giphy.translateItem++ item ^. Giphy.gifId `shouldBe` "Zlkbqhurvg4Zq"+ item ^. Giphy.gifSlug `shouldBe` "superman-Zlkbqhurvg4Zq"+ item ^. Giphy.gifUrl `shouldBe` fromJust (parseURI "https://giphy.com/gifs/superman-Zlkbqhurvg4Zq")++ let images = item ^. Giphy.gifImages++ Map.keys images `shouldBe` [+ "downsized"+ , "downsized_large"+ , "downsized_medium"+ , "downsized_still"+ , "fixed_height"+ , "fixed_height_downsampled"+ , "fixed_height_small"+ , "fixed_height_small_still"+ , "fixed_height_still"+ , "fixed_width"+ , "fixed_width_downsampled"+ , "fixed_width_small"+ , "fixed_width_small_still"+ , "fixed_width_still"+ , "looping"+ , "original"+ , "original_still"+ ]++ images ^? at "fixed_height" . traverse . Giphy.imageUrl . traverse `shouldBe`+ parseURI "https://media0.giphy.com/media/Zlkbqhurvg4Zq/200.gif"++ it "parses a 'random' response" $ do+ resp <- readFixture "random_response.json"+ let item = case Aeson.eitherDecode resp of+ Left err -> error err+ Right i -> i ^?! Giphy.randomGifItem++ item ^. Giphy.gifId `shouldBe` "13bGOCL29Loy5O"+ item ^. Giphy.gifSlug `shouldBe` ""+ item ^. Giphy.gifUrl `shouldBe` fromJust (parseURI "http://giphy.com/gifs/american-psycho-quote-text-13bGOCL29Loy5O")++ let images = item ^. Giphy.gifImages++ Map.keys images `shouldMatchList` [+ "fixed_height_downsampled"+ , "fixed_height_small"+ , "fixed_width_downsampled"+ , "fixed_width_small"+ , "fixed_width"+ , "original"+ ]++ images ^? at "original" . traverse . Giphy.imageUrl . traverse `shouldBe`+ parseURI "http://media0.giphy.com/media/13bGOCL29Loy5O/giphy.gif"++ images ^? at "fixed_height_small" . traverse . Giphy.imageUrl . traverse `shouldBe`+ parseURI "http://media0.giphy.com/media/13bGOCL29Loy5O/100.gif"++ images ^? at "fixed_width_downsampled" . traverse . Giphy.imageHeight . traverse `shouldBe`+ pure 82