packages feed

googleplus 0.2.2 → 0.3.0

raw patch · 4 files changed

+342/−57 lines, 4 files

Files

Web/GooglePlus.hs view
@@ -37,9 +37,23 @@ {-# LANGUAGE OverloadedStrings, FlexibleContexts, TypeSynonymInstances #-} module Web.GooglePlus (getPerson,                        getActivity,+                       getComment,                        getLatestActivityFeed,                        enumActivityFeed,-                       enumActivities) where+                       getActivityFeed,+                       enumActivities,+                       getActivities,+                       enumPersonSearch,+                       getPersonSearch,+                       enumPeopleByActivity,+                       getPeopleByActivity,+                       enumActivitySearch,+                       getActivitySearch,+                       enumComments,+                       getComments,+                       SearchOrderBy(..),+                       ActivityCollection(..),+                       ListByActivityCollection(..)) where  import Web.GooglePlus.Types import Web.GooglePlus.Monad@@ -53,6 +67,7 @@                              fromJSON,                              parseJSON,                              Result(..),+                             (.:),                              (.:?),                              Value(Object)) import           Data.Aeson.Types (typeMismatch)@@ -62,12 +77,11 @@ import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Lazy as LBS import           Data.Enumerator (Enumerator,-                                  joinI,                                   checkContinue1,                                   continue,                                   Stream (Chunks),                                   (>>==),-                                  ($=),+                                  run_,                                   ($$)) import qualified Data.Enumerator.List as EL import           Data.Maybe (fromMaybe)@@ -83,12 +97,18 @@   where pth = personIdPath pid  --- | Get an activity who matches the given activity ID+-- | Get an activity which matches the given activity ID getActivity :: ID -- ^ Specific ID to fetch                -> GooglePlusM (Either Text Activity) getActivity aid = genericGet pth []-  where pth = append "/plus/v1/activities/" $ encodeUtf8 aid+  where pth = "/plus/v1/activities/" `append` encodeUtf8 aid +-- | Get a comment which matches the given comment ID+getComment :: ID -- ^ Specific ID to fetch+              -> GooglePlusM (Either Text Comment)+getComment cid = genericGet pth []+  where pth = "/plus/v1/comments/" `append` encodeUtf8 cid+ -- | Get an activity who matches the given activity ID and collection to use. -- Default page size is (20) and only fetches the first page. -- You will receive an error from the server if the page size exceeds 100.@@ -97,7 +117,7 @@                          -> Maybe Integer      -- ^ Page size. Should be between 1 and 100. Default 20.                          -> GooglePlusM (Either Text ActivityFeed) getLatestActivityFeed pid coll perPage = do-  feed <- getActivityFeedPage pid coll (perPage' perPage) Nothing+  feed <- getActivityFeedPage pid coll (perPageActivity perPage) Nothing   return $ fst `fmap` feed  -- | Paginating enumerator to consume a user's activity stream. Each chunk will@@ -113,25 +133,157 @@                     -> Maybe Integer      -- ^ Page size. Should be between 1 and 100. Defualt 20                     -> Enumerator ActivityFeed GooglePlusM b enumActivityFeed pid coll perPage = EL.unfoldM depaginate FirstPage-  where depaginate = depaginateActivityFeed pid coll $ perPage' perPage+  where depaginate = depaginateActivityFeed pid coll $ perPageActivity perPage +-- | Simplified version of enumActivityFeed which retrieves all pages of an+-- activity feed and merges them into one. Note that this will not be as+-- efficient as enumActivityFeed in terms of memory/time because it collects+-- them all in memory first. Note that this should incur 1 API call per page of+-- results, so the max page size of 100 is used.+getActivityFeed :: PersonID+                   -> ActivityCollection+                   -> GooglePlusM ActivityFeed+getActivityFeed pid coll = do+  feeds <- run_ $ enumActivityFeed pid coll (Just 100) $$ EL.consume+  return $ foldl1 mergeFeeds feeds+  where mergeFeeds a ActivityFeed { activityFeedItems = is} = a { activityFeedItems = activityFeedItems a ++ is }+ -- | Paginating enumerator yielding a Chunk for each page. Use this if you -- don't need the feed metadata that enumActivityFeed provides. enumActivities :: PersonID              -- ^ Feed owner ID                   -> ActivityCollection -- ^ Indicates what type of feed to retrieve                   -> Maybe Integer      -- ^ Page size. Should be between 1 and 100. Defualt 20                   -> Enumerator Activity GooglePlusM b-enumActivities pid coll perPage = unfoldListM depaginate FirstPage-  where depaginate = depaginateActivities pid coll (perPage' perPage)+enumActivities pid coll perPage = simpleDepaginator depaginate+  where depaginate = simpleDepaginationStep perPage' pth params+        pth        = pidP `append` actP+        actP       = "/activities/" `append` collectionPath coll+        pidP       = personIdPath pid+        params     = []+        perPage'   = perPageActivity perPage +-- | Simplified version of enumActivities that fetches all the activitys of a+-- Person first, thus returning them. Note that this should incur 1 API call+-- per page of results, so the max page size of 100 is used.+getActivities :: PersonID              -- ^ Feed owner ID+                 -> ActivityCollection -- ^ Indicates what type of feed to retrieve+                 -> GooglePlusM [Activity]+getActivities pid coll = run_ $ enumActivities pid coll (Just 100) $$ EL.consume+++-- | Search for a member of Google+. Paginating enumerator yielding a Chunk for+-- each page. Note that this Enumerator will abort if it encounters an error+-- from the server, thus cutting the list short.+enumPersonSearch :: Text             -- ^ Search string+                    -> Maybe Integer -- ^ Optional page size. Shold be between 1 and 20. Default 10+                    -> Enumerator PersonSearchResult GooglePlusM b+enumPersonSearch search perPage = simpleDepaginator depaginate+  where depaginate = simpleDepaginationStep perPage' pth params+        pth        = "/plus/v1/people"+        params     = [("query", Just $ encodeUtf8 search)]+        perPage'   = perPageSearch perPage++-- | Returns the full result set for a person search given a search string.+-- This interface is simpler to use but does not have the flexibility/memory+-- usage benefit of enumPersonSearch.+getPersonSearch :: Text -- ^ Search string+                -> GooglePlusM [PersonSearchResult]+getPersonSearch search = run_ $ enumPersonSearch search (Just 20) $$ EL.consume++-- | Find people associated with a particular Activity. Paginating enumerator+-- yielding a Chunk for each page. Paginating enumerator yielding a Chunk for+-- each page. Note that this Enumerator will abort if it encounters an error+-- from the server, thus cutting the list short.+enumPeopleByActivity :: ID                          -- ^ Activity ID+                        -> ListByActivityCollection -- ^ Indicates which collection of people to list+                        -> Maybe Integer            -- ^ Optional page size. Should be between 1 and 100. Default 20.+                        -> Enumerator Person GooglePlusM b+enumPeopleByActivity aid coll perPage = simpleDepaginator depaginate+  where depaginate      = simpleDepaginationStep perPage' pth params+        pth             = "/plus/v1/activities/" `append` encodeUtf8 aid `append` peopleP `append` collP coll+        peopleP         = "/people/"+        collP PlusOners = "plusoners"+        collP Resharers = "resharers"+        params          = []+        perPage'        = perPageActivity perPage++-- | Returns the full result set for a person search given a search string.+-- This interface is simpler to use but does not have the flexibility/memory+-- usage benefit of enumPeopleByActivity.+getPeopleByActivity :: ID                          -- ^ Activity ID+                       -> ListByActivityCollection -- ^ Indicates which collection of people to list+                       -> GooglePlusM [Person]+getPeopleByActivity aid coll = run_ $ enumPeopleByActivity aid coll (Just 100) $$ EL.consume++-- | Search for an activity on Google+. Paginating enumerator yielding a Chunk+-- for each page. Note that this Enumerator will abort if it encounters an error+-- from the server, thus cutting the list short.+enumActivitySearch :: Text             -- ^ Search string+                      -> SearchOrderBy -- ^ Order of search results+                      -> Maybe Integer -- ^ Optional page size. Shold be between 1 and 20. Default 10+                      -> Enumerator Activity GooglePlusM b+enumActivitySearch search orderBy perPage = simpleDepaginator depaginate+  where depaginate        = simpleDepaginationStep perPage' pth params+        pth               = "/plus/v1/activities"+        params            = [("query", Just $ encodeUtf8 search),+                             ("orderBy", Just $ orderParam orderBy)]+        orderParam Best   = "best"+        orderParam Recent = "recent"+        perPage'          = perPageSearch perPage++-- | Returns the full result set for an activity search given a search string.+-- This interface is simpler to use but does not have the flexibility/memory+-- usage benefit of enumActivitySearch.+getActivitySearch :: Text             -- ^ Search string+                     -> SearchOrderBy -- ^ Order of search results+                     -> GooglePlusM [Activity]+getActivitySearch search orderBy = run_ $ enumActivitySearch search orderBy (Just 20) $$ EL.consume++-- | Find comments for an activity on Google+. Paginating enumerator yielding a+-- Chunk for each page. Note that this Enumerator will abort if it encounters+-- an error from the server, thus cutting the list short.+enumComments :: ID               -- ^ Activity ID+                -> Maybe Integer -- ^ Optional page size. Should be between 1 and 100. Default 20+                -> Enumerator Comment GooglePlusM b+enumComments aid perPage = simpleDepaginator depaginate+  where depaginate = simpleDepaginationStep perPage' pth params+        pth        = "/plus/v1/activities/" `append` encodeUtf8 aid `append` "/comments"+        params     = []+        perPage'   = perPageActivity perPage++-- | Returns the full result set for an activity's comments. This interface is+-- simpler to use but does not have the flexibility/memory usage benefit of+-- enumComments.+getComments :: ID -- ^ Activity ID+              -> GooglePlusM [Comment]+getComments aid = run_ $ enumComments aid (Just 100) $$ EL.consume++-- | Specifies the type of Activities to get in an Activity listing. Currently+-- the API only allows public.+data ActivityCollection = PublicCollection deriving (Show, Eq)++data ListByActivityCollection = PlusOners | -- ^ List of people who have +1ed an activity+                                Resharers   -- ^ List of people who have reshared an activity+                                deriving (Show, Eq)++data SearchOrderBy = Best | -- ^ Sort by relevance to the to the user, most relevant first+                     Recent -- ^ Sort by most recent results first+                     deriving (Show, Eq)+ ---- Helpers -perPage' :: Maybe Integer -> Integer-perPage' = fromMaybe defaultPageSize+simpleDepaginator  :: Monad m => (DepaginationState -> m (Maybe ([a], DepaginationState)))+                                 -> Enumerator a m b+simpleDepaginator depaginate = unfoldListM depaginate FirstPage -defaultPageSize :: Integer-defaultPageSize = 20+perPageActivity :: Maybe Integer+                   -> Integer+perPageActivity = fromMaybe 20 +perPageSearch :: Maybe Integer+                 -> Integer+perPageSearch = fromMaybe 10+ type PageToken             = Text type PaginatedActivityFeed = (ActivityFeed, Maybe PageToken) @@ -146,67 +298,125 @@  -- Exactly the same as unfoldM but takes the result of the stateful function -- and uses it as the chunks, rather than a Chunks with a singleton list-unfoldListM :: Monad m => (s -> m (Maybe ([a], s))) -> s -> Enumerator a m b+unfoldListM :: Monad m => (s -> m (Maybe ([a], s)))+                          -> s+                          -> Enumerator a m b unfoldListM f = checkContinue1 $ \loop s k -> do 	fs <- lift (f s) 	case fs of 		Nothing -> continue k 		Just (as, s') -> k (Chunks as) >>== loop s' -depaginateActivities:: PersonID -> ActivityCollection -> Integer -> DepaginationState -> GooglePlusM (Maybe ([Activity], DepaginationState))-depaginateActivities pid coll perPage state = (return . (fmap unwrap)) =<< depaginateActivityFeed pid coll perPage state-  where unwrap (feed, s) = (activityFeedItems feed, s)+simpleGetFirstPage :: FromJSON a => Integer+                                    -> Ascii+                                    -> Query+                                    -> GooglePlusM (Maybe (PaginatedResource a))+simpleGetFirstPage perPage = simpleGetPage perPage Nothing -depaginateActivityFeed :: PersonID -> ActivityCollection -> Integer -> DepaginationState -> GooglePlusM (Maybe (ActivityFeed, DepaginationState))+simpleGetPage :: FromJSON a => Integer+                            -> Maybe PageToken +                            -> Ascii +                            -> Query +                            -> GooglePlusM (Maybe (PaginatedResource a))+simpleGetPage perPage tok pth params = do+  page <- genericGet pth $ params ++ pageParams+  return $ eitherMaybe page+  where pageParam    = BS8.pack . show $ perPage+        pageParams   = case tok of+                         Nothing -> [("maxResults", Just pageParam)]+                         Just t  -> [("maxResults", Just pageParam), ("pageToken", Just $ encodeUtf8 t)]++simpleDepaginationStep :: FromJSON a => Integer+                                     -> Ascii +                                     -> Query +                                     -> DepaginationState +                                     -> GooglePlusM (Maybe ([a], DepaginationState))+simpleDepaginationStep perPage pth params FirstPage       = (return . fmap paginatedState) =<< simpleGetFirstPage perPage pth params+simpleDepaginationStep perPage pth params (MorePages tok) = (return . fmap paginatedState) =<< simpleGetPage perPage (Just tok) pth params+simpleDepaginationStep _ _ _ NoMorePages = return Nothing++-- Activities Specifics++depaginateActivityFeed :: PersonID+                          -> ActivityCollection+                          -> Integer+                          -> DepaginationState+                          -> GooglePlusM (Maybe (ActivityFeed, DepaginationState)) depaginateActivityFeed pid coll perPage FirstPage       = do  page <- getFirstFeedPage pid coll perPage- return $ paginatedFeedState `fmap` page+ return $ paginatedState `fmap` page depaginateActivityFeed pid coll perPage (MorePages tok) = do  page <- getActivityFeedPage pid coll perPage $ Just tok- return $ paginatedFeedState `fmap` eitherMaybe page+ return $ paginatedState `fmap` eitherMaybe page depaginateActivityFeed _ _ _ NoMorePages                = return Nothing -paginatedFeedState :: PaginatedActivityFeed -> (ActivityFeed, DepaginationState)-paginatedFeedState (feed, token) = (feed, maybe NoMorePages MorePages token)--getFirstFeedPage :: PersonID -> ActivityCollection -> Integer -> GooglePlusM (Maybe PaginatedActivityFeed)+getFirstFeedPage :: PersonID+                    -> ActivityCollection+                    -> Integer+                    -> GooglePlusM (Maybe PaginatedActivityFeed) getFirstFeedPage pid coll perPage = do   page <- getActivityFeedPage pid coll perPage Nothing   return $ eitherMaybe page -getActivityFeedPage :: PersonID -> ActivityCollection -> Integer -> Maybe PageToken -> GooglePlusM (Either Text PaginatedActivityFeed)+getActivityFeedPage :: PersonID+                       -> ActivityCollection+                       -> Integer+                       -> Maybe PageToken+                       -> GooglePlusM (Either Text PaginatedActivityFeed) getActivityFeedPage pid coll perPage tok = genericGet pth params-  where pth  = append pidP actP+  where pth  = pidP `append` actP         pidP = personIdPath pid-        actP = append "/activities/" $ collectionPath coll+        actP = "/activities/" `append` collectionPath coll         pageParam = BS8.pack . show $ perPage         params = case tok of                   Nothing -> [("maxResults", Just pageParam)]                   Just t  -> [("maxResults", Just pageParam), ("pageToken", Just $ encodeUtf8 t)] -eitherMaybe :: Either a b -> Maybe b+type PaginatedResource a = ([a], Maybe PageToken)++instance FromJSON a => FromJSON (PaginatedResource a) where+  parseJSON (Object v) = (,) <$> v .: "items"+                            <*> v .:? "nextPageToken"+  parseJSON v          = typeMismatch "PaginatedResource" v++paginatedState :: (a, Maybe PageToken)+                  -> (a, DepaginationState)+paginatedState (results, token) = (results, maybe NoMorePages MorePages token)++-- Internals++eitherMaybe :: Either a b+               -> Maybe b eitherMaybe (Left _)  = Nothing eitherMaybe (Right x) = Just x -genericGet :: FromJSON a => Ascii -> Query -> GooglePlusM (Either Text a)-genericGet pth qs = withEnv $ \auth -> do-  resp <- doGet auth pth qs-  return $ handleResponse resp+genericGet :: FromJSON a => Ascii+                            -> Query+                            -> GooglePlusM (Either Text a)+genericGet pth qs = withEnv $ \auth -> return . handleResponse =<< doGet auth pth qs -collectionPath :: ActivityCollection -> ByteString+collectionPath :: ActivityCollection+                  -> ByteString collectionPath PublicCollection = "public" -personIdPath :: PersonID -> ByteString-personIdPath (PersonID i) = append "/plus/v1/people/" $ encodeUtf8 i+personIdPath :: PersonID+                -> ByteString+personIdPath (PersonID i) = "/plus/v1/people/" `append` encodeUtf8 i personIdPath Me           = "/plus/v1/people/me" -doGet :: GooglePlusAuth -> Ascii -> Query -> GooglePlusM (Int, LBS.ByteString)+doGet :: GooglePlusAuth+         -> Ascii+         -> Query+         -> GooglePlusM (Int, LBS.ByteString) doGet auth pth q = liftIO $ withManager $ \manager -> do   Response { statusCode = c, responseBody = b} <- httpLbsRedirect req manager   return (c, b)   where req = genRequest auth pth q -genRequest :: GooglePlusAuth -> Ascii -> Query -> Request m+genRequest :: GooglePlusAuth+              -> Ascii+              -> Query+              -> Request m genRequest auth pth q = def { host        = h,                               path        = pth,                               port        = 443,@@ -216,11 +426,13 @@         authq = authParam auth         q'    = authq:q -authParam :: GooglePlusAuth -> QueryItem+authParam :: GooglePlusAuth+             -> QueryItem authParam (APIKey key)     = ("key", Just $ encodeUtf8 key) authParam (OAuthToken tok) = ("access_token", Just $ encodeUtf8 tok) -handleResponse :: FromJSON a => (Int, LBS.ByteString) -> Either Text a+handleResponse :: FromJSON a => (Int, LBS.ByteString)+                                -> Either Text a handleResponse (200, str) = packLeft $ fjson =<< parsed   where fjson v = case fromJSON v of                     Success a -> Right a@@ -228,9 +440,11 @@         parsed  = eitherResult $ parse json str handleResponse (_, str) = Left $ decodeUtf8 . BS.concat . LBS.toChunks $ str -packLeft :: Either String a -> Either Text a+packLeft :: Either String a+            -> Either Text a packLeft (Right x)  = Right x packLeft (Left str) = Left $ pack str -withEnv :: (GooglePlusAuth -> GooglePlusM a) -> GooglePlusM a+withEnv :: (GooglePlusAuth -> GooglePlusM a)+           -> GooglePlusM a withEnv fn = fn =<< asks gpAuth 
Web/GooglePlus/Monad.hs view
@@ -14,13 +14,17 @@ {-# LANGUAGE OverloadedStrings, GeneralizedNewtypeDeriving #-} module Web.GooglePlus.Monad where -import Data.Text (Text)+import Data.Text (Text,unpack) import Control.Monad.Reader  -- |Represents authentication data with GooglePlus. Currently supports an OAuth --  token or an API key data GooglePlusAuth = APIKey Text | -- ^ Authentication using an API key                       OAuthToken Text -- ^ Authenticate using a token obtianed via OAuth V2. Currently no way in the library to obtain refresh tokens++instance Show GooglePlusAuth where+  show (APIKey a) = show $ unpack a+  show (OAuthToken a) = show $ unpack a  --TODO: getting tokens and using refresh tokens 
Web/GooglePlus/Types.hs view
@@ -13,8 +13,8 @@  {-# LANGUAGE OverloadedStrings #-} module Web.GooglePlus.Types (Person(..),+                             PersonSearchResult(..),                              PersonID(..),-                             ActivityCollection(..),                              ID,                              Actor(..),                              Verb(..),@@ -40,9 +40,12 @@                              Organization(..),                              OrganizationType(..),                              Place(..),-                             RelationshipStatus(..)) where+                             RelationshipStatus(..),+                             Comment(..),+                             CommentObject(..),+                             InReplyTo(..)) where -import           Control.Applicative ((<$>), (<*>), pure)+import           Control.Applicative ((<$>), (<*>), pure, Applicative) import           Data.Aeson (Value(..),                              Object,                              FromJSON,@@ -50,6 +53,7 @@                              (.:),                              (.:?)) import           Data.Aeson.Types (Parser, typeMismatch)+import           Data.List (intercalate) import qualified Data.Map as M import           Data.Time.Calendar (Day(..)) import           Data.Time.LocalTime (ZonedTime(..), zonedTimeToUTC)@@ -69,9 +73,6 @@                                    activityFeedItems   :: [Activity] -- ^ Activities in the feed (currently limited to first page                                    } deriving (Show, Eq) --- |Specifies the type of Activities to get. Currently the API only allows public.-data ActivityCollection = PublicCollection deriving (Show, Eq)- instance FromJSON ActivityFeed where   parseJSON (Object v) = ActivityFeed <$> v .: "title"                                       <*> v .: "updated"@@ -125,7 +126,7 @@   a == b = zonedTimeToUTC a == zonedTimeToUTC b  instance FromJSON ZonedTime where-  parseJSON (String str) = maybe (fail $ "Failed to parse ZonedTime " ++ unpack str) pure parsed+  parseJSON (String str) = maybeToParser parsed $ "Failed to parse ZonedTime " ++ unpack str     where parsed = readRFC3339 . unpack $ str   parseJSON v            = typeMismatch "ZonedTime" v @@ -339,6 +340,20 @@                                 <*> v .:? "hasApp"   parseJSON v          = typeMismatch "Person" v +-- |A Person search result with limited informaiton. The full person's profile must be retrieved to get the rest+data PersonSearchResult = PersonSearchResult { personSRId          :: ID,    -- ^ Id of the Person+                                               personSRDisplayName :: Text,  -- ^ Name of the Person, suitable for display+                                               personSRImage       :: Image, -- ^ Profile image for the Person+                                               personSRProfileURL  :: URL    -- ^ URL to the person's profile+                                             } deriving (Show, Eq)++instance FromJSON PersonSearchResult where+  parseJSON (Object v) = PersonSearchResult <$> v .:  "id"+                                            <*> v .:  "displayName"+                                            <*> v .:  "image"+                                            <*> v .:  "url"+  parseJSON v          = typeMismatch "PersonSearchResult" v+ instance FromJSON Day where   parseJSON (String str) = pure . read . unpack $ str   parseJSON v            = typeMismatch "Day" v@@ -492,7 +507,7 @@                    } deriving (Show, Eq)  instance FromJSON Place where-  parseJSON (Object v) = Place <$> v .: "primary"+  parseJSON (Object v) = Place <$> v .:| ("primary", False)                                <*> v .: "value"   parseJSON v          = typeMismatch "Place" v @@ -522,11 +537,61 @@   parseJSON (String "in_civil_union")          = pure InCivilUnion   parseJSON v                                  = typeMismatch "RelationshipStatus" v -(.:|) :: (FromJSON a) => Object -> (Text, a) -> Parser a+-- |Activity comment on Google++data Comment = Comment { commentId          :: ID,            -- ^ ID of the comment+                         commentPublished   :: ZonedTime,     -- ^ Date originally published+                         commentUpdated     :: ZonedTime,     -- ^ Date updated+                         commentActor       :: Actor,         -- ^ The actor who posted the comment+                         commentVerb        :: Verb,          -- ^ Indicates what action was performed+                         commentObject      :: CommentObject, -- ^ The content of the object+                         commentUrl         :: URL,           -- ^ URL to the comment+                         commentActivities  :: [InReplyTo]    -- ^ The activities to which this comment is a reply+                       } deriving (Show, Eq)++instance FromJSON Comment where+  parseJSON (Object v) = Comment <$> v .:  "id"+                                 <*> v .:  "published"+                                 <*> v .:  "updated"+                                 <*> v .:  "actor"+                                 <*> v .:| ("verb", Post)+                                 <*> v .:  "object"+                                 <*> v .:  "selfLink"+                                 <*> v .:  "inReplyTo"+  parseJSON v          = typeMismatch "Comment" v+++data CommentObject = CommentObject { commentObjectContent :: Text -- ^ Text content of the comments+                                   } deriving (Show, Eq)++instance FromJSON CommentObject where+  parseJSON (Object v) = CommentObject <$> v .: "content"+  parseJSON v          = typeMismatch "CommentObject" v++data InReplyTo = InReplyTo { inReplyToId :: ID,  -- ^ ID of the article+                             inReplyToUrl :: URL -- ^ URL of the article+                           } deriving (Show, Eq)++instance FromJSON InReplyTo where+  parseJSON (Object v) = InReplyTo <$> v .: "id"+                                   <*> v .: "url"+  parseJSON v          = typeMismatch "InReplyTo" v++---- Helpers++(.:|) :: (FromJSON a) => Object+                         -> (Text, a)+                         -> Parser a obj .:| (key, d) = case M.lookup key obj of                         Nothing -> pure d                         Just v  -> parseJSON v -spanSkip :: Char -> Text -> (Text, Text)+spanSkip :: Char+            -> Text+            -> (Text, Text) spanSkip cond xs = (left, T.tail right)   where (left, right) = T.span (/= cond) xs++maybeToParser :: (Applicative m, Monad m) => Maybe a+                                             -> String+                                             -> m a+maybeToParser parsed msg = maybe (fail msg) pure parsed
googleplus.cabal view
@@ -1,13 +1,15 @@ name: googleplus-version: 0.2.2-synopsis: Haskell implementation of the Google+ API+version: 0.3.0+synopsis: Haskell implementation of the Google+ API v1 description:    Will implement the Google+ REST API. Google+ is a social network made by    Google. Found out more at <http://plus.google.com>. -   Currently supports the API-key authentication only. OAuth should be coming-   along at some point. Only features read-only API access beacuse that is all-   that Google has published thus far.+   Implements the full API as of Oct 15, 2011. Only features read-only API+   access beacuse that is all that Google has published thus far. Both API and+   OAuth authentication is supported, but note that if you intend to use OAuth,+   this library does not currently provide the means to procure and renew OAuth+   tokens.  category:           Web license:            BSD3