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 +130/−20
- Web/GooglePlus/Types.hs +1/−2
- googleplus.cabal +8/−7
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