packages feed

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 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