packages feed

googleplus 0.1.0 → 0.2.1

raw patch · 3 files changed

+139/−29 lines, 3 filesdep +enumerator

Dependencies added: enumerator

Files

Web/GooglePlus.hs view
@@ -23,7 +23,7 @@ -- > doStuff :: GooglePlusM () -- > doStuff = do -- >   Right person <- getPerson Me--- >   Right feed   <- getActivityFeed Me PublicCollection+-- >   Right feed   <- getLatestActivityFeed Me PublicCollection -- >   -- ... -- >   return () -- > @@ -34,48 +34,159 @@ --  -------------------------------------------------------------------- -{-# LANGUAGE OverloadedStrings #-}-module Web.GooglePlus (getPerson, getActivity, getActivityFeed) where+{-# LANGUAGE OverloadedStrings, FlexibleContexts, TypeSynonymInstances #-}+module Web.GooglePlus (getPerson,+                       getActivity,+                       getLatestActivityFeed,+                       enumActivityFeed,+                       enumActivities) where  import Web.GooglePlus.Types import Web.GooglePlus.Monad +import           Control.Applicative ((<$>), (<*>)) import           Control.Monad.IO.Class (liftIO) import           Control.Monad.Reader (asks)-import           Data.Aeson (json, FromJSON, fromJSON, Result(..))+import           Control.Monad.Trans.Class (lift)+import           Data.Aeson (json,+                             FromJSON,+                             fromJSON,+                             parseJSON,+                             Result(..),+                             (.:?),+                             Value(Object))+import           Data.Aeson.Types (typeMismatch) import           Data.Attoparsec.Lazy (parse, eitherResult)-import           Data.ByteString (ByteString)+import           Data.ByteString (ByteString, append) import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Lazy as LBS-import           Data.ByteString (append)+import           Data.Enumerator (Enumerator,+                                  joinI,+                                  checkContinue1,+                                  continue,+                                  Stream (Chunks),+                                  (>>==),+                                  ($=),+                                  ($$))+import qualified Data.Enumerator.List as EL+import           Data.Maybe (fromMaybe) import           Data.Text (Text, pack) import           Data.Text.Encoding (encodeUtf8, decodeUtf8) import           Network.HTTP.Enumerator import           Network.HTTP.Types (Ascii, Query, QueryItem)  -- | Get a person who matches the given identifier-getPerson :: PersonID -> GooglePlusM (Either Text Person)+getPerson :: PersonID -- ^ Identifier for the person to fetch+             -> GooglePlusM (Either Text Person) getPerson pid = genericGet pth []   where pth = personIdPath pid   -- | Get an activity who matches the given activity ID-getActivity :: ID -> GooglePlusM (Either Text Activity)+getActivity :: ID -- ^ Specific ID to fetch+               -> GooglePlusM (Either Text Activity) getActivity aid = genericGet pth []   where pth = append "/plus/v1/activities/" $ encodeUtf8 aid ---TODO: pagetoken, pagination, limits, enumerator?- -- | Get an activity who matches the given activity ID and collection to use.--- Currently uses the default page size (20) and only fetches the first page. I--- plan to add pagination fairly soon and possibly an enumerator a bit later-getActivityFeed :: PersonID -> ActivityCollection -> GooglePlusM (Either Text ActivityFeed)-getActivityFeed pid coll = genericGet pth []+-- 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.+getLatestActivityFeed :: PersonID              -- ^ Feed owner ID+                         -> ActivityCollection -- ^ Indicates what type of feed to retrieve+                         -> 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+  return $ fst `fmap` feed++-- | Paginating enumerator to consume a user's activity stream. Each chunk will+-- end up being an array with a single ActivityFeed in it with 1 page of data+-- in it. This weirdness about the chunks only containing 1 element is mostly+-- to maintain the metadata available on ActivityFeed and have it available in+-- each chunk. For a more natural chunking of just Activities if you don't need+-- that additional metadata, see enumActivities. Note that this Enumerator will+-- abort if it encounters an error from the server, thus cutting the list+-- short.+enumActivityFeed :: 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 ActivityFeed GooglePlusM b+enumActivityFeed pid coll perPage = EL.unfoldM depaginate FirstPage+  where depaginate = depaginateActivityFeed pid coll $ perPage' perPage++-- | 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)++---- Helpers++perPage' :: Maybe Integer -> Integer+perPage' = fromMaybe defaultPageSize++defaultPageSize :: Integer+defaultPageSize = 20++type PageToken             = Text+type PaginatedActivityFeed = (ActivityFeed, Maybe PageToken)++instance FromJSON PaginatedActivityFeed where+  parseJSON (Object v) = (,) <$> parseJSON (Object v)+                             <*> v .:? "nextPageToken"+  parseJSON v          = typeMismatch "PaginatedActivityFeed" v++data DepaginationState     = FirstPage |+                             MorePages PageToken |+                             NoMorePages++-- 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 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)++depaginateActivityFeed :: PersonID -> ActivityCollection -> Integer -> DepaginationState -> GooglePlusM (Maybe (ActivityFeed, DepaginationState))+depaginateActivityFeed pid coll perPage FirstPage       = do+ page <- getFirstFeedPage pid coll perPage+ return $ paginatedFeedState `fmap` page+depaginateActivityFeed pid coll perPage (MorePages tok) = do+ page <- getActivityFeedPage pid coll perPage $ Just tok+ return $ paginatedFeedState `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 pid coll perPage = do+  page <- getActivityFeedPage pid coll perPage Nothing+  return $ eitherMaybe page++getActivityFeedPage :: PersonID -> ActivityCollection -> Integer -> Maybe PageToken -> GooglePlusM (Either Text PaginatedActivityFeed)+getActivityFeedPage pid coll perPage tok = genericGet pth params   where pth  = append pidP actP         pidP = personIdPath pid         actP = append "/activities/" $ collectionPath coll+        pageParam = BS8.pack . show $ perPage+        params = case tok of+                  Nothing -> [("maxResults", Just pageParam)]+                  Just t  -> [("maxResults", Just pageParam), ("pageToken", Just $ encodeUtf8 t)] ----- Helpers+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@@ -96,15 +207,14 @@   where req = genRequest auth pth q  genRequest :: GooglePlusAuth -> Ascii -> Query -> Request m-genRequest auth pth q = def { host = h,-                              path = pth,-                              port = 443,-                              secure = True,+genRequest auth pth q = def { host        = h,+                              path        = pth,+                              port        = 443,+                              secure      = True,                               queryString = q' }   where h     = "www.googleapis.com"         authq = authParam auth         q'    = authq:q-  authParam :: GooglePlusAuth -> QueryItem authParam (APIKey key)     = ("key", Just $ encodeUtf8 key)
Web/GooglePlus/Types.hs view
@@ -42,7 +42,6 @@                              Place(..),                              RelationshipStatus(..)) where -import           Data.Aeson.Types (Parser, typeMismatch) import           Control.Applicative ((<$>), (<*>), pure) import           Data.Aeson (Value(..),                              Object,@@ -50,6 +49,7 @@                              parseJSON,                              (.:),                              (.:?))+import           Data.Aeson.Types (Parser, typeMismatch) import qualified Data.Map as M import           Data.Time.Calendar (Day(..)) import           Data.Time.LocalTime (ZonedTime(..), zonedTimeToUTC)@@ -100,7 +100,6 @@                            activityPlaceName       :: Maybe Text                          } deriving (Show, Eq) ---TODO: activities instance FromJSON Activity where   parseJSON (Object v) = Activity <$> v .:? "placeholder"                                   <*> v .:  "title"
googleplus.cabal view
@@ -1,5 +1,5 @@ name: googleplus-version: 0.1.0+version: 0.2.1 synopsis: Haskell implementation of the Google+ API description:    Will implement the Google+ REST API. Google+ is a social network made by@@ -26,18 +26,19 @@  Ghc-Options:     -Wall  build-depends:   base            >= 4        && < 5,                   aeson           >= 0.3.1.1  && < 0.4,-                  bytestring      >= 0.9.1.10 && < 0.10,-                  text            >= 0.11.0.5 && < 0.12,                   attoparsec      >= 0.9.1.2  && < 0.10,+                  containers      >= 0.4.0.0  && < 0.5,+                  bytestring      >= 0.9.1.10 && < 0.10,+                  enumerator      >= 0.4.9    && < 0.5,+                  haskell98       >= 1.1.0.1  && < 1.2,                   http-enumerator >= 0.7.0    && < 0.8,+                  http-types      >= 0.6.0    && < 0.7,                   mtl             >= 2.0.1.0  && < 2.1,                   rfc3339         >= 1.0.4    && < 1.1,+                  text            >= 0.11.0.5 && < 0.12,                   time            >= 1.2.0.3  && < 1.5,                   transformers    >= 0.2.2.0  && < 0.3,-                  http-types      >= 0.6.0    && < 0.7,-                  url             >= 2.1.2    && < 2.3,-                  haskell98       >= 1.1.0.1  && < 1.2,-                  containers      >= 0.4.0.0  && < 0.5+                  url             >= 2.1.2    && < 2.3  source-repository head   type:     git