packages feed

twitter-types 0.3.20140801 → 0.11.0

raw patch · 39 files changed

Files

− .gitignore
@@ -1,30 +0,0 @@-# General-\#*#-.*~-*~-.#*-*.swp-.DS_Store-.gdb_history-TAGS-# Object files, etc-*.a-*.o-*.so-*.hi-*.p_hi-*.chi-*.chs.h-*.tix-.hpc-a.out-# autotool-autom4te.cache-stamp-h1-# misc-*.sqlite-Main-dist/-cabal-dev/-.cabal-sandbox/-cabal.sandbox.config
− .travis.yml
@@ -1,8 +0,0 @@-language: haskell-before_install:-    - cabal install hpc-coveralls-script:-    - cabal configure --enable-tests --enable-library-coverage && cabal build-    - run-cabal-test-after_script:-    - hpc-coveralls tests
+ README.md view
@@ -0,0 +1,8 @@+twitter-types +=============++![CI](https://github.com/himura/twitter-types/workflows/CI/badge.svg)+[![Hackage](https://img.shields.io/hackage/v/twitter-types.svg?style=flat)](https://hackage.haskell.org/package/twitter-types)+[![Hackage-Deps](https://img.shields.io/hackage-deps/v/twitter-types.svg)](http://packdeps.haskellers.com/feed?needle=twitter-types)++This library treats the Twitter JSON API in the Haskell way.
Setup.hs view
@@ -1,3 +1,5 @@ #!/usr/bin/env runhaskell+ import Distribution.Simple+ main = defaultMain
Web/Twitter/Types.hs view
@@ -1,497 +1,1166 @@-{-# LANGUAGE OverloadedStrings, DeriveDataTypeable #-}--module Web.Twitter.Types-       ( DateString-       , UserId-       , Friends-       , URIString-       , UserName-       , StatusId-       , LanguageCode-       , StreamingAPI(..)-       , Status(..)-       , SearchResult(..)-       , SearchStatus(..)-       , SearchMetadata(..)-       , RetweetedStatus(..)-       , DirectMessage(..)-       , EventTarget(..)-       , Event(..)-       , Delete(..)-       , User(..)-       , List(..)-       , Entities(..)-       , EntityIndices-       , Entity(..)-       , HashTagEntity(..)-       , UserEntity(..)-       , URLEntity(..)-       , MediaEntity(..)-       , MediaSize(..)-       , Coordinates(..)-       , Place(..)-       , BoundingBox(..)-       , checkError-       )-       where--import Data.Aeson-import Data.Aeson.Types (Parser)-import Data.Text (Text)-import Data.HashMap.Strict (HashMap)-import Control.Applicative-import Control.Monad--type DateString   = String-type UserId       = Integer-type Friends      = [UserId]-type URIString    = Text-type UserName     = Text-type StatusId     = Integer-type LanguageCode = String--data StreamingAPI = SStatus Status-                  | SRetweetedStatus RetweetedStatus-                  | SEvent Event-                  | SDelete Delete-                  -- -- | SScrubGeo ScrubGeo-                  | SFriends Friends-                  | SUnknown Value-                  deriving (Show, Eq)--checkError :: Object -> Parser ()-checkError o = do-  err <- o .:? "error"-  case err of-    Just msg -> fail msg-    Nothing -> return ()--instance FromJSON StreamingAPI where-  parseJSON v@(Object o) =-    SRetweetedStatus <$> js <|>-    SStatus <$> js <|>-    SEvent <$> js <|>-    SDelete <$> js <|>-    SFriends <$> (o .: "friends") <|>-    return (SUnknown v)-    where-      js :: FromJSON a => Parser a-      js = parseJSON v-  parseJSON _ = mzero--data Status =-  Status-  { statusCreatedAt          :: DateString-  , statusId                 :: StatusId-  , statusText               :: Text-  , statusSource             :: Text-  , statusTruncated          :: Bool-  , statusEntities           :: Maybe Entities-  , statusExtendedEntities   :: Maybe Entities-  , statusInReplyTo          :: Maybe StatusId-  , statusInReplyToUser      :: Maybe UserId-  , statusFavorite           :: Maybe Bool-  , statusRetweetCount       :: Maybe Integer-  , statusUser               :: User-  , statusRetweet            :: Maybe Status-  , statusPlace              :: Maybe Place-  , statusFavoriteCount      :: Integer-  , statusLang               :: Maybe Text-  , statusPossiblySensitive  :: Maybe Bool-  , statusCoordinates        :: Maybe Coordinates-  } deriving (Show, Eq)--instance FromJSON Status where-  parseJSON (Object o) = checkError o >>-    Status <$> o .:  "created_at"-           <*> o .:  "id"-           <*> o .:  "text"-           <*> o .:  "source"-           <*> o .:  "truncated"-           <*> o .:? "entities"-           <*> o .:? "extended_entities"-           <*> o .:? "in_reply_to_status_id"-           <*> o .:? "in_reply_to_user_id"-           <*> o .:? "favorited"-           <*> o .:? "retweet_count"-           <*> o .:  "user"-           <*> o .:? "retweeted_status"-           <*> o .:? "place"-           <*> o .:? "favorite_count" .!= 0-           <*> o .:? "lang"-           <*> o .:? "possibly_sensitive"-           <*> o .:? "coordinates"-  parseJSON _ = mzero--data SearchResult body =-  SearchResult-  { searchResultStatuses :: body-  , searchResultSearchMetadata :: SearchMetadata-  } deriving (Show, Eq)--instance FromJSON body =>-         FromJSON (SearchResult body) where-  parseJSON (Object o) = checkError o >>-    SearchResult <$> o .:  "statuses"-                 <*> o .:  "search_metadata"-  parseJSON _ = mzero--data SearchStatus =-  SearchStatus-  { searchStatusCreatedAt     :: DateString-  , searchStatusId            :: StatusId-  , searchStatusText          :: Text-  , searchStatusSource        :: Text-  , searchStatusUser          :: User-  , searchStatusCoordinates   :: Maybe Coordinates-  } deriving (Show, Eq)--instance FromJSON SearchStatus where-  parseJSON (Object o) = checkError o >>-    SearchStatus <$> o .:  "created_at"-                 <*> o .:  "id"-                 <*> o .:  "text"-                 <*> o .:  "source"-                 <*> o .:  "user"-                 <*> o .:? "coordinates"-  parseJSON _ = mzero--data SearchMetadata =-  SearchMetadata-  { searchMetadataMaxId         :: StatusId-  , searchMetadataSinceId       :: StatusId-  , searchMetadataRefreshUrl    :: URIString-  , searchMetadataNextResults   :: Maybe URIString-  , searchMetadataCount         :: Int-  , searchMetadataCompletedIn   :: Maybe Float-  , searchMetadataSinceIdStr    :: String-  , searchMetadataQuery         :: String-  , searchMetadataMaxIdStr      :: String-  } deriving (Show, Eq)--instance FromJSON SearchMetadata where-  parseJSON (Object o) = checkError o >>-    SearchMetadata <$> o .:  "max_id"-                   <*> o .:  "since_id"-                   <*> o .:  "refresh_url"-                   <*> o .:? "next_results"-                   <*> o .:  "count"-                   <*> o .:? "completed_in"-                   <*> o .:  "since_id_str"-                   <*> o .:  "query"-                   <*> o .:  "max_id_str"-  parseJSON _ = mzero--data RetweetedStatus =-  RetweetedStatus-  { rsCreatedAt       :: DateString-  , rsId              :: StatusId-  , rsText            :: Text-  , rsSource          :: Text-  , rsTruncated       :: Bool-  , rsEntities        :: Maybe Entities-  , rsUser            :: User-  , rsRetweetedStatus :: Status-  , rsCoordinates     :: Maybe Coordinates-  } deriving (Show, Eq)--instance FromJSON RetweetedStatus where-  parseJSON (Object o) = checkError o >>-    RetweetedStatus <$> o .:  "created_at"-                    <*> o .:  "id"-                    <*> o .:  "text"-                    <*> o .:  "source"-                    <*> o .:  "truncated"-                    <*> o .:? "entities"-                    <*> o .:  "user"-                    <*> o .:  "retweeted_status"-                    <*> o .:? "coordinates"-  parseJSON _ = mzero--data DirectMessage =-  DirectMessage-  { dmCreatedAt          :: DateString-  , dmSenderScreenName   :: Text-  , dmSender             :: User-  , dmText               :: Text-  , dmRecipientScreeName :: Text-  , dmId                 :: StatusId-  , dmRecipient          :: User-  , dmRecipientId        :: UserId-  , dmSenderId           :: UserId-  , dmCoordinates        :: Maybe Coordinates-  } deriving (Show, Eq)--instance FromJSON DirectMessage where-  parseJSON (Object o) = checkError o >>-    DirectMessage <$> o .:  "created_at"-                  <*> o .:  "sender_screen_name"-                  <*> o .:  "sender"-                  <*> o .:  "text"-                  <*> o .:  "recipient_screen_name"-                  <*> o .:  "id"-                  <*> o .:  "recipient"-                  <*> o .:  "recipient_id"-                  <*> o .:  "sender_id"-                  <*> o .:? "coordinates"-  parseJSON _ = mzero--data EventType = Favorite | Unfavorite-               | ListCreated | ListUpdated | ListMemberAdded-               | UserUpdate | Block | Unblock | Follow-               deriving (Show, Eq)--data EventTarget = ETUser User | ETStatus Status | ETList List | ETUnknown Value-                 deriving (Show, Eq)--instance FromJSON EventTarget where-  parseJSON v@(Object o) = checkError o >>-    ETUser <$> parseJSON v <|>-    ETStatus <$> parseJSON v <|>-    ETList <$> parseJSON v <|>-    return (ETUnknown v)-  parseJSON _ = mzero--data Event =-  Event-  { evCreatedAt       :: DateString-  , evTargetObject    :: Maybe EventTarget-  , evEvent           :: Text-  , evTarget          :: EventTarget-  , evSource          :: EventTarget-  } deriving (Show, Eq)--instance FromJSON Event where-  parseJSON (Object o) = checkError o >>-    Event <$> o .:  "created_at"-          <*> o .:? "target_object"-          <*> o .:  "event"-          <*> o .:  "target"-          <*> o .:  "source"-  parseJSON _ = mzero--data Delete =-  Delete-  { delId  :: StatusId-  , delUserId :: UserId-  } deriving (Show, Eq)--instance FromJSON Delete where-  parseJSON (Object o) = checkError o >> do-    s <- o .: "delete" >>= (.: "status")-    Delete <$> s .: "id"-           <*> s .: "user_id"-  parseJSON _ = mzero--data User =-  User-  { userId              :: UserId-  , userName            :: UserName-  , userScreenName      :: Text-  , userDescription     :: Maybe Text-  , userLocation        :: Maybe Text-  , userProfileImageURL :: Maybe URIString-  , userURL             :: Maybe URIString-  , userProtected       :: Maybe Bool-  , userFollowers       :: Maybe Int-  , userFriends         :: Maybe Int-  , userTweets          :: Maybe Int-  , userLangCode        :: Maybe LanguageCode-  , userCreatedAt       :: Maybe DateString-  } deriving (Show, Eq)--instance FromJSON User where-  parseJSON (Object o) = checkError o >>-    User <$> o .:  "id"-         <*> o .:  "name"-         <*> o .:  "screen_name"-         <*> o .:? "description"-         <*> o .:? "location"-         <*> o .:? "profile_image_url"-         <*> o .:? "url"-         <*> o .:? "protected"-         <*> o .:? "followers_count"-         <*> o .:? "friends_count"-         <*> o .:? "statuses_count"-         <*> o .:? "lang"-         <*> o .:? "created_at"-  parseJSON _ = mzero--data List =-  List-  { listId :: Int-  , listName :: Text-  , listFullName :: Text-  , listMemberCount :: Int-  , listSubscriberCount :: Int-  , listMode :: Text-  , listUser :: User-  } deriving (Show, Eq)--instance FromJSON List where-  parseJSON (Object o) = checkError o >>-    List <$> o .:  "id"-         <*> o .:  "name"-         <*> o .:  "full_name"-         <*> o .:  "member_count"-         <*> o .:  "subscriber_count"-         <*> o .:  "mode"-         <*> o .:  "user"-  parseJSON _ = mzero--data HashTagEntity =-  HashTagEntity-  { hashTagText :: Text -- ^ The Hashtag text-  } deriving (Show, Eq)--instance FromJSON HashTagEntity where-  parseJSON (Object o) =-    HashTagEntity <$> o .: "text"-  parseJSON _ = mzero--data UserEntity =-  UserEntity-  { userEntityUserId              :: UserId-  , userEntityUserName            :: UserName-  , userEntityUserScreenName      :: Text-  } deriving (Show, Eq)--instance FromJSON UserEntity where-  parseJSON (Object o) =-    UserEntity <$> o .:  "id"-               <*> o .:  "name"-               <*> o .:  "screen_name"-  parseJSON _ = mzero--data URLEntity =-  URLEntity-  { ueURL      :: URIString -- ^ The URL that was extracted-  , ueExpanded :: URIString -- ^ The fully resolved URL (only for t.co links)-  , ueDisplay  :: Text    -- ^ Not a URL but a string to display instead of the URL (only for t.co links)-  } deriving (Show, Eq)--instance FromJSON URLEntity where-  parseJSON (Object o) =-    URLEntity <$> o .:  "url"-              <*> o .:  "expanded_url"-              <*> o .:  "display_url"-  parseJSON _ = mzero--data MediaEntity =-  MediaEntity-  { meType :: Text-  , meId :: StatusId-  , meSizes :: HashMap Text MediaSize-  , meMediaURL :: URIString-  , meMediaURLHttps :: URIString-  , meURL :: URLEntity-  } deriving (Show, Eq)--instance FromJSON MediaEntity where-  parseJSON v@(Object o) =-    MediaEntity <$> o .: "type"-                <*> o .: "id"-                <*> o .: "sizes"-                <*> o .: "media_url"-                <*> o .: "media_url_https"-                <*> parseJSON v-  parseJSON _ = mzero--data MediaSize =-  MediaSize-  { msWidth :: Int-  , msHeight :: Int-  , msResize :: Text-  } deriving (Show, Eq)--instance FromJSON MediaSize where-  parseJSON (Object o) =-    MediaSize <$> o .: "w"-              <*> o .: "h"-              <*> o .: "resize"-  parseJSON _ = mzero--data Coordinates =-  Coordinates-  { coordinates :: [Double]-  , coordinatesType :: Text-  } deriving (Show, Eq)--instance FromJSON Coordinates where-  parseJSON (Object o) =-    Coordinates <$> o .: "coordinates"-                <*> o .: "type"-  parseJSON _ = mzero--data Place =-  Place-  { placeAttributes   :: HashMap Text Text-  , placeBoundingBox  :: BoundingBox-  , placeCountry      :: Text-  , placeCountryCode  :: Text-  , placeFullName     :: Text-  , placeId           :: Text-  , placeName         :: Text-  , placeType         :: Text-  , placeUrl          :: Text-  } deriving (Show, Eq)--instance FromJSON Place where-  parseJSON (Object o) =-    Place <$> o .: "attributes"-          <*> o .: "bounding_box"-          <*> o .: "country"-          <*> o .: "country_code"-          <*> o .: "full_name"-          <*> o .: "id"-          <*> o .: "name"-          <*> o .: "place_type"-          <*> o .: "url"-  parseJSON _ = mzero--data BoundingBox =-  BoundingBox-  { boundingBoxCoordinates  :: [[[Double]]]-  , boundingBoxType         :: Text-  } deriving (Show, Eq)--instance FromJSON BoundingBox where-  parseJSON (Object o) =-    BoundingBox <$> o .: "coordinates"-                <*> o .: "type"-  parseJSON _ = mzero---- | Entity handling.-data Entities =-  Entities-  { enHashTags     :: [Entity HashTagEntity]-  , enUserMentions :: [Entity UserEntity]-  , enURLs         :: [Entity URLEntity]-  , enMedia        :: [Entity MediaEntity]-  } deriving (Show, Eq)--instance FromJSON Entities where-  parseJSON (Object o) =-    Entities <$> o .:? "hashtags" .!= []-             <*> o .:? "user_mentions" .!= []-             <*> o .:? "urls" .!= []-             <*> o .:? "media" .!= []-  parseJSON _ = mzero---- | The character positions the Entity was extracted from------   This is experimental implementation.---   This may be replaced by more definite types.-type EntityIndices = [Int]--data Entity a =-  Entity-  { entityBody    :: a             -- ^ The detail information of the specific entity types (HashTag, URL, User)-  , entityIndices :: EntityIndices -- ^ The character positions the Entity was extracted from-  } deriving (Show, Eq)--instance FromJSON a => FromJSON (Entity a) where-  parseJSON v@(Object o) =-    Entity <$> parseJSON v-           <*> o .: "indices"-  parseJSON _ = mzero+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Web.Twitter.Types (+    UserId,+    Friends,+    URIString,+    UserName,+    StatusId,+    LanguageCode,+    StreamingAPI (..),+    Status (..),+    SearchResult (..),+    SearchStatus (..),+    SearchMetadata (..),+    RetweetedStatus (..),+    DirectMessage (..),+    EventTarget (..),+    Event (..),+    Delete (..),+    User (..),+    List (..),+    Entities (..),+    EntityIndices,+    ExtendedEntities (..),+    Variant (..),+    VideoInfo (..),+    ExtendedEntity (..),+    Entity (..),+    HashTagEntity (..),+    UserEntity (..),+    URLEntity (..),+    MediaEntity (..),+    MediaSize (..),+    Coordinates (..),+    Place (..),+    BoundingBox (..),+    Contributor (..),+    UploadedMedia (..),+    ImageSizeType (..),+    DisplayTextRange (..),+    checkError,+    twitterTimeFormat,+) where++import Control.Applicative+import Control.Monad+import Data.Aeson+import Data.Aeson.Types (Parser)+import Data.Data+import Data.HashMap.Strict (HashMap)++#if MIN_VERSION_aeson(2, 0, 0)+import qualified Data.Aeson.KeyMap as KeyMap+#else+import qualified Data.HashMap.Strict as KeyMap+#endif+import Data.Int+import Data.Ratio+import Data.Text (Text, pack, unpack)+import Data.Text.Read (decimal)+import Data.Time+import Data.Time.Clock.POSIX+import GHC.Generics++newtype TwitterTime = TwitterTime {fromTwitterTime :: UTCTime}++type UserId = Integer+type Friends = [UserId]+type URIString = Text+type UserName = Text+type StatusId = Integer+type LanguageCode = String++data StreamingAPI+    = SStatus Status+    | SRetweetedStatus RetweetedStatus+    | SEvent Event+    | SDelete Delete+    | -- | SScrubGeo ScrubGeo+      SFriends Friends+    | SDirectMessage DirectMessage+    | SUnknown Value+    deriving (Show, Eq, Data, Typeable, Generic)++checkError :: Object -> Parser ()+checkError o = do+    err <- o .:? "error"+    case err of+        Just msg -> fail msg+        Nothing -> return ()++twitterTimeFormat :: String+twitterTimeFormat = "%a %b %d %T %z %Y"++instance FromJSON TwitterTime where+    parseJSON = withText "TwitterTime" $ \t ->+        case parseTimeM True defaultTimeLocale twitterTimeFormat (unpack t) of+            Just d -> pure $ TwitterTime d+            Nothing -> fail $ "Could not parse twitter time. Text was: " ++ unpack t++instance ToJSON TwitterTime where+    toJSON t = String $ pack $ formatTime defaultTimeLocale twitterTimeFormat $ fromTwitterTime t++instance FromJSON StreamingAPI where+    parseJSON v@(Object o) =+        SRetweetedStatus <$> js+            <|> SStatus <$> js+            <|> SEvent <$> js+            <|> SDelete <$> js+            <|> SFriends <$> (o .: "friends")+            <|> SDirectMessage <$> (o .: "direct_message")+            <|> return (SUnknown v)+      where+        js :: FromJSON a => Parser a+        js = parseJSON v+    parseJSON v = fail $ "couldn't parse StreamingAPI from: " ++ show v++instance ToJSON StreamingAPI where+    toJSON (SStatus s) = toJSON s+    toJSON (SRetweetedStatus s) = toJSON s+    toJSON (SEvent e) = toJSON e+    toJSON (SDelete d) = toJSON d+    toJSON (SFriends f) = toJSON f+    toJSON (SDirectMessage m) = toJSON m+    toJSON (SUnknown v) = v++-- | This type represents a Twitter tweet structure.+-- See <https://dev.twitter.com/docs/platform-objects/tweets>.+data Status = Status+    { statusContributors :: Maybe [Contributor]+    , statusCoordinates :: Maybe Coordinates+    , statusCreatedAt :: UTCTime+    , statusCurrentUserRetweet :: Maybe StatusId+    , statusEntities :: Maybe Entities+    , statusExtendedEntities :: Maybe ExtendedEntities+    , statusFavoriteCount :: Integer+    , statusFavorited :: Maybe Bool+    , statusFilterLevel :: Maybe Text+    , statusId :: StatusId+    , statusInReplyToScreenName :: Maybe Text+    , statusInReplyToStatusId :: Maybe StatusId+    , statusInReplyToUserId :: Maybe UserId+    , statusLang :: Maybe LanguageCode+    , statusPlace :: Maybe Place+    , statusPossiblySensitive :: Maybe Bool+    , statusScopes :: Maybe Object+    , statusQuotedStatusId :: Maybe StatusId+    , statusQuotedStatus :: Maybe Status+    , statusRetweetCount :: Integer+    , statusRetweeted :: Maybe Bool+    , statusRetweetedStatus :: Maybe Status+    , statusSource :: Text+    , statusText :: Text+    , statusTruncated :: Bool+    , statusUser :: User+    , statusWithheldCopyright :: Maybe Bool+    , statusWithheldInCountries :: Maybe [Text]+    , statusWithheldScope :: Maybe Text+    , statusDisplayTextRange :: Maybe DisplayTextRange+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON Status where+    parseJSON (Object o) =+        checkError o+            >> Status <$> o .:? "contributors" .!= Nothing+            <*> o .:? "coordinates" .!= Nothing+            <*> (o .: "created_at" >>= return . fromTwitterTime)+            <*> ((o .: "current_user_retweet" >>= (.: "id")) <|> return Nothing)+            <*> o .:? "entities"+            <*> o .:? "extended_entities"+            <*> o .:? "favorite_count" .!= 0+            <*> o .:? "favorited"+            <*> o .:? "filter_level"+            <*> o .: "id"+            <*> o .:? "in_reply_to_screen_name" .!= Nothing+            <*> o .:? "in_reply_to_status_id" .!= Nothing+            <*> o .:? "in_reply_to_user_id" .!= Nothing+            <*> o .:? "lang"+            <*> o .:? "place" .!= Nothing+            <*> o .:? "possibly_sensitive"+            <*> o .:? "scopes"+            <*> o .:? "quoted_status_id"+            <*> o .:? "quoted_status"+            <*> o .:? "retweet_count" .!= 0+            <*> o .:? "retweeted"+            <*> o .:? "retweeted_status"+            <*> o .: "source"+            <*> (o .: "full_text" <|> o .: "text")+            <*> o .: "truncated"+            <*> o .: "user"+            <*> o .:? "withheld_copyright"+            <*> o .:? "withheld_in_countries"+            <*> o .:? "withheld_scope"+            <*> o .:? "display_text_range"+    parseJSON v = fail $ "couldn't parse status from: " ++ show v++instance ToJSON Status where+    toJSON Status {..} =+        object+            [ "contributors" .= statusContributors+            , "coordinates" .= statusCoordinates+            , "created_at" .= TwitterTime statusCreatedAt+            , "current_user_retweet"+                .= object+                    [ "id" .= statusCurrentUserRetweet+                    , "id_str" .= show statusCurrentUserRetweet+                    ]+            , "entities" .= statusEntities+            , "extended_entities" .= statusExtendedEntities+            , "favorite_count" .= statusFavoriteCount+            , "favorited" .= statusFavorited+            , "filter_level" .= statusFilterLevel+            , "id" .= statusId+            , "in_reply_to_screen_name" .= statusInReplyToScreenName+            , "in_reply_to_status_id" .= statusInReplyToStatusId+            , "in_reply_to_user_id" .= statusInReplyToUserId+            , "lang" .= statusLang+            , "place" .= statusPlace+            , "possibly_sensitive" .= statusPossiblySensitive+            , "scopes" .= statusScopes+            , "quoted_status_id" .= statusQuotedStatusId+            , "quoted_status" .= statusQuotedStatus+            , "retweet_count" .= statusRetweetCount+            , "retweeted" .= statusRetweeted+            , "retweeted_status" .= statusRetweetedStatus+            , "source" .= statusSource+            , "text" .= statusText+            , "truncated" .= statusTruncated+            , "user" .= statusUser+            , "withheld_copyright" .= statusWithheldCopyright+            , "withheld_in_countries" .= statusWithheldInCountries+            , "withheld_scope" .= statusWithheldScope+            , "display_text_range" .= statusDisplayTextRange+            ]++data SearchResult body = SearchResult+    { searchResultStatuses :: body+    , searchResultSearchMetadata :: SearchMetadata+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance+    FromJSON body =>+    FromJSON (SearchResult body)+    where+    parseJSON (Object o) =+        checkError o+            >> SearchResult <$> o .: "statuses"+            <*> o .: "search_metadata"+    parseJSON v = fail $ "couldn't parse search result from: " ++ show v++instance+    ToJSON body =>+    ToJSON (SearchResult body)+    where+    toJSON SearchResult {..} =+        object+            [ "statuses" .= searchResultStatuses+            , "search_metadata" .= searchResultSearchMetadata+            ]++data SearchStatus = SearchStatus+    { searchStatusCreatedAt :: UTCTime+    , searchStatusId :: StatusId+    , searchStatusText :: Text+    , searchStatusSource :: Text+    , searchStatusUser :: User+    , searchStatusCoordinates :: Maybe Coordinates+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON SearchStatus where+    parseJSON (Object o) =+        checkError o+            >> SearchStatus <$> (o .: "created_at" >>= return . fromTwitterTime)+            <*> o .: "id"+            <*> o .: "text"+            <*> o .: "source"+            <*> o .: "user"+            <*> o .:? "coordinates"+    parseJSON v = fail $ "couldn't parse status search result from: " ++ show v++instance ToJSON SearchStatus where+    toJSON SearchStatus {..} =+        object+            [ "created_at" .= TwitterTime searchStatusCreatedAt+            , "id" .= searchStatusId+            , "text" .= searchStatusText+            , "source" .= searchStatusSource+            , "user" .= searchStatusUser+            , "coordinates" .= searchStatusCoordinates+            ]++data SearchMetadata = SearchMetadata+    { searchMetadataMaxId :: StatusId+    , searchMetadataSinceId :: StatusId+    , searchMetadataRefreshURL :: URIString+    , searchMetadataNextResults :: Maybe URIString+    , searchMetadataCount :: Int+    , searchMetadataCompletedIn :: Maybe Float+    , searchMetadataSinceIdStr :: String+    , searchMetadataQuery :: String+    , searchMetadataMaxIdStr :: String+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON SearchMetadata where+    parseJSON (Object o) =+        checkError o+            >> SearchMetadata <$> o .: "max_id"+            <*> o .: "since_id"+            <*> o .: "refresh_url"+            <*> o .:? "next_results"+            <*> o .: "count"+            <*> o .:? "completed_in"+            <*> o .: "since_id_str"+            <*> o .: "query"+            <*> o .: "max_id_str"+    parseJSON v = fail $ "couldn't parse search metadata from: " ++ show v++instance ToJSON SearchMetadata where+    toJSON SearchMetadata {..} =+        object+            [ "max_id" .= searchMetadataMaxId+            , "since_id" .= searchMetadataSinceId+            , "refresh_url" .= searchMetadataRefreshURL+            , "next_results" .= searchMetadataNextResults+            , "count" .= searchMetadataCount+            , "completed_in" .= searchMetadataCompletedIn+            , "since_id_str" .= searchMetadataSinceIdStr+            , "query" .= searchMetadataQuery+            , "max_id_str" .= searchMetadataMaxIdStr+            ]++data RetweetedStatus = RetweetedStatus+    { rsCreatedAt :: UTCTime+    , rsId :: StatusId+    , rsText :: Text+    , rsSource :: Text+    , rsTruncated :: Bool+    , rsEntities :: Maybe Entities+    , rsUser :: User+    , rsRetweetedStatus :: Status+    , rsCoordinates :: Maybe Coordinates+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON RetweetedStatus where+    parseJSON (Object o) =+        checkError o+            >> RetweetedStatus <$> (o .: "created_at" >>= return . fromTwitterTime)+            <*> o .: "id"+            <*> o .: "text"+            <*> o .: "source"+            <*> o .: "truncated"+            <*> o .:? "entities"+            <*> o .: "user"+            <*> o .: "retweeted_status"+            <*> o .:? "coordinates"+    parseJSON v = fail $ "couldn't parse retweeted status from: " ++ show v++instance ToJSON RetweetedStatus where+    toJSON RetweetedStatus {..} =+        object+            [ "created_at" .= TwitterTime rsCreatedAt+            , "id" .= rsId+            , "text" .= rsText+            , "source" .= rsSource+            , "truncated" .= rsTruncated+            , "entities" .= rsEntities+            , "user" .= rsUser+            , "retweeted_status" .= rsRetweetedStatus+            , "coordinates" .= rsCoordinates+            ]++type EventId = Integer++data DirectMessage = DirectMessage+    { dmId :: EventId+    , dmCreatedTimestamp :: UTCTime+    , dmTargetRecipientId :: UserId+    , dmSenderId :: UserId+    , dmText :: Text+    , dmEntities :: Entities+    }+    deriving (Show, Eq, Data, Typeable, Generic)++parseIntegral :: Integral a => Text -> Parser a+parseIntegral v = either (\_ -> fail $ "couldn't parse stringized int: " ++ show v) (return . fst) $ decimal v++epochMsToUTCTime :: Int64 -> UTCTime+epochMsToUTCTime = posixSecondsToUTCTime . fromRational . (% 1000) . fromIntegral++parseUnixTimeString :: Text -> Parser UTCTime+parseUnixTimeString = fmap epochMsToUTCTime <$> parseIntegral++unixTimeToEpochInt :: UTCTime -> Int+unixTimeToEpochInt = floor . (* 1000) . utcTimeToPOSIXSeconds++instance FromJSON DirectMessage where+    parseJSON (Object o) = do+        _ <- checkError o+        messageCreate <- o .: "message_create"+        messageData <- messageCreate .: "message_data"++        DirectMessage+            <$> (o .: "id" >>= parseIntegral)+            <*> (o .: "created_timestamp" >>= parseUnixTimeString)+            <*> (messageCreate .: "target" >>= (.: "recipient_id") >>= parseIntegral)+            <*> (messageCreate .: "sender_id" >>= parseIntegral)+            <*> messageData .: "text"+            <*> messageData .: "entities"+    parseJSON v = fail $ "couldn't parse direct message create event from: " ++ show v++instance ToJSON DirectMessage where+    toJSON DirectMessage {..} =+        object+            [ "id" .= show dmId+            , "created_timestamp" .= show (unixTimeToEpochInt dmCreatedTimestamp)+            , "message_create"+                .= object+                    [ "message_data" .= object ["text" .= dmText, "entities" .= dmEntities]+                    , "target" .= object ["recipient_id" .= show dmTargetRecipientId]+                    , "sender_id" .= show dmSenderId+                    ]+            ]++data EventType+    = Favorite+    | Unfavorite+    | ListCreated+    | ListUpdated+    | ListMemberAdded+    | UserUpdate+    | Block+    | Unblock+    | Follow+    deriving (Show, Eq, Data, Typeable, Generic)++data EventTarget = ETUser User | ETStatus Status | ETList List | ETUnknown Value+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON EventTarget where+    parseJSON v@(Object o) =+        checkError o+            >> ETUser <$> parseJSON v+            <|> ETStatus <$> parseJSON v+            <|> ETList <$> parseJSON v+            <|> return (ETUnknown v)+    parseJSON v = fail $ "couldn't parse event target from: " ++ show v++instance ToJSON EventTarget where+    toJSON (ETUser u) = toJSON u+    toJSON (ETStatus s) = toJSON s+    toJSON (ETList l) = toJSON l+    toJSON (ETUnknown v) = v++data Event = Event+    { evCreatedAt :: UTCTime+    , evTargetObject :: Maybe EventTarget+    , evEvent :: Text+    , evTarget :: EventTarget+    , evSource :: EventTarget+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON Event where+    parseJSON (Object o) =+        checkError o+            >> Event <$> (o .: "created_at" >>= return . fromTwitterTime)+            <*> o .:? "target_object"+            <*> o .: "event"+            <*> o .: "target"+            <*> o .: "source"+    parseJSON v = fail $ "couldn't parse event from: " ++ show v++instance ToJSON Event where+    toJSON Event {..} =+        object+            [ "created_at" .= TwitterTime evCreatedAt+            , "target_object" .= evTargetObject+            , "event" .= evEvent+            , "target" .= evTarget+            , "source" .= evSource+            ]++data Delete = Delete+    { delId :: StatusId+    , delUserId :: UserId+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON Delete where+    parseJSON (Object o) =+        checkError o >> do+            s <- o .: "delete" >>= (.: "status")+            Delete <$> s .: "id"+                <*> s .: "user_id"+    parseJSON v = fail $ "couldn't parse delete from: " ++ show v++instance ToJSON Delete where+    toJSON Delete {..} =+        object+            [ "delete"+                .= object+                    [ "status"+                        .= object+                            [ "id" .= delId+                            , "user_id" .= delUserId+                            ]+                    ]+            ]++-- | This type represents the Twitter user.+-- See <https://dev.twitter.com/docs/platform-objects/users>.+data User = User+    { userContributorsEnabled :: Bool+    , userCreatedAt :: UTCTime+    , userDefaultProfile :: Bool+    , userDefaultProfileImage :: Bool+    , userDescription :: Maybe Text+    , userEmail :: Maybe Text+    , userFavoritesCount :: Int+    , userFollowRequestSent :: Maybe Bool+    , userFollowing :: Maybe Bool+    , userFollowersCount :: Int+    , userFriendsCount :: Int+    , userGeoEnabled :: Bool+    , userId :: UserId+    , userIsTranslator :: Bool+    , userLang :: Maybe LanguageCode+    , userListedCount :: Int+    , userLocation :: Maybe Text+    , userName :: Text+    , userNotifications :: Maybe Bool+    , userProfileBackgroundColor :: Maybe Text+    , userProfileBackgroundImageURL :: Maybe URIString+    , userProfileBackgroundImageURLHttps :: Maybe URIString+    , userProfileBackgroundTile :: Maybe Bool+    , userProfileBannerURL :: Maybe URIString+    , userProfileImageURL :: Maybe URIString+    , userProfileImageURLHttps :: Maybe URIString+    , userProfileLinkColor :: Text+    , userProfileSidebarBorderColor :: Text+    , userProfileSidebarFillColor :: Text+    , userProfileTextColor :: Text+    , userProfileUseBackgroundImage :: Bool+    , userProtected :: Bool+    , userScreenName :: Text+    , userShowAllInlineMedia :: Maybe Bool+    , userStatusesCount :: Int+    , userTimeZone :: Maybe Text+    , userURL :: Maybe URIString+    , userUtcOffset :: Maybe Int+    , userVerified :: Bool+    , userWithheldInCountries :: Maybe [Text]+    , userWithheldScope :: Maybe Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON User where+    parseJSON (Object o) =+        checkError o+            >> User <$> o .: "contributors_enabled"+            <*> (o .: "created_at" >>= return . fromTwitterTime)+            <*> o .: "default_profile"+            <*> o .: "default_profile_image"+            <*> o .:? "description"+            <*> fmap join (o .:? "email") -- The field can be a null value+            <*> o .: "favourites_count"+            <*> o .:? "follow_request_sent" .!= Nothing+            <*> o .:? "following" .!= Nothing+            <*> o .: "followers_count"+            <*> o .: "friends_count"+            <*> o .: "geo_enabled"+            <*> o .: "id"+            <*> o .: "is_translator"+            <*> o .: "lang"+            <*> o .: "listed_count"+            <*> o .:? "location"+            <*> o .: "name"+            <*> o .:? "notifications" .!= Nothing+            <*> o .:? "profile_background_color"+            <*> o .:? "profile_background_image_url"+            <*> o .:? "profile_background_image_url_https"+            <*> o .:? "profile_background_tile"+            <*> o .:? "profile_banner_url"+            <*> o .:? "profile_image_url"+            <*> o .:? "profile_image_url_https"+            <*> o .: "profile_link_color"+            <*> o .: "profile_sidebar_border_color"+            <*> o .: "profile_sidebar_fill_color"+            <*> o .: "profile_text_color"+            <*> o .: "profile_use_background_image"+            <*> o .: "protected"+            <*> o .: "screen_name"+            <*> o .:? "show_all_inline_media"+            <*> o .: "statuses_count"+            <*> o .:? "time_zone"+            <*> o .:? "url" .!= Nothing+            <*> o .:? "utc_offset"+            <*> o .: "verified"+            <*> o .:? "withheld_in_countries"+            <*> o .:? "withheld_scope"+    parseJSON v = fail $ "couldn't parse user from: " ++ show v++instance ToJSON User where+    toJSON User {..} =+        object+            [ "contributors_enabled" .= userContributorsEnabled+            , "created_at" .= TwitterTime userCreatedAt+            , "default_profile" .= userDefaultProfile+            , "default_profile_image" .= userDefaultProfileImage+            , "description" .= userDescription+            , "email" .= userEmail+            , "favourites_count" .= userFavoritesCount+            , "follow_request_sent" .= userFollowRequestSent+            , "following" .= userFollowing+            , "followers_count" .= userFollowersCount+            , "friends_count" .= userFriendsCount+            , "geo_enabled" .= userGeoEnabled+            , "id" .= userId+            , "is_translator" .= userIsTranslator+            , "lang" .= userLang+            , "listed_count" .= userListedCount+            , "location" .= userLocation+            , "name" .= userName+            , "notifications" .= userNotifications+            , "profile_background_color" .= userProfileBackgroundColor+            , "profile_background_image_url" .= userProfileBackgroundImageURL+            , "profile_background_image_url_https" .= userProfileBackgroundImageURLHttps+            , "profile_background_tile" .= userProfileBackgroundTile+            , "profile_banner_url" .= userProfileBannerURL+            , "profile_image_url" .= userProfileImageURL+            , "profile_image_url_https" .= userProfileImageURLHttps+            , "profile_link_color" .= userProfileLinkColor+            , "profile_sidebar_border_color" .= userProfileSidebarBorderColor+            , "profile_sidebar_fill_color" .= userProfileSidebarFillColor+            , "profile_text_color" .= userProfileTextColor+            , "profile_use_background_image" .= userProfileUseBackgroundImage+            , "protected" .= userProtected+            , "screen_name" .= userScreenName+            , "show_all_inline_media" .= userShowAllInlineMedia+            , "statuses_count" .= userStatusesCount+            , "time_zone" .= userTimeZone+            , "url" .= userURL+            , "utc_offset" .= userUtcOffset+            , "verified" .= userVerified+            , "withheld_in_countries" .= userWithheldInCountries+            , "withheld_scope" .= userWithheldScope+            ]++data List = List+    { listId :: Int+    , listName :: Text+    , listFullName :: Text+    , listMemberCount :: Int+    , listSubscriberCount :: Int+    , listMode :: Text+    , listUser :: User+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON List where+    parseJSON (Object o) =+        checkError o+            >> List <$> o .: "id"+            <*> o .: "name"+            <*> o .: "full_name"+            <*> o .: "member_count"+            <*> o .: "subscriber_count"+            <*> o .: "mode"+            <*> o .: "user"+    parseJSON v = fail $ "couldn't parse List from: " ++ show v++instance ToJSON List where+    toJSON List {..} =+        object+            [ "id" .= listId+            , "name" .= listName+            , "full_name" .= listFullName+            , "member_count" .= listMemberCount+            , "subscriber_count" .= listSubscriberCount+            , "mode" .= listMode+            , "user" .= listUser+            ]++-- | Hashtag entity.+-- See <https://dev.twitter.com/docs/platform-objects/entities#obj-hashtags>.+data HashTagEntity = HashTagEntity+    { -- | The Hashtag text+      hashTagText :: Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON HashTagEntity where+    parseJSON (Object o) =+        HashTagEntity <$> o .: "text"+    parseJSON v = fail $ "couldn't parse hashtag entity from: " ++ show v++instance ToJSON HashTagEntity where+    toJSON HashTagEntity {..} = object ["text" .= hashTagText]++-- | User mention entity.+-- See <https://dev.twitter.com/docs/platform-objects/entities#obj-usermention>.+data UserEntity = UserEntity+    { userEntityUserId :: UserId+    , userEntityUserName :: UserName+    , userEntityUserScreenName :: Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON UserEntity where+    parseJSON (Object o) =+        UserEntity <$> o .: "id"+            <*> o .: "name"+            <*> o .: "screen_name"+    parseJSON v = fail $ "couldn't parse user entity from: " ++ show v++instance ToJSON UserEntity where+    toJSON UserEntity {..} =+        object+            [ "id" .= userEntityUserId+            , "name" .= userEntityUserName+            , "screen_name" .= userEntityUserScreenName+            ]++-- | URL entity.+-- See <https://dev.twitter.com/docs/platform-objects/entities#obj-url>.+data URLEntity = URLEntity+    { -- | The URL that was extracted+      ueURL :: URIString+    , -- | The fully resolved URL (only for t.co links)+      ueExpanded :: URIString+    , -- | Not a URL but a string to display instead of the URL (only for t.co links)+      ueDisplay :: Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON URLEntity where+    parseJSON (Object o) =+        URLEntity <$> o .: "url"+            <*> o .: "expanded_url"+            <*> o .: "display_url"+    parseJSON v = fail $ "couldn't parse url entity from: " ++ show v++instance ToJSON URLEntity where+    toJSON URLEntity {..} =+        object+            [ "url" .= ueURL+            , "expanded_url" .= ueExpanded+            , "display_url" .= ueDisplay+            ]++data MediaEntity = MediaEntity+    { meType :: Text+    , meId :: StatusId+    , meSizes :: HashMap Text MediaSize+    , meMediaURL :: URIString+    , meMediaURLHttps :: URIString+    , meURL :: URLEntity+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON MediaEntity where+    parseJSON v@(Object o) =+        MediaEntity <$> o .: "type"+            <*> o .: "id"+            <*> o .: "sizes"+            <*> o .: "media_url"+            <*> o .: "media_url_https"+            <*> parseJSON v+    parseJSON v = fail $ "couldn't parse media entity from: " ++ show v++instance ToJSON MediaEntity where+    toJSON MediaEntity {..} =+        object+            [ "type" .= meType+            , "id" .= meId+            , "sizes" .= meSizes+            , "media_url" .= meMediaURL+            , "media_url_https" .= meMediaURLHttps+            , "url" .= ueURL meURL+            , "expanded_url" .= ueExpanded meURL+            , "display_url" .= ueDisplay meURL+            ]++-- | Size entity.+-- See <https://dev.twitter.com/docs/platform-objects/entities#obj-size>.+data MediaSize = MediaSize+    { msWidth :: Int+    , msHeight :: Int+    , msResize :: Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON MediaSize where+    parseJSON (Object o) =+        MediaSize <$> o .: "w"+            <*> o .: "h"+            <*> o .: "resize"+    parseJSON v = fail $ "couldn't parse media size from: " ++ show v++instance ToJSON MediaSize where+    toJSON MediaSize {..} =+        object+            [ "w" .= msWidth+            , "h" .= msHeight+            , "resize" .= msResize+            ]++data Coordinates = Coordinates+    { coordinates :: [Double]+    , coordinatesType :: Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON Coordinates where+    parseJSON (Object o) =+        Coordinates <$> o .: "coordinates"+            <*> o .: "type"+    parseJSON v = fail $ "couldn't parse coordinates from: " ++ show v++instance ToJSON Coordinates where+    toJSON Coordinates {..} =+        object+            [ "coordinates" .= coordinates+            , "type" .= coordinatesType+            ]++-- | This type represents a place, named locations with corresponding geo coordinates.+-- See <https://dev.twitter.com/docs/platform-objects/places>.+data Place = Place+    { placeAttributes :: HashMap Text Text+    , placeBoundingBox :: Maybe BoundingBox+    , placeCountry :: Text+    , placeCountryCode :: Text+    , placeFullName :: Text+    , placeId :: Text+    , placeName :: Text+    , placeType :: Text+    , placeURL :: Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON Place where+    parseJSON (Object o) =+        Place <$> o .: "attributes"+            <*> o .:? "bounding_box"+            <*> o .: "country"+            <*> o .: "country_code"+            <*> o .: "full_name"+            <*> o .: "id"+            <*> o .: "name"+            <*> o .: "place_type"+            <*> o .: "url"+    parseJSON v = fail $ "couldn't parse place from: " ++ show v++instance ToJSON Place where+    toJSON Place {..} =+        object+            [ "attributes" .= placeAttributes+            , "bounding_box" .= placeBoundingBox+            , "country" .= placeCountry+            , "country_code" .= placeCountryCode+            , "full_name" .= placeFullName+            , "id" .= placeId+            , "name" .= placeName+            , "place_type" .= placeType+            , "url" .= placeURL+            ]++-- | A bounding box of coordinates which encloses the place.+-- See <https://dev.twitter.com/docs/platform-objects/places#obj-boundingbox>.+data BoundingBox = BoundingBox+    { boundingBoxCoordinates :: [[[Double]]]+    , boundingBoxType :: Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON BoundingBox where+    parseJSON (Object o) =+        BoundingBox <$> o .: "coordinates"+            <*> o .: "type"+    parseJSON v = fail $ "couldn't parse bounding box from: " ++ show v++instance ToJSON BoundingBox where+    toJSON BoundingBox {..} =+        object+            [ "coordinates" .= boundingBoxCoordinates+            , "type" .= boundingBoxType+            ]++-- | Entity handling.+-- See <https://dev.twitter.com/docs/platform-objects/entities>.+data Entities = Entities+    { enHashTags :: [Entity HashTagEntity]+    , enUserMentions :: [Entity UserEntity]+    , enURLs :: [Entity URLEntity]+    , enMedia :: [Entity MediaEntity]+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON Entities where+    parseJSON (Object o) =+        Entities <$> o .:? "hashtags" .!= []+            <*> o .:? "user_mentions" .!= []+            <*> o .:? "urls" .!= []+            <*> o .:? "media" .!= []+    parseJSON v = fail $ "couldn't parse entities from: " ++ show v++instance ToJSON Entities where+    toJSON Entities {..} =+        object+            [ "hashtags" .= enHashTags+            , "user_mentions" .= enUserMentions+            , "urls" .= enURLs+            , "media" .= enMedia+            ]++-- | The character positions the Entity was extracted from+--+--   This is experimental implementation.+--   This may be replaced by more definite types.+type EntityIndices = [Int]++data Entity a = Entity+    { -- | The detail information of the specific entity types (HashTag, URL, User)+      entityBody :: a+    , -- | The character positions the Entity was extracted from+      entityIndices :: EntityIndices+    }+    deriving (Show, Eq, Data, Typeable, Generic, Generic1)++instance FromJSON a => FromJSON (Entity a) where+    parseJSON v@(Object o) =+        Entity <$> parseJSON v+            <*> o .: "indices"+    parseJSON v = fail $ "couldn't parse entity wrapper from: " ++ show v++instance ToJSON a => ToJSON (Entity a) where+    toJSON Entity {..} =+        case toJSON entityBody of+            (Object o) -> Object $ KeyMap.insert "indices" (toJSON entityIndices) o+            _ -> error "Entity body must produce an object."++data ExtendedEntities = ExtendedEntities+    { exeMedia :: [Entity ExtendedEntity]+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON ExtendedEntities where+    parseJSON (Object o) =+        ExtendedEntities <$> o .:? "media" .!= []+    parseJSON v = fail $ "couldn't parse extended entity from: " ++ show v++instance ToJSON ExtendedEntities where+    toJSON ExtendedEntities {..} = object ["media" .= exeMedia]++-- "video_info": {+--   "aspect_ratio": [+--     9,+--     16+--   ],+--   "duration_millis": 10704,+--   "variants": [+--     {+--       "bitrate": 320000,+--       "content_type": "video/mp4",+--       "url": "https://video.twimg.com/ext_tw_video/869317980307415040/pu/vid/180x320/FMei8yCw7yc_Z7e-.mp4"+--     },+--     {+--       "bitrate": 2176000,+--       "content_type": "video/mp4",+--       "url": "https://video.twimg.com/ext_tw_video/869317980307415040/pu/vid/720x1280/octt5pFbISkef8RB.mp4"+--     },+--     {+--       "bitrate": 832000,+--       "content_type": "video/mp4",+--       "url": "https://video.twimg.com/ext_tw_video/869317980307415040/pu/vid/360x640/2OmqK74SQ9jNX8mZ.mp4"+--     },+--     {+--       "content_type": "application/x-mpegURL",+--       "url": "https://video.twimg.com/ext_tw_video/869317980307415040/pu/pl/wcJQJ2nxiFU4ZZng.m3u8"+--     }+--   ]+-- }++data Variant = Variant+    { vBitrate :: Maybe Int+    , vContentType :: Text+    , vUrl :: URIString+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON Variant where+    parseJSON (Object o) =+        Variant <$> o .:? "bitrate"+            <*> o .: "content_type"+            <*> o .: "url"+    parseJSON v = fail $ "couldn't parse variant from:" ++ show v++instance ToJSON Variant where+    toJSON Variant {..} =+        object+            [ "bitrate" .= vBitrate+            , "content_type" .= vContentType+            , "url" .= vUrl+            ]++data VideoInfo = VideoInfo+    { vsAspectRatio :: [Int]+    , vsDurationMillis :: Maybe Int+    , vsVariants :: [Variant]+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON VideoInfo where+    parseJSON (Object o) =+        VideoInfo <$> o .: "aspect_ratio" .!= []+            <*> o .:? "duration_millis"+            <*> o .: "variants" .!= []+    parseJSON v = fail $ "couldn't parse video info from:" ++ show v++instance ToJSON VideoInfo where+    toJSON VideoInfo {..} =+        object+            [ "aspect_ratio" .= vsAspectRatio+            , "duration_millis" .= vsDurationMillis+            , "variants" .= vsVariants+            ]++-- Extended entities are like entities, but contain special media features like+-- video or multiple photos+data ExtendedEntity = ExtendedEntity+    { exeID :: StatusId+    , exeMediaUrl :: URIString+    , exeMediaUrlHttps :: URIString+    , exeSizes :: HashMap Text MediaSize+    , exeType :: Text+    , exeVideoInfo :: Maybe VideoInfo+    , exeDurationMillis :: Maybe Double+    , exeExtAltText :: Maybe String+    , exeURL :: URLEntity+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON ExtendedEntity where+    parseJSON v@(Object o) =+        ExtendedEntity <$> o .: "id"+            <*> o .: "media_url"+            <*> o .: "media_url_https"+            <*> o .: "sizes"+            <*> o .: "type"+            <*> o .:? "video_info"+            <*> o .:? "duration_millis"+            <*> o .:? "ext_alt_text"+            <*> parseJSON v+    parseJSON v = fail $ "couldn't parse extended entity from:" ++ show v++instance ToJSON ExtendedEntity where+    toJSON ExtendedEntity {..} =+        object+            [ "id" .= exeID+            , "media_url" .= exeMediaUrl+            , "media_url_https" .= exeMediaUrlHttps+            , "sizes" .= exeSizes+            , "type" .= exeType+            , "video_info" .= exeVideoInfo+            , "duration_millis" .= exeDurationMillis+            , "ext_alt_text" .= exeExtAltText+            , "url" .= ueURL exeURL+            , "expanded_url" .= ueExpanded exeURL+            , "display_url" .= ueDisplay exeURL+            ]++data Contributor = Contributor+    { contributorId :: UserId+    , contributorScreenName :: Maybe Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON Contributor where+    parseJSON (Object o) =+        Contributor <$> o .: "id"+            <*> o .:? "screen_name"+    parseJSON v@(Number _) =+        Contributor <$> parseJSON v <*> pure Nothing+    parseJSON v = fail $ "couldn't parse contributor from: " ++ show v++instance ToJSON Contributor where+    toJSON Contributor {..} =+        object+            [ "id" .= contributorId+            , "screen_name" .= contributorScreenName+            ]++-- | Image size type. This type is included in the API response of \"\/1.1\/media\/upload.json\".+data ImageSizeType = ImageSizeType+    { imageSizeTypeWidth :: Int+    , imageSizeTypeHeight :: Int+    , imageSizeTypeType :: Text+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON ImageSizeType where+    parseJSON (Object o) =+        ImageSizeType <$> o .: "w"+            <*> o .: "h"+            <*> o .: "image_type"+    parseJSON v = fail $ "unknown value: " ++ show v++instance ToJSON ImageSizeType where+    toJSON ImageSizeType {..} =+        object+            [ "w" .= imageSizeTypeWidth+            , "h" .= imageSizeTypeHeight+            , "image_type" .= imageSizeTypeType+            ]++-- | This type is represents the API response of \"\/1.1\/media\/upload.json\".+-- See <https://dev.twitter.com/docs/api/multiple-media-extended-entities>.+data UploadedMedia = UploadedMedia+    { uploadedMediaId :: Integer+    , uploadedMediaSize :: Integer+    , uploadedMediaImage :: ImageSizeType+    }+    deriving (Show, Eq, Data, Typeable, Generic)++instance FromJSON UploadedMedia where+    parseJSON (Object o) =+        UploadedMedia <$> o .: "media_id"+            <*> o .: "size"+            <*> o .: "image"+    parseJSON v = fail $ "unknown value: " ++ show v++instance ToJSON UploadedMedia where+    toJSON UploadedMedia {..} =+        object+            [ "media_id" .= uploadedMediaId+            , "size" .= uploadedMediaSize+            , "image" .= uploadedMediaImage+            ]++-- | unicode code point indices, identifying the inclusive start and exclusive end of the displayable content of the Tweet.+data DisplayTextRange = DisplayTextRange+    { displayTextRangeStart :: Int+    , -- | exclusive+      displayTextRangeEnd :: Int+    }+    deriving (Show, Eq, Ord, Data, Typeable, Generic)++instance FromJSON DisplayTextRange where+    parseJSON v = do+        parseJSON v >>= \case+            [s, e] -> pure $ DisplayTextRange s e+            unexpected ->+                fail $ "parsing DisplayTextRange failed, expected [Int, Int], but got: " ++ show unexpected+instance ToJSON DisplayTextRange where+    toJSON (DisplayTextRange s e) = toJSON [s, e]
− Web/Twitter/Types/Lens.hs
@@ -1,415 +0,0 @@-{-# LANGUAGE OverloadedStrings, DeriveDataTypeable, RankNTypes, CPP, FlexibleInstances #-}--module Web.Twitter.Types.Lens-       ( DateString-       , UserId-       , Friends-       , URIString-       , UserName-       , StatusId-       , LanguageCode-       , StreamingAPI(..)-       , Status-       , SearchResult-       , SearchStatus-       , SearchMetadata-       , RetweetedStatus-       , DirectMessage-       , EventTarget(..)-       , Event-       , Delete-       , User-       , List-       , Entities-       , EntityIndices-       , Entity-       , HashTagEntity-       , UserEntity-       , URLEntity-       , MediaEntity-       , MediaSize-       , Coordinates-       , Place-       , BoundingBox--       , statusCreatedAt-       , statusId-       , statusText-       , statusSource-       , statusTruncated-       , statusEntities-       , statusExtendedEntities-       , statusInReplyTo-       , statusInReplyToUser-       , statusFavorite-       , statusRetweetCount-       , statusRetweet-       , statusUser-       , statusPlace-       , statusFavoriteCount-       , statusLang-       , statusPossiblySensitive-       , statusCoordinates--       , searchResultStatuses-       , searchResultSearchMetadata--       , searchStatusCreatedAt-       , searchStatusId-       , searchStatusText-       , searchStatusSource-       , searchStatusUser-       , searchStatusCoordinates--       , searchMetadataMaxId-       , searchMetadataSinceId-       , searchMetadataRefreshUrl-       , searchMetadataNextResults-       , searchMetadataCount-       , searchMetadataCompletedIn-       , searchMetadataSinceIdStr-       , searchMetadataQuery-       , searchMetadataMaxIdStr--       , rsCreatedAt-       , rsId-       , rsText-       , rsSource-       , rsTruncated-       , rsEntities-       , rsUser-       , rsRetweetedStatus-       , rsCoordinates--       , dmCreatedAt-       , dmSenderScreenName-       , dmSender-       , dmText-       , dmRecipientScreeName-       , dmId-       , dmRecipient-       , dmRecipientId-       , dmSenderId-       , dmCoordinates--       , evCreatedAt-       , evTargetObject-       , evEvent-       , evTarget-       , evSource--       , delId-       , delUserId--       , userId-       , userName-       , userScreenName-       , userDescription-       , userLocation-       , userProfileImageURL-       , userURL-       , userProtected-       , userFollowers-       , userFriends-       , userTweets-       , userLangCode-       , userCreatedAt--       , listId-       , listName-       , listFullName-       , listMemberCount-       , listSubscriberCount-       , listMode-       , listUser--       , hashTagText--       , userEntityUserId-       , userEntityUserName-       , userEntityUserScreenName--       , ueURL-       , ueExpanded-       , ueDisplay--       , meType-       , meId-       , meSizes-       , meMediaURL-       , meMediaURLHttps-       , meURL--       , msWidth-       , msHeight-       , msResize--       , coordinates-       , coordinatesType--       , placeAttributes-       , placeBoundingBox-       , placeCountry-       , placeCountryCode-       , placeFullName-       , placeId-       , placeName-       , placeType-       , placeUrl--       , boundingBoxCoordinates-       , boundingBoxType--       , enHashTags-       , enUserMentions-       , enURLs-       , enMedia--       , entityBody-       , entityIndices--       , AsStatus(..)-       , AsUser(..)-       )-       where--import Data.Functor-import qualified Web.Twitter.Types as TT-import Web.Twitter.Types-       ( DateString-       , UserId-       , Friends-       , URIString-       , UserName-       , StatusId-       , LanguageCode-       , StreamingAPI-       , Status-       , SearchResult-       , SearchStatus-       , SearchMetadata-       , RetweetedStatus-       , DirectMessage-       , EventTarget-       , Event-       , Delete-       , User-       , List-       , Entities-       , EntityIndices-       , Entity-       , HashTagEntity-       , UserEntity-       , URLEntity-       , MediaEntity-       , MediaSize-       , Coordinates-       , Place-       , BoundingBox-       )-import Data.Text (Text)-import Data.HashMap.Strict (HashMap)--type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t-type SimpleLens s a = Lens s s a a--#define HASHSIGN #-#define SIMPLE_LENS(name, s, a) \-name :: SimpleLens (s) (a);\-name f record = (\newVal -> record { TT.name = newVal }) <$> (f (TT.name record));\-{-HASHSIGN INLINE name HASHSIGN-}--#define TYPECHANGE_LENS(name, s) \-name :: Lens ((s) a) ((s) b) (a) (b);\-name f record = (\newVal -> record { TT.name = newVal }) <$> (f (TT.name record))--SIMPLE_LENS(statusCreatedAt           , Status,  DateString                   )-SIMPLE_LENS(statusId                  , Status,  StatusId                     )-SIMPLE_LENS(statusText                , Status,  Text                         )-SIMPLE_LENS(statusSource              , Status,  Text                         )-SIMPLE_LENS(statusTruncated           , Status,  Bool                         )-SIMPLE_LENS(statusEntities            , Status,  Maybe Entities               )-SIMPLE_LENS(statusExtendedEntities    , Status,  Maybe Entities               )-SIMPLE_LENS(statusInReplyTo           , Status,  Maybe StatusId               )-SIMPLE_LENS(statusInReplyToUser       , Status,  Maybe UserId                 )-SIMPLE_LENS(statusFavorite            , Status,  Maybe Bool                   )-SIMPLE_LENS(statusRetweetCount        , Status,  Maybe Integer                )-SIMPLE_LENS(statusRetweet             , Status,  Maybe Status                 )-SIMPLE_LENS(statusUser                , Status,  User                         )-SIMPLE_LENS(statusPlace               , Status,  Maybe Place                  )-SIMPLE_LENS(statusFavoriteCount       , Status,  Integer                      )-SIMPLE_LENS(statusLang                , Status,  Maybe Text                   )-SIMPLE_LENS(statusPossiblySensitive   , Status,  Maybe Bool                   )-SIMPLE_LENS(statusCoordinates         , Status,  Maybe Coordinates            )--TYPECHANGE_LENS(searchResultStatuses  , SearchResult                          )-SIMPLE_LENS(searchResultSearchMetadata, SearchResult body,  SearchMetadata    )--SIMPLE_LENS(searchStatusCreatedAt     , SearchStatus,  DateString             )-SIMPLE_LENS(searchStatusId            , SearchStatus,  StatusId               )-SIMPLE_LENS(searchStatusText          , SearchStatus,  Text                   )-SIMPLE_LENS(searchStatusSource        , SearchStatus,  Text                   )-SIMPLE_LENS(searchStatusUser          , SearchStatus,  User                   )-SIMPLE_LENS(searchStatusCoordinates   , SearchStatus,  Maybe Coordinates      )--SIMPLE_LENS(searchMetadataMaxId       , SearchMetadata,  StatusId             )-SIMPLE_LENS(searchMetadataSinceId     , SearchMetadata,  StatusId             )-SIMPLE_LENS(searchMetadataRefreshUrl  , SearchMetadata,  URIString            )-SIMPLE_LENS(searchMetadataNextResults , SearchMetadata,  Maybe URIString      )-SIMPLE_LENS(searchMetadataCount       , SearchMetadata,  Int                  )-SIMPLE_LENS(searchMetadataCompletedIn , SearchMetadata,  Maybe Float          )-SIMPLE_LENS(searchMetadataSinceIdStr  , SearchMetadata,  String               )-SIMPLE_LENS(searchMetadataQuery       , SearchMetadata,  String               )-SIMPLE_LENS(searchMetadataMaxIdStr    , SearchMetadata,  String               )--SIMPLE_LENS(rsCreatedAt               , RetweetedStatus,  DateString          )-SIMPLE_LENS(rsId                      , RetweetedStatus,  StatusId            )-SIMPLE_LENS(rsText                    , RetweetedStatus,  Text                )-SIMPLE_LENS(rsSource                  , RetweetedStatus,  Text                )-SIMPLE_LENS(rsTruncated               , RetweetedStatus,  Bool                )-SIMPLE_LENS(rsEntities                , RetweetedStatus,  Maybe Entities      )-SIMPLE_LENS(rsUser                    , RetweetedStatus,  User                )-SIMPLE_LENS(rsRetweetedStatus         , RetweetedStatus,  Status              )-SIMPLE_LENS(rsCoordinates             , RetweetedStatus,  Maybe Coordinates   )--SIMPLE_LENS(dmCreatedAt               , DirectMessage,  DateString            )-SIMPLE_LENS(dmSenderScreenName        , DirectMessage,  Text                  )-SIMPLE_LENS(dmSender                  , DirectMessage,  User                  )-SIMPLE_LENS(dmText                    , DirectMessage,  Text                  )-SIMPLE_LENS(dmRecipientScreeName      , DirectMessage,  Text                  )-SIMPLE_LENS(dmId                      , DirectMessage,  StatusId              )-SIMPLE_LENS(dmRecipient               , DirectMessage,  User                  )-SIMPLE_LENS(dmRecipientId             , DirectMessage,  UserId                )-SIMPLE_LENS(dmSenderId                , DirectMessage,  UserId                )-SIMPLE_LENS(dmCoordinates             , DirectMessage,  Maybe Coordinates     )--SIMPLE_LENS(evCreatedAt               , Event,  DateString                    )-SIMPLE_LENS(evTargetObject            , Event,  Maybe EventTarget             )-SIMPLE_LENS(evEvent                   , Event,  Text                          )-SIMPLE_LENS(evTarget                  , Event,  EventTarget                   )-SIMPLE_LENS(evSource                  , Event,  EventTarget                   )--SIMPLE_LENS(delId                     , Delete,  StatusId                     )-SIMPLE_LENS(delUserId                 , Delete,  UserId                       )--SIMPLE_LENS(userId                    , User,  UserId                         )-SIMPLE_LENS(userName                  , User,  UserName                       )-SIMPLE_LENS(userScreenName            , User,  Text                           )-SIMPLE_LENS(userDescription           , User,  Maybe Text                     )-SIMPLE_LENS(userLocation              , User,  Maybe Text                     )-SIMPLE_LENS(userProfileImageURL       , User,  Maybe URIString                )-SIMPLE_LENS(userURL                   , User,  Maybe URIString                )-SIMPLE_LENS(userProtected             , User,  Maybe Bool                     )-SIMPLE_LENS(userFollowers             , User,  Maybe Int                      )-SIMPLE_LENS(userFriends               , User,  Maybe Int                      )-SIMPLE_LENS(userTweets                , User,  Maybe Int                      )-SIMPLE_LENS(userLangCode              , User,  Maybe LanguageCode             )-SIMPLE_LENS(userCreatedAt             , User,  Maybe DateString               )--SIMPLE_LENS(listId                    , List,  Int                            )-SIMPLE_LENS(listName                  , List,  Text                           )-SIMPLE_LENS(listFullName              , List,  Text                           )-SIMPLE_LENS(listMemberCount           , List,  Int                            )-SIMPLE_LENS(listSubscriberCount       , List,  Int                            )-SIMPLE_LENS(listMode                  , List,  Text                           )-SIMPLE_LENS(listUser                  , List,  User                           )--SIMPLE_LENS(hashTagText               , HashTagEntity,  Text                  )--SIMPLE_LENS(userEntityUserId          , UserEntity,  UserId                   )-SIMPLE_LENS(userEntityUserName        , UserEntity,  UserName                 )-SIMPLE_LENS(userEntityUserScreenName  , UserEntity,  Text                     )--SIMPLE_LENS(ueURL                     , URLEntity,  URIString                 )-SIMPLE_LENS(ueExpanded                , URLEntity,  URIString                 )-SIMPLE_LENS(ueDisplay                 , URLEntity,  Text                      )--SIMPLE_LENS(meType                    , MediaEntity,  Text                    )-SIMPLE_LENS(meId                      , MediaEntity,  StatusId                )-SIMPLE_LENS(meSizes                   , MediaEntity,  HashMap Text MediaSize  )-SIMPLE_LENS(meMediaURL                , MediaEntity,  URIString               )-SIMPLE_LENS(meMediaURLHttps           , MediaEntity,  URIString               )-SIMPLE_LENS(meURL                     , MediaEntity,  URLEntity               )--SIMPLE_LENS(msWidth                   , MediaSize,  Int                       )-SIMPLE_LENS(msHeight                  , MediaSize,  Int                       )-SIMPLE_LENS(msResize                  , MediaSize,  Text                      )--SIMPLE_LENS(coordinates               , Coordinates,  [Double]                )-SIMPLE_LENS(coordinatesType           , Coordinates,  Text                    )--SIMPLE_LENS(placeAttributes           , Place,  HashMap Text Text             )-SIMPLE_LENS(placeBoundingBox          , Place,  BoundingBox                   )-SIMPLE_LENS(placeCountry              , Place,  Text                          )-SIMPLE_LENS(placeCountryCode          , Place,  Text                          )-SIMPLE_LENS(placeFullName             , Place,  Text                          )-SIMPLE_LENS(placeId                   , Place,  Text                          )-SIMPLE_LENS(placeName                 , Place,  Text                          )-SIMPLE_LENS(placeType                 , Place,  Text                          )-SIMPLE_LENS(placeUrl                  , Place,  Text                          )--SIMPLE_LENS(boundingBoxCoordinates    , BoundingBox,  [[[Double]]]            )-SIMPLE_LENS(boundingBoxType           , BoundingBox,  Text                    )--SIMPLE_LENS(enHashTags                , Entities,  [Entity HashTagEntity]     )-SIMPLE_LENS(enUserMentions            , Entities,  [Entity UserEntity]        )-SIMPLE_LENS(enURLs                    , Entities,  [Entity URLEntity]         )-SIMPLE_LENS(enMedia                   , Entities,  [Entity MediaEntity]       )--TYPECHANGE_LENS(entityBody            , Entity                                )-SIMPLE_LENS(entityIndices             , Entity a,  EntityIndices              )--class AsStatus s where-    status_id :: SimpleLens s StatusId-    text :: SimpleLens s Text-    user :: SimpleLens s User-    created_at :: SimpleLens s DateString-    geolocation :: SimpleLens s (Maybe Coordinates)--instance AsStatus Status where-    status_id = statusId-    text = statusText-    user = statusUser-    created_at = statusCreatedAt-    geolocation = statusCoordinates--instance AsStatus SearchStatus where-    status_id = searchStatusId-    text = searchStatusText-    user = searchStatusUser-    created_at = searchStatusCreatedAt-    geolocation = searchStatusCoordinates--instance AsStatus RetweetedStatus where-    status_id = rsId-    text = rsText-    user = rsUser-    created_at = rsCreatedAt-    geolocation = rsCoordinates--instance AsStatus DirectMessage where-    status_id = dmId-    text = dmText-    user = dmSender-    created_at = dmCreatedAt-    geolocation = dmCoordinates--class AsUser u where-    user_id :: SimpleLens u UserId-    name :: SimpleLens u UserName-    screen_name :: SimpleLens u Text--instance AsUser User where-    user_id = userId-    name = userName-    screen_name = userScreenName--instance AsUser UserEntity where-    user_id = userEntityUserId-    name = userEntityUserName-    screen_name = userEntityUserScreenName--instance AsUser (Entity UserEntity) where-    user_id = entityBody.userEntityUserId-    name = entityBody.userEntityUserName-    screen_name = entityBody.userEntityUserScreenName
tests/Fixtures.hs view
@@ -1,29 +1,25 @@-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-}  module Fixtures where  import Data.Aeson-import Data.Attoparsec.ByteString-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import Data.Maybe (fromJust)-import Text.Shakespeare.Text--fj :: T.Text -> Value-fj = fromJust . maybeResult . parse json . T.encodeUtf8--errorMsgJson :: Value-errorMsgJson = fj [st|{"request":"\/1\/statuses\/user_timeline.json","error":"Not authorized"}|]+import Data.ByteString as S+import System.FilePath+import Test.Tasty.HUnit -statusJson :: Value-statusJson = fj [st|{"created_at":"Sat Sep 10 22:23:38 +0000 2011","id":112652479837110273,"id_str":"112652479837110273","text":"@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http:\/\/t.co\/6J2EgYM","source":"\u003ca href=\"http:\/\/instagr.am\" rel=\"nofollow\"\u003eInstagram\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":783214,"in_reply_to_user_id_str":"783214","in_reply_to_screen_name":"twitter","user":{"id":299862462,"id_str":"299862462","name":"Eoin McMillan ","screen_name":"imeoin","location":"Twitter","description":"Eoin's photography account. See @mceoin for tweets.","url":"http:\/\/www.eoin.me","protected":false,"followers_count":6,"friends_count":0,"listed_count":0,"created_at":"Mon May 16 20:07:59 +0000 2011","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":277,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"131516","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_tile":true,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1380912173\/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/1380912173\/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png","profile_link_color":"009999","profile_sidebar_border_color":"EEEEEE","profile_sidebar_fill_color":"EFEFEF","profile_text_color":"333333","profile_use_background_image":true,"show_all_inline_media":false,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"favorited":false,"retweeted":false,"possibly_sensitive":false}|]+fixturePath :: FilePath -> FilePath+fixturePath filename = takeDirectory __FILE__ </> "fixtures" </> filename -statusEntityJson :: Value-statusEntityJson = fj [st|{"created_at":"Sat Sep 10 22:23:38 +0000 2011","id":112652479837110273,"id_str":"112652479837110273","text":"@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http:\/\/t.co\/6J2EgYM","source":"\u003ca href=\"http:\/\/instagr.am\" rel=\"nofollow\"\u003eInstagram\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":783214,"in_reply_to_user_id_str":"783214","in_reply_to_screen_name":"twitter","user":{"id":299862462,"id_str":"299862462","name":"Eoin McMillan ","screen_name":"imeoin","location":"Twitter","description":"Eoin's photography account. See @mceoin for tweets.","url":"http:\/\/www.eoin.me","protected":false,"followers_count":6,"friends_count":0,"listed_count":0,"created_at":"Mon May 16 20:07:59 +0000 2011","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":277,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"131516","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_tile":true,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1380912173\/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/1380912173\/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png","profile_link_color":"009999","profile_sidebar_border_color":"EEEEEE","profile_sidebar_fill_color":"EFEFEF","profile_text_color":"333333","profile_use_background_image":true,"show_all_inline_media":false,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"entities":{"hashtags":[{"text":"tcdisrupt","indices":[32,42]}],"urls":[{"url":"http:\/\/t.co\/6J2EgYM","expanded_url":"http:\/\/instagr.am\/p\/MuW67\/","display_url":"instagr.am\/p\/MuW67\/","indices":[67,86]}],"user_mentions":[{"screen_name":"twitter","name":"Twitter","id":783214,"id_str":"783214","indices":[0,8]},{"screen_name":"boscomonkey","name":"Bosco So","id":14792670,"id_str":"14792670","indices":[46,58]},{"screen_name":"episod","name":"Taylor Singletary","id":819797,"id_str":"819797","indices":[59,66]}]},"favorited":false,"retweeted":false,"possibly_sensitive":false}|]+readFixtureFile :: FilePath -> IO ByteString+readFixtureFile = S.readFile . fixturePath -mediaEntityJson :: Value-mediaEntityJson = fj [st|{"created_at":"Wed Sep 14 20:58:04 +0000 2011","id":114080493036773378,"id_str":"114080493036773378","text":"Hello America! http:\/\/t.co\/rJC5Pxsu","source":"web","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":373487136,"id_str":"373487136","name":"Y U No @rno?","screen_name":"yunorno","location":"Paris, France","url":"http:\/\/twitter.com\/rno","description":"Oui oui!","protected":false,"followers_count":32,"friends_count":3,"listed_count":2,"created_at":"Wed Sep 14 17:24:54 +0000 2011","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":27,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1542800106\/oui_normal.png","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/1542800106\/oui_normal.png","profile_link_color":"0084B4","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"default_profile":true,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":4,"entities":{"hashtags":[],"urls":[],"user_mentions":[],"media":[{"id":114080493040967680,"id_str":"114080493040967680","indices":[15,35],"media_url":"http:\/\/pbs.twimg.com\/media\/AZVLmp-CIAAbkyy.jpg","media_url_https":"https:\/\/pbs.twimg.com\/media\/AZVLmp-CIAAbkyy.jpg","url":"http:\/\/t.co\/rJC5Pxsu","display_url":"pic.twitter.com\/rJC5Pxsu","expanded_url":"http:\/\/twitter.com\/yunorno\/status\/114080493036773378\/photo\/1","type":"photo","sizes":{"large":{"w":226,"h":238,"resize":"fit"},"thumb":{"w":150,"h":150,"resize":"crop"},"medium":{"w":226,"h":238,"resize":"fit"},"small":{"w":226,"h":238,"resize":"fit"}}}]},"favorited":false,"retweeted":false,"possibly_sensitive":false}|]+withFixtureJSON :: FromJSON a => FilePath -> (a -> Assertion) -> Assertion+withFixtureJSON filename assertions = do+    body <- readFixtureFile filename+    withJSON body assertions -mediaExtendedEntityJson :: Value-mediaExtendedEntityJson = fj [st|{"in_reply_to_status_id":null,"id_str":"479666034073296896","truncated":false,"possibly_sensitive":false,"in_reply_to_screen_name":null,"extended_entities":{"media":[{"id_str":"479665892922376192","expanded_url":"http://twitter.com/thimura_test/status/479666034073296896/photo/1","url":"http://t.co/qOjPwmgLKO","media_url_https":"https://pbs.twimg.com/media/BqgdlpaCQAA5OSu.jpg","indices":[36,58],"id":479665892922376192,"media_url":"http://pbs.twimg.com/media/BqgdlpaCQAA5OSu.jpg","type":"photo","sizes":{"small":{"w":340,"resize":"fit","h":604},"large":{"w":576,"resize":"fit","h":1024},"medium":{"w":576,"resize":"fit","h":1024},"thumb":{"w":150,"resize":"crop","h":150}},"display_url":"pic.twitter.com/qOjPwmgLKO"},{"id_str":"479665897150234624","expanded_url":"http://twitter.com/thimura_test/status/479666034073296896/photo/1","url":"http://t.co/qOjPwmgLKO","media_url_https":"https://pbs.twimg.com/media/Bqgdl5KCQAA9g9V.jpg","indices":[36,58],"id":479665897150234624,"media_url":"http://pbs.twimg.com/media/Bqgdl5KCQAA9g9V.jpg","type":"photo","sizes":{"small":{"w":339,"resize":"fit","h":191},"large":{"w":1024,"resize":"fit","h":576},"medium":{"w":599,"resize":"fit","h":337},"thumb":{"w":150,"resize":"crop","h":150}},"display_url":"pic.twitter.com/qOjPwmgLKO"},{"id_str":"479665901545852929","expanded_url":"http://twitter.com/thimura_test/status/479666034073296896/photo/1","url":"http://t.co/qOjPwmgLKO","media_url_https":"https://pbs.twimg.com/media/BqgdmJiCEAEp0EI.jpg","indices":[36,58],"id":479665901545852929,"media_url":"http://pbs.twimg.com/media/BqgdmJiCEAEp0EI.jpg","type":"photo","sizes":{"small":{"w":339,"resize":"fit","h":225},"large":{"w":1024,"resize":"fit","h":678},"medium":{"w":599,"resize":"fit","h":397},"thumb":{"w":150,"resize":"crop","h":150}},"display_url":"pic.twitter.com/qOjPwmgLKO"},{"id_str":"479665905375256576","expanded_url":"http://twitter.com/thimura_test/status/479666034073296896/photo/1","url":"http://t.co/qOjPwmgLKO","media_url_https":"https://pbs.twimg.com/media/BqgdmXzCIAAa2lU.jpg","indices":[36,58],"id":479665905375256576,"media_url":"http://pbs.twimg.com/media/BqgdmXzCIAAa2lU.jpg","type":"photo","sizes":{"small":{"w":339,"resize":"fit","h":225},"large":{"w":1024,"resize":"fit","h":678},"medium":{"w":599,"resize":"fit","h":397},"thumb":{"w":150,"resize":"crop","h":150}},"display_url":"pic.twitter.com/qOjPwmgLKO"}]},"entities":{"symbols":[],"urls":[],"media":[{"id_str":"479665892922376192","expanded_url":"http://twitter.com/thimura_test/status/479666034073296896/photo/1","url":"http://t.co/qOjPwmgLKO","media_url_https":"https://pbs.twimg.com/media/BqgdlpaCQAA5OSu.jpg","indices":[36,58],"id":479665892922376192,"media_url":"http://pbs.twimg.com/media/BqgdlpaCQAA5OSu.jpg","type":"photo","sizes":{"small":{"w":340,"resize":"fit","h":604},"large":{"w":576,"resize":"fit","h":1024},"medium":{"w":576,"resize":"fit","h":1024},"thumb":{"w":150,"resize":"crop","h":150}},"display_url":"pic.twitter.com/qOjPwmgLKO"}],"user_mentions":[],"hashtags":[{"text":"testnyan","indices":[26,35]}]},"text":"multiple image tweet test #testnyan http://t.co/qOjPwmgLKO","in_reply_to_user_id_str":null,"favorited":false,"coordinates":null,"retweeted":false,"user":{"screen_name":"thimura_test","is_translation_enabled":false,"default_profile":true,"profile_image_url":"http://abs.twimg.com/sticky/default_profile_images/default_profile_6_normal.png","default_profile_image":true,"id_str":"2418883074","profile_background_image_url_https":"https://abs.twimg.com/images/themes/theme1/bg.png","protected":false,"location":"","entities":{"description":{"urls":[]}},"profile_background_color":"C0DEED","utc_offset":null,"url":null,"profile_text_color":"333333","profile_image_url_https":"https://abs.twimg.com/sticky/default_profile_images/default_profile_6_normal.png","verified":false,"statuses_count":5,"profile_background_tile":false,"following":false,"lang":"ja","follow_request_sent":false,"profile_sidebar_fill_color":"DDEEF6","time_zone":null,"name":"thimura test","profile_sidebar_border_color":"C0DEED","geo_enabled":false,"listed_count":0,"contributors_enabled":false,"created_at":"Sun Mar 30 11:47:17 +0000 2014","id":2418883074,"friends_count":2,"is_translator":false,"favourites_count":0,"notifications":false,"profile_background_image_url":"http://abs.twimg.com/images/themes/theme1/bg.png","profile_use_background_image":true,"description":"","profile_link_color":"0084B4","followers_count":2},"lang":"et","retweet_count":0,"in_reply_to_user_id":null,"created_at":"Thu Jun 19 16:44:28 +0000 2014","source":"\u003ca href=\"https://twitter.com/thimura\" rel=\"nofollow\"\u003e二階堂真紅\u003c/a\u003e","geo":null,"id":479666034073296896,"in_reply_to_status_id_str":null,"favorite_count":0,"contributors":null,"place":null}|]+withJSON :: FromJSON a => ByteString -> (a -> Assertion) -> Assertion+withJSON body assertions = do+    case eitherDecodeStrict' body of+        Left err -> assertFailure $ err+        Right result -> assertions result
+ tests/Instances.hs view
@@ -0,0 +1,180 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeOperators #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Instances where++import Control.Applicative+import Data.Aeson++#if MIN_VERSION_aeson(2, 0, 0)+import Data.Aeson.KeyMap as KeyMap+#else+import Data.HashMap.Strict as KeyMap+#endif+import Data.HashMap.Strict as HashMap+import Data.String+import qualified Data.Text as T+import Data.Time (UTCTime (..), defaultTimeLocale, fromGregorian, readTime)+import Generic.Random+import Test.Tasty.QuickCheck+import Web.Twitter.Types++instance IsString UTCTime where+    fromString = readTime defaultTimeLocale twitterTimeFormat++instance Arbitrary UTCTime where+    arbitrary =+        do+            randomDay <- choose (1, 29) :: Gen Int+            randomMonth <- choose (1, 12) :: Gen Int+            randomYear <- choose (2001, 2002) :: Gen Integer+            randomTime <- choose (0, 86401) :: Gen Int+            return $ UTCTime (fromGregorian randomYear randomMonth randomDay) (fromIntegral randomTime)++instance Arbitrary T.Text where+    arbitrary = T.pack <$> arbitrary++instance Arbitrary Value where+    arbitrary =+        elements+            [ Object KeyMap.empty+            , Object (KeyMap.fromList [("test", Number 2), ("value", String "non empty")])+            ]++-- derive makeArbitrary ''StreamingAPI++instance Arbitrary Status where+    arbitrary = do+        qt <- frequency [(5, Just <$> arbitrary), (95, pure Nothing)] :: Gen (Maybe Status)+        rt <- frequency [(5, Just <$> arbitrary), (95, pure Nothing)] :: Gen (Maybe Status)+        Status <$> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> pure Nothing+            <*> pure (statusId <$> qt)+            <*> pure qt+            <*> arbitrary+            <*> arbitrary+            <*> pure rt+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary++instance Arbitrary SearchStatus where+    arbitrary = genericArbitraryU+instance Arbitrary SearchMetadata where+    arbitrary = genericArbitraryU+instance Arbitrary RetweetedStatus where+    arbitrary = genericArbitraryU++instance Arbitrary DirectMessage where+    arbitrary = genericArbitrarySingleG customGens+      where+        customGens :: Gen Integer :+ ()+        customGens =+            (getNonNegative <$> arbitrary) :+ ()++instance Arbitrary EventTarget where+    arbitrary = genericArbitraryU+instance Arbitrary Event where+    arbitrary = genericArbitraryU+instance Arbitrary Delete where+    arbitrary = genericArbitraryU+instance Arbitrary User where+    arbitrary = genericArbitraryU+instance Arbitrary List where+    arbitrary = genericArbitraryU+instance Arbitrary HashTagEntity where+    arbitrary = genericArbitraryU+instance Arbitrary UserEntity where+    arbitrary = genericArbitraryU+instance Arbitrary URLEntity where+    arbitrary = genericArbitraryU++instance Arbitrary MediaEntity where+    arbitrary = do+        ms <- arbitrary+        MediaEntity+            <$> arbitrary+            <*> arbitrary+            <*> pure (HashMap.fromList [("medium", ms)])+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary++instance Arbitrary MediaSize where+    arbitrary = genericArbitraryU+instance Arbitrary Coordinates where+    arbitrary = genericArbitraryU++instance Arbitrary Place where+    arbitrary = do+        Place HashMap.empty+            <$> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary++instance Arbitrary BoundingBox where+    arbitrary = genericArbitraryU+instance Arbitrary Entities where+    arbitrary = genericArbitraryU+instance Arbitrary ExtendedEntities where+    arbitrary = genericArbitraryU+instance Arbitrary Variant where+    arbitrary = genericArbitraryU+instance Arbitrary VideoInfo where+    arbitrary = genericArbitraryU+instance Arbitrary ExtendedEntity where+    arbitrary = do+        ms <- arbitrary+        ExtendedEntity+            <$> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> pure (HashMap.fromList [("medium", ms)])+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary+            <*> arbitrary++instance Arbitrary a => Arbitrary (Entity a) where+    arbitrary = do+        a <- arbitrary+        ind <- arbitrary+        return $ Entity a ind++instance Arbitrary Contributor where+    arbitrary = genericArbitraryU+instance Arbitrary ImageSizeType where+    arbitrary = genericArbitraryU+instance Arbitrary UploadedMedia where+    arbitrary = genericArbitraryU+instance Arbitrary DisplayTextRange where+    arbitrary = genericArbitraryU
+ tests/PropFromToJSONTest.hs view
@@ -0,0 +1,85 @@+{-# LANGUAGE TemplateHaskell #-}++module PropFromToJSONTest where++import Data.Aeson+import qualified Data.Aeson.Types as Aeson+import Instances ()+import Test.Tasty+import Test.Tasty.QuickCheck+import Test.Tasty.TH+import Web.Twitter.Types++fromToJSON :: (Eq a, FromJSON a, ToJSON a) => a -> Bool+fromToJSON obj = case fromJSON . toJSON $ obj of+    Aeson.Error _ -> False+    Aeson.Success a -> a == obj++prop_fromToStatus :: Status -> Bool+prop_fromToStatus = fromToJSON++prop_fromToSearchStatus :: SearchStatus -> Bool+prop_fromToSearchStatus = fromToJSON++prop_fromToSearchMetadata :: SearchMetadata -> Bool+prop_fromToSearchMetadata = fromToJSON++prop_fromToRetweetedStatus :: RetweetedStatus -> Bool+prop_fromToRetweetedStatus = fromToJSON++prop_fromToDirectMessage :: DirectMessage -> Bool+prop_fromToDirectMessage = fromToJSON++prop_fromToEventTarget :: EventTarget -> Bool+prop_fromToEventTarget = fromToJSON++prop_fromToEvent :: Event -> Bool+prop_fromToEvent = fromToJSON++prop_fromToDelete :: Delete -> Bool+prop_fromToDelete = fromToJSON++prop_fromToUser :: User -> Bool+prop_fromToUser = fromToJSON++prop_fromToList :: List -> Bool+prop_fromToList = fromToJSON++prop_fromToHashTagEntity :: HashTagEntity -> Bool+prop_fromToHashTagEntity = fromToJSON++prop_fromToUserEntity :: UserEntity -> Bool+prop_fromToUserEntity = fromToJSON++prop_fromToURLEntity :: URLEntity -> Bool+prop_fromToURLEntity = fromToJSON++prop_fromToMediaEntity :: MediaEntity -> Bool+prop_fromToMediaEntity = fromToJSON++prop_fromToMediaSize :: MediaSize -> Bool+prop_fromToMediaSize = fromToJSON++prop_fromToCoordinates :: Coordinates -> Bool+prop_fromToCoordinates = fromToJSON++prop_fromToPlace :: Place -> Bool+prop_fromToPlace = fromToJSON++prop_fromToBoundingBox :: BoundingBox -> Bool+prop_fromToBoundingBox = fromToJSON++prop_fromToEntities :: Entities -> Bool+prop_fromToEntities = fromToJSON++prop_fromToContributor :: Contributor -> Bool+prop_fromToContributor = fromToJSON++prop_fromToImageSizeType :: ImageSizeType -> Bool+prop_fromToImageSizeType = fromToJSON++prop_fromToUploadedMedia :: UploadedMedia -> Bool+prop_fromToUploadedMedia = fromToJSON++tests :: TestTree+tests = $(testGroupGenerator)
+ tests/StatusTest.hs view
@@ -0,0 +1,245 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module StatusTest where++import Data.Aeson+import qualified Data.Aeson.Types as Aeson+import qualified Data.HashMap.Strict as M+import Data.Maybe+import Fixtures+import Instances ()+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.TH+import Web.Twitter.Types++example_compatibility_classic_13995 :: Status -> Assertion+example_compatibility_classic_13995 obj = do+    statusCreatedAt obj @?= "Mon Mar 07 15:13:47 +0000 2016"+    statusId obj @?= 706860403981099008+    statusText obj @?= "Peek-a-boo! https://t.co/R3P6waHxRa"+    statusSource obj @?= "<a href=\"http://www.apple.com/\" rel=\"nofollow\">OS X</a>"+    statusTruncated obj @?= False+    statusEntities obj+        @?= Just+            ( Entities+                { enHashTags = []+                , enUserMentions = []+                , enURLs = []+                , enMedia =+                    [ Entity+                        { entityBody =+                            MediaEntity+                                { meType = "photo"+                                , meId = 706860403746181121+                                , meSizes =+                                    M.fromList+                                        [+                                            ( "small"+                                            , MediaSize+                                                { msWidth = 340+                                                , msHeight = 226+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "large"+                                            , MediaSize+                                                { msWidth = 1024+                                                , msHeight = 680+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "medium"+                                            , MediaSize+                                                { msWidth = 600+                                                , msHeight = 398+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "thumb"+                                            , MediaSize+                                                { msWidth = 150+                                                , msHeight = 150+                                                , msResize = "crop"+                                                }+                                            )+                                        ]+                                , meMediaURL = "http://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg"+                                , meMediaURLHttps = "https://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg"+                                , meURL =+                                    URLEntity+                                        { ueURL = "https://t.co/R3P6waHxRa"+                                        , ueExpanded = "http://twitter.com/jeremycloud/status/706860403981099008/photo/1"+                                        , ueDisplay = "pic.twitter.com/R3P6waHxRa"+                                        }+                                }+                        , entityIndices = [12, 35]+                        }+                    ]+                }+            )+    statusExtendedEntities obj+        @?= Just+            ( ExtendedEntities+                { exeMedia =+                    [ Entity+                        { entityBody =+                            ExtendedEntity+                                { exeID = 706860403746181121+                                , exeMediaUrl = "http://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg"+                                , exeMediaUrlHttps = "https://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg"+                                , exeSizes =+                                    M.fromList+                                        [+                                            ( "small"+                                            , MediaSize+                                                { msWidth = 340+                                                , msHeight = 226+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "large"+                                            , MediaSize+                                                { msWidth = 1024+                                                , msHeight = 680+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "medium"+                                            , MediaSize+                                                { msWidth = 600+                                                , msHeight = 398+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "thumb"+                                            , MediaSize+                                                { msWidth = 150+                                                , msHeight = 150+                                                , msResize = "crop"+                                                }+                                            )+                                        ]+                                , exeType = "photo"+                                , exeVideoInfo = Nothing+                                , exeDurationMillis = Nothing+                                , exeExtAltText = Nothing+                                , exeURL =+                                    URLEntity+                                        { ueURL = "https://t.co/R3P6waHxRa"+                                        , ueExpanded = "http://twitter.com/jeremycloud/status/706860403981099008/photo/1"+                                        , ueDisplay = "pic.twitter.com/R3P6waHxRa"+                                        }+                                }+                        , entityIndices = [12, 35]+                        }+                    ]+                }+            )+    statusInReplyToStatusId obj @?= Nothing+    statusInReplyToUserId obj @?= Nothing+    statusFavorited obj @?= Just False+    statusQuotedStatus obj @?= Nothing+    statusQuotedStatusId obj @?= Nothing+    statusRetweetCount obj @?= 0+    (userScreenName . statusUser) obj @?= "jeremycloud"+    statusRetweetedStatus obj @?= Nothing+    statusPlace obj @?= Nothing+    statusFavoriteCount obj @?= 8+    statusLang obj @?= Just "en"+    statusPossiblySensitive obj @?= Just False+    statusCoordinates obj @?= Nothing++case_compatibility_classic_13995 :: Assertion+case_compatibility_classic_13995 = do+    withFixtureJSON "tweet-updates/compatibility_classic_13995.json" $ example_compatibility_classic_13995+    withFixtureJSON "tweet-updates/compatibility_classic_13995_extended.json" $ example_compatibility_classic_13995++-- case_compatibility_classic_hidden_13797 :: Assertion+-- case_compatibility_classic_hidden_13797 = withFixtureJSON "tweet-updates/compatibility_classic_hidden_13797" $ \obj -> do+-- case_compatibility_extended_13996 :: Assertion+-- case_compatibility_extended_13996 = withFixtureJSON "tweet-updates/compatibility_extended_13996" $ \obj -> do+-- case_compatibilityplus_classic_13994 :: Assertion+-- case_compatibilityplus_classic_13994 = withFixtureJSON "tweet-updates/compatibilityplus_classic_13994" $ \obj -> do+-- case_compatibilityplus_classic_hidden_13797 :: Assertion+-- case_compatibilityplus_classic_hidden_13797 = withFixtureJSON "tweet-updates/compatibilityplus_classic_hidden_13797" $ \obj -> do+-- case_compatibilityplus_extended_13997 :: Assertion+-- case_compatibilityplus_extended_13997 = withFixtureJSON "tweet-updates/compatibilityplus_extended_13997" $ \obj -> do+-- case_extended_classic_14002 :: Assertion+-- case_extended_classic_14002 = withFixtureJSON "tweet-updates/extended_classic_14002" $ \obj -> do++case_extended_classic_hidden_13761 :: Assertion+case_extended_classic_hidden_13761 = withFixtureJSON "tweet-updates/extended_classic_hidden_13761.json" $ \obj -> do+    statusTruncated obj @?= False+    statusExtendedEntities obj+        @?= Just+            ( ExtendedEntities+                { exeMedia =+                    [ Entity+                        { entityBody =+                            ExtendedEntity+                                { exeID = 743479379079004160+                                , exeMediaUrl = "http://pbs.twimg.com/tweet_video_thumb/ClFejl_VAAAo9Xk.jpg"+                                , exeMediaUrlHttps = "https://pbs.twimg.com/tweet_video_thumb/ClFejl_VAAAo9Xk.jpg"+                                , exeSizes =+                                    M.fromList+                                        [+                                            ( "small"+                                            , MediaSize+                                                { msWidth = 340+                                                , msHeight = 190+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "large"+                                            , MediaSize+                                                { msWidth = 480+                                                , msHeight = 268+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "medium"+                                            , MediaSize+                                                { msWidth = 480+                                                , msHeight = 268+                                                , msResize = "fit"+                                                }+                                            )+                                        ,+                                            ( "thumb"+                                            , MediaSize+                                                { msWidth = 150+                                                , msHeight = 150+                                                , msResize = "crop"+                                                }+                                            )+                                        ]+                                , exeType = "animated_gif"+                                , exeVideoInfo = Just (VideoInfo {vsAspectRatio = [120, 67], vsDurationMillis = Nothing, vsVariants = [Variant {vBitrate = Just 0, vContentType = "video/mp4", vUrl = "https://pbs.twimg.com/tweet_video/ClFejl_VAAAo9Xk.mp4"}]})+                                , exeDurationMillis = Nothing+                                , exeExtAltText = Nothing+                                , exeURL = URLEntity {ueURL = "https://t.co/VnJMDg3cbS", ueExpanded = "http://twitter.com/beyond_oneforty/status/743479431658758145/photo/1", ueDisplay = "pic.twitter.com/VnJMDg3cbS"}+                                }+                        , entityIndices = [48, 71]+                        }+                    ]+                }+            )++case_extended_extended_14001 :: Assertion+case_extended_extended_14001 = withFixtureJSON "tweet-updates/extended_extended_14001.json" $ \obj -> do+    statusText obj @?= "@twitter @twitterdev has more details about these changes at https://t.co/ZnXoRQy8mK.  Thanks for making @twitter more expressive! https://t.co/AWmiH870F7"+    statusTruncated obj @?= False+    statusDisplayTextRange obj @?= Just (DisplayTextRange 9 130)++tests :: TestTree+tests = $(testGroupGenerator)
tests/TypesTest.hs view
@@ -1,81 +1,266 @@-{-# LANGUAGE TemplateHaskell, QuasiQuotes #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-} -module Main where+module TypesTest where +import Data.Aeson+import qualified Data.Aeson.Types as Aeson+import qualified Data.HashMap.Strict as M+import Data.Maybe+import Data.Time.Clock.POSIX+import Fixtures+import Instances ()+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.TH import Web.Twitter.Types -import Test.Framework.TH.Prime-import Test.Framework.Providers.HUnit-import Test.HUnit-import Text.Shakespeare.Text+case_parseStatus :: Assertion+case_parseStatus = withFixtureJSON "status01.json" $ \obj -> do+    statusCreatedAt obj @?= "Sat Sep 10 22:23:38 +0000 2011"+    statusId obj @?= 112652479837110273+    statusText obj @?= "@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http://t.co/6J2EgYM"+    statusSource obj @?= "<a href=\"http://instagr.am\" rel=\"nofollow\">Instagram</a>"+    statusTruncated obj @?= False+    statusEntities obj @?= Nothing+    statusExtendedEntities obj @?= Nothing+    statusInReplyToStatusId obj @?= Nothing+    statusInReplyToUserId obj @?= Just 783214+    statusFavorited obj @?= Just False+    statusQuotedStatus obj @?= Nothing+    statusQuotedStatusId obj @?= Nothing+    statusRetweetCount obj @?= 0+    (userScreenName . statusUser) obj @?= "imeoin"+    statusRetweetedStatus obj @?= Nothing+    statusPlace obj @?= Nothing+    statusFavoriteCount obj @?= 0+    statusLang obj @?= Nothing+    statusPossiblySensitive obj @?= Just False+    statusCoordinates obj @?= Nothing -import Data.Aeson hiding (Error)-import Data.Aeson.Types (parseEither)-import qualified Data.HashMap.Strict as M-import Data.Maybe+case_parseStatusQuoted :: Assertion+case_parseStatusQuoted = withFixtureJSON "status_quoted.json" $ \obj -> do+    statusId obj @?= 641660763770372100+    statusText obj @?= "Wow! Congrats! https://t.co/EPMMldEcci"+    statusQuotedStatusId obj @?= Just 641653574284537900 -import Fixtures+    let qs = fromJust $ statusQuotedStatus obj+    statusCreatedAt qs @?= "Wed Sep 09 16:45:08 +0000 2015"+    statusId qs @?= 641653574284537900+    statusText qs @?= "Very happy to say that I'm joining @mesosphere as a Distributed Systems Engineer!"+    statusSource qs @?= "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>" -main :: IO ()-main = $(defaultMainGenerator)+    let ent = fromJust $ statusEntities qs+    enURLs ent @?= []+    enMedia ent @?= []+    enHashTags ent @?= []+    map (userEntityUserId . entityBody) (enUserMentions ent) @?= [1872399366]+    map (userEntityUserScreenName . entityBody) (enUserMentions ent) @?= ["mesosphere"] -withJSON :: FromJSON a-         => Value-         -> (a -> Assertion)-         -> Assertion-withJSON js f = either assertFailure id $ do-  o <- parseEither parseJSON js-  return $ f o+    statusExtendedEntities qs @?= Nothing+    statusInReplyToStatusId qs @?= Nothing+    statusInReplyToUserId qs @?= Nothing+    statusFavorited qs @?= Just False+    statusQuotedStatus qs @?= Nothing+    statusQuotedStatusId qs @?= Nothing+    statusRetweetCount qs @?= 7+    (userScreenName . statusUser) qs @?= "neil_conway"+    statusRetweeted qs @?= Just False+    statusRetweetedStatus qs @?= Nothing+    statusPlace qs @?= Nothing+    statusFavoriteCount qs @?= 63+    statusLang qs @?= Just "en"+    statusPossiblySensitive qs @?= Nothing+    statusCoordinates qs @?= Nothing -case_parseStatus :: Assertion-case_parseStatus = withJSON statusJson $ \obj -> do-  statusId obj @?= 112652479837110273-  statusRetweetCount obj @?= Just 0-  (userScreenName . statusUser) obj @?= "imeoin"-  statusEntities obj @?= Nothing+case_parseStatusWithPhoto :: Assertion+case_parseStatusWithPhoto = withFixtureJSON "status_thimura_with_photo.json" $ \obj -> do+    statusId obj @?= 491143410770657280+    statusText obj @?= "近所の海です http://t.co/FjSOU8dDoD"+    statusTruncated obj @?= False +    let ent = fromJust $ statusEntities obj+    enHashTags ent @?= []+    enUserMentions ent @?= []+    enURLs ent @?= []+    length (enMedia ent) @?= 1+    map (meMediaURLHttps . entityBody) (enMedia ent) @?= ["https://pbs.twimg.com/media/BtDkUVaCQAIpWBU.jpg"]++    let exents = fromJust $ statusExtendedEntities obj+    let media = exeMedia exents+    length media @?= 1+    let exent = entityBody $ head media+    exeID exent @?= 491143397378244610+    ueURL (exeURL exent) @?= "http://t.co/FjSOU8dDoD"++    statusInReplyToStatusId obj @?= Nothing+    statusInReplyToUserId obj @?= Nothing+    statusFavorited obj @?= Just False+    statusRetweetCount obj @?= 4+    (userScreenName . statusUser) obj @?= "thimura"+    statusRetweetedStatus obj @?= Nothing+    statusPlace obj @?= Nothing+    statusFavoriteCount obj @?= 9+    statusLang obj @?= Just "ja"+    statusPossiblySensitive obj @?= Just False+    statusCoordinates obj @?= Nothing+ case_parseStatusIncludeEntities :: Assertion-case_parseStatusIncludeEntities = withJSON statusEntityJson $ \obj -> do-  statusId obj @?= 112652479837110273-  statusRetweetCount obj @?= Just 0-  (userScreenName . statusUser) obj @?= "imeoin"-  let ent = fromMaybe (Entities [] [] [] []) $ statusEntities obj-  (map entityIndices . enHashTags) ent @?= [[32,42]]-  (hashTagText . entityBody . head . enHashTags) ent @?= "tcdisrupt"+case_parseStatusIncludeEntities = withFixtureJSON "status_with_entity.json" $ \obj -> do+    statusId obj @?= 112652479837110273+    statusRetweetCount obj @?= 0+    (userScreenName . statusUser) obj @?= "imeoin"+    let ent = fromMaybe (Entities [] [] [] []) $ statusEntities obj+    (map entityIndices . enHashTags) ent @?= [[32, 42]]+    (hashTagText . entityBody . head . enHashTags) ent @?= "tcdisrupt" +case_parseSearchStatusMetadata :: Assertion+case_parseSearchStatusMetadata = withFixtureJSON "search_haskell.json" $ \obj -> do+    let status = (searchResultStatuses obj) :: [Status]+    length status @?= 1++    let metadata = searchResultSearchMetadata obj+    searchMetadataMaxId metadata @?= 495597397733433345+    searchMetadataSinceId metadata @?= 0+    searchMetadataRefreshURL metadata @?= "?since_id=495597397733433345&q=haskell&include_entities=1"+    searchMetadataNextResults metadata @?= Just "?max_id=495594369802440705&q=haskell&include_entities=1"+    searchMetadataCount metadata @?= 1+    searchMetadataCompletedIn metadata @?= Just 0.043+    searchMetadataSinceIdStr metadata @?= "0"+    searchMetadataQuery metadata @?= "haskell"+    searchMetadataMaxIdStr metadata @?= "495597397733433345"++case_parseSearchStatusBodyStatus :: Assertion+case_parseSearchStatusBodyStatus = withFixtureJSON "search_haskell.json" $ \obj -> do+    let status = (searchResultStatuses obj) :: [Status]+    length status @?= 1+    statusText (head status) @?= "haskell"++case_parseSearchStatusBodySearchStatus :: Assertion+case_parseSearchStatusBodySearchStatus = withFixtureJSON "search_haskell.json" $ \obj -> do+    let status = (searchResultStatuses obj) :: [SearchStatus]+    length status @?= 1+    searchStatusText (head status) @?= "haskell"++data DMList = DMList+    { dmList :: [DirectMessage]+    }+    deriving (Show, Eq)+instance FromJSON DMList where+    parseJSON = withObject "DMList" $ \obj -> DMList <$> obj .: "events"++case_parseDirectMessageList :: Assertion+case_parseDirectMessageList =+    withFixtureJSON "direct_message_event_list.json" $ \obj -> do+        dmList obj+            @?= [ DirectMessage+                    { dmId = 123123123123123123+                    , dmCreatedTimestamp = read "2019-10-13 18:15:48.951 UTC"+                    , dmTargetRecipientId = 186712193+                    , dmSenderId = 69179963+                    , dmText = "hello @thimura"+                    , dmEntities =+                        Entities+                            { enHashTags = []+                            , enUserMentions =+                                [ Entity+                                    { entityBody =+                                        UserEntity+                                            { userEntityUserId = 69179963+                                            , userEntityUserName = "ちむら"+                                            , userEntityUserScreenName = "thimura"+                                            }+                                    , entityIndices = [6, 14]+                                    }+                                ]+                            , enURLs = []+                            , enMedia = []+                            }+                    }+                , DirectMessage+                    { dmId = 25252525252525+                    , dmCreatedTimestamp = read "2019-10-13 18:06:46.14 UTC"+                    , dmTargetRecipientId = 186712193+                    , dmSenderId = 69179963+                    , dmText = "hello"+                    , dmEntities = Entities {enHashTags = [], enUserMentions = [], enURLs = [], enMedia = []}+                    }+                ]++case_parseEventFavorite :: Assertion+case_parseEventFavorite = withFixtureJSON "event_favorite_thimura.json" $ \obj -> do+    evCreatedAt obj @?= "Sat Aug 02 16:32:01 +0000 2014"+    evEvent obj @?= "favorite"+    let Just (ETStatus targetObj) = evTargetObject obj+    statusId targetObj @?= 495597326736449536+    statusText targetObj @?= "haskell"++    let ETUser targetUser = evTarget obj+    userScreenName targetUser @?= "thimura"++    let ETUser sourceUser = evSource obj+    userScreenName sourceUser @?= "thimura_shinku"++case_parseEventUnfavorite :: Assertion+case_parseEventUnfavorite = withFixtureJSON "event_unfavorite_thimura.json" $ \obj -> do+    evCreatedAt obj @?= "Sat Aug 02 16:32:10 +0000 2014"+    evEvent obj @?= "unfavorite"+    let Just (ETStatus targetObj) = evTargetObject obj+    statusId targetObj @?= 495597326736449536+    statusText targetObj @?= "haskell"++    let ETUser targetUser = evTarget obj+    userScreenName targetUser @?= "thimura"++    let ETUser sourceUser = evSource obj+    userScreenName sourceUser @?= "thimura_shinku"++case_parseDelete :: Assertion+case_parseDelete = withFixtureJSON "delete.json" $ \obj -> do+    delId obj @?= 495607981833064448+    delUserId obj @?= 2566877347+ case_parseErrorMsg :: Assertion-case_parseErrorMsg =-  case parseStatus errorMsgJson of-    Left str -> "Not authorized" @=? str-    Right _ -> assertFailure "errorMsgJson should be parsed as an error."+case_parseErrorMsg = withFixtureJSON "error_not_authorized.json" $ \value ->+    case parseStatus value of+        Aeson.Error str -> "Not authorized" @=? str+        Aeson.Success _ -> assertFailure "errorMsgJson should be parsed as an error."   where-    parseStatus :: Value -> Either String Status-    parseStatus = parseEither parseJSON+    parseStatus :: Value -> Aeson.Result Status+    parseStatus = Aeson.parse parseJSON  case_parseMediaEntity :: Assertion-case_parseMediaEntity = withJSON mediaEntityJson $ \obj -> do-  let entities = statusEntities obj-  assert $ isJust entities-  let Just ent = entities-      media = enMedia ent-  length media @?= 1-  let me = entityBody $ head media-  ueURL (meURL me) @?= "http://t.co/rJC5Pxsu"-  meMediaURLHttps me @?= "https://pbs.twimg.com/media/AZVLmp-CIAAbkyy.jpg"-  let sizes = meSizes me-  assert $ M.member "thumb" sizes-  assert $ M.member "large" sizes+case_parseMediaEntity = withFixtureJSON "media_entity.json" $ \obj -> do+    let entities = statusEntities obj+    assertBool "entities should not empty" $ isJust entities+    let Just ent = entities+        media = enMedia ent+    length media @?= 1+    let me = entityBody $ head media+    meType me @?= "photo"+    meId me @?= 114080493040967680+    let sizes = meSizes me+    assertBool "sizes must contains \"thumb\"" $ M.member "thumb" sizes+    assertBool "sizes must contains \"large\"" $ M.member "large" sizes +    let Just mediaSize = M.lookup "large" sizes++    msWidth mediaSize @?= 226+    msHeight mediaSize @?= 238+    msResize mediaSize @?= "fit"++    ueURL (meURL me) @?= "http://t.co/rJC5Pxsu"+    meMediaURLHttps me @?= "https://pbs.twimg.com/media/AZVLmp-CIAAbkyy.jpg"+ case_parseEmptyEntity :: Assertion-case_parseEmptyEntity = withJSON (fj [st|{}|]) $ \entity -> do+case_parseEmptyEntity = withJSON "{}" $ \entity -> do     length (enHashTags entity) @?= 0     length (enUserMentions entity) @?= 0     length (enURLs entity) @?= 0     length (enMedia entity) @?= 0  case_parseEntityHashTag :: Assertion-case_parseEntityHashTag = withJSON (fj [st|{"symbols":[],"urls":[{"indices":[32,52], "url":"http://t.co/IOwBrTZR", "display_url":"youtube.com/watch?v=oHg5SJ\u2026", "expanded_url":"http://www.youtube.com/watch?v=oHg5SJYRHA0"}],"user_mentions":[{"name":"Twitter API", "indices":[4,15], "screen_name":"twitterapi", "id":6253282, "id_str":"6253282"}],"hashtags":[{"indices":[32,36],"text":"lol"}]}|]) $ \entity -> do+case_parseEntityHashTag = withFixtureJSON "entity01.json" $ \entity -> do     length (enHashTags entity) @?= 1     length (enUserMentions entity) @?= 1     length (enURLs entity) @?= 1@@ -95,12 +280,62 @@     hashtag @?= "lol"  case_parseExtendedEntities :: Assertion-case_parseExtendedEntities = withJSON mediaExtendedEntityJson $ \obj -> do+case_parseExtendedEntities = withFixtureJSON "media_extended_entity.json" $ \obj -> do     let entities = statusExtendedEntities obj-    assert $ isJust entities+    assertBool "entities should not empty" $ isJust entities     let Just ent = entities-        media = enMedia ent+        media = exeMedia ent     length media @?= 4     let me = entityBody $ head media-    ueURL (meURL me) @?= "http://t.co/qOjPwmgLKO"-    meMediaURL me @?= "http://pbs.twimg.com/media/BqgdlpaCQAA5OSu.jpg"+    ueURL (exeURL me) @?= "https://t.co/Qi316FhOwe"+    exeMediaUrl me @?= "http://pbs.twimg.com/media/Coju86fUIAEUcRC.jpg"+    exeExtAltText me @?= Just "A small tabby kitten"+    exeType me @?= "photo"++case_parseUser :: Assertion+case_parseUser = withFixtureJSON "user_thimura.json" $ \obj -> do+    userId obj @?= 69179963+    userName obj @?= "ちむら"+    userScreenName obj @?= "thimura"+    userDescription obj @?= Just "真紅かわいい"+    userLocation obj @?= Just "State# Irotoridori.No.World"+    userProfileImageURL obj @?= Just "http://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png"+    userURL obj @?= Just "http://t.co/TFUAsAffX0"+    userProtected obj @?= False+    userFollowersCount obj @?= 754+    userFriendsCount obj @?= 780+    userStatusesCount obj @?= 24709+    userLang obj @?= Just "en"+    userCreatedAt obj @?= "Thu Aug 27 02:48:06 +0000 2009"+    userFavoritesCount obj @?= 17313++case_parseUserLangNull :: Assertion+case_parseUserLangNull = withFixtureJSON "user_thimura_lang_null.json" $ \obj -> do+    userId obj @?= 69179963+    userName obj @?= "ちむら"+    userScreenName obj @?= "thimura"+    userDescription obj @?= Just "真紅かわいい"+    userLocation obj @?= Just "State# Irotoridori.No.World"+    userProfileImageURL obj @?= Just "http://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png"+    userURL obj @?= Just "http://t.co/TFUAsAffX0"+    userProtected obj @?= False+    userFollowersCount obj @?= 754+    userFriendsCount obj @?= 780+    userStatusesCount obj @?= 24709+    -- N.B. the only difference between this one and case_parseUser is in lang+    userLang obj @?= Nothing+    userCreatedAt obj @?= "Thu Aug 27 02:48:06 +0000 2009"+    userFavoritesCount obj @?= 17313++case_parseList :: Assertion+case_parseList = withFixtureJSON "list_thimura_haskell.json" $ \obj -> do+    listId obj @?= 20849097+    listName obj @?= "haskell"+    listFullName obj @?= "@thimura/haskell"+    listMemberCount obj @?= 50+    listSubscriberCount obj @?= 1+    listMode obj @?= "public"+    (userScreenName . listUser) obj @?= "thimura"++tests :: TestTree+tests = $(testGroupGenerator)
+ tests/fixtures/delete.json view
@@ -0,0 +1,1 @@+{"delete":{"status":{"id":495607981833064448,"user_id":2566877347,"id_str":"495607981833064448","user_id_str":"2566877347"}}}
+ tests/fixtures/direct_message_event_list.json view
@@ -0,0 +1,69 @@+{+  "apps": {+    "258901": {+      "url": "http://twitter.com/download/android",+      "name": "Twitter for Android",+      "id": "258901"+    },+    "3033300": {+      "url": "https://mobile.twitter.com",+      "name": "Twitter Web App",+      "id": "3033300"+    }+  },+  "events": [+    {+      "message_create": {+        "source_app_id": "3033300",+        "message_data": {+          "entities": {+            "symbols": [],+            "urls": [],+            "user_mentions": [+              {+                "screen_name": "thimura",+                "id_str": "69179963",+                "name": "ちむら",+                "indices": [+                  6,+                  14+                ],+                "id": 69179963+              }+            ],+            "hashtags": []+          },+          "text": "hello @thimura"+        },+        "target": {+          "recipient_id": "186712193"+        },+        "sender_id": "69179963"+      },+      "created_timestamp": "1570990548951",+      "id": "123123123123123123",+      "type": "message_create"+    },+    {+      "message_create": {+        "source_app_id": "4237790",+        "message_data": {+          "entities": {+            "symbols": [],+            "urls": [],+            "user_mentions": [],+            "hashtags": []+          },+          "text": "hello"+        },+        "target": {+          "recipient_id": "186712193"+        },+        "sender_id": "69179963"+      },+      "created_timestamp": "1570990006140",+      "id": "25252525252525",+      "type": "message_create"+    }+  ]+}
+ tests/fixtures/entity01.json view
@@ -0,0 +1,1 @@+{"symbols":[],"urls":[{"indices":[32,52], "url":"http://t.co/IOwBrTZR", "display_url":"youtube.com/watch?v=oHg5SJ\u2026", "expanded_url":"http://www.youtube.com/watch?v=oHg5SJYRHA0"}],"user_mentions":[{"name":"Twitter API", "indices":[4,15], "screen_name":"twitterapi", "id":6253282, "id_str":"6253282"}],"hashtags":[{"indices":[32,36],"text":"lol"}]}
+ tests/fixtures/error_not_authorized.json view
@@ -0,0 +1,1 @@+{"request":"\/1\/statuses\/user_timeline.json","error":"Not authorized"}
+ tests/fixtures/event_favorite_thimura.json view
@@ -0,0 +1,1 @@+{"event":"favorite","created_at":"Sat Aug 02 16:32:01 +0000 2014","source":{"id":2566877347,"id_str":"2566877347","name":"thimura shinku","screen_name":"thimura_shinku","location":null,"url":null,"description":"\u4e8c\u968e\u5802 \u771f\u7d05","protected":false,"followers_count":4,"friends_count":3,"listed_count":0,"created_at":"Sat Jun 14 10:15:19 +0000 2014","favourites_count":2,"utc_offset":32400,"time_zone":"Irkutsk","geo_enabled":false,"verified":false,"statuses_count":1,"lang":"ja","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/477757821895704577\/rVwTwORU_normal.jpeg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/477757821895704577\/rVwTwORU_normal.jpeg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/2566877347\/1402741935","profile_link_color":"0084B4","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"default_profile":true,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"target":{"id":69179963,"id_str":"69179963","name":"\u3061\u3080\u3089","screen_name":"thimura","location":"State# Irotoridori.No.World","url":"http:\/\/d.hatena.ne.jp\/thimura","description":"\u771f\u7d05\u304b\u308f\u3044\u3044","protected":false,"followers_count":755,"friends_count":781,"listed_count":102,"created_at":"Thu Aug 27 02:48:06 +0000 2009","favourites_count":17363,"utc_offset":32400,"time_zone":"Tokyo","geo_enabled":false,"verified":false,"statuses_count":24747,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"000000","profile_background_image_url":"http:\/\/pbs.twimg.com\/profile_background_images\/378800000154132099\/hn0DlU5i.png","profile_background_image_url_https":"https:\/\/pbs.twimg.com\/profile_background_images\/378800000154132099\/hn0DlU5i.png","profile_background_tile":true,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/414044387346116609\/VNMfLpY7_normal.png","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/414044387346116609\/VNMfLpY7_normal.png","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/69179963\/1402419439","profile_link_color":"00660F","profile_sidebar_border_color":"FFFFFF","profile_sidebar_fill_color":"BABDD1","profile_text_color":"333333","profile_use_background_image":true,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"target_object":{"created_at":"Sat Aug 02 15:49:45 +0000 2014","id":495597326736449536,"id_str":"495597326736449536","text":"haskell","source":"\u003ca href=\"http:\/\/twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":69179963,"id_str":"69179963","name":"\u3061\u3080\u3089","screen_name":"thimura","location":"State# Irotoridori.No.World","url":"http:\/\/d.hatena.ne.jp\/thimura","description":"\u771f\u7d05\u304b\u308f\u3044\u3044","protected":false,"followers_count":755,"friends_count":781,"listed_count":102,"created_at":"Thu Aug 27 02:48:06 +0000 2009","favourites_count":17363,"utc_offset":32400,"time_zone":"Tokyo","geo_enabled":false,"verified":false,"statuses_count":24747,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"000000","profile_background_image_url":"http:\/\/pbs.twimg.com\/profile_background_images\/378800000154132099\/hn0DlU5i.png","profile_background_image_url_https":"https:\/\/pbs.twimg.com\/profile_background_images\/378800000154132099\/hn0DlU5i.png","profile_background_tile":true,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/414044387346116609\/VNMfLpY7_normal.png","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/414044387346116609\/VNMfLpY7_normal.png","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/69179963\/1402419439","profile_link_color":"00660F","profile_sidebar_border_color":"FFFFFF","profile_sidebar_fill_color":"BABDD1","profile_text_color":"333333","profile_use_background_image":true,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"favorite_count":2,"entities":{"hashtags":[],"symbols":[],"urls":[],"user_mentions":[]},"favorited":false,"retweeted":false,"lang":"en"}}
+ tests/fixtures/event_unfavorite_thimura.json view
@@ -0,0 +1,1 @@+{"event":"unfavorite","created_at":"Sat Aug 02 16:32:10 +0000 2014","source":{"id":2566877347,"id_str":"2566877347","name":"thimura shinku","screen_name":"thimura_shinku","location":null,"url":null,"description":"\u4e8c\u968e\u5802 \u771f\u7d05","protected":false,"followers_count":4,"friends_count":3,"listed_count":0,"created_at":"Sat Jun 14 10:15:19 +0000 2014","favourites_count":1,"utc_offset":32400,"time_zone":"Irkutsk","geo_enabled":false,"verified":false,"statuses_count":2,"lang":"ja","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/477757821895704577\/rVwTwORU_normal.jpeg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/477757821895704577\/rVwTwORU_normal.jpeg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/2566877347\/1402741935","profile_link_color":"0084B4","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"default_profile":true,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"target":{"id":69179963,"id_str":"69179963","name":"\u3061\u3080\u3089","screen_name":"thimura","location":"State# Irotoridori.No.World","url":"http:\/\/d.hatena.ne.jp\/thimura","description":"\u771f\u7d05\u304b\u308f\u3044\u3044","protected":false,"followers_count":755,"friends_count":781,"listed_count":102,"created_at":"Thu Aug 27 02:48:06 +0000 2009","favourites_count":17363,"utc_offset":32400,"time_zone":"Tokyo","geo_enabled":false,"verified":false,"statuses_count":24747,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"000000","profile_background_image_url":"http:\/\/pbs.twimg.com\/profile_background_images\/378800000154132099\/hn0DlU5i.png","profile_background_image_url_https":"https:\/\/pbs.twimg.com\/profile_background_images\/378800000154132099\/hn0DlU5i.png","profile_background_tile":true,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/414044387346116609\/VNMfLpY7_normal.png","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/414044387346116609\/VNMfLpY7_normal.png","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/69179963\/1402419439","profile_link_color":"00660F","profile_sidebar_border_color":"FFFFFF","profile_sidebar_fill_color":"BABDD1","profile_text_color":"333333","profile_use_background_image":true,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"target_object":{"created_at":"Sat Aug 02 15:49:45 +0000 2014","id":495597326736449536,"id_str":"495597326736449536","text":"haskell","source":"\u003ca href=\"http:\/\/twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":69179963,"id_str":"69179963","name":"\u3061\u3080\u3089","screen_name":"thimura","location":"State# Irotoridori.No.World","url":"http:\/\/d.hatena.ne.jp\/thimura","description":"\u771f\u7d05\u304b\u308f\u3044\u3044","protected":false,"followers_count":755,"friends_count":781,"listed_count":102,"created_at":"Thu Aug 27 02:48:06 +0000 2009","favourites_count":17363,"utc_offset":32400,"time_zone":"Tokyo","geo_enabled":false,"verified":false,"statuses_count":24747,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"000000","profile_background_image_url":"http:\/\/pbs.twimg.com\/profile_background_images\/378800000154132099\/hn0DlU5i.png","profile_background_image_url_https":"https:\/\/pbs.twimg.com\/profile_background_images\/378800000154132099\/hn0DlU5i.png","profile_background_tile":true,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/414044387346116609\/VNMfLpY7_normal.png","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/414044387346116609\/VNMfLpY7_normal.png","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/69179963\/1402419439","profile_link_color":"00660F","profile_sidebar_border_color":"FFFFFF","profile_sidebar_fill_color":"BABDD1","profile_text_color":"333333","profile_use_background_image":true,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":1,"favorite_count":1,"entities":{"hashtags":[],"symbols":[],"urls":[],"user_mentions":[]},"favorited":false,"retweeted":false,"lang":"en"}}
+ tests/fixtures/list_thimura_haskell.json view
@@ -0,0 +1,1 @@+{"id_str":"20849097","slug":"haskell","mode":"public","full_name":"@thimura/haskell","uri":"/thimura/haskell","following":true,"user":{"screen_name":"thimura","profile_banner_url":"https://pbs.twimg.com/profile_banners/69179963/1402419439","is_translation_enabled":false,"default_profile":false,"profile_image_url":"http://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","default_profile_image":false,"id_str":"69179963","profile_background_image_url_https":"https://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","protected":false,"location":"State# Irotoridori.No.World","entities":{"url":{"urls":[{"expanded_url":"http://d.hatena.ne.jp/thimura","url":"http://t.co/TFUAsAffX0","indices":[0,22],"display_url":"d.hatena.ne.jp/thimura"}]},"description":{"urls":[]}},"profile_background_color":"000000","utc_offset":32400,"url":"http://t.co/TFUAsAffX0","profile_text_color":"333333","profile_image_url_https":"https://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","verified":false,"statuses_count":24734,"profile_background_tile":true,"following":false,"lang":"en","follow_request_sent":false,"profile_sidebar_fill_color":"BABDD1","time_zone":"Tokyo","name":"ちむら","profile_sidebar_border_color":"FFFFFF","geo_enabled":false,"listed_count":102,"contributors_enabled":false,"created_at":"Thu Aug 27 02:48:06 +0000 2009","id":69179963,"friends_count":781,"is_translator":false,"favourites_count":17345,"notifications":false,"profile_background_image_url":"http://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","profile_use_background_image":true,"description":"真紅かわいい","profile_link_color":"00660F","followers_count":755},"member_count":50,"name":"haskell","created_at":"Sat Sep 04 11:48:02 +0000 2010","id":20849097,"subscriber_count":1,"description":""}
+ tests/fixtures/media_entity.json view
@@ -0,0 +1,1 @@+{"created_at":"Wed Sep 14 20:58:04 +0000 2011","id":114080493036773378,"id_str":"114080493036773378","text":"Hello America! http:\/\/t.co\/rJC5Pxsu","source":"web","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":373487136,"id_str":"373487136","name":"Y U No @rno?","screen_name":"yunorno","location":"Paris, France","url":"http:\/\/twitter.com\/rno","description":"Oui oui!","protected":false,"followers_count":32,"friends_count":3,"listed_count":2,"created_at":"Wed Sep 14 17:24:54 +0000 2011","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":27,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1542800106\/oui_normal.png","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/1542800106\/oui_normal.png","profile_link_color":"0084B4","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"default_profile":true,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":4,"entities":{"hashtags":[],"urls":[],"user_mentions":[],"media":[{"id":114080493040967680,"id_str":"114080493040967680","indices":[15,35],"media_url":"http:\/\/pbs.twimg.com\/media\/AZVLmp-CIAAbkyy.jpg","media_url_https":"https:\/\/pbs.twimg.com\/media\/AZVLmp-CIAAbkyy.jpg","url":"http:\/\/t.co\/rJC5Pxsu","display_url":"pic.twitter.com\/rJC5Pxsu","expanded_url":"http:\/\/twitter.com\/yunorno\/status\/114080493036773378\/photo\/1","type":"photo","sizes":{"large":{"w":226,"h":238,"resize":"fit"},"thumb":{"w":150,"h":150,"resize":"crop"},"medium":{"w":226,"h":238,"resize":"fit"},"small":{"w":226,"h":238,"resize":"fit"}}}]},"favorited":false,"retweeted":false,"possibly_sensitive":false}
+ tests/fixtures/media_extended_entity.json view
@@ -0,0 +1,1 @@+{"created_at":"Fri Jul 29 20:14:40 +0000 2016","id":759119974329823200,"id_str":"759119974329823233","text":"look at all these cats tho https://t.co/Qi316FhOwe","truncated":false,"entities":{"hashtags":[],"symbols":[],"user_mentions":[],"urls":[],"media":[{"id":759119267472089100,"id_str":"759119267472089089","indices":[27,50],"media_url":"http://pbs.twimg.com/media/Coju86fUIAEUcRC.jpg","media_url_https":"https://pbs.twimg.com/media/Coju86fUIAEUcRC.jpg","url":"https://t.co/Qi316FhOwe","display_url":"pic.twitter.com/Qi316FhOwe","expanded_url":"http://twitter.com/PleaseCaption/status/759119974329823233/photo/1","type":"photo","sizes":{"large":{"w":300,"h":400,"resize":"fit"},"medium":{"w":300,"h":400,"resize":"fit"},"thumb":{"w":150,"h":150,"resize":"crop"},"small":{"w":300,"h":400,"resize":"fit"}}}]},"extended_entities":{"media":[{"id":759119267472089100,"id_str":"759119267472089089","indices":[27,50],"media_url":"http://pbs.twimg.com/media/Coju86fUIAEUcRC.jpg","media_url_https":"https://pbs.twimg.com/media/Coju86fUIAEUcRC.jpg","url":"https://t.co/Qi316FhOwe","display_url":"pic.twitter.com/Qi316FhOwe","expanded_url":"http://twitter.com/PleaseCaption/status/759119974329823233/photo/1","type":"photo","sizes":{"large":{"w":300,"h":400,"resize":"fit"},"medium":{"w":300,"h":400,"resize":"fit"},"thumb":{"w":150,"h":150,"resize":"crop"},"small":{"w":300,"h":400,"resize":"fit"}},"ext_alt_text":"A small tabby kitten"},{"id":759119396350505000,"id_str":"759119396350504960","indices":[27,50],"media_url":"http://pbs.twimg.com/media/CojvEamUsAARiX2.jpg","media_url_https":"https://pbs.twimg.com/media/CojvEamUsAARiX2.jpg","url":"https://t.co/Qi316FhOwe","display_url":"pic.twitter.com/Qi316FhOwe","expanded_url":"http://twitter.com/PleaseCaption/status/759119974329823233/photo/1","type":"photo","sizes":{"medium":{"w":500,"h":301,"resize":"fit"},"small":{"w":500,"h":301,"resize":"fit"},"thumb":{"w":150,"h":150,"resize":"crop"},"large":{"w":500,"h":301,"resize":"fit"}},"ext_alt_text":"four kittens sitting together"},{"id":759119476621086700,"id_str":"759119476621086720","indices":[27,50],"media_url":"http://pbs.twimg.com/media/CojvJFoUkAAoWIR.jpg","media_url_https":"https://pbs.twimg.com/media/CojvJFoUkAAoWIR.jpg","url":"https://t.co/Qi316FhOwe","display_url":"pic.twitter.com/Qi316FhOwe","expanded_url":"http://twitter.com/PleaseCaption/status/759119974329823233/photo/1","type":"photo","sizes":{"thumb":{"w":150,"h":150,"resize":"crop"},"large":{"w":500,"h":600,"resize":"fit"},"medium":{"w":500,"h":600,"resize":"fit"},"small":{"w":500,"h":600,"resize":"fit"}},"ext_alt_text":"a cute soft kitten with a red ornament"},{"id":759119583466758100,"id_str":"759119583466758145","indices":[27,50],"media_url":"http://pbs.twimg.com/media/CojvPTqUIAEYF7l.jpg","media_url_https":"https://pbs.twimg.com/media/CojvPTqUIAEYF7l.jpg","url":"https://t.co/Qi316FhOwe","display_url":"pic.twitter.com/Qi316FhOwe","expanded_url":"http://twitter.com/PleaseCaption/status/759119974329823233/photo/1","type":"photo","sizes":{"medium":{"w":650,"h":600,"resize":"fit"},"thumb":{"w":150,"h":150,"resize":"crop"},"small":{"w":650,"h":600,"resize":"fit"},"large":{"w":650,"h":600,"resize":"fit"}},"ext_alt_text":"a skeptical-looking tabby cat outside"}]},"source":"<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>","in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":753372895682949100,"id_str":"753372895682949120","name":"Please Caption!","screen_name":"PleaseCaption","location":"","description":"","url":null,"entities":{"description":{"urls":[]}},"protected":false,"followers_count":0,"friends_count":1,"listed_count":0,"created_at":"Wed Jul 13 23:37:50 +0000 2016","favourites_count":0,"utc_offset":-25200,"time_zone":"Pacific Time (US & Canada)","geo_enabled":false,"verified":false,"statuses_count":7,"lang":"en-gb","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"F5F8FA","profile_background_image_url":null,"profile_background_image_url_https":null,"profile_background_tile":false,"profile_image_url":"http://abs.twimg.com/sticky/default_profile_images/default_profile_5_normal.png","profile_image_url_https":"https://abs.twimg.com/sticky/default_profile_images/default_profile_5_normal.png","profile_link_color":"2B7BB9","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"has_extended_profile":false,"default_profile":true,"default_profile_image":true,"following":false,"follow_request_sent":false,"notifications":false},"geo":null,"coordinates":null,"place":null,"contributors":null,"is_quote_status":false,"retweet_count":0,"favorite_count":0,"favorited":false,"retweeted":false,"possibly_sensitive":false,"possibly_sensitive_appealable":false,"lang":"en"}
+ tests/fixtures/search_haskell.json view
@@ -0,0 +1,1 @@+{"search_metadata":{"next_results":"?max_id=495594369802440705&q=haskell&include_entities=1","since_id_str":"0","completed_in":0.043,"count":1,"refresh_url":"?since_id=495597397733433345&q=haskell&include_entities=1","max_id_str":"495597397733433345","query":"haskell","since_id":0,"max_id":495597397733433345},"statuses":[{"in_reply_to_status_id":null,"id_str":"495597326736449536","truncated":false,"in_reply_to_screen_name":null,"entities":{"symbols":[],"urls":[],"user_mentions":[],"hashtags":[]},"text":"haskell","in_reply_to_user_id_str":null,"favorited":false,"coordinates":null,"retweeted":false,"user":{"screen_name":"thimura","profile_banner_url":"https://pbs.twimg.com/profile_banners/69179963/1402419439","is_translation_enabled":false,"default_profile":false,"profile_image_url":"http://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","default_profile_image":false,"id_str":"69179963","profile_background_image_url_https":"https://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","protected":false,"location":"State# Irotoridori.No.World","entities":{"url":{"urls":[{"expanded_url":"http://d.hatena.ne.jp/thimura","url":"http://t.co/TFUAsAffX0","indices":[0,22],"display_url":"d.hatena.ne.jp/thimura"}]},"description":{"urls":[]}},"profile_background_color":"000000","utc_offset":32400,"url":"http://t.co/TFUAsAffX0","profile_text_color":"333333","profile_image_url_https":"https://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","verified":false,"statuses_count":24747,"profile_background_tile":true,"following":false,"lang":"en","follow_request_sent":false,"profile_sidebar_fill_color":"BABDD1","time_zone":"Tokyo","name":"ちむら","profile_sidebar_border_color":"FFFFFF","geo_enabled":false,"listed_count":102,"contributors_enabled":false,"created_at":"Thu Aug 27 02:48:06 +0000 2009","id":69179963,"friends_count":781,"is_translator":false,"favourites_count":17362,"notifications":false,"profile_background_image_url":"http://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","profile_use_background_image":true,"description":"真紅かわいい","profile_link_color":"00660F","followers_count":755},"lang":"et","retweet_count":0,"in_reply_to_user_id":null,"created_at":"Sat Aug 02 15:49:45 +0000 2014","metadata":{"result_type":"recent","iso_language_code":"et"},"source":"\u003ca href=\"http://twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c/a\u003e","geo":null,"id":495597326736449536,"in_reply_to_status_id_str":null,"favorite_count":0,"contributors":null,"place":null}]}
+ tests/fixtures/status01.json view
@@ -0,0 +1,1 @@+{"created_at":"Sat Sep 10 22:23:38 +0000 2011","id":112652479837110273,"id_str":"112652479837110273","text":"@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http:\/\/t.co\/6J2EgYM","source":"\u003ca href=\"http:\/\/instagr.am\" rel=\"nofollow\"\u003eInstagram\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":783214,"in_reply_to_user_id_str":"783214","in_reply_to_screen_name":"twitter","user":{"id":299862462,"id_str":"299862462","name":"Eoin McMillan ","screen_name":"imeoin","location":"Twitter","description":"Eoin's photography account. See @mceoin for tweets.","url":"http:\/\/www.eoin.me","protected":false,"followers_count":6,"friends_count":0,"listed_count":0,"created_at":"Mon May 16 20:07:59 +0000 2011","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":277,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"131516","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_tile":true,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1380912173\/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/1380912173\/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png","profile_link_color":"009999","profile_sidebar_border_color":"EEEEEE","profile_sidebar_fill_color":"EFEFEF","profile_text_color":"333333","profile_use_background_image":true,"show_all_inline_media":false,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"favorited":false,"retweeted":false,"possibly_sensitive":false}
+ tests/fixtures/status_quoted.json view
@@ -0,0 +1,202 @@+{+  "created_at": "Wed Sep 09 17:13:42 +0000 2015",+  "id": 641660763770372100,+  "id_str": "641660763770372097",+  "text": "Wow! Congrats! https://t.co/EPMMldEcci",+  "source": "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>",+  "truncated": false,+  "in_reply_to_status_id": null,+  "in_reply_to_status_id_str": null,+  "in_reply_to_user_id": null,+  "in_reply_to_user_id_str": null,+  "in_reply_to_screen_name": null,+  "user":  {+    "id": 15232432,+    "id_str": "15232432",+    "name": "Dean Wampler",+    "screen_name": "deanwampler",+    "location": "Chicago",+    "description": "Minor irritant, major pedant, Big Data, IoT, and Scala poser. Lurks at Typesafe. O'Reilly author. Opinions are my own..",+    "url": "http://t.co/bzGTwCnmvO",+    "entities":  {+      "url":  {+        "urls":  [+           {+            "url": "http://t.co/bzGTwCnmvO",+            "expanded_url": "http://typesafe.com",+            "display_url": "typesafe.com",+            "indices":  [+              0,+              22+            ]+          }+        ]+      },+      "description":  {+        "urls":  []+      }+    },+    "protected": false,+    "followers_count": 7221,+    "friends_count": 811,+    "listed_count": 645,+    "created_at": "Wed Jun 25 15:24:31 +0000 2008",+    "favourites_count": 2841,+    "utc_offset": -18000,+    "time_zone": "Central Time (US & Canada)",+    "geo_enabled": true,+    "verified": false,+    "statuses_count": 13483,+    "lang": "en",+    "contributors_enabled": false,+    "is_translator": false,+    "is_translation_enabled": false,+    "profile_background_color": "9AE4E8",+    "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/670120165/5514a5b016bf4d3d617ece5f117643d0.jpeg",+    "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/670120165/5514a5b016bf4d3d617ece5f117643d0.jpeg",+    "profile_background_tile": true,+    "profile_image_url": "http://pbs.twimg.com/profile_images/639619926374748160/QhODNcWz_normal.jpg",+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/639619926374748160/QhODNcWz_normal.jpg",+    "profile_banner_url": "https://pbs.twimg.com/profile_banners/15232432/1359038744",+    "profile_link_color": "0000FF",+    "profile_sidebar_border_color": "FFFFFF",+    "profile_sidebar_fill_color": "DFF8A0",+    "profile_text_color": "070708",+    "profile_use_background_image": true,+    "has_extended_profile": false,+    "default_profile": false,+    "default_profile_image": false,+    "following": false,+    "follow_request_sent": false,+    "notifications": false+  },+  "geo": null,+  "coordinates": null,+  "place": null,+  "contributors": null,+  "quoted_status_id": 641653574284537900,+  "quoted_status_id_str": "641653574284537856",+  "quoted_status":  {+    "created_at": "Wed Sep 09 16:45:08 +0000 2015",+    "id": 641653574284537900,+    "id_str": "641653574284537856",+    "text": "Very happy to say that I'm joining @mesosphere as a Distributed Systems Engineer!",+    "source": "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>",+    "truncated": false,+    "in_reply_to_status_id": null,+    "in_reply_to_status_id_str": null,+    "in_reply_to_user_id": null,+    "in_reply_to_user_id_str": null,+    "in_reply_to_screen_name": null,+    "user":  {+      "id": 54129186,+      "id_str": "54129186",+      "name": "Neil Conway",+      "screen_name": "neil_conway",+      "location": "Oakland, CA",+      "description": "Large-scale data management and distributed systems.",+      "url": "http://t.co/grthxRo5Q1",+      "entities":  {+        "url":  {+          "urls":  [+             {+              "url": "http://t.co/grthxRo5Q1",+              "expanded_url": "http://neilconway.org",+              "display_url": "neilconway.org",+              "indices":  [+                0,+                22+              ]+            }+          ]+        },+        "description":  {+          "urls":  []+        }+      },+      "protected": false,+      "followers_count": 3380,+      "friends_count": 680,+      "listed_count": 202,+      "created_at": "Mon Jul 06 05:43:15 +0000 2009",+      "favourites_count": 611,+      "utc_offset": -25200,+      "time_zone": "Pacific Time (US & Canada)",+      "geo_enabled": true,+      "verified": false,+      "statuses_count": 3559,+      "lang": "en",+      "contributors_enabled": false,+      "is_translator": false,+      "is_translation_enabled": false,+      "profile_background_color": "FFFFFF",+      "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/204823473/Untitled-2.jpg",+      "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/204823473/Untitled-2.jpg",+      "profile_background_tile": true,+      "profile_image_url": "http://pbs.twimg.com/profile_images/299445365/3613789225_66f1cc848f_b_normal.jpg",+      "profile_image_url_https": "https://pbs.twimg.com/profile_images/299445365/3613789225_66f1cc848f_b_normal.jpg",+      "profile_link_color": "036E6E",+      "profile_sidebar_border_color": "B05607",+      "profile_sidebar_fill_color": "E8F0F0",+      "profile_text_color": "998308",+      "profile_use_background_image": true,+      "has_extended_profile": false,+      "default_profile": false,+      "default_profile_image": false,+      "following": true,+      "follow_request_sent": false,+      "notifications": false+    },+    "geo": null,+    "coordinates": null,+    "place": null,+    "contributors": null,+    "is_quote_status": false,+    "retweet_count": 7,+    "favorite_count": 63,+    "entities":  {+      "hashtags":  [],+      "symbols":  [],+      "user_mentions":  [+         {+          "screen_name": "mesosphere",+          "name": "Mesosphere",+          "id": 1872399366,+          "id_str": "1872399366",+          "indices":  [+            35,+            46+          ]+        }+      ],+      "urls":  []+    },+    "favorited": false,+    "retweeted": false,+    "lang": "en"+  },+  "is_quote_status": true,+  "retweet_count": 0,+  "favorite_count": 1,+  "entities":  {+    "hashtags":  [],+    "symbols":  [],+    "user_mentions":  [],+    "urls":  [+       {+        "url": "https://t.co/EPMMldEcci",+        "expanded_url": "https://twitter.com/neil_conway/status/641653574284537856",+        "display_url": "twitter.com/neil_conway/st…",+        "indices":  [+          15,+          38+        ]+      }+    ]+  },+  "favorited": false,+  "retweeted": false,+  "possibly_sensitive": false,+  "possibly_sensitive_appealable": false,+  "lang": "en"+}
+ tests/fixtures/status_thimura_with_photo.json view
@@ -0,0 +1,1 @@+{"in_reply_to_status_id":null,"id_str":"491143410770657280","truncated":false,"possibly_sensitive":false,"in_reply_to_screen_name":null,"extended_entities":{"media":[{"id_str":"491143397378244610","expanded_url":"http://twitter.com/thimura/status/491143410770657280/photo/1","url":"http://t.co/FjSOU8dDoD","media_url_https":"https://pbs.twimg.com/media/BtDkUVaCQAIpWBU.jpg","indices":[7,29],"id":491143397378244610,"media_url":"http://pbs.twimg.com/media/BtDkUVaCQAIpWBU.jpg","type":"photo","sizes":{"small":{"w":340,"resize":"fit","h":191},"large":{"w":1024,"resize":"fit","h":576},"medium":{"w":600,"resize":"fit","h":338},"thumb":{"w":150,"resize":"crop","h":150}},"display_url":"pic.twitter.com/FjSOU8dDoD"}]},"entities":{"symbols":[],"urls":[],"media":[{"id_str":"491143397378244610","expanded_url":"http://twitter.com/thimura/status/491143410770657280/photo/1","url":"http://t.co/FjSOU8dDoD","media_url_https":"https://pbs.twimg.com/media/BtDkUVaCQAIpWBU.jpg","indices":[7,29],"id":491143397378244610,"media_url":"http://pbs.twimg.com/media/BtDkUVaCQAIpWBU.jpg","type":"photo","sizes":{"small":{"w":340,"resize":"fit","h":191},"large":{"w":1024,"resize":"fit","h":576},"medium":{"w":600,"resize":"fit","h":338},"thumb":{"w":150,"resize":"crop","h":150}},"display_url":"pic.twitter.com/FjSOU8dDoD"}],"user_mentions":[],"hashtags":[]},"text":"近所の海です http://t.co/FjSOU8dDoD","in_reply_to_user_id_str":null,"favorited":false,"coordinates":null,"retweeted":false,"user":{"screen_name":"thimura","profile_banner_url":"https://pbs.twimg.com/profile_banners/69179963/1402419439","is_translation_enabled":false,"default_profile":false,"profile_image_url":"http://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","default_profile_image":false,"id_str":"69179963","profile_background_image_url_https":"https://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","protected":false,"location":"State# Irotoridori.No.World","entities":{"url":{"urls":[{"expanded_url":"http://d.hatena.ne.jp/thimura","url":"http://t.co/TFUAsAffX0","indices":[0,22],"display_url":"d.hatena.ne.jp/thimura"}]},"description":{"urls":[]}},"profile_background_color":"000000","utc_offset":32400,"url":"http://t.co/TFUAsAffX0","profile_text_color":"333333","profile_image_url_https":"https://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","verified":false,"statuses_count":24734,"profile_background_tile":true,"following":false,"lang":"en","follow_request_sent":false,"profile_sidebar_fill_color":"BABDD1","time_zone":"Tokyo","name":"ちむら","profile_sidebar_border_color":"FFFFFF","geo_enabled":false,"listed_count":102,"contributors_enabled":false,"created_at":"Thu Aug 27 02:48:06 +0000 2009","id":69179963,"friends_count":781,"is_translator":false,"favourites_count":17345,"notifications":false,"profile_background_image_url":"http://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","profile_use_background_image":true,"description":"真紅かわいい","profile_link_color":"00660F","followers_count":755},"lang":"ja","retweet_count":4,"in_reply_to_user_id":null,"created_at":"Mon Jul 21 08:51:28 +0000 2014","source":"\u003ca href=\"https://sites.google.com/site/wakamesoba98/sobacha\" rel=\"nofollow\"\u003eSobaCha\u003c/a\u003e","geo":null,"id":491143410770657280,"in_reply_to_status_id_str":null,"favorite_count":9,"contributors":null,"place":null}
+ tests/fixtures/status_with_entity.json view
@@ -0,0 +1,1 @@+{"created_at":"Sat Sep 10 22:23:38 +0000 2011","id":112652479837110273,"id_str":"112652479837110273","text":"@twitter meets @seepicturely at #tcdisrupt cc.@boscomonkey @episod http:\/\/t.co\/6J2EgYM","source":"\u003ca href=\"http:\/\/instagr.am\" rel=\"nofollow\"\u003eInstagram\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":783214,"in_reply_to_user_id_str":"783214","in_reply_to_screen_name":"twitter","user":{"id":299862462,"id_str":"299862462","name":"Eoin McMillan ","screen_name":"imeoin","location":"Twitter","description":"Eoin's photography account. See @mceoin for tweets.","url":"http:\/\/www.eoin.me","protected":false,"followers_count":6,"friends_count":0,"listed_count":0,"created_at":"Mon May 16 20:07:59 +0000 2011","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":277,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"131516","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_tile":true,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1380912173\/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/1380912173\/Screen_shot_2011-06-03_at_7.35.36_PM_normal.png","profile_link_color":"009999","profile_sidebar_border_color":"EEEEEE","profile_sidebar_fill_color":"EFEFEF","profile_text_color":"333333","profile_use_background_image":true,"show_all_inline_media":false,"default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"entities":{"hashtags":[{"text":"tcdisrupt","indices":[32,42]}],"urls":[{"url":"http:\/\/t.co\/6J2EgYM","expanded_url":"http:\/\/instagr.am\/p\/MuW67\/","display_url":"instagr.am\/p\/MuW67\/","indices":[67,86]}],"user_mentions":[{"screen_name":"twitter","name":"Twitter","id":783214,"id_str":"783214","indices":[0,8]},{"screen_name":"boscomonkey","name":"Bosco So","id":14792670,"id_str":"14792670","indices":[46,58]},{"screen_name":"episod","name":"Taylor Singletary","id":819797,"id_str":"819797","indices":[59,66]}]},"favorited":false,"retweeted":false,"possibly_sensitive":false}
+ tests/fixtures/tweet-updates/compatibility_classic_13995.json view
@@ -0,0 +1,169 @@+{+  "created_at": "Mon Mar 07 15:13:47 +0000 2016",+  "id": 706860403981099008,+  "id_str": "706860403981099008",+  "text": "Peek-a-boo! https://t.co/R3P6waHxRa",+  "entities": {+    "hashtags": [],+    "symbols": [],+    "user_mentions": [],+    "urls": [],+    "media": [+      {+        "id": 706860403746181121,+        "id_str": "706860403746181121",+        "indices": [+          12,+          35+        ],+        "media_url": "http://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg",+        "media_url_https": "https://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg",+        "url": "https://t.co/R3P6waHxRa",+        "display_url": "pic.twitter.com/R3P6waHxRa",+        "expanded_url": "http://twitter.com/jeremycloud/status/706860403981099008/photo/1",+        "type": "photo",+        "sizes": {+          "medium": {+            "w": 600,+            "h": 398,+            "resize": "fit"+          },+          "small": {+            "w": 340,+            "h": 226,+            "resize": "fit"+          },+          "thumb": {+            "w": 150,+            "h": 150,+            "resize": "crop"+          },+          "large": {+            "w": 1024,+            "h": 680,+            "resize": "fit"+          }+        }+      }+    ]+  },+  "extended_entities": {+    "media": [+      {+        "id": 706860403746181121,+        "id_str": "706860403746181121",+        "indices": [+          12,+          35+        ],+        "media_url": "http://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg",+        "media_url_https": "https://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg",+        "url": "https://t.co/R3P6waHxRa",+        "display_url": "pic.twitter.com/R3P6waHxRa",+        "expanded_url": "http://twitter.com/jeremycloud/status/706860403981099008/photo/1",+        "type": "photo",+        "sizes": {+          "medium": {+            "w": 600,+            "h": 398,+            "resize": "fit"+          },+          "small": {+            "w": 340,+            "h": 226,+            "resize": "fit"+          },+          "thumb": {+            "w": 150,+            "h": 150,+            "resize": "crop"+          },+          "large": {+            "w": 1024,+            "h": 680,+            "resize": "fit"+          }+        }+      }+    ]+  },+  "truncated": false,+  "source": "<a href=\"http://www.apple.com/\" rel=\"nofollow\">OS X</a>",+  "in_reply_to_status_id": null,+  "in_reply_to_status_id_str": null,+  "in_reply_to_user_id": null,+  "in_reply_to_user_id_str": null,+  "in_reply_to_screen_name": null,+  "user": {+    "id": 15062340,+    "id_str": "15062340",+    "name": "/dev/cloud/jeremy",+    "screen_name": "jeremycloud",+    "location": "Madison, Wisconsin",+    "description": "Professional yak shaver. Amateur bike shedder.",+    "url": "https://t.co/FcYeBkOpVY",+    "entities": {+      "url": {+        "urls": [+          {+            "url": "https://t.co/FcYeBkOpVY",+            "expanded_url": "http://about.me/jeremy.cloud",+            "display_url": "about.me/jeremy.cloud",+            "indices": [+              0,+              23+            ]+          }+        ]+      },+      "description": {+        "urls": []+      }+    },+    "protected": false,+    "followers_count": 4324,+    "friends_count": 410,+    "listed_count": 103,+    "created_at": "Mon Jun 09 17:00:58 +0000 2008",+    "favourites_count": 815,+    "utc_offset": -18000,+    "time_zone": "Central Time (US & Canada)",+    "geo_enabled": true,+    "verified": false,+    "statuses_count": 2218,+    "lang": "en",+    "contributors_enabled": false,+    "is_translator": false,+    "is_translation_enabled": false,+    "profile_background_color": "000000",+    "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",+    "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",+    "profile_background_tile": false,+    "profile_image_url": "http://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",+    "profile_banner_url": "https://pbs.twimg.com/profile_banners/15062340/1447451621",+    "profile_link_color": "4A913C",+    "profile_sidebar_border_color": "000000",+    "profile_sidebar_fill_color": "000000",+    "profile_text_color": "000000",+    "profile_use_background_image": false,+    "has_extended_profile": true,+    "default_profile": false,+    "default_profile_image": false,+    "following": true,+    "follow_request_sent": false,+    "notifications": false+  },+  "geo": null,+  "coordinates": null,+  "place": null,+  "contributors": null,+  "is_quote_status": false,+  "retweet_count": 0,+  "favorite_count": 8,+  "favorited": false,+  "retweeted": false,+  "possibly_sensitive": false,+  "possibly_sensitive_appealable": false,+  "lang": "en"+}
+ tests/fixtures/tweet-updates/compatibility_classic_13995_extended.json view
@@ -0,0 +1,173 @@+{+  "created_at": "Mon Mar 07 15:13:47 +0000 2016",+  "id": 706860403981099008,+  "id_str": "706860403981099008",+  "full_text": "Peek-a-boo! https://t.co/R3P6waHxRa",+  "display_text_range": [+    0,+    35+  ],+  "entities": {+    "hashtags": [],+    "symbols": [],+    "user_mentions": [],+    "urls": [],+    "media": [+      {+        "id": 706860403746181121,+        "id_str": "706860403746181121",+        "indices": [+          12,+          35+        ],+        "media_url": "http://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg",+        "media_url_https": "https://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg",+        "url": "https://t.co/R3P6waHxRa",+        "display_url": "pic.twitter.com/R3P6waHxRa",+        "expanded_url": "http://twitter.com/jeremycloud/status/706860403981099008/photo/1",+        "type": "photo",+        "sizes": {+          "medium": {+            "w": 600,+            "h": 398,+            "resize": "fit"+          },+          "small": {+            "w": 340,+            "h": 226,+            "resize": "fit"+          },+          "thumb": {+            "w": 150,+            "h": 150,+            "resize": "crop"+          },+          "large": {+            "w": 1024,+            "h": 680,+            "resize": "fit"+          }+        }+      }+    ]+  },+  "extended_entities": {+    "media": [+      {+        "id": 706860403746181121,+        "id_str": "706860403746181121",+        "indices": [+          12,+          35+        ],+        "media_url": "http://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg",+        "media_url_https": "https://pbs.twimg.com/media/Cc9FyscUkAEQaOw.jpg",+        "url": "https://t.co/R3P6waHxRa",+        "display_url": "pic.twitter.com/R3P6waHxRa",+        "expanded_url": "http://twitter.com/jeremycloud/status/706860403981099008/photo/1",+        "type": "photo",+        "sizes": {+          "medium": {+            "w": 600,+            "h": 398,+            "resize": "fit"+          },+          "small": {+            "w": 340,+            "h": 226,+            "resize": "fit"+          },+          "thumb": {+            "w": 150,+            "h": 150,+            "resize": "crop"+          },+          "large": {+            "w": 1024,+            "h": 680,+            "resize": "fit"+          }+        }+      }+    ]+  },+  "truncated": false,+  "source": "<a href=\"http://www.apple.com/\" rel=\"nofollow\">OS X</a>",+  "in_reply_to_status_id": null,+  "in_reply_to_status_id_str": null,+  "in_reply_to_user_id": null,+  "in_reply_to_user_id_str": null,+  "in_reply_to_screen_name": null,+  "user": {+    "id": 15062340,+    "id_str": "15062340",+    "name": "/dev/cloud/jeremy",+    "screen_name": "jeremycloud",+    "location": "Madison, Wisconsin",+    "description": "Professional yak shaver. Amateur bike shedder.",+    "url": "https://t.co/FcYeBkOpVY",+    "entities": {+      "url": {+        "urls": [+          {+            "url": "https://t.co/FcYeBkOpVY",+            "expanded_url": "http://about.me/jeremy.cloud",+            "display_url": "about.me/jeremy.cloud",+            "indices": [+              0,+              23+            ]+          }+        ]+      },+      "description": {+        "urls": []+      }+    },+    "protected": false,+    "followers_count": 4324,+    "friends_count": 410,+    "listed_count": 103,+    "created_at": "Mon Jun 09 17:00:58 +0000 2008",+    "favourites_count": 815,+    "utc_offset": -18000,+    "time_zone": "Central Time (US & Canada)",+    "geo_enabled": true,+    "verified": false,+    "statuses_count": 2218,+    "lang": "en",+    "contributors_enabled": false,+    "is_translator": false,+    "is_translation_enabled": false,+    "profile_background_color": "000000",+    "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",+    "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",+    "profile_background_tile": false,+    "profile_image_url": "http://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",+    "profile_banner_url": "https://pbs.twimg.com/profile_banners/15062340/1447451621",+    "profile_link_color": "4A913C",+    "profile_sidebar_border_color": "000000",+    "profile_sidebar_fill_color": "000000",+    "profile_text_color": "000000",+    "profile_use_background_image": false,+    "has_extended_profile": true,+    "default_profile": false,+    "default_profile_image": false,+    "following": true,+    "follow_request_sent": false,+    "notifications": false+  },+  "geo": null,+  "coordinates": null,+  "place": null,+  "contributors": null,+  "is_quote_status": false,+  "retweet_count": 0,+  "favorite_count": 8,+  "favorited": false,+  "retweeted": false,+  "possibly_sensitive": false,+  "possibly_sensitive_appealable": false,+  "lang": "en"+}
+ tests/fixtures/tweet-updates/compatibility_classic_hidden_13797.json view
@@ -0,0 +1,270 @@+{+  "created_at": "Thu Mar 10 23:12:12 +0000 2016",+  "id": 708067963060916224,+  "id_str": "708067963060916224",+  "text": "@jeremycloud Who would win in a battle between a Barred Owl and a Cooper's Hawk? https://t.co/FamikDro2h",+  "entities": {+    "hashtags": [],+    "symbols": [],+    "user_mentions": [+      {+        "screen_name": "jeremycloud",+        "name": "/dev/cloud/jeremy",+        "id": 15062340,+        "id_str": "15062340",+        "indices": [+          0,+          12+        ]+      }+    ],+    "urls": [+      {+        "url": "https://t.co/FamikDro2h",+        "expanded_url": "https://twitter.com/jeremycloud/status/703621193417379840",+        "display_url": "twitter.com/jeremycloud/st…",+        "indices": [+          81,+          104+        ]+      }+    ]+  },+  "truncated": false,+  "source": "bonesTwurl",+  "in_reply_to_status_id": 704059336788606976,+  "in_reply_to_status_id_str": "704059336788606976",+  "in_reply_to_user_id": 15062340,+  "in_reply_to_user_id_str": "15062340",+  "in_reply_to_screen_name": "jeremycloud",+  "user": {+    "id": 4449621923,+    "id_str": "4449621923",+    "name": "Mr Bones",+    "screen_name": "MrBonesDroid",+    "location": "",+    "profile_location": null,+    "description": "",+    "url": null,+    "entities": {+    "description": {+    "urls": []+     }+    },+    "protected": true,+    "followers_count": 5,+    "friends_count": 7,+    "listed_count": 0,+    "created_at": "Fri Dec 11 15:18:02 +0000 2015",+    "favourites_count": 7,+    "utc_offset": -25200,+    "time_zone": "Pacific Time (US & Canada)",+    "geo_enabled": false,+    "verified": false,+    "statuses_count": 35,+    "lang": "en-gb",+    "contributors_enabled": false,+    "is_translator": false,+    "is_translation_enabled": false,+    "profile_background_color": "F5F8FA",+    "profile_background_image_url": null,+    "profile_background_image_url_https": null,+    "profile_background_tile": false,+    "profile_image_url": "http://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",+    "profile_link_color": "2B7BB9",+    "profile_sidebar_border_color": "C0DEED",+    "profile_sidebar_fill_color": "DDEEF6",+    "profile_text_color": "333333",+    "profile_use_background_image": true,+    "has_extended_profile": false,+    "default_profile": true,+    "default_profile_image": false,+    "following": true,+    "follow_request_sent": false,+    "notifications": false+  },+  "geo": null,+  "coordinates": null,+  "place": null,+  "contributors": null,+  "quoted_status_id": 703621193417379840,+  "quoted_status_id_str": "703621193417379840",+  "quoted_status": {+    "created_at": "Sat Feb 27 16:42:19 +0000 2016",+    "id": 703621193417379840,+    "id_str": "703621193417379840",+    "text": "Cooper’s Hawk https://t.co/nppuOGne9X",+    "entities": {+      "hashtags": [],+      "symbols": [],+      "user_mentions": [],+      "urls": [],+      "media": [+        {+          "id": 703621193182502913,+          "id_str": "703621193182502913",+          "indices": [+            14,+            37+          ],+          "media_url": "http://pbs.twimg.com/media/CcPDv0wUYAE3D-2.jpg",+          "media_url_https": "https://pbs.twimg.com/media/CcPDv0wUYAE3D-2.jpg",+          "url": "https://t.co/nppuOGne9X",+          "display_url": "pic.twitter.com/nppuOGne9X",+          "expanded_url": "http://twitter.com/jeremycloud/status/703621193417379840/photo/1",+          "type": "photo",+          "sizes": {+            "medium": {+              "w": 600,+              "h": 398,+              "resize": "fit"+            },+            "thumb": {+              "w": 150,+              "h": 150,+              "resize": "crop"+            },+            "large": {+              "w": 1024,+              "h": 680,+              "resize": "fit"+            },+            "small": {+              "w": 340,+              "h": 226,+              "resize": "fit"+            }+          }+        }+      ]+    },+    "extended_entities": {+      "media": [+        {+          "id": 703621193182502913,+          "id_str": "703621193182502913",+          "indices": [+            14,+            37+          ],+          "media_url": "http://pbs.twimg.com/media/CcPDv0wUYAE3D-2.jpg",+          "media_url_https": "https://pbs.twimg.com/media/CcPDv0wUYAE3D-2.jpg",+          "url": "https://t.co/nppuOGne9X",+          "display_url": "pic.twitter.com/nppuOGne9X",+          "expanded_url": "http://twitter.com/jeremycloud/status/703621193417379840/photo/1",+          "type": "photo",+          "sizes": {+            "medium": {+              "w": 600,+              "h": 398,+              "resize": "fit"+            },+            "thumb": {+              "w": 150,+              "h": 150,+              "resize": "crop"+            },+            "large": {+              "w": 1024,+              "h": 680,+              "resize": "fit"+            },+            "small": {+              "w": 340,+              "h": 226,+              "resize": "fit"+            }+          }+        }+      ]+    },+    "truncated": false,+    "source": "<a href=\"http://www.apple.com/\" rel=\"nofollow\">OS X</a>",+    "in_reply_to_status_id": null,+    "in_reply_to_status_id_str": null,+    "in_reply_to_user_id": null,+    "in_reply_to_user_id_str": null,+    "in_reply_to_screen_name": null,+    "user": {+      "id": 15062340,+      "id_str": "15062340",+      "name": "/dev/cloud/jeremy",+      "screen_name": "jeremycloud",+      "location": "Madison, Wisconsin",+      "description": "Professional yak shaver. Amateur bike shedder.",+      "url": "https://t.co/FcYeBkOpVY",+      "entities": {+        "url": {+          "urls": [+            {+              "url": "https://t.co/FcYeBkOpVY",+              "expanded_url": "http://about.me/jeremy.cloud",+              "display_url": "about.me/jeremy.cloud",+              "indices": [+                0,+                23+              ]+            }+          ]+        },+        "description": {+          "urls": []+        }+      },+      "protected": false,+      "followers_count": 4329,+      "friends_count": 411,+      "listed_count": 103,+      "created_at": "Mon Jun 09 17:00:58 +0000 2008",+      "favourites_count": 803,+      "utc_offset": -21600,+      "time_zone": "Central Time (US & Canada)",+      "geo_enabled": true,+      "verified": false,+      "statuses_count": 2216,+      "lang": "en",+      "contributors_enabled": false,+      "is_translator": false,+      "is_translation_enabled": false,+      "profile_background_color": "000000",+      "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",+      "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",+      "profile_background_tile": false,+      "profile_image_url": "http://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",+      "profile_image_url_https": "https://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",+      "profile_banner_url": "https://pbs.twimg.com/profile_banners/15062340/1447451621",+      "profile_link_color": "4A913C",+      "profile_sidebar_border_color": "000000",+      "profile_sidebar_fill_color": "000000",+      "profile_text_color": "000000",+      "profile_use_background_image": false,+      "has_extended_profile": true,+      "default_profile": false,+      "default_profile_image": false,+      "following": true,+      "follow_request_sent": false,+      "notifications": false+    },+    "geo": null,+    "coordinates": null,+    "place": null,+    "contributors": null,+    "is_quote_status": false,+    "retweet_count": 0,+    "favorite_count": 2,+    "favorited": false,+    "retweeted": false,+    "possibly_sensitive": false,+    "possibly_sensitive_appealable": false,+    "lang": "en"+  },+  "is_quote_status": true,+  "retweet_count": 0,+  "favorite_count": 0,+  "favorited": false,+  "retweeted": false,+  "possibly_sensitive": false,+  "possibly_sensitive_appealable": false,+  "lang": "en"+}
+ tests/fixtures/tweet-updates/compatibility_extended_13996.json view
@@ -0,0 +1,278 @@+{+  "created_at": "Thu Mar 10 23:12:12 +0000 2016",+  "id": 708067963060916224,+  "id_str": "708067963060916224",+  "full_text": "@jeremycloud Who would win in a battle between a Barred Owl and a Cooper's Hawk? https://t.co/FamikDro2h",+  "display_text_range": [+    13,+    80+  ],+  "entities": {+    "hashtags": [],+    "symbols": [],+    "user_mentions": [+      {+        "screen_name": "jeremycloud",+        "name": "/dev/cloud/jeremy",+        "id": 15062340,+        "id_str": "15062340",+        "indices": [+          0,+          12+        ]+      }+    ],+    "urls": [+      {+        "url": "https://t.co/FamikDro2h",+        "expanded_url": "https://twitter.com/jeremycloud/status/703621193417379840",+        "display_url": "twitter.com/jeremycloud/st…",+        "indices": [+          81,+          104+        ]+      }+    ]+  },+  "truncated": false,+  "source": "bonesTwurl",+  "in_reply_to_status_id": 704059336788606976,+  "in_reply_to_status_id_str": "704059336788606976",+  "in_reply_to_user_id": 15062340,+  "in_reply_to_user_id_str": "15062340",+  "in_reply_to_screen_name": "jeremycloud",+  "user": {+    "id": 4449621923,+    "id_str": "4449621923",+    "name": "Mr Bones",+    "screen_name": "MrBonesDroid",+    "location": "",+    "profile_location": null,+    "description": "",+    "url": null,+    "entities": {+    "description": {+    "urls": []+     }+    },+    "protected": true,+    "followers_count": 5,+    "friends_count": 7,+    "listed_count": 0,+    "created_at": "Fri Dec 11 15:18:02 +0000 2015",+    "favourites_count": 7,+    "utc_offset": -25200,+    "time_zone": "Pacific Time (US & Canada)",+    "geo_enabled": false,+    "verified": false,+    "statuses_count": 35,+    "lang": "en-gb",+    "contributors_enabled": false,+    "is_translator": false,+    "is_translation_enabled": false,+    "profile_background_color": "F5F8FA",+    "profile_background_image_url": null,+    "profile_background_image_url_https": null,+    "profile_background_tile": false,+    "profile_image_url": "http://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",+    "profile_link_color": "2B7BB9",+    "profile_sidebar_border_color": "C0DEED",+    "profile_sidebar_fill_color": "DDEEF6",+    "profile_text_color": "333333",+    "profile_use_background_image": true,+    "has_extended_profile": false,+    "default_profile": true,+    "default_profile_image": false,+    "following": true,+    "follow_request_sent": false,+    "notifications": false+  },+  "geo": null,+  "coordinates": null,+  "place": null,+  "contributors": null,+  "quoted_status_id": 703621193417379840,+  "quoted_status_id_str": "703621193417379840",+  "quoted_status": {+    "created_at": "Sat Feb 27 16:42:19 +0000 2016",+    "id": 703621193417379840,+    "id_str": "703621193417379840",+    "full_text": "Cooper’s Hawk https://t.co/nppuOGne9X",+    "display_text_range": [+      0,+      37+    ],+    "entities": {+      "hashtags": [],+      "symbols": [],+      "user_mentions": [],+      "urls": [],+      "media": [+        {+          "id": 703621193182502913,+          "id_str": "703621193182502913",+          "indices": [+            14,+            37+          ],+          "media_url": "http://pbs.twimg.com/media/CcPDv0wUYAE3D-2.jpg",+          "media_url_https": "https://pbs.twimg.com/media/CcPDv0wUYAE3D-2.jpg",+          "url": "https://t.co/nppuOGne9X",+          "display_url": "pic.twitter.com/nppuOGne9X",+          "expanded_url": "http://twitter.com/jeremycloud/status/703621193417379840/photo/1",+          "type": "photo",+          "sizes": {+            "medium": {+              "w": 600,+              "h": 398,+              "resize": "fit"+            },+            "thumb": {+              "w": 150,+              "h": 150,+              "resize": "crop"+            },+            "large": {+              "w": 1024,+              "h": 680,+              "resize": "fit"+            },+            "small": {+              "w": 340,+              "h": 226,+              "resize": "fit"+            }+          }+        }+      ]+    },+    "extended_entities": {+      "media": [+        {+          "id": 703621193182502913,+          "id_str": "703621193182502913",+          "indices": [+            14,+            37+          ],+          "media_url": "http://pbs.twimg.com/media/CcPDv0wUYAE3D-2.jpg",+          "media_url_https": "https://pbs.twimg.com/media/CcPDv0wUYAE3D-2.jpg",+          "url": "https://t.co/nppuOGne9X",+          "display_url": "pic.twitter.com/nppuOGne9X",+          "expanded_url": "http://twitter.com/jeremycloud/status/703621193417379840/photo/1",+          "type": "photo",+          "sizes": {+            "medium": {+              "w": 600,+              "h": 398,+              "resize": "fit"+            },+            "thumb": {+              "w": 150,+              "h": 150,+              "resize": "crop"+            },+            "large": {+              "w": 1024,+              "h": 680,+              "resize": "fit"+            },+            "small": {+              "w": 340,+              "h": 226,+              "resize": "fit"+            }+          }+        }+      ]+    },+    "truncated": false,+    "source": "<a href=\"http://www.apple.com/\" rel=\"nofollow\">OS X</a>",+    "in_reply_to_status_id": null,+    "in_reply_to_status_id_str": null,+    "in_reply_to_user_id": null,+    "in_reply_to_user_id_str": null,+    "in_reply_to_screen_name": null,+    "user": {+      "id": 15062340,+      "id_str": "15062340",+      "name": "/dev/cloud/jeremy",+      "screen_name": "jeremycloud",+      "location": "Madison, Wisconsin",+      "description": "Professional yak shaver. Amateur bike shedder.",+      "url": "https://t.co/FcYeBkOpVY",+      "entities": {+        "url": {+          "urls": [+            {+              "url": "https://t.co/FcYeBkOpVY",+              "expanded_url": "http://about.me/jeremy.cloud",+              "display_url": "about.me/jeremy.cloud",+              "indices": [+                0,+                23+              ]+            }+          ]+        },+        "description": {+          "urls": []+        }+      },+      "protected": false,+      "followers_count": 4329,+      "friends_count": 411,+      "listed_count": 103,+      "created_at": "Mon Jun 09 17:00:58 +0000 2008",+      "favourites_count": 803,+      "utc_offset": -21600,+      "time_zone": "Central Time (US & Canada)",+      "geo_enabled": true,+      "verified": false,+      "statuses_count": 2216,+      "lang": "en",+      "contributors_enabled": false,+      "is_translator": false,+      "is_translation_enabled": false,+      "profile_background_color": "000000",+      "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",+      "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",+      "profile_background_tile": false,+      "profile_image_url": "http://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",+      "profile_image_url_https": "https://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",+      "profile_banner_url": "https://pbs.twimg.com/profile_banners/15062340/1447451621",+      "profile_link_color": "4A913C",+      "profile_sidebar_border_color": "000000",+      "profile_sidebar_fill_color": "000000",+      "profile_text_color": "000000",+      "profile_use_background_image": false,+      "has_extended_profile": true,+      "default_profile": false,+      "default_profile_image": false,+      "following": true,+      "follow_request_sent": false,+      "notifications": false+    },+    "geo": null,+    "coordinates": null,+    "place": null,+    "contributors": null,+    "is_quote_status": false,+    "retweet_count": 0,+    "favorite_count": 2,+    "favorited": false,+    "retweeted": false,+    "possibly_sensitive": false,+    "possibly_sensitive_appealable": false,+    "lang": "en"+  },+  "is_quote_status": true,+  "retweet_count": 0,+  "favorite_count": 0,+  "favorited": false,+  "retweeted": false,+  "possibly_sensitive": false,+  "possibly_sensitive_appealable": false,+  "lang": "en"+}
+ tests/fixtures/tweet-updates/compatibilityplus_classic_13994.json view
@@ -0,0 +1,270 @@+{+  "created_at": "Mon Mar 28 14:39:13 +0000 2016",+  "id": 714461850188926976,+  "id_str": "714461850188926976",+  "text": "@jeremycloud It's neat to have owls and raccoons around until you realize that raccoons will eat the eggs from the … https://t.co/OY7qmdJQnO",+  "entities": {+    "hashtags": [],+    "symbols": [],+    "user_mentions": [+      {+        "screen_name": "jeremycloud",+        "name": "/dev/cloud/jeremy",+        "id": 15062340,+        "id_str": "15062340",+        "indices": [+          0,+          12+        ]+      }+    ],+    "urls": [+      {+        "url": "https://t.co/OY7qmdJQnO",+        "expanded_url": "https://twitter.com/i/web/status/714461850188926976",+        "display_url": "twitter.com/i/web/status/7…",+        "indices": [+          117,+          140+        ]+      }+    ]+  },+  "truncated": true,+  "source": "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>",+  "in_reply_to_status_id": 706860403981099008,+  "in_reply_to_status_id_str": "706860403981099008",+  "in_reply_to_user_id": 15062340,+  "in_reply_to_user_id_str": "15062340",+  "in_reply_to_screen_name": "jeremycloud",+  "user": {+    "id": 4449621923,+    "id_str": "4449621923",+    "name": "Mr Bones",+    "screen_name": "MrBonesDroid",+    "location": "",+    "profile_location": null,+    "description": "",+    "url": null,+    "entities": {+    "description": {+    "urls": []+     }+    },+    "protected": true,+    "followers_count": 5,+    "friends_count": 7,+    "listed_count": 0,+    "created_at": "Fri Dec 11 15:18:02 +0000 2015",+    "favourites_count": 7,+    "utc_offset": -25200,+    "time_zone": "Pacific Time (US & Canada)",+    "geo_enabled": false,+    "verified": false,+    "statuses_count": 35,+    "lang": "en-gb",+    "contributors_enabled": false,+    "is_translator": false,+    "is_translation_enabled": false,+    "profile_background_color": "F5F8FA",+    "profile_background_image_url": null,+    "profile_background_image_url_https": null,+    "profile_background_tile": false,+    "profile_image_url": "http://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",+    "profile_link_color": "2B7BB9",+    "profile_sidebar_border_color": "C0DEED",+    "profile_sidebar_fill_color": "DDEEF6",+    "profile_text_color": "333333",+    "profile_use_background_image": true,+    "has_extended_profile": false,+    "default_profile": true,+    "default_profile_image": false,+    "following": true,+    "follow_request_sent": false,+    "notifications": false+  },+  "geo": null,+  "coordinates": null,+  "place": null,+  "contributors": null,+  "quoted_status_id": 704059336788606976,+  "quoted_status_id_str": "704059336788606976",+  "quoted_status": {+    "created_at": "Sun Feb 28 21:43:21 +0000 2016",+    "id": 704059336788606976,+    "id_str": "704059336788606976",+    "text": "My favorite photographic subject, up closer than ever before. https://t.co/K958bKh9Sd",+    "entities": {+      "hashtags": [],+      "symbols": [],+      "user_mentions": [],+      "urls": [],+      "media": [+        {+          "id": 704059330149031936,+          "id_str": "704059330149031936",+          "indices": [+            62,+            85+          ],+          "media_url": "http://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",+          "media_url_https": "https://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",+          "url": "https://t.co/K958bKh9Sd",+          "display_url": "pic.twitter.com/K958bKh9Sd",+          "expanded_url": "http://twitter.com/jeremycloud/status/704059336788606976/photo/1",+          "type": "photo",+          "sizes": {+            "medium": {+              "w": 600,+              "h": 600,+              "resize": "fit"+            },+            "thumb": {+              "w": 150,+              "h": 150,+              "resize": "crop"+            },+            "large": {+              "w": 871,+              "h": 871,+              "resize": "fit"+            },+            "small": {+              "w": 340,+              "h": 340,+              "resize": "fit"+            }+          }+        }+      ]+    },+    "extended_entities": {+      "media": [+        {+          "id": 704059330149031936,+          "id_str": "704059330149031936",+          "indices": [+            62,+            85+          ],+          "media_url": "http://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",+          "media_url_https": "https://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",+          "url": "https://t.co/K958bKh9Sd",+          "display_url": "pic.twitter.com/K958bKh9Sd",+          "expanded_url": "http://twitter.com/jeremycloud/status/704059336788606976/photo/1",+          "type": "photo",+          "sizes": {+            "medium": {+              "w": 600,+              "h": 600,+              "resize": "fit"+            },+            "thumb": {+              "w": 150,+              "h": 150,+              "resize": "crop"+            },+            "large": {+              "w": 871,+              "h": 871,+              "resize": "fit"+            },+            "small": {+              "w": 340,+              "h": 340,+              "resize": "fit"+            }+          }+        }+      ]+    },+    "truncated": false,+    "source": "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>",+    "in_reply_to_status_id": null,+    "in_reply_to_status_id_str": null,+    "in_reply_to_user_id": null,+    "in_reply_to_user_id_str": null,+    "in_reply_to_screen_name": null,+    "user": {+      "id": 15062340,+      "id_str": "15062340",+      "name": "/dev/cloud/jeremy",+      "screen_name": "jeremycloud",+      "location": "Madison, Wisconsin",+      "description": "Professional yak shaver. Amateur bike shedder.",+      "url": "https://t.co/FcYeBkOpVY",+      "entities": {+        "url": {+          "urls": [+            {+              "url": "https://t.co/FcYeBkOpVY",+              "expanded_url": "http://about.me/jeremy.cloud",+              "display_url": "about.me/jeremy.cloud",+              "indices": [+                0,+                23+              ]+            }+          ]+        },+        "description": {+          "urls": []+        }+      },+      "protected": false,+      "followers_count": 4324,+      "friends_count": 410,+      "listed_count": 103,+      "created_at": "Mon Jun 09 17:00:58 +0000 2008",+      "favourites_count": 815,+      "utc_offset": -18000,+      "time_zone": "Central Time (US & Canada)",+      "geo_enabled": true,+      "verified": false,+      "statuses_count": 2218,+      "lang": "en",+      "contributors_enabled": false,+      "is_translator": false,+      "is_translation_enabled": false,+      "profile_background_color": "000000",+      "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",+      "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",+      "profile_background_tile": false,+      "profile_image_url": "http://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",+      "profile_image_url_https": "https://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",+      "profile_banner_url": "https://pbs.twimg.com/profile_banners/15062340/1447451621",+      "profile_link_color": "4A913C",+      "profile_sidebar_border_color": "000000",+      "profile_sidebar_fill_color": "000000",+      "profile_text_color": "000000",+      "profile_use_background_image": false,+      "has_extended_profile": true,+      "default_profile": false,+      "default_profile_image": false,+      "following": true,+      "follow_request_sent": false,+      "notifications": false+    },+    "geo": null,+    "coordinates": null,+    "place": null,+    "contributors": null,+    "is_quote_status": false,+    "retweet_count": 0,+    "favorite_count": 11,+    "favorited": false,+    "retweeted": false,+    "possibly_sensitive": false,+    "possibly_sensitive_appealable": false,+    "lang": "en"+  },+  "is_quote_status": true,+  "retweet_count": 0,+  "favorite_count": 0,+  "favorited": false,+  "retweeted": false,+  "possibly_sensitive": false,+  "possibly_sensitive_appealable": false,+  "lang": "en"+}
+ tests/fixtures/tweet-updates/compatibilityplus_classic_hidden_13797.json view
@@ -0,0 +1,304 @@+{+  "created_at": "Mon Mar 28 14:39:13 +0000 2016",+  "id": 714461850188926976,+  "id_str": "714461850188926976",+  "text": "@jeremycloud It's neat to have owls and raccoons around until you realize that raccoons will eat the eggs from the … https://t.co/OY7qmdJQnO",+  "entities": {+    "hashtags": [],+    "symbols": [],+    "user_mentions": [+      {+        "screen_name": "jeremycloud",+        "name": "/dev/cloud/jeremy",+        "id": 15062340,+        "id_str": "15062340",+        "indices": [+          0,+          12+        ]+      }+    ],+    "urls": [+      {+        "url": "https://t.co/OY7qmdJQnO",+        "expanded_url": "https://twitter.com/i/web/status/714461850188926976",+        "display_url": "twitter.com/i/web/status/7…",+        "indices": [+          117,+          140+        ]+      }+    ]+  },+  "truncated": true,+  "extended_tweet": {+    "full_text": "@jeremycloud It's neat to have owls and raccoons around until you realize that raccoons will eat the eggs from the owl's nest https://t.co/Q0pkaU4ORH",+    "display_text_range": [+      13,+      125+    ],+    "entities": {+      "hashtags": [],+      "symbols": [],+      "user_mentions": [+        {+          "screen_name": "jeremycloud",+          "name": "/dev/cloud/jeremy",+          "id": 15062340,+          "id_str": "15062340",+          "indices": [+            0,+            12+          ]+        }+      ],+      "urls": [+        {+          "url": "https://t.co/Q0pkaU4ORH",+          "expanded_url": "https://twitter.com/jeremycloud/status/704059336788606976",+          "display_url": "twitter.com/jeremycloud/st…",+          "indices": [+            126,+            149+          ]+        }+      ]+    }+  },+  "source": "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>",+  "in_reply_to_status_id": 706860403981099008,+  "in_reply_to_status_id_str": "706860403981099008",+  "in_reply_to_user_id": 15062340,+  "in_reply_to_user_id_str": "15062340",+  "in_reply_to_screen_name": "jeremycloud",+  "user": {+    "id": 4449621923,+    "id_str": "4449621923",+    "name": "Mr Bones",+    "screen_name": "MrBonesDroid",+    "location": "",+    "profile_location": null,+    "description": "",+    "url": null,+    "entities": {+    "description": {+    "urls": []+     }+    },+    "protected": true,+    "followers_count": 5,+    "friends_count": 7,+    "listed_count": 0,+    "created_at": "Fri Dec 11 15:18:02 +0000 2015",+    "favourites_count": 7,+    "utc_offset": -25200,+    "time_zone": "Pacific Time (US & Canada)",+    "geo_enabled": false,+    "verified": false,+    "statuses_count": 35,+    "lang": "en-gb",+    "contributors_enabled": false,+    "is_translator": false,+    "is_translation_enabled": false,+    "profile_background_color": "F5F8FA",+    "profile_background_image_url": null,+    "profile_background_image_url_https": null,+    "profile_background_tile": false,+    "profile_image_url": "http://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",+    "profile_link_color": "2B7BB9",+    "profile_sidebar_border_color": "C0DEED",+    "profile_sidebar_fill_color": "DDEEF6",+    "profile_text_color": "333333",+    "profile_use_background_image": true,+    "has_extended_profile": false,+    "default_profile": true,+    "default_profile_image": false,+    "following": true,+    "follow_request_sent": false,+    "notifications": false+  },+  "geo": null,+  "coordinates": null,+  "place": null,+  "contributors": null,+  "quoted_status_id": 704059336788606976,+  "quoted_status_id_str": "704059336788606976",+  "quoted_status": {+    "created_at": "Sun Feb 28 21:43:21 +0000 2016",+    "id": 704059336788606976,+    "id_str": "704059336788606976",+    "text": "My favorite photographic subject, up closer than ever before. https://t.co/K958bKh9Sd",+    "entities": {+      "hashtags": [],+      "symbols": [],+      "user_mentions": [],+      "urls": [],+      "media": [+        {+          "id": 704059330149031936,+          "id_str": "704059330149031936",+          "indices": [+            62,+            85+          ],+          "media_url": "http://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",+          "media_url_https": "https://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",+          "url": "https://t.co/K958bKh9Sd",+          "display_url": "pic.twitter.com/K958bKh9Sd",+          "expanded_url": "http://twitter.com/jeremycloud/status/704059336788606976/photo/1",+          "type": "photo",+          "sizes": {+            "medium": {+              "w": 600,+              "h": 600,+              "resize": "fit"+            },+            "thumb": {+              "w": 150,+              "h": 150,+              "resize": "crop"+            },+            "large": {+              "w": 871,+              "h": 871,+              "resize": "fit"+            },+            "small": {+              "w": 340,+              "h": 340,+              "resize": "fit"+            }+          }+        }+      ]+    },+    "extended_entities": {+      "media": [+        {+          "id": 704059330149031936,+          "id_str": "704059330149031936",+          "indices": [+            62,+            85+          ],+          "media_url": "http://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",+          "media_url_https": "https://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",+          "url": "https://t.co/K958bKh9Sd",+          "display_url": "pic.twitter.com/K958bKh9Sd",+          "expanded_url": "http://twitter.com/jeremycloud/status/704059336788606976/photo/1",+          "type": "photo",+          "sizes": {+            "medium": {+              "w": 600,+              "h": 600,+              "resize": "fit"+            },+            "thumb": {+              "w": 150,+              "h": 150,+              "resize": "crop"+            },+            "large": {+              "w": 871,+              "h": 871,+              "resize": "fit"+            },+            "small": {+              "w": 340,+              "h": 340,+              "resize": "fit"+            }+          }+        }+      ]+    },+    "truncated": false,+    "source": "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>",+    "in_reply_to_status_id": null,+    "in_reply_to_status_id_str": null,+    "in_reply_to_user_id": null,+    "in_reply_to_user_id_str": null,+    "in_reply_to_screen_name": null,+    "user": {+      "id": 15062340,+      "id_str": "15062340",+      "name": "/dev/cloud/jeremy",+      "screen_name": "jeremycloud",+      "location": "Madison, Wisconsin",+      "description": "Professional yak shaver. Amateur bike shedder.",+      "url": "https://t.co/FcYeBkOpVY",+      "entities": {+        "url": {+          "urls": [+            {+              "url": "https://t.co/FcYeBkOpVY",+              "expanded_url": "http://about.me/jeremy.cloud",+              "display_url": "about.me/jeremy.cloud",+              "indices": [+                0,+                23+              ]+            }+          ]+        },+        "description": {+          "urls": []+        }+      },+      "protected": false,+      "followers_count": 4324,+      "friends_count": 410,+      "listed_count": 103,+      "created_at": "Mon Jun 09 17:00:58 +0000 2008",+      "favourites_count": 815,+      "utc_offset": -18000,+      "time_zone": "Central Time (US & Canada)",+      "geo_enabled": true,+      "verified": false,+      "statuses_count": 2218,+      "lang": "en",+      "contributors_enabled": false,+      "is_translator": false,+      "is_translation_enabled": false,+      "profile_background_color": "000000",+      "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",+      "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",+      "profile_background_tile": false,+      "profile_image_url": "http://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",+      "profile_image_url_https": "https://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",+      "profile_banner_url": "https://pbs.twimg.com/profile_banners/15062340/1447451621",+      "profile_link_color": "4A913C",+      "profile_sidebar_border_color": "000000",+      "profile_sidebar_fill_color": "000000",+      "profile_text_color": "000000",+      "profile_use_background_image": false,+      "has_extended_profile": true,+      "default_profile": false,+      "default_profile_image": false,+      "following": true,+      "follow_request_sent": false,+      "notifications": false+    },+    "geo": null,+    "coordinates": null,+    "place": null,+    "contributors": null,+    "is_quote_status": false,+    "retweet_count": 0,+    "favorite_count": 11,+    "favorited": false,+    "retweeted": false,+    "possibly_sensitive": false,+    "possibly_sensitive_appealable": false,+    "lang": "en"+  },+  "is_quote_status": true,+  "retweet_count": 0,+  "favorite_count": 0,+  "favorited": false,+  "retweeted": false,+  "possibly_sensitive": false,+  "possibly_sensitive_appealable": false,+  "lang": "en"+}
+ tests/fixtures/tweet-updates/compatibilityplus_extended_13997.json view
@@ -0,0 +1,278 @@+{+  "created_at": "Mon Mar 28 14:39:13 +0000 2016",+  "id": 714461850188926976,+  "id_str": "714461850188926976",+  "full_text": "@jeremycloud It's neat to have owls and raccoons around until you realize that raccoons will eat the eggs from the owl's nest https://t.co/Q0pkaU4ORH",+  "display_text_range": [+    13,+    125+  ],+  "entities": {+    "hashtags": [],+    "symbols": [],+    "user_mentions": [+      {+        "screen_name": "jeremycloud",+        "name": "/dev/cloud/jeremy",+        "id": 15062340,+        "id_str": "15062340",+        "indices": [+          0,+          12+        ]+      }+    ],+    "urls": [+      {+        "url": "https://t.co/Q0pkaU4ORH",+        "expanded_url": "https://twitter.com/jeremycloud/status/704059336788606976",+        "display_url": "twitter.com/jeremycloud/st…",+        "indices": [+          126,+          149+        ]+      }+    ]+  },+  "truncated": false,+  "source": "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>",+  "in_reply_to_status_id": 706860403981099008,+  "in_reply_to_status_id_str": "706860403981099008",+  "in_reply_to_user_id": 15062340,+  "in_reply_to_user_id_str": "15062340",+  "in_reply_to_screen_name": "jeremycloud",+  "user": {+    "id": 4449621923,+    "id_str": "4449621923",+    "name": "Mr Bones",+    "screen_name": "MrBonesDroid",+    "location": "",+    "profile_location": null,+    "description": "",+    "url": null,+    "entities": {+    "description": {+    "urls": []+     }+    },+    "protected": true,+    "followers_count": 5,+    "friends_count": 7,+    "listed_count": 0,+    "created_at": "Fri Dec 11 15:18:02 +0000 2015",+    "favourites_count": 7,+    "utc_offset": -25200,+    "time_zone": "Pacific Time (US & Canada)",+    "geo_enabled": false,+    "verified": false,+    "statuses_count": 35,+    "lang": "en-gb",+    "contributors_enabled": false,+    "is_translator": false,+    "is_translation_enabled": false,+    "profile_background_color": "F5F8FA",+    "profile_background_image_url": null,+    "profile_background_image_url_https": null,+    "profile_background_tile": false,+    "profile_image_url": "http://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/677097663288860672/zZxWCPSI_normal.jpg",+    "profile_link_color": "2B7BB9",+    "profile_sidebar_border_color": "C0DEED",+    "profile_sidebar_fill_color": "DDEEF6",+    "profile_text_color": "333333",+    "profile_use_background_image": true,+    "has_extended_profile": false,+    "default_profile": true,+    "default_profile_image": false,+    "following": true,+    "follow_request_sent": false,+    "notifications": false+  },+  "geo": null,+  "coordinates": null,+  "place": null,+  "contributors": null,+  "quoted_status_id": 704059336788606976,+  "quoted_status_id_str": "704059336788606976",+  "quoted_status": {+    "created_at": "Sun Feb 28 21:43:21 +0000 2016",+    "id": 704059336788606976,+    "id_str": "704059336788606976",+    "full_text": "My favorite photographic subject, up closer than ever before. https://t.co/K958bKh9Sd",+    "display_text_range": [+      0,+      85+    ],+    "entities": {+      "hashtags": [],+      "symbols": [],+      "user_mentions": [],+      "urls": [],+      "media": [+        {+          "id": 704059330149031936,+          "id_str": "704059330149031936",+          "indices": [+            62,+            85+          ],+          "media_url": "http://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",+          "media_url_https": "https://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",+          "url": "https://t.co/K958bKh9Sd",+          "display_url": "pic.twitter.com/K958bKh9Sd",+          "expanded_url": "http://twitter.com/jeremycloud/status/704059336788606976/photo/1",+          "type": "photo",+          "sizes": {+            "medium": {+              "w": 600,+              "h": 600,+              "resize": "fit"+            },+            "thumb": {+              "w": 150,+              "h": 150,+              "resize": "crop"+            },+            "large": {+              "w": 871,+              "h": 871,+              "resize": "fit"+            },+            "small": {+              "w": 340,+              "h": 340,+              "resize": "fit"+            }+          }+        }+      ]+    },+    "extended_entities": {+      "media": [+        {+          "id": 704059330149031936,+          "id_str": "704059330149031936",+          "indices": [+            62,+            85+          ],+          "media_url": "http://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",+          "media_url_https": "https://pbs.twimg.com/media/CcVSOwJVIAAKwE6.jpg",+          "url": "https://t.co/K958bKh9Sd",+          "display_url": "pic.twitter.com/K958bKh9Sd",+          "expanded_url": "http://twitter.com/jeremycloud/status/704059336788606976/photo/1",+          "type": "photo",+          "sizes": {+            "medium": {+              "w": 600,+              "h": 600,+              "resize": "fit"+            },+            "thumb": {+              "w": 150,+              "h": 150,+              "resize": "crop"+            },+            "large": {+              "w": 871,+              "h": 871,+              "resize": "fit"+            },+            "small": {+              "w": 340,+              "h": 340,+              "resize": "fit"+            }+          }+        }+      ]+    },+    "truncated": false,+    "source": "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>",+    "in_reply_to_status_id": null,+    "in_reply_to_status_id_str": null,+    "in_reply_to_user_id": null,+    "in_reply_to_user_id_str": null,+    "in_reply_to_screen_name": null,+    "user": {+      "id": 15062340,+      "id_str": "15062340",+      "name": "/dev/cloud/jeremy",+      "screen_name": "jeremycloud",+      "location": "Madison, Wisconsin",+      "description": "Professional yak shaver. Amateur bike shedder.",+      "url": "https://t.co/FcYeBkOpVY",+      "entities": {+        "url": {+          "urls": [+            {+              "url": "https://t.co/FcYeBkOpVY",+              "expanded_url": "http://about.me/jeremy.cloud",+              "display_url": "about.me/jeremy.cloud",+              "indices": [+                0,+                23+              ]+            }+          ]+        },+        "description": {+          "urls": []+        }+      },+      "protected": false,+      "followers_count": 4324,+      "friends_count": 410,+      "listed_count": 103,+      "created_at": "Mon Jun 09 17:00:58 +0000 2008",+      "favourites_count": 815,+      "utc_offset": -18000,+      "time_zone": "Central Time (US & Canada)",+      "geo_enabled": true,+      "verified": false,+      "statuses_count": 2218,+      "lang": "en",+      "contributors_enabled": false,+      "is_translator": false,+      "is_translation_enabled": false,+      "profile_background_color": "000000",+      "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",+      "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",+      "profile_background_tile": false,+      "profile_image_url": "http://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",+      "profile_image_url_https": "https://pbs.twimg.com/profile_images/436903139183054849/i_MbCcoW_normal.jpeg",+      "profile_banner_url": "https://pbs.twimg.com/profile_banners/15062340/1447451621",+      "profile_link_color": "4A913C",+      "profile_sidebar_border_color": "000000",+      "profile_sidebar_fill_color": "000000",+      "profile_text_color": "000000",+      "profile_use_background_image": false,+      "has_extended_profile": true,+      "default_profile": false,+      "default_profile_image": false,+      "following": true,+      "follow_request_sent": false,+      "notifications": false+    },+    "geo": null,+    "coordinates": null,+    "place": null,+    "contributors": null,+    "is_quote_status": false,+    "retweet_count": 0,+    "favorite_count": 11,+    "favorited": false,+    "retweeted": false,+    "possibly_sensitive": false,+    "possibly_sensitive_appealable": false,+    "lang": "en"+  },+  "is_quote_status": true,+  "retweet_count": 0,+  "favorite_count": 0,+  "favorited": false,+  "retweeted": false,+  "possibly_sensitive": false,+  "possibly_sensitive_appealable": false,+  "lang": "en"+}
+ tests/fixtures/tweet-updates/extended_classic_14002.json view
@@ -0,0 +1,87 @@+{+  "created_at": "Thu Jun 16 15:57:14 +0000 2016",+  "id": 743472511740870657,+  "id_str": "743472511740870657",+  "full_text": "Just setting up my Twitter. #myfirstTweet",+  "truncated": false,+  "display_text_range": [+    0,+    41+  ],+  "entities": {+    "hashtags": [+      {+        "text": "myfirstTweet",+        "indices": [+          28,+          41+        ]+      }+    ],+    "symbols": [],+    "user_mentions": [],+    "urls": []+  },+  "source": "&lt;a href=\"http://twitter.com\" rel=\"nofollow\"&gt;Twitter Web Client&lt;/a&gt;",+  "in_reply_to_status_id": null,+  "in_reply_to_status_id_str": null,+  "in_reply_to_user_id": null,+  "in_reply_to_user_id_str": null,+  "in_reply_to_screen_name": null,+  "user": {+    "id": 3883872981,+    "id_str": "3883872981",+    "name": "Beyond 140",+    "screen_name": "beyond_oneforty",+    "location": "",+    "description": "",+    "url": null,+    "entities": {+      "description": {+        "urls": []+      }+    },+    "protected": true,+    "followers_count": 51,+    "friends_count": 0,+    "listed_count": 0,+    "created_at": "Tue Oct 06 21:08:37 +0000 2015",+    "favourites_count": 1,+    "utc_offset": null,+    "time_zone": null,+    "geo_enabled": false,+    "verified": false,+    "statuses_count": 9,+    "lang": "en",+    "contributors_enabled": false,+    "is_translator": false,+    "is_translation_enabled": false,+    "profile_background_color": "C0DEED",+    "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",+    "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",+    "profile_background_tile": false,+    "profile_image_url": "http://pbs.twimg.com/profile_images/651506565225271296/PMEWfvS__normal.jpg",+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/651506565225271296/PMEWfvS__normal.jpg",+    "profile_link_color": "0084B4",+    "profile_sidebar_border_color": "C0DEED",+    "profile_sidebar_fill_color": "DDEEF6",+    "profile_text_color": "333333",+    "profile_use_background_image": true,+    "has_extended_profile": false,+    "default_profile": true,+    "default_profile_image": false,+    "following": false,+    "follow_request_sent": false,+    "notifications": false+  },+  "geo": null,+  "coordinates": null,+  "place": null,+  "contributors": null,+  "is_quote_status": false,+  "retweet_count": 0,+  "favorite_count": 1,+  "favorited": false,+  "retweeted": false,+  "lang": "en"+}
+ tests/fixtures/tweet-updates/extended_classic_hidden_13761.json view
@@ -0,0 +1,183 @@+{+  "created_at": "Thu Jun 16 16:24:44 +0000 2016",+  "id": 743479431658758145,+  "id_str": "743479431658758145",+  "full_text": "@TwitterDev I'm really excited for this change! https://t.co/VnJMDg3cbS",+  "truncated": false,+  "display_text_range": [+    12,+    47+  ],+  "entities": {+    "hashtags": [],+    "symbols": [],+    "user_mentions": [+      {+        "screen_name": "TwitterDev",+        "name": "TwitterDev",+        "id": 2244994945,+        "id_str": "2244994945",+        "indices": [+          0,+          11+        ]+      }+    ],+    "urls": [],+    "media": [+      {+        "id": 743479379079004160,+        "id_str": "743479379079004160",+        "indices": [+          48,+          71+        ],+        "media_url": "http://pbs.twimg.com/tweet_video_thumb/ClFejl_VAAAo9Xk.jpg",+        "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/ClFejl_VAAAo9Xk.jpg",+        "url": "https://t.co/VnJMDg3cbS",+        "display_url": "pic.twitter.com/VnJMDg3cbS",+        "expanded_url": "http://twitter.com/beyond_oneforty/status/743479431658758145/photo/1",+        "type": "photo",+        "sizes": {+          "medium": {+            "w": 480,+            "h": 268,+            "resize": "fit"+          },+          "thumb": {+            "w": 150,+            "h": 150,+            "resize": "crop"+          },+          "large": {+            "w": 480,+            "h": 268,+            "resize": "fit"+          },+          "small": {+            "w": 340,+            "h": 190,+            "resize": "fit"+          }+        }+      }+    ]+  },+  "extended_entities": {+    "media": [+      {+        "id": 743479379079004160,+        "id_str": "743479379079004160",+        "indices": [+          48,+          71+        ],+        "media_url": "http://pbs.twimg.com/tweet_video_thumb/ClFejl_VAAAo9Xk.jpg",+        "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/ClFejl_VAAAo9Xk.jpg",+        "url": "https://t.co/VnJMDg3cbS",+        "display_url": "pic.twitter.com/VnJMDg3cbS",+        "expanded_url": "http://twitter.com/beyond_oneforty/status/743479431658758145/photo/1",+        "type": "animated_gif",+        "sizes": {+          "medium": {+            "w": 480,+            "h": 268,+            "resize": "fit"+          },+          "thumb": {+            "w": 150,+            "h": 150,+            "resize": "crop"+          },+          "large": {+            "w": 480,+            "h": 268,+            "resize": "fit"+          },+          "small": {+            "w": 340,+            "h": 190,+            "resize": "fit"+          }+        },+        "video_info": {+          "aspect_ratio": [+            120,+            67+          ],+          "variants": [+            {+              "bitrate": 0,+              "content_type": "video/mp4",+              "url": "https://pbs.twimg.com/tweet_video/ClFejl_VAAAo9Xk.mp4"+            }+          ]+        }+      }+    ]+  },+  "source": "&lt;a href=\"http://twitter.com\" rel=\"nofollow\"&gt;Twitter Web Client&lt;/a&gt;",+  "in_reply_to_status_id": 735111578660438017,+  "in_reply_to_status_id_str": "735111578660438017",+  "in_reply_to_user_id": 2244994945,+  "in_reply_to_user_id_str": "2244994945",+  "in_reply_to_screen_name": "TwitterDev",+  "user": {+    "id": 3883872981,+    "id_str": "3883872981",+    "name": "Beyond 140",+    "screen_name": "beyond_oneforty",+    "location": "",+    "description": "",+    "url": null,+    "entities": {+      "description": {+        "urls": []+      }+    },+    "protected": true,+    "followers_count": 51,+    "friends_count": 0,+    "listed_count": 0,+    "created_at": "Tue Oct 06 21:08:37 +0000 2015",+    "favourites_count": 1,+    "utc_offset": null,+    "time_zone": null,+    "geo_enabled": false,+    "verified": false,+    "statuses_count": 10,+    "lang": "en",+    "contributors_enabled": false,+    "is_translator": false,+    "is_translation_enabled": false,+    "profile_background_color": "C0DEED",+    "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",+    "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",+    "profile_background_tile": false,+    "profile_image_url": "http://pbs.twimg.com/profile_images/651506565225271296/PMEWfvS__normal.jpg",+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/651506565225271296/PMEWfvS__normal.jpg",+    "profile_link_color": "0084B4",+    "profile_sidebar_border_color": "C0DEED",+    "profile_sidebar_fill_color": "DDEEF6",+    "profile_text_color": "333333",+    "profile_use_background_image": true,+    "has_extended_profile": false,+    "default_profile": true,+    "default_profile_image": false,+    "following": false,+    "follow_request_sent": false,+    "notifications": false+  },+  "geo": null,+  "coordinates": null,+  "place": null,+  "contributors": null,+  "is_quote_status": false,+  "retweet_count": 0,+  "favorite_count": 1,+  "favorited": false,+  "retweeted": false,+  "possibly_sensitive": false,+  "possibly_sensitive_appealable": false,+  "lang": "en"+}
+ tests/fixtures/tweet-updates/extended_extended_14001.json view
@@ -0,0 +1,213 @@+{+  "created_at": "Thu Jun 16 17:33:23 +0000 2016",+  "id": 743496707711733760,+  "id_str": "743496707711733760",+  "full_text": "@twitter @twitterdev has more details about these changes at https://t.co/ZnXoRQy8mK.  Thanks for making @twitter more expressive! https://t.co/AWmiH870F7",+  "truncated": false,+  "display_text_range": [+    9,+    130+  ],+  "entities": {+    "hashtags": [],+    "symbols": [],+    "user_mentions": [+      {+        "screen_name": "twitter",+        "name": "Twitter",+        "id": 783214,+        "id_str": "783214",+        "indices": [+          0,+          8+        ]+      },+      {+        "screen_name": "TwitterDev",+        "name": "TwitterDev",+        "id": 2244994945,+        "id_str": "2244994945",+        "indices": [+          9,+          20+        ]+      },+      {+        "screen_name": "twitter",+        "name": "Twitter",+        "id": 783214,+        "id_str": "783214",+        "indices": [+          105,+          113+        ]+      }+    ],+    "urls": [+      {+        "url": "https://t.co/ZnXoRQy8mK",+        "expanded_url": "https://blog.twitter.com/2016/doing-more-with-140-characters",+        "display_url": "blog.twitter.com/2016/doing-mor…",+        "indices": [+          61,+          84+        ]+      }+    ],+    "media": [+      {+        "id": 743495942192566272,+        "id_str": "743495942192566272",+        "indices": [+          131,+          154+        ],+        "media_url": "http://pbs.twimg.com/tweet_video_thumb/ClFtnsZVEAAE4oA.jpg",+        "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/ClFtnsZVEAAE4oA.jpg",+        "url": "https://t.co/AWmiH870F7",+        "display_url": "pic.twitter.com/AWmiH870F7",+        "expanded_url": "http://twitter.com/beyond_oneforty/status/743496707711733760/photo/1",+        "type": "photo",+        "sizes": {+          "small": {+            "w": 340,+            "h": 255,+            "resize": "fit"+          },+          "medium": {+            "w": 480,+            "h": 360,+            "resize": "fit"+          },+          "thumb": {+            "w": 150,+            "h": 150,+            "resize": "crop"+          },+          "large": {+            "w": 480,+            "h": 360,+            "resize": "fit"+          }+        }+      }+    ]+  },+  "extended_entities": {+    "media": [+      {+        "id": 743495942192566272,+        "id_str": "743495942192566272",+        "indices": [+          131,+          154+        ],+        "media_url": "http://pbs.twimg.com/tweet_video_thumb/ClFtnsZVEAAE4oA.jpg",+        "media_url_https": "https://pbs.twimg.com/tweet_video_thumb/ClFtnsZVEAAE4oA.jpg",+        "url": "https://t.co/AWmiH870F7",+        "display_url": "pic.twitter.com/AWmiH870F7",+        "expanded_url": "http://twitter.com/beyond_oneforty/status/743496707711733760/photo/1",+        "type": "animated_gif",+        "sizes": {+          "small": {+            "w": 340,+            "h": 255,+            "resize": "fit"+          },+          "medium": {+            "w": 480,+            "h": 360,+            "resize": "fit"+          },+          "thumb": {+            "w": 150,+            "h": 150,+            "resize": "crop"+          },+          "large": {+            "w": 480,+            "h": 360,+            "resize": "fit"+          }+        },+        "video_info": {+          "aspect_ratio": [+            4,+            3+          ],+          "variants": [+            {+              "bitrate": 0,+              "content_type": "video/mp4",+              "url": "https://pbs.twimg.com/tweet_video/ClFtnsZVEAAE4oA.mp4"+            }+          ]+        }+      }+    ]+  },+  "source": "&lt;a href=\"http://twitter.com\" rel=\"nofollow\"&gt;Twitter Web Client&lt;/a&gt;",+  "in_reply_to_status_id": 735108260718469121,+  "in_reply_to_status_id_str": "735108260718469121",+  "in_reply_to_user_id": 783214,+  "in_reply_to_user_id_str": "783214",+  "in_reply_to_screen_name": "twitter",+  "user": {+    "id": 3883872981,+    "id_str": "3883872981",+    "name": "Beyond 140",+    "screen_name": "beyond_oneforty",+    "location": "",+    "description": "",+    "url": null,+    "entities": {+      "description": {+        "urls": []+      }+    },+    "protected": true,+    "followers_count": 51,+    "friends_count": 2,+    "listed_count": 0,+    "created_at": "Tue Oct 06 21:08:37 +0000 2015",+    "favourites_count": 1,+    "utc_offset": null,+    "time_zone": null,+    "geo_enabled": false,+    "verified": false,+    "statuses_count": 11,+    "lang": "en",+    "contributors_enabled": false,+    "is_translator": false,+    "is_translation_enabled": false,+    "profile_background_color": "C0DEED",+    "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",+    "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",+    "profile_background_tile": false,+    "profile_image_url": "http://pbs.twimg.com/profile_images/743489433861685249/VOcKbH8Z_normal.jpg",+    "profile_image_url_https": "https://pbs.twimg.com/profile_images/743489433861685249/VOcKbH8Z_normal.jpg",+    "profile_link_color": "0084B4",+    "profile_sidebar_border_color": "C0DEED",+    "profile_sidebar_fill_color": "DDEEF6",+    "profile_text_color": "333333",+    "profile_use_background_image": true,+    "has_extended_profile": false,+    "default_profile": true,+    "default_profile_image": false,+    "following": false,+    "follow_request_sent": false,+    "notifications": false+  },+  "geo": null,+  "coordinates": null,+  "place": null,+  "contributors": null,+  "is_quote_status": false,+  "retweet_count": 0,+  "favorite_count": 0,+  "favorited": false,+  "retweeted": false,+  "possibly_sensitive": false,+  "possibly_sensitive_appealable": false,+  "lang": "en"+}
+ tests/fixtures/user_thimura.json view
@@ -0,0 +1,1 @@+{"screen_name":"thimura","profile_banner_url":"https://pbs.twimg.com/profile_banners/69179963/1402419439","status":{"in_reply_to_status_id":null,"id_str":"495192122836783104","truncated":false,"in_reply_to_screen_name":null,"entities":{"symbols":[],"urls":[],"user_mentions":[],"hashtags":[]},"text":"くそあつい","in_reply_to_user_id_str":null,"favorited":false,"coordinates":null,"retweeted":false,"lang":"ja","retweet_count":0,"in_reply_to_user_id":null,"created_at":"Fri Aug 01 12:59:36 +0000 2014","source":"\u003ca href=\"http://twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c/a\u003e","geo":null,"id":495192122836783104,"in_reply_to_status_id_str":null,"favorite_count":0,"contributors":null,"place":null},"is_translation_enabled":false,"needs_phone_verification":false,"default_profile":false,"profile_image_url":"http://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","default_profile_image":false,"id_str":"69179963","profile_background_image_url_https":"https://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","protected":false,"location":"State# Irotoridori.No.World","entities":{"url":{"urls":[{"expanded_url":"http://d.hatena.ne.jp/thimura","url":"http://t.co/TFUAsAffX0","indices":[0,22],"display_url":"d.hatena.ne.jp/thimura"}]},"description":{"urls":[]}},"profile_background_color":"000000","utc_offset":32400,"url":"http://t.co/TFUAsAffX0","profile_text_color":"333333","profile_image_url_https":"https://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","suspended":false,"verified":false,"statuses_count":24709,"profile_background_tile":true,"following":false,"lang":"en","follow_request_sent":false,"profile_sidebar_fill_color":"BABDD1","time_zone":"Tokyo","name":"ちむら","profile_sidebar_border_color":"FFFFFF","geo_enabled":false,"listed_count":102,"contributors_enabled":false,"created_at":"Thu Aug 27 02:48:06 +0000 2009","id":69179963,"friends_count":780,"is_translator":false,"favourites_count":17313,"notifications":false,"profile_background_image_url":"http://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","profile_use_background_image":true,"description":"真紅かわいい","profile_link_color":"00660F","followers_count":754}
+ tests/fixtures/user_thimura_lang_null.json view
@@ -0,0 +1,1 @@+{"screen_name":"thimura","profile_banner_url":"https://pbs.twimg.com/profile_banners/69179963/1402419439","status":{"in_reply_to_status_id":null,"id_str":"495192122836783104","truncated":false,"in_reply_to_screen_name":null,"entities":{"symbols":[],"urls":[],"user_mentions":[],"hashtags":[]},"text":"くそあつい","in_reply_to_user_id_str":null,"favorited":false,"coordinates":null,"retweeted":false,"lang":null,"retweet_count":0,"in_reply_to_user_id":null,"created_at":"Fri Aug 01 12:59:36 +0000 2014","source":"\u003ca href=\"http://twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c/a\u003e","geo":null,"id":495192122836783104,"in_reply_to_status_id_str":null,"favorite_count":0,"contributors":null,"place":null},"is_translation_enabled":false,"needs_phone_verification":false,"default_profile":false,"profile_image_url":"http://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","default_profile_image":false,"id_str":"69179963","profile_background_image_url_https":"https://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","protected":false,"location":"State# Irotoridori.No.World","entities":{"url":{"urls":[{"expanded_url":"http://d.hatena.ne.jp/thimura","url":"http://t.co/TFUAsAffX0","indices":[0,22],"display_url":"d.hatena.ne.jp/thimura"}]},"description":{"urls":[]}},"profile_background_color":"000000","utc_offset":32400,"url":"http://t.co/TFUAsAffX0","profile_text_color":"333333","profile_image_url_https":"https://pbs.twimg.com/profile_images/414044387346116609/VNMfLpY7_normal.png","suspended":false,"verified":false,"statuses_count":24709,"profile_background_tile":true,"following":false,"lang":null,"follow_request_sent":false,"profile_sidebar_fill_color":"BABDD1","time_zone":"Tokyo","name":"ちむら","profile_sidebar_border_color":"FFFFFF","geo_enabled":false,"listed_count":102,"contributors_enabled":false,"created_at":"Thu Aug 27 02:48:06 +0000 2009","id":69179963,"friends_count":780,"is_translator":false,"favourites_count":17313,"notifications":false,"profile_background_image_url":"http://pbs.twimg.com/profile_background_images/378800000154132099/hn0DlU5i.png","profile_use_background_image":true,"description":"真紅かわいい","profile_link_color":"00660F","followers_count":754}
+ tests/spec_main.hs view
@@ -0,0 +1,17 @@+module Main where++import qualified PropFromToJSONTest+import qualified StatusTest+import Test.Tasty+import qualified TypesTest++tests :: TestTree+tests =+    testGroup+        "Tests"+        [ testGroup "Unit Test" [TypesTest.tests, StatusTest.tests]+        , testGroup "Property Test" [PropFromToJSONTest.tests]+        ]++main :: IO ()+main = defaultMain tests
twitter-types.cabal view
@@ -1,57 +1,65 @@-name:              twitter-types-version:           0.3.20140801-license:           BSD3-license-file:      LICENSE-author:            Takahiro HIMURA-maintainer:        Takahiro HIMURA <taka@himura.jp>-synopsis:          Twitter JSON parser and types-description:       This package uses enumerator package for access Twitter API.-category:          Web, Enumerator-stability:         Experimental-cabal-version:     >= 1.8-build-type:        Simple-homepage:          https://github.com/himura/twitter-types+cabal-version:      >=1.10+name:               twitter-types+version:            0.11.0+license:            BSD3+license-file:       LICENSE+maintainer:         Takahiro HIMURA <taka@himura.jp>+author:             Takahiro HIMURA+stability:          Experimental+tested-with:        ghc ==8.8.4 ghc ==8.10.4 ghc ==9.0.1+homepage:           https://github.com/himura/twitter-types+synopsis:           Twitter JSON parser and types+description:+    Please see the README on Github at <https://github.com/himura/twitter-types#readme> +category:           Web+build-type:         Simple extra-source-files:-  .gitignore-  .travis.yml+    README.md+    tests/fixtures/*.json+    tests/fixtures/tweet-updates/*.json  source-repository head-  type: git-  location: git://github.com/himura/twitter-types.git+    type:     git+    location: git://github.com/himura/twitter-types.git  library-  ghc-options: -Wall--  build-depends:-      base >= 4 && < 5-    , http-types-    , aeson >= 0.3.2.2-    , text-    , unordered-containers--  exposed-modules:-    Web.Twitter.Types-    Web.Twitter.Types.Lens+    exposed-modules:  Web.Twitter.Types+    default-language: Haskell2010+    ghc-options:      -Wall+    build-depends:+        base >=4.9 && <5,+        aeson >=0.3.2.2,+        text,+        time >=1.5,+        unordered-containers  test-suite tests-  type:              exitcode-stdio-1.0-  hs-source-dirs:    tests, .-  main-is:           TypesTest.hs-  build-depends:-      base >= 4.0 && < 5-    , template-haskell-    , test-framework >= 0.3.3-    , test-framework-th-prime-    , test-framework-hunit-    , HUnit-    , shakespeare >= 2.0-    , http-types-    , aeson-    , attoparsec-    , text-    , unordered-containers-  other-modules:-    Fixtures+    type:             exitcode-stdio-1.0+    main-is:          spec_main.hs+    hs-source-dirs:   tests+    other-modules:+        Fixtures+        Instances+        PropFromToJSONTest+        StatusTest+        TypesTest -  ghc-options:       -Wall+    default-language: Haskell2010+    ghc-options:      -Wall+    build-depends:+        base >=4.9 && <5,+        twitter-types,+        aeson,+        attoparsec,+        bytestring,+        generic-random,+        directory,+        filepath,+        tasty >=0.7,+        tasty-hunit,+        tasty-quickcheck,+        tasty-th,+        text,+        time,+        unordered-containers