hackernews (empty) → 0.1.0.0
raw patch · 14 files changed
+526/−0 lines, 14 filesdep +HsOpenSSLdep +aesondep +attoparsecsetup-changed
Dependencies added: HsOpenSSL, aeson, attoparsec, base, bytestring, hackernews, hspec, http-streams, io-streams, text, time
Files
- LICENSE +21/−0
- Setup.lhs +3/−0
- hackernews.cabal +51/−0
- src/Web/HackerNews.hs +73/−0
- src/Web/HackerNews/Client.hs +41/−0
- src/Web/HackerNews/Comment.hs +43/−0
- src/Web/HackerNews/Person.hs +42/−0
- src/Web/HackerNews/Poll.hs +69/−0
- src/Web/HackerNews/Story.hs +43/−0
- src/Web/HackerNews/Types.hs +24/−0
- src/Web/HackerNews/Update.hs +24/−0
- src/Web/HackerNews/User.hs +39/−0
- src/Web/HackerNews/Util.hs +18/−0
- tests/Test.hs +35/−0
+ LICENSE view
@@ -0,0 +1,21 @@+The MIT License (MIT)++Copyright (c) 2014 David Johnson++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.
+ Setup.lhs view
@@ -0,0 +1,3 @@+#!/usr/bin/env runhaskell+> import Distribution.Simple+> main = defaultMain
+ hackernews.cabal view
@@ -0,0 +1,51 @@+name: hackernews+version: 0.1.0.0+description: API for news.ycombinator.com+license: MIT+synopsis: API for Hacker News+license-file: LICENSE+author: David Johnson+maintainer: djohnson.m@gmail.com+category: Web+build-type: Simple+cabal-version: >=1.10++library+ ghc-options: -Wall+ exposed-modules: Web.HackerNews+ other-modules: Web.HackerNews.Types+ , Web.HackerNews.Client+ , Web.HackerNews.User+ , Web.HackerNews.Poll+ , Web.HackerNews.Person+ , Web.HackerNews.Update+ , Web.HackerNews.Comment+ , Web.HackerNews.Story+ , Web.HackerNews.Util+ hs-source-dirs: src+ build-depends: HsOpenSSL >= 0.10.5+ , aeson+ , attoparsec >= 0.12.1.2+ , base >=4.7 && <4.8+ , bytestring >= 0.10.4.0+ , hspec >= 1.11.4+ , http-streams+ , io-streams >= 1.1.4.6+ , text >= 1.2.0.0+ , time >= 1.4.2+ default-language: Haskell2010++Test-Suite tests+ type: exitcode-stdio-1.0+ hs-source-dirs: tests+ main-is: Test.hs+ build-depends: base+ , hackernews+ , hspec+ , hspec >= 1.11.4+ default-language: Haskell2010+ ghc-options: -Wall++source-repository head+ type: git+ location: https://github.com/dmjio/hackernews
+ src/Web/HackerNews.hs view
@@ -0,0 +1,73 @@+{-# LANGUAGE OverloadedStrings #-}+module Web.HackerNews+ ( -- * API Calls+ getStory+ , getComment+ , getPoll+ , getPollOpt+ , getUser+ , getTopStories+ , getMaxItem+ , getUpdates+ -- * Types+ , Comment (..)+ , CommentId (..)+ , Poll (..)+ , PollId (..)+ , PollOpt (..)+ , PollOptId (..)+ , Story (..)+ , StoryId (..)+ , User (..)+ , UserId (..)+ , Update (..)+ , MaxItem+ , TopStories+ ) where++import Data.Monoid ((<>))++import Web.HackerNews.Types+import Web.HackerNews.Util (toText)+import Web.HackerNews.Client (getItem)++------------------------------------------------------------------------------+-- | Retrieve a `Story` by `StoryId`+getStory :: StoryId -> IO (Maybe Story)+getStory (StoryId storyid) = getItem $ "item/" <> toText storyid++------------------------------------------------------------------------------+-- | Retrieve a `Comment` by `CommentId`+getComment :: CommentId -> IO (Maybe Comment)+getComment (CommentId commentid) = getItem $ "item/" <> toText commentid++------------------------------------------------------------------------------+-- | Retrieve a `Poll` by `PollId`+getPoll :: PollId -> IO (Maybe Poll)+getPoll (PollId pollid) = getItem $ "item/" <> toText pollid++------------------------------------------------------------------------------+-- | Retrieve a `PollOpt` by `PollOptId`+getPollOpt :: PollOptId -> IO (Maybe PollOpt)+getPollOpt (PollOptId polloptid) = getItem $ "item/" <> toText polloptid++------------------------------------------------------------------------------+-- | Retrieve a `User` by `UserId`+getUser :: UserId -> IO (Maybe User)+getUser (UserId userid) = getItem $ "user/" <> userid++------------------------------------------------------------------------------+-- | Retrieve the Top Stories on Hacker News+getTopStories :: IO (Maybe TopStories)+getTopStories = getItem "topstories"++------------------------------------------------------------------------------+-- | Retrieve the largest ItemId+getMaxItem :: IO (Maybe MaxItem)+getMaxItem = getItem "maxitem"++------------------------------------------------------------------------------+-- | Retrieve the largest ItemId+getUpdates :: IO (Maybe Update)+getUpdates = getItem "updates"+
+ src/Web/HackerNews/Client.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE OverloadedStrings #-}+module Web.HackerNews.Client ( getItem ) where++import Data.Aeson hiding (Result)+import Data.Aeson.Parser (value)+import Data.Attoparsec.ByteString (parseOnly)+import Data.Either (rights)++import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text.Encoding as T+import Network.Http.Client+import OpenSSL (withOpenSSL)+import qualified System.IO.Streams as Streams++------------------------------------------------------------------------------+-- | HackerNews API request method+getItem+ :: FromJSON a+ => Text + -> IO (Maybe a)+getItem url = withOpenSSL $ do+ ctx <- baselineContextSSL+ con <- openConnectionSSL ctx "hacker-news.firebaseio.com" 443+ req <- buildRequest $ do+ http GET $ "/v0/" <> T.encodeUtf8 url <> ".json"+ setAccept "application/json"+ sendRequest con req emptyBody+ bytes <- receiveResponse con $ const $ Streams.read+ closeConnection con+ 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+
+ src/Web/HackerNews/Comment.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE OverloadedStrings #-}+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 Web.HackerNews.Util (fromSeconds)++------------------------------------------------------------------------------+-- | Types+data Comment = Comment {+ commentBy :: Text+ , commentId :: CommentId+ , commentKids :: Maybe [Int]+ , commentParent :: Int+ , commentText :: Text+ , commentTime :: UTCTime+ , commentType :: Text+ } deriving Show++newtype CommentId+ = CommentId Int+ deriving (Show, Eq)++------------------------------------------------------------------------------+-- | JSON Instances+instance FromJSON Comment where+ parseJSON (Object o) =+ Comment <$> o .: "by"+ <*> (CommentId <$> o .: "id")+ <*> o .:? "kids"+ <*> o .: "parent"+ <*> o .: "text"+ <*> (fromSeconds <$> o .: "time")+ <*> o .: "type"+ parseJSON _ = mzero+++
+ src/Web/HackerNews/Person.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE OverloadedStrings #-}+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+data Person = Person {+ personBy :: Text+ , personId :: PersonId+ , personKids :: Maybe [Int]+ , personScore :: Maybe Int+ , personTime :: UTCTime+ , personTitle :: Maybe Text+ , personType :: Text+ , personUrl :: Maybe Text+ } deriving (Show, Eq)++newtype PersonId+ = PersonId Text+ deriving (Show, Eq)++------------------------------------------------------------------------------+-- | JSON Instances+instance FromJSON Person where+ parseJSON (Object o) =+ Person <$> o .: "by"+ <*> (PersonId <$> o .: "id")+ <*> o .:? "kids"+ <*> o .:? "score"+ <*> (fromSeconds <$> o .: "time")+ <*> o .:? "title"+ <*> o .: "type"+ <*> o .:? "url"+ parseJSON _ = mzero
+ src/Web/HackerNews/Poll.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE OverloadedStrings #-}+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 Web.HackerNews.Util (fromSeconds)++------------------------------------------------------------------------------+-- | Types+data Poll = Poll {+ pollBy :: Text+ , pollId :: PollId+ , pollKids :: [Int]+ , pollParts :: [Int]+ , pollScore :: Int+ , pollText :: Text+ , pollTime :: UTCTime+ , pollTitle :: Text+ , pollType :: Text+ } deriving (Show, Eq)++data PollOpt = PollOpt {+ pollOptBy :: Text+ , pollOptId :: PollOptId+ , pollOptParent :: Int+ , pollOptScore :: Int+ , pollOptText :: Text+ , pollOptTime :: UTCTime+ , pollOptType :: Text+ } deriving (Show, Eq)++newtype PollOptId+ = PollOptId Int+ deriving (Show, Eq)++newtype PollId+ = PollId Int+ deriving (Show, Eq)++------------------------------------------------------------------------------+-- | JSON Instances+instance FromJSON Poll where+ parseJSON (Object o) =+ Poll <$> o .: "by"+ <*> (PollId <$> o .: "id")+ <*> o .: "kids"+ <*> o .: "parts"+ <*> o .: "score"+ <*> o .: "text"+ <*> (fromSeconds <$> o .: "time")+ <*> o .: "title"+ <*> o .: "type"+ parseJSON _ = mzero++instance FromJSON PollOpt where+ parseJSON (Object o) =+ PollOpt <$> o .: "by"+ <*> (PollOptId <$> o .: "id")+ <*> o .: "parent"+ <*> o .: "score"+ <*> o .: "text"+ <*> (fromSeconds <$> o .: "time")+ <*> o .: "type"+ parseJSON _ = mzero
+ src/Web/HackerNews/Story.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE OverloadedStrings #-}+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 Web.HackerNews.Util (fromSeconds)++------------------------------------------------------------------------------+-- | Types+data Story = Story {+ storyBy :: Text+ , storyId :: Int+ , storyKids :: [Int]+ , storyScore :: Int+ , storyTime :: UTCTime+ , storyTitle :: Text+ , storyType :: Text+ , storyUrl :: Text+ } deriving Show++newtype StoryId+ = StoryId Int+ deriving (Show, Eq)++------------------------------------------------------------------------------+-- | JSON Instances+instance FromJSON Story where+ parseJSON (Object o) =+ Story <$> o .: "by"+ <*> o .: "id"+ <*> o .: "kids"+ <*> o .: "score"+ <*> (fromSeconds <$> o .: "time")+ <*> o .: "title"+ <*> o .: "type"+ <*> o .: "url"+ parseJSON _ = mzero+
+ src/Web/HackerNews/Types.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE OverloadedStrings #-}+module Web.HackerNews.Types+ ( module H+ , StoryId (..)+ , CommentId (..)+ , PollId (..)+ , PollOptId (..)+ , UserId (..)+ , MaxItem+ , TopStories+ ) where++import Web.HackerNews.Comment as H+import Web.HackerNews.Poll as H+import Web.HackerNews.Story as H+import Web.HackerNews.User as H+import Web.HackerNews.Update as H++type MaxItem = Int+type TopStories = [Int]++++
+ src/Web/HackerNews/Update.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE OverloadedStrings #-}+module Web.HackerNews.Update where++import Control.Applicative ((<$>), (<*>))+import Control.Monad (MonadPlus (mzero))+import Data.Aeson (FromJSON (parseJSON), Value (Object),+ (.:))+import Data.Text (Text)++------------------------------------------------------------------------------+-- | Types+data Update = Update {+ updateItems :: [Int]+ , updateProfiles :: [Text]+ } deriving (Show, Eq)++------------------------------------------------------------------------------+-- | JSON Instances+instance FromJSON Update where+ parseJSON (Object o) =+ Update <$> o .: "items"+ <*> o .: "profiles"+ parseJSON _ = mzero+
+ src/Web/HackerNews/User.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE OverloadedStrings #-}+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 Web.HackerNews.Util (fromSeconds)++------------------------------------------------------------------------------+-- | Types+data User = User {+ userAbout :: Text+ , userCreated :: UTCTime+ , userDelay :: Int+ , userId :: UserId+ , userKarma :: Int+ , userSubmitted :: [Int]+ } deriving (Show)++newtype UserId+ = UserId Text+ deriving (Show, Eq)++------------------------------------------------------------------------------+-- | JSON Instances+instance FromJSON User where+ parseJSON (Object o) =+ User <$> o .: "about"+ <*> (fromSeconds <$> o .: "created")+ <*> o .: "delay"+ <*> (UserId <$> o .: "id")+ <*> o .: "karma"+ <*> o .: "submitted"+ parseJSON _ = mzero+
+ src/Web/HackerNews/Util.hs view
@@ -0,0 +1,18 @@+module Web.HackerNews.Util where++import qualified Data.Text as T+import Data.Text (Text)+import Data.Time.Clock.POSIX (posixSecondsToUTCTime)+import Data.Time (UTCTime)++------------------------------------------------------------------------------+-- | Convert `Integer` to `UTCTime`+fromSeconds :: Integer -> UTCTime+fromSeconds = posixSecondsToUTCTime . fromInteger++------------------------------------------------------------------------------+-- | Convert `Show` constrained a to `Text`+toText :: Show a => a -> Text+toText = T.pack . show++
+ tests/Test.hs view
@@ -0,0 +1,35 @@+module Main where++import Data.Maybe (isJust)+import Test.Hspec (it, runIO, hspec, describe)+import Web.HackerNews++main :: IO ()+main = hspec $ do+ describe "Hacker News API Tests" $ do+ story <- runIO $ getStory (StoryId 8863)+ it "Retrieves a Story" $ isJust story+ comment <- runIO $ getComment (CommentId 2921983)+ it "Retrieves a Comment" $ isJust comment+ user <- runIO $ getUser (UserId "dmjio")+ it "Retrieves a User" $ isJust user+ poll <- runIO $ getPoll (PollId 126809)+ it "Retrieves a Poll" $ isJust poll+ pollOpt <- runIO $ getPollOpt (PollOptId 160705)+ it "Retrieves a Pollopt" $ isJust pollOpt+ topStories <- runIO getTopStories+ it "Retrieves Top Stories" $ isJust topStories+ maxItem <- runIO getMaxItem+ it "Retrieves Max Item" $ isJust maxItem+ updates <- runIO getUpdates+ it "Retrieves Updates" $ isJust updates++++++++++