hackernews 0.2.3.2 → 0.3.0.0
raw patch · 15 files changed
+441/−154 lines, 15 filesdep +eitherPVP ok
version bump matches the API change (PVP)
Dependencies added: either
API changes (from Hackage documentation)
- Web.HackerNews: type MaxItem = Int
- Web.HackerNews: type TopStories = [Int]
+ Web.HackerNews: ConnectionError :: HackerNewsError
+ Web.HackerNews: ItemComment :: Comment -> Item
+ Web.HackerNews: ItemId :: Int -> ItemId
+ Web.HackerNews: ItemJob :: Job -> Item
+ Web.HackerNews: ItemPoll :: Poll -> Item
+ Web.HackerNews: ItemPollOpt :: PollOpt -> Item
+ Web.HackerNews: ItemStory :: Story -> Item
+ Web.HackerNews: ParseError :: HackerNewsError
+ Web.HackerNews: RequestError :: HackerNewsError
+ Web.HackerNews: commentDead :: Comment -> Bool
+ Web.HackerNews: data HackerNewsError
+ Web.HackerNews: data Item
+ Web.HackerNews: data MaxItem
+ Web.HackerNews: data TopStories
+ Web.HackerNews: getItem :: ItemId -> HackerNews Item
+ Web.HackerNews: jobDead :: Job -> Bool
+ Web.HackerNews: newtype ItemId
+ Web.HackerNews: pollDead :: Poll -> Bool
+ Web.HackerNews: pollOptDead :: PollOpt -> Bool
+ Web.HackerNews: storyDead :: Story -> Bool
+ Web.HackerNews: updateDead :: Update -> Bool
+ Web.HackerNews: userDead :: User -> Bool
- Web.HackerNews: Comment :: Text -> CommentId -> Maybe [Int] -> Int -> Text -> UTCTime -> Text -> Bool -> Comment
+ Web.HackerNews: Comment :: Text -> CommentId -> Maybe [Int] -> Int -> Text -> UTCTime -> Text -> Bool -> Bool -> Comment
- Web.HackerNews: Job :: Text -> JobId -> Int -> Text -> UTCTime -> Text -> Text -> Text -> Bool -> Job
+ Web.HackerNews: Job :: Text -> JobId -> Int -> Text -> UTCTime -> Text -> Text -> Text -> Bool -> Bool -> Job
- Web.HackerNews: Poll :: Text -> PollId -> [Int] -> [Int] -> Int -> Text -> UTCTime -> Text -> Text -> Bool -> Poll
+ Web.HackerNews: Poll :: Text -> PollId -> [Int] -> [Int] -> Int -> Text -> UTCTime -> Text -> Text -> Bool -> Bool -> Poll
- Web.HackerNews: PollOpt :: Text -> PollOptId -> Int -> Int -> Text -> UTCTime -> Text -> Bool -> PollOpt
+ Web.HackerNews: PollOpt :: Text -> PollOptId -> Int -> Int -> Text -> UTCTime -> Text -> Bool -> Bool -> PollOpt
- Web.HackerNews: Story :: Text -> Int -> [Int] -> Int -> UTCTime -> Text -> Text -> Text -> Bool -> Story
+ Web.HackerNews: Story :: Text -> StoryId -> [Int] -> Int -> UTCTime -> Text -> Text -> Text -> Bool -> Bool -> Story
- Web.HackerNews: Update :: [Int] -> [Text] -> Bool -> Update
+ Web.HackerNews: Update :: [Int] -> [Text] -> Bool -> Bool -> Update
- Web.HackerNews: User :: Maybe Text -> UTCTime -> Int -> UserId -> Int -> [Int] -> Bool -> User
+ Web.HackerNews: User :: Maybe Text -> UTCTime -> Int -> UserId -> Int -> [Int] -> Bool -> Bool -> User
- Web.HackerNews: getComment :: CommentId -> HackerNews (Maybe Comment)
+ Web.HackerNews: getComment :: CommentId -> HackerNews Comment
- Web.HackerNews: getJob :: JobId -> HackerNews (Maybe Job)
+ Web.HackerNews: getJob :: JobId -> HackerNews Job
- Web.HackerNews: getMaxItem :: HackerNews (Maybe MaxItem)
+ Web.HackerNews: getMaxItem :: HackerNews MaxItem
- Web.HackerNews: getPoll :: PollId -> HackerNews (Maybe Poll)
+ Web.HackerNews: getPoll :: PollId -> HackerNews Poll
- Web.HackerNews: getPollOpt :: PollOptId -> HackerNews (Maybe PollOpt)
+ Web.HackerNews: getPollOpt :: PollOptId -> HackerNews PollOpt
- Web.HackerNews: getStory :: StoryId -> HackerNews (Maybe Story)
+ Web.HackerNews: getStory :: StoryId -> HackerNews Story
- Web.HackerNews: getTopStories :: HackerNews (Maybe TopStories)
+ Web.HackerNews: getTopStories :: HackerNews TopStories
- Web.HackerNews: getUpdates :: HackerNews (Maybe Update)
+ Web.HackerNews: getUpdates :: HackerNews Update
- Web.HackerNews: getUser :: UserId -> HackerNews (Maybe User)
+ Web.HackerNews: getUser :: UserId -> HackerNews User
- Web.HackerNews: hackerNews :: (Show a, FromJSON a) => HackerNews a -> IO a
+ Web.HackerNews: hackerNews :: FromJSON a => HackerNews a -> IO (Either HackerNewsError a)
- Web.HackerNews: storyId :: Story -> Int
+ Web.HackerNews: storyId :: Story -> StoryId
- Web.HackerNews: type HackerNews a = ReaderT Connection IO a
+ Web.HackerNews: type HackerNews a = EitherT HackerNewsError (ReaderT Connection IO) a
Files
- hackernews.cabal +6/−3
- src/Web/HackerNews.hs +34/−22
- src/Web/HackerNews/Client.hs +44/−22
- src/Web/HackerNews/Comment.hs +33/−16
- src/Web/HackerNews/Endpoint.hs +32/−0
- src/Web/HackerNews/Item.hs +72/−0
- src/Web/HackerNews/Job.hs +34/−17
- src/Web/HackerNews/Person.hs +12/−2
- src/Web/HackerNews/Poll.hs +40/−10
- src/Web/HackerNews/Story.hs +45/−13
- src/Web/HackerNews/Types.hs +9/−7
- src/Web/HackerNews/Update.hs +27/−7
- src/Web/HackerNews/User.hs +27/−9
- src/Web/HackerNews/Util.hs +12/−5
- tests/Test.hs +14/−21
hackernews.cabal view
@@ -1,5 +1,5 @@ name: hackernews-version: 0.2.3.2+version: 0.3.0.0 description: API for news.ycombinator.com license: MIT synopsis: API for Hacker News@@ -11,7 +11,7 @@ cabal-version: >=1.10 library- ghc-options: -Wall -threaded+ ghc-options: -Wall -threaded -rtsopts exposed-modules: Web.HackerNews other-modules: Web.HackerNews.Types , Web.HackerNews.Client@@ -22,13 +22,16 @@ , Web.HackerNews.Comment , Web.HackerNews.Story , Web.HackerNews.Job+ , Web.HackerNews.Item , Web.HackerNews.Util+ , Web.HackerNews.Endpoint hs-source-dirs: src build-depends: HsOpenSSL >= 0.10.5 , aeson >= 0.8.0.1 , attoparsec >= 0.12.1.2 , base >= 4.7 && <4.8 , bytestring >= 0.10.4.0+ , either >= 4.3.1 , http-streams >= 0.7.2.2 , io-streams >= 1.1.4.6 , text >= 1.2.0.0@@ -38,7 +41,7 @@ Test-Suite tests type: exitcode-stdio-1.0- ghc-options: -rtsopts -threaded+ ghc-options: -rtsopts -threaded hs-source-dirs: tests main-is: Test.hs build-depends: base
src/Web/HackerNews.hs view
@@ -1,8 +1,15 @@ {-# LANGUAGE OverloadedStrings #-}+-- |+-- Module : Web.HackerNews+-- Copyright : (c) David Johnson, Konstantin Zudov, 2014+-- Maintainer : djohnson.m@gmail.com+-- Stability : experimental+-- Portability : POSIX module Web.HackerNews ( -- * Hacker News Monad hackerNews -- * API Calls+ , getItem , getStory , getComment , getPoll@@ -14,6 +21,9 @@ , getUpdates -- * Types , HackerNews+ , HackerNewsError (..)+ , Item (..)+ , ItemId (..) , Comment (..) , CommentId (..) , Poll (..)@@ -31,54 +41,56 @@ , TopStories ) where -import Data.Monoid ((<>))- import Web.HackerNews.Types-import Web.HackerNews.Util (toText)-import Web.HackerNews.Client+import Web.HackerNews.Client (HackerNews, hackerNews, HackerNewsError(..)) ------------------------------------------------------------------------------+-- | Retrieve a `Item` by `ItemId`+getItem :: ItemId -> HackerNews Item+getItem = getEndpoint++------------------------------------------------------------------------------ -- | Retrieve a `Story` by `StoryId`-getStory :: StoryId -> HackerNews (Maybe Story)-getStory (StoryId storyid) = buildHNRequest $ "item/" <> toText storyid+getStory :: StoryId -> HackerNews Story+getStory = getEndpoint ------------------------------------------------------------------------------ -- | Retrieve a `Comment` by `CommentId`-getComment :: CommentId -> HackerNews (Maybe Comment)-getComment (CommentId commentid) = buildHNRequest $ "item/" <> toText commentid+getComment :: CommentId -> HackerNews Comment+getComment = getEndpoint ------------------------------------------------------------------------------ -- | Retrieve a `Poll` by `PollId`-getPoll :: PollId -> HackerNews (Maybe Poll)-getPoll (PollId pollid) = buildHNRequest $ "item/" <> toText pollid+getPoll :: PollId -> HackerNews Poll+getPoll = getEndpoint ------------------------------------------------------------------------------ -- | Retrieve a `PollOpt` by `PollOptId`-getPollOpt :: PollOptId -> HackerNews (Maybe PollOpt)-getPollOpt (PollOptId polloptid) = buildHNRequest $ "item/" <> toText polloptid+getPollOpt :: PollOptId -> HackerNews PollOpt+getPollOpt = getEndpoint ------------------------------------------------------------------------------ -- | Retrieve a `User` by `UserId`-getUser :: UserId -> HackerNews (Maybe User)-getUser (UserId userid) = buildHNRequest $ "user/" <> userid+getUser :: UserId -> HackerNews User+getUser = getEndpoint ------------------------------------------------------------------------------ -- | Retrieve a Job-getJob :: JobId -> HackerNews (Maybe Job)-getJob (JobId jobid) = buildHNRequest $ "item/" <> toText jobid+getJob :: JobId -> HackerNews Job+getJob = getEndpoint ------------------------------------------------------------------------------ -- | Retrieve the Top Stories on Hacker News-getTopStories :: HackerNews (Maybe TopStories)-getTopStories = buildHNRequest "topstories"+getTopStories :: HackerNews TopStories+getTopStories = getEndpoint TopStoriesId ------------------------------------------------------------------------------ -- | Retrieve the largest ItemId-getMaxItem :: HackerNews (Maybe MaxItem)-getMaxItem = buildHNRequest "maxitem"+getMaxItem :: HackerNews MaxItem+getMaxItem = getEndpoint MaxItemId ------------------------------------------------------------------------------ -- | Retrieve the latest updates-getUpdates :: HackerNews (Maybe Update)-getUpdates = buildHNRequest "updates"+getUpdates :: HackerNews Update+getUpdates = getEndpoint UpdateId
src/Web/HackerNews/Client.hs view
@@ -1,60 +1,82 @@-{-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-}+-- |+-- Module : Web.HackerNews.Client+-- Copyright : (c) David Johnson, 2014+-- Maintainer : djohnson.m@gmail.com+-- Stability : experimental+-- Portability : POSIX module Web.HackerNews.Client ( hackerNews , buildHNRequest , HackerNews+ , HackerNewsError (..) ) where import Data.Aeson hiding (Result) import Data.Aeson.Parser (value) import Data.Attoparsec.ByteString (parseOnly) import Data.Either (rights)+import Control.Monad.Trans.Either +import Control.Exception (try, SomeException) import Control.Monad.IO.Class (liftIO)+import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Reader (ReaderT, ask, runReaderT) import Data.Monoid ((<>))-import Data.Text (Text) import qualified Data.Text.Encoding as T+import Data.Text (Text) import Network.Http.Client import OpenSSL (withOpenSSL) import qualified System.IO.Streams as Streams ------------------------------------------------------------------------------ -- | Core Type-type HackerNews a = ReaderT Connection IO a+type HackerNews a = EitherT HackerNewsError (ReaderT Connection IO) a ------------------------------------------------------------------------------+-- | Error Types+data HackerNewsError =+ ConnectionError+ | ParseError+ | RequestError+ deriving (Show, Eq)++------------------------------------------------------------------------------ -- | HackerNews API request method-hackerNews :: (Show a, FromJSON a) => HackerNews a -> IO a+hackerNews :: FromJSON a => HackerNews a -> IO (Either HackerNewsError a) hackerNews requests = withOpenSSL $ do ctx <- baselineContextSSL- con <- openConnectionSSL ctx "hacker-news.firebaseio.com" 443- result <- runReaderT requests con - closeConnection con- return result+ con <- try (openConnectionSSL ctx "hacker-news.firebaseio.com" 443) :: IO (Either SomeException Connection)+ case con of+ Left _ -> return $ Left ConnectionError+ Right conn -> do+ result <- flip runReaderT conn $ runEitherT requests+ closeConnection conn+ return result ------------------------------------------------------------------------------ -- | Request Builder for API-buildHNRequest :: FromJSON a => Text -> HackerNews (Maybe a)+buildHNRequest :: FromJSON a => Text -> HackerNews a buildHNRequest url = do- con <- ask- liftIO $ do+ con <- lift ask+ bytes <- liftIO $ do req <- buildRequest $ do http GET $ "/v0/" <> T.encodeUtf8 url <> ".json" setHeader "Connection" "Keep-Alive" setAccept "application/json" sendRequest con req emptyBody- !bytes <- receiveResponse con $ const Streams.read- return $ case bytes of- Nothing -> Nothing- Just bs -> do- let xs = rights [parseOnly value bs, parseOnly json bs]- case xs of- [] -> Nothing- x : _ ->- case fromJSON x of- Success a -> Just a- _ -> Nothing+ receiveResponse con $ const Streams.read+ case bytes of+ Nothing -> left RequestError+ Just bs -> do+ let xs = rights [parseOnly value bs, parseOnly json bs]+ case xs of+ [] -> left ParseError+ x : _ ->+ case fromJSON x of+ Success jsonBody -> right jsonBody+ _ -> left ParseError+ +
src/Web/HackerNews/Comment.hs view
@@ -1,33 +1,49 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module : Web.HackerNews.Comment+-- Copyright : (c) David Johnson, Konstantin Zudov 2014+-- Maintainer : djohnson.m@gmail.com+-- Stability : experimental+-- Portability : POSIX module Web.HackerNews.Comment where -import Control.Applicative ((<$>), (<*>))-import Control.Monad (MonadPlus (mzero))-import Data.Aeson (FromJSON (parseJSON), Value (Object),- (.:), (.:?), (.!=))-import Data.Text (Text)-import Data.Time (UTCTime)+import Control.Applicative ((<$>), (<*>))+import Control.Monad (MonadPlus (mzero))+import Data.Aeson (FromJSON (parseJSON), Value (Object),+ (.!=), (.:), (.:?))+import Data.Text (Text)+import Data.Time (UTCTime) -import Web.HackerNews.Util (fromSeconds)+import Web.HackerNews.Endpoint (Endpoint (endpoint), itemEndpoint)+import Web.HackerNews.Util (fromSeconds) --------------------------------------------------------------------------------- | Types+-- | Comment Object data Comment = Comment {- commentBy :: Text- , commentId :: CommentId- , commentKids :: Maybe [Int]- , commentParent :: Int- , commentText :: Text- , commentTime :: UTCTime- , commentType :: Text+ commentBy :: Text+ , commentId :: CommentId+ , commentKids :: Maybe [Int]+ , commentParent :: Int+ , commentText :: Text+ , commentTime :: UTCTime+ , commentType :: Text , commentDeleted :: Bool+ , commentDead :: Bool } deriving Show +------------------------------------------------------------------------------+-- | `CommentId` for a `Comment` Object newtype CommentId = CommentId Int deriving (Show, Eq) ------------------------------------------------------------------------------+-- | Endpoint instances+instance Endpoint CommentId Comment where+ endpoint (CommentId id') = itemEndpoint id'++------------------------------------------------------------------------------ -- | JSON Instances instance FromJSON Comment where parseJSON (Object o) =@@ -39,6 +55,7 @@ <*> (fromSeconds <$> o .: "time") <*> o .: "type" <*> o .:? "deleted" .!= False+ <*> o .:? "dead" .!= False parseJSON _ = mzero
+ src/Web/HackerNews/Endpoint.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module : Web.HackerNews.Endpoint+-- Copyright : (c) David Johnson, Konstantin Zudov, 2014+-- Maintainer : djohnson.m@gmail.com+-- Stability : experimental+-- Portability : POSIX+module Web.HackerNews.Endpoint where++import Data.Aeson (FromJSON)+import Data.Text (Text, append)++import Web.HackerNews.Client (HackerNews, buildHNRequest)+import Web.HackerNews.Util (toText)++------------------------------------------------------------------------------+-- | Endpoint maps the id to the returned type on a type level+-- The function dependency @id -> resp@ specifies that @id@ uniquely determines @resp@+class Endpoint id resp | id -> resp where+ endpoint :: id -> Text -- ^ Turn @id@ into path that points to resource++------------------------------------------------------------------------------+-- | Endpoint for `Item`+itemEndpoint :: Int -> Text+itemEndpoint = append "item/" . toText++------------------------------------------------------------------------------+-- | Generic function for making requests+getEndpoint :: (Endpoint a b, FromJSON b) => a -> HackerNews b+getEndpoint id' = buildHNRequest $ endpoint id'
+ src/Web/HackerNews/Item.hs view
@@ -0,0 +1,72 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module : Web.Stripe.Stripe+-- Copyright : (c) David Johnson, Konstantin Zudov, 2014+-- Maintainer : djohnson.m@gmail.com+-- Stability : experimental+-- Portability : POSIX+module Web.HackerNews.Item where++import Control.Applicative ((<$>))+import Control.Monad (MonadPlus (mzero))+import Data.Aeson (FromJSON (parseJSON), Value (Object),+ (.:))+import Data.Text (Text)++import Web.HackerNews.Comment (Comment)+import Web.HackerNews.Endpoint (Endpoint (endpoint), itemEndpoint)+import Web.HackerNews.Job (Job)+import Web.HackerNews.Poll (Poll, PollOpt)+import Web.HackerNews.Story (Story)++------------------------------------------------------------------------------+-- | Item Type+data Item = ItemComment Comment+ | ItemPoll Poll+ | ItemPollOpt PollOpt+ | ItemStory Story+ | ItemJob Job+ deriving (Show)++------------------------------------------------------------------------------+-- | Item ID for a `Item` object+newtype ItemId = ItemId Int deriving (Show,Eq)++------------------------------------------------------------------------------+-- | Max Item ID for a `Item`+data MaxItemId = MaxItemId deriving (Show, Eq)++------------------------------------------------------------------------------+-- | Max Item Int+newtype MaxItem = MaxItem Int deriving (Show, Eq)++------------------------------------------------------------------------------+-- | Endpoint Instances for `MaxItemId` and `MaxItem`+instance Endpoint MaxItemId MaxItem where+ endpoint _ = "maxitem"++------------------------------------------------------------------------------+-- | Endpoint Instances for `ItemId` & `Item`+instance Endpoint ItemId Item where+ endpoint (ItemId id') = itemEndpoint id'++------------------------------------------------------------------------------+-- | JSON Instances+instance FromJSON Item where+ parseJSON v@(Object o) = do+ itemType <- o .: "type"+ case (itemType :: Text) of+ "job" -> ItemJob <$> parseJSON v+ "story" -> ItemStory <$> parseJSON v+ "comment" -> ItemComment <$> parseJSON v+ "poll" -> ItemPoll <$> parseJSON v+ "pollopt" -> ItemPollOpt <$> parseJSON v+ _ -> mzero+ parseJSON _ = mzero++------------------------------------------------------------------------------+-- | JSON MaxItem Instance+instance FromJSON MaxItem where+ parseJSON = fmap MaxItem . parseJSON+
src/Web/HackerNews/Job.hs view
@@ -1,34 +1,50 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module : Web.HackerNews.Job+-- Copyright : (c) David Johnson, Konstantin Zudov, 2014+-- Maintainer : djohnson.m@gmail.com+-- Stability : experimental+-- Portability : POSIX module Web.HackerNews.Job where -import Control.Applicative ((<*>), (<$>))-import Control.Monad (MonadPlus (mzero))-import Data.Aeson (FromJSON (parseJSON), Value (Object),- (.:), (.:?), (.!=))-import Data.Text (Text)-import Data.Time (UTCTime)+import Control.Applicative ((<$>), (<*>))+import Control.Monad (MonadPlus (mzero))+import Data.Aeson (FromJSON (parseJSON), Value (Object),+ (.!=), (.:), (.:?))+import Data.Text (Text)+import Data.Time (UTCTime) -import Web.HackerNews.Util (fromSeconds)+import Web.HackerNews.Endpoint (Endpoint (endpoint), itemEndpoint)+import Web.HackerNews.Util (fromSeconds) ------------------------------------------------------------------------------ -- | Types data Job = Job {- jobBy :: Text- , jobId :: JobId- , jobScore :: Int- , jobText :: Text- , jobTime :: UTCTime- , jobTitle :: Text- , jobType :: Text- , jobUrl :: Text- , jobDeleted :: Bool+ jobBy :: Text+ , jobId :: JobId+ , jobScore :: Int+ , jobText :: Text+ , jobTime :: UTCTime+ , jobTitle :: Text+ , jobType :: Text+ , jobUrl :: Text+ , jobDeleted :: Bool+ , jobDead :: Bool } deriving (Show) +------------------------------------------------------------------------------+-- | ID for a `Job` type newtype JobId = JobId Int deriving (Show, Eq) ------------------------------------------------------------------------------+-- | Endpoint Instances+instance Endpoint JobId Job where+ endpoint (JobId id') = itemEndpoint id'++------------------------------------------------------------------------------ -- | JSON Instances instance FromJSON Job where parseJSON (Object o) =@@ -41,5 +57,6 @@ <*> o .: "type" <*> o .: "url" <*> o .:? "deleted" .!= False+ <*> o .:? "dead" .!= False parseJSON _ = mzero
src/Web/HackerNews/Person.hs view
@@ -1,17 +1,23 @@ {-# LANGUAGE OverloadedStrings #-}+-- |+-- Module : Web.HackerNews.Person+-- Copyright : (c) David Johnson, 2014+-- Maintainer : djohnson.m@gmail.com+-- Stability : experimental+-- Portability : POSIX module Web.HackerNews.Person where import Control.Applicative ((<$>), (<*>)) import Control.Monad (MonadPlus (mzero)) import Data.Aeson (FromJSON (parseJSON), Value (Object),- (.:), (.:?), (.!=))+ (.!=), (.:), (.:?)) import Data.Text (Text) import Data.Time (UTCTime) import Web.HackerNews.Util (fromSeconds) --------------------------------------------------------------------------------- | Types+-- | Person Object data Person = Person { personBy :: Text , personId :: PersonId@@ -22,8 +28,11 @@ , personType :: Text , personUrl :: Maybe Text , personDeleted :: Bool+ , personDead :: Bool } deriving (Show, Eq) +------------------------------------------------------------------------------+-- | Person ID for a `Person` Object newtype PersonId = PersonId Text deriving (Show, Eq)@@ -41,4 +50,5 @@ <*> o .: "type" <*> o .:? "url" <*> o .:? "deleted" .!= False+ <*> o .:? "dead" .!= False parseJSON _ = mzero
src/Web/HackerNews/Poll.hs view
@@ -1,17 +1,25 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module : Web.HackerNews.Poll+-- Copyright : (c) David Johnson, Konstantin Zudov, 2014+-- Maintainer : djohnson.m@gmail.com+-- Stability : experimental+-- Portability : POSIX module Web.HackerNews.Poll where -import Control.Applicative ((<$>), (<*>))-import Control.Monad (MonadPlus (mzero))-import Data.Aeson (FromJSON (parseJSON), Value (Object),- (.:), (.!=), (.:?))-import Data.Text (Text)-import Data.Time (UTCTime)+import Control.Applicative ((<$>), (<*>))+import Control.Monad (MonadPlus (mzero))+import Data.Aeson (FromJSON (parseJSON), Value (Object),+ (.!=), (.:), (.:?))+import Data.Text (Text)+import Data.Time (UTCTime) -import Web.HackerNews.Util (fromSeconds)+import Web.HackerNews.Endpoint (Endpoint (endpoint), itemEndpoint)+import Web.HackerNews.Util (fromSeconds) --------------------------------------------------------------------------------- | Types+-- | Poll Object data Poll = Poll { pollBy :: Text , pollId :: PollId@@ -23,8 +31,11 @@ , pollTitle :: Text , pollType :: Text , pollDeleted :: Bool+ , pollDead :: Bool } deriving (Show, Eq) +------------------------------------------------------------------------------+-- | Poll Opt Object data PollOpt = PollOpt { pollOptBy :: Text , pollOptId :: PollOptId@@ -34,18 +45,33 @@ , pollOptTime :: UTCTime , pollOptType :: Text , pollOptDeleted :: Bool+ , pollOptDead :: Bool } deriving (Show, Eq) +------------------------------------------------------------------------------+-- | Poll Option Id for a `PollOpt` newtype PollOptId = PollOptId Int deriving (Show, Eq) +------------------------------------------------------------------------------+-- | Poll Id for a `Poll` newtype PollId = PollId Int deriving (Show, Eq) --------------------------------------------------------------------------------- | JSON Instances+-- | `Endpoint` Instance for `PollOptId`+instance Endpoint PollOptId PollOpt where+ endpoint (PollOptId id') = itemEndpoint id'++------------------------------------------------------------------------------+-- | `Endpoint` Instance for `PollId`+instance Endpoint PollId Poll where+ endpoint (PollId id') = itemEndpoint id'++------------------------------------------------------------------------------+-- | Poll JSON Instances instance FromJSON Poll where parseJSON (Object o) = Poll <$> o .: "by"@@ -58,8 +84,11 @@ <*> o .: "title" <*> o .: "type" <*> o .:? "deleted" .!= False+ <*> o .:? "dead" .!= False parseJSON _ = mzero +------------------------------------------------------------------------------+-- | Poll JSON Instances instance FromJSON PollOpt where parseJSON (Object o) = PollOpt <$> o .: "by"@@ -70,4 +99,5 @@ <*> (fromSeconds <$> o .: "time") <*> o .: "type" <*> o .:? "deleted" .!= False+ <*> o .:? "dead" .!= False parseJSON _ = mzero
src/Web/HackerNews/Story.hs view
@@ -1,20 +1,28 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module : Web.HackerNews.Story+-- Copyright : (c) David Johnson, Konstantin Zudov, 2014+-- Maintainer : djohnson.m@gmail.com+-- Stability : experimental+-- Portability : POSIX module Web.HackerNews.Story where -import Control.Applicative ((<$>), (<*>))-import Control.Monad (MonadPlus (mzero))-import Data.Aeson (FromJSON (parseJSON), Value (Object),- (.:), (.!=), (.:?))-import Data.Text (Text)-import Data.Time (UTCTime)+import Control.Applicative ((<$>), (<*>))+import Control.Monad (MonadPlus (mzero))+import Data.Aeson (FromJSON (parseJSON), Value (Object),+ (.!=), (.:), (.:?))+import Data.Text (Text)+import Data.Time (UTCTime) -import Web.HackerNews.Util (fromSeconds)+import Web.HackerNews.Endpoint (Endpoint (endpoint), itemEndpoint)+import Web.HackerNews.Util (fromSeconds) --------------------------------------------------------------------------------- | Types+-- | Story Object data Story = Story { storyBy :: Text- , storyId :: Int+ , storyId :: StoryId , storyKids :: [Int] , storyScore :: Int , storyTime :: UTCTime@@ -22,21 +30,39 @@ , storyType :: Text , storyUrl :: Text , storyDeleted :: Bool+ , storyDead :: Bool } deriving Show +------------------------------------------------------------------------------+-- | ID for a `Story` newtype StoryId = StoryId Int deriving (Show, Eq) -type TopStories = [Int]-type MaxItem = Int+------------------------------------------------------------------------------+-- | `Story` ID+data TopStoriesId = TopStoriesId deriving (Show, Eq) ------------------------------------------------------------------------------+-- | TopStories List+newtype TopStories = TopStories [Int] deriving (Show, Eq)++------------------------------------------------------------------------------+-- | Endpoint Instances for `StoryID` & `Story`+instance Endpoint StoryId Story where+ endpoint (StoryId id') = itemEndpoint id'++------------------------------------------------------------------------------+-- | Endpoint Instances for `TopStoriesID` & `TopStories`+instance Endpoint TopStoriesId TopStories where+ endpoint _ = "topstories"++------------------------------------------------------------------------------ -- | JSON Instances instance FromJSON Story where parseJSON (Object o) = Story <$> o .: "by"- <*> o .: "id"+ <*> (StoryId <$> o .: "id") <*> o .: "kids" <*> o .: "score" <*> (fromSeconds <$> o .: "time")@@ -44,5 +70,11 @@ <*> o .: "type" <*> o .: "url" <*> o .:? "deleted" .!= False+ <*> o .:? "dead" .!= False parseJSON _ = mzero++------------------------------------------------------------------------------+-- | JSON instances `TopStories`+instance FromJSON TopStories where+ parseJSON = fmap TopStories . parseJSON
src/Web/HackerNews/Types.hs view
@@ -1,11 +1,11 @@-{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module : Web.HackerNews.Types+-- Copyright : (c) David Johnson, Konstantin Zudov, 2014+-- Maintainer : djohnson.m@gmail.com+-- Stability : experimental+-- Portability : POSIX module Web.HackerNews.Types ( module H- , StoryId (..)- , CommentId (..)- , PollId (..)- , PollOptId (..)- , UserId (..) ) where import Web.HackerNews.Comment as H@@ -13,7 +13,9 @@ import Web.HackerNews.Story as H import Web.HackerNews.User as H import Web.HackerNews.Update as H-import Web.HackerNews.Job as H+import Web.HackerNews.Job as H+import Web.HackerNews.Item as H+import Web.HackerNews.Endpoint as H
src/Web/HackerNews/Update.hs view
@@ -1,26 +1,46 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module : Web.HackerNews.Update+-- Copyright : (c) David Johnson, Konstantin Zudov, 2014+-- Maintainer : djohnson.m@gmail.com+-- Stability : experimental+-- Portability : POSIX module Web.HackerNews.Update where -import Control.Applicative ((<$>), (<*>))-import Control.Monad (MonadPlus (mzero))-import Data.Aeson (FromJSON (parseJSON), Value (Object),- (.:), (.!=), (.:?))-import Data.Text (Text)+import Control.Applicative ((<$>), (<*>))+import Control.Monad (MonadPlus (mzero))+import Data.Aeson (FromJSON (parseJSON), Value (Object),+ (.!=), (.:), (.:?))+import Data.Text (Text) +import Web.HackerNews.Endpoint (Endpoint (endpoint))+ --------------------------------------------------------------------------------- | Types+-- | Update Object data Update = Update { updateItems :: [Int] , updateProfiles :: [Text] , updateDeleted :: Bool+ , updateDead :: Bool } deriving (Show, Eq) ------------------------------------------------------------------------------+-- | Update ID for an `Updated` Object+data UpdateId = UpdateId deriving (Show, Eq)++------------------------------------------------------------------------------+-- | Endpoint Instances+instance Endpoint UpdateId Update where+ endpoint _ = "updates"++------------------------------------------------------------------------------ -- | JSON Instances instance FromJSON Update where parseJSON (Object o) = Update <$> o .: "items" <*> o .: "profiles" <*> o .:? "deleted" .!= False+ <*> o .:? "dead" .!= False parseJSON _ = mzero
src/Web/HackerNews/User.hs view
@@ -1,17 +1,26 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module : Web.HackerNews.User+-- Copyright : (c) David Johnson, Konstantin Zudov, 2014+-- Maintainer : djohnson.m@gmail.com+-- Stability : experimental+-- Portability : POSIX module Web.HackerNews.User where -import Control.Applicative ((<*>), (<$>))-import Control.Monad (MonadPlus (mzero))-import Data.Aeson (FromJSON (parseJSON), Value (Object),- (.:), (.:?), (.!=))-import Data.Text (Text)-import Data.Time (UTCTime)+import Control.Applicative ((<$>), (<*>))+import Control.Monad (MonadPlus (mzero))+import Data.Aeson (FromJSON (parseJSON), Value (Object),+ (.!=), (.:), (.:?))+import Data.Monoid ((<>))+import Data.Text (Text)+import Data.Time (UTCTime) -import Web.HackerNews.Util (fromSeconds, monoidToMaybe)+import Web.HackerNews.Endpoint (Endpoint (endpoint))+import Web.HackerNews.Util (fromSeconds, monoidToMaybe) --------------------------------------------------------------------------------- | Types+-- | `User` Object data User = User { userAbout :: Maybe Text , userCreated :: UTCTime@@ -20,13 +29,21 @@ , userKarma :: Int , userSubmitted :: [Int] , userDeleted :: Bool+ , userDead :: Bool } deriving (Show) +------------------------------------------------------------------------------+-- | User ID for a `User` Object newtype UserId = UserId Text deriving (Show, Eq) ------------------------------------------------------------------------------+-- | Endpoint instances+instance Endpoint UserId User where+ endpoint (UserId id') = "user/" <> id'++------------------------------------------------------------------------------ -- | JSON Instances instance FromJSON User where parseJSON (Object o) =@@ -37,5 +54,6 @@ <*> o .: "karma" <*> o .: "submitted" <*> o .:? "deleted" .!= False+ <*> o .:? "dead" .!= False parseJSON _ = mzero
src/Web/HackerNews/Util.hs view
@@ -1,10 +1,16 @@+-- |+-- Module : Web.HackerNews.Util+-- Copyright : (c) David Johnson, Konstantin Zudov, 2014+-- Maintainer : djohnson.m@gmail.com+-- Stability : experimental+-- Portability : POSIX module Web.HackerNews.Util where -import qualified Data.Text as T-import Data.Text (Text)+import Data.Monoid (Monoid, mempty)+import Data.Text (Text)+import qualified Data.Text as T+import Data.Time (UTCTime) import Data.Time.Clock.POSIX (posixSecondsToUTCTime)-import Data.Time (UTCTime)-import Data.Monoid (Monoid, mempty) ------------------------------------------------------------------------------ -- | Convert `Integer` to `UTCTime`@@ -16,7 +22,8 @@ toText :: Show a => a -> Text toText = T.pack . show +------------------------------------------------------------------------------ -- | Turns empty monoids into Nothing--- | @Data.Maybe.listToMaybe@ generalized for monoids+-- `listToMaybe` generalized for monoids monoidToMaybe :: (Eq a, Monoid a) => a -> Maybe a monoidToMaybe m = if m == mempty then Nothing else Just m
tests/Test.hs view
@@ -1,34 +1,27 @@ {-# LANGUAGE OverloadedStrings #-} module Main where -import Data.Maybe (isJust)-import Test.Hspec (it, runIO, hspec, describe)+import Data.Either (isRight)+import Test.Hspec (it, runIO, hspec, describe, shouldSatisfy) import Web.HackerNews import Control.Applicative main :: IO () main = hspec $ do describe "Hacker News API Tests" $ do- (story, comment, user, job, poll, pollOpt, topStories, maxItem, updates) <- - runIO $ hackerNews $ (,,,,,,,,) <$> - getStory (StoryId 8863) <*>- getComment (CommentId 2921983) <*>- getUser (UserId "dmjio") <*>- getJob (JobId 8437631) <*>- getPoll (PollId 126809) <*>- getPollOpt (PollOptId 160705) <*>- getTopStories <*>- getMaxItem <*>+ it "Retrieves all" $ do+ result <- hackerNews $ (,,,,,,,,) <$> + getStory (StoryId 8863) <*>+ getComment (CommentId 2921983) <*>+ getUser (UserId "dmjio") <*>+ getJob (JobId 8437631) <*>+ getPoll (PollId 126809) <*>+ getPollOpt (PollOptId 160705) <*>+ getTopStories <*>+ getMaxItem <*> getUpdates- it "Retrieves a Story" $ isJust story- it "Retrieves a Comment" $ isJust comment- it "Retrieves a User" $ isJust user- it "Retrieves a Job" $ isJust job- it "Retrieves a Poll" $ isJust poll- it "Retrieves a Pollopt" $ isJust pollOpt- it "Retrieves Top Stories" $ isJust topStories- it "Retrieves Max Item" $ isJust maxItem- it "Retrieves Updates" $ isJust updates+ result `shouldSatisfy` isRight+