Here is my Haskell program designed to list all of my GitHub repos along with their descriptions and languages via the GitHub JSON APIs:
{-# LANGUAGE DeriveGeneric #-}{-# LANGUAGE OverloadedStrings #-}module Main whereimport Control.Monadimport Data.Aesonimport qualified Data.ByteString as BSimport qualified Data.ByteString.Char8 as C8import qualified Data.List as Limport Data.Maybeimport Data.Textimport qualified Data.Text.Encoding as Eimport Data.Word (Word16)import GHC.Generics (Generic)import Network.Http.Clientimport Network.HTTP.Linkimport Network.URIimport OpenSSLuriIsSsl :: URI -> BooluriIsSsl uri = uriScheme uri == "https:"uriGetHostName :: URI -> Maybe StringuriGetHostName uri = uriRegName <$> uriAuthority uriuriGetPort :: URI -> Word16 -> Maybe Word16uriGetPort uri defaultPort = do auth <- uriAuthority uri return $ case uriPort auth of "" -> defaultPort p -> (Prelude.read $ Prelude.tail p) :: Word16uriGetFullPath :: URI -> StringuriGetFullPath uri = uriPath uri ++ uriQuery uri ++ uriFragment uricontainsLinkParam :: Link -> LinkParam -> Text -> BoolcontainsLinkParam link linkParam value = isJust $ L.find (\(lp, v) -> lp == linkParam && v == value) $ linkParams linkhasRelNext :: Link -> BoolhasRelNext link = containsLinkParam link Rel "next"findNextLink :: BS.ByteString -> Maybe LinkfindNextLink value = do links <- parseLinkHeader $ E.decodeUtf8 value L.find hasRelNext linksgetLinkHeader :: Response -> Maybe BS.ByteStringgetLinkHeader p = getHeader p "Link"nextLinkFromResponse :: Response -> Maybe LinknextLinkFromResponse p = getLinkHeader p >>= findNextLinkopenUri :: URI -> (Connection -> BS.ByteString -> IO a) -> IO aopenUri uri f = let isSsl = uriIsSsl uri hostName = C8.pack $ fromJust $ uriGetHostName uri port = fromJust $ uriGetPort uri (if isSsl then 443 else 80) fullPath = C8.pack $ uriGetFullPath uri wrappedF c = f c fullPath in if isSsl then withOpenSSL $ do ctx <- baselineContextSSL withConnection (openConnectionSSL ctx hostName port) wrappedF else withConnection (openConnection hostName port) wrappedFdata Repo = Repo { name :: String , description :: String , language :: Maybe String} deriving (Show, Generic)instance FromJSON RepofetchRepos :: URI -> IO [Repo]fetchRepos uri = openUri uri $ \c fullPath -> do request <- buildRequest $ do http GET fullPath setAccept "application/json" setHeader "User-Agent" "MyGitHubApiClient" sendRequest c request emptyBody receiveResponse c $ \p i -> do repos <- jsonHandler p i nextRepos <- case nextLinkFromResponse p of Just link -> fetchRepos $ href link Nothing -> return [] return $ repos ++ nextReposmain :: IO ()main = do repos <- fetchRepos $ fromJust $ parseURI "https://api.github.com/users/rcook/repos" putStrLn $ show (Prelude.length repos) ++ " repos:" forM_ repos $ \repo -> print repoThe trickiest thing I had to implement in this program was parsing and following "Link" headers in the HTTP response in order to deal with the API's built-in pagination behaviour. Fortunately, thehttp-link-header module exists so I didn't have to write the parser from scratch. However, figuring out where to follow theRel="next" links was challenging at first.
I'm interested in hearing any constructive criticisms or suggestions, e.g.:
- Code that could be refactored into more idiomatic Haskell style
- Outright bugs
- Improvements to error handling
- Indentation!
I'd also welcome any suggestions about how I'd write automated tests for this code, given that much of the code is in theIO monad.
1 Answer1
You could addcomments in your code to highlight important stages that the program passes through, not only this, but the code is more readable.
- 4\$\begingroup\$Just because you don't understand the code doesn't mean that everyone else also doesn't understand it.\$\endgroup\$Simon Forsberg– Simon Forsberg2016-01-06 12:11:50 +00:00CommentedJan 6, 2016 at 12:11
You mustlog in to answer this question.
Explore related questions
See similar questions with these tags.
