packages feed

reddit 0.2.3.0 → 0.3.0.0

raw patch · 12 files changed

+241/−105 lines, 12 filesdep +directorydep ~aesondep ~api-builderdep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: directory

Dependency ranges changed: aeson, api-builder, base, bytestring, data-default-class, free, http-client, http-client-tls, http-types, network, text, time, transformers, unordered-containers, vector

API changes (from Hackage documentation)

- Reddit.Types.Listing: instance (GHC.Classes.Eq a, GHC.Classes.Eq t) => GHC.Classes.Eq (Reddit.Types.Listing.Listing t a)
- Reddit.Types.Listing: instance (GHC.Read.Read a, GHC.Read.Read t) => GHC.Read.Read (Reddit.Types.Listing.Listing t a)
- Reddit.Types.Listing: instance (GHC.Show.Show a, GHC.Show.Show t) => GHC.Show.Show (Reddit.Types.Listing.Listing t a)
- Reddit.Types.Reddit: instance GHC.Classes.Eq Reddit.Types.Reddit.LoginDetails
+ Reddit: ClientParams :: Text -> Text -> ClientParams
+ Reddit: LoginDetails :: Modhash -> CookieJar -> LoginDetails
+ Reddit: Modhash :: Text -> Modhash
+ Reddit: POSTWrapped :: a -> POSTWrapped a
+ Reddit: RateLimitInfo :: Integer -> Integer -> UTCTime -> RateLimitInfo
+ Reddit: RateLimits :: ShouldRateLimit -> Maybe RateLimitInfo -> RateLimits
+ Reddit: RedditT :: FreeT (RedditF m) m a -> RedditT m a
+ Reddit: [FailWith] :: APIError RedditError -> RedditF m a
+ Reddit: [NestResuming] :: RedditT m b -> (Either (APIError RedditError, Maybe (RedditT m b)) b -> a) -> RedditF m a
+ Reddit: [Nest] :: RedditT m b -> (Either (APIError RedditError) b -> a) -> RedditF m a
+ Reddit: [ReceiveRoute] :: Receivable b => Route -> (b -> a) -> RedditF m a
+ Reddit: [RunRoute] :: FromJSON b => Route -> (b -> a) -> RedditF m a
+ Reddit: [WithBaseURL] :: Text -> RedditT m b -> (b -> a) -> RedditF m a
+ Reddit: [WithHeaders] :: ([Header] -> [Header]) -> RedditT m b -> (b -> a) -> RedditF m a
+ Reddit: [remaining] :: RateLimitInfo -> Integer
+ Reddit: [resetTime] :: RateLimitInfo -> UTCTime
+ Reddit: [used] :: RateLimitInfo -> Integer
+ Reddit: addAPIType :: Route -> Route
+ Reddit: data ClientParams
+ Reddit: data LoginDetails
+ Reddit: data RateLimitInfo
+ Reddit: data RateLimits
+ Reddit: data RedditF m a
+ Reddit: failWith :: Monad m => APIError RedditError -> RedditT m a
+ Reddit: headersToRateLimitInfo :: ResponseHeaders -> UTCTime -> Maybe RateLimitInfo
+ Reddit: loginBaseURL :: Text
+ Reddit: mainBaseURL :: Text
+ Reddit: mkClientParamsHeader :: ClientParams -> (HeaderName, ByteString)
+ Reddit: nest :: Monad m => RedditT m a -> RedditT m (Either (APIError RedditError) a)
+ Reddit: newtype Modhash
+ Reddit: newtype POSTWrapped a
+ Reddit: newtype RedditT m a
+ Reddit: receiveRoute :: (Receivable a, Monad m) => Route -> RedditT m a
+ Reddit: runRoute :: (FromJSON a, Monad m) => Route -> RedditT m a
+ Reddit: type Reddit a = RedditT IO a
+ Reddit: type ShouldRateLimit = Bool
+ Reddit: withBaseURL :: Monad m => Text -> RedditT m a -> RedditT m a
+ Reddit: withHeaders :: Monad m => ([Header] -> [Header]) -> RedditT m a -> RedditT m a
+ Reddit.Types: FlairTooLong :: RedditError
+ Reddit.Types.Error: FlairTooLong :: RedditError
+ Reddit.Types.Listing: instance (GHC.Classes.Eq t, GHC.Classes.Eq a) => GHC.Classes.Eq (Reddit.Types.Listing.Listing t a)
+ Reddit.Types.Listing: instance (GHC.Read.Read t, GHC.Read.Read a) => GHC.Read.Read (Reddit.Types.Listing.Listing t a)
+ Reddit.Types.Listing: instance (GHC.Show.Show t, GHC.Show.Show a) => GHC.Show.Show (Reddit.Types.Listing.Listing t a)
+ Reddit.Types.Listing: instance GHC.Classes.Ord t => GHC.Base.Semigroup (Reddit.Types.Listing.Listing t a)
+ Reddit.Types.Reddit: ClientParams :: Text -> Text -> ClientParams
+ Reddit.Types.Reddit: [WithHeaders] :: ([Header] -> [Header]) -> RedditT m b -> (b -> a) -> RedditF m a
+ Reddit.Types.Reddit: data ClientParams
+ Reddit.Types.Reddit: instance GHC.Classes.Eq Reddit.Types.Reddit.ClientParams
+ Reddit.Types.Reddit: instance GHC.Classes.Ord Reddit.Types.Reddit.ClientParams
+ Reddit.Types.Reddit: instance GHC.Read.Read Reddit.Types.Reddit.ClientParams
+ Reddit.Types.Reddit: instance GHC.Show.Show Reddit.Types.Reddit.ClientParams
+ Reddit.Types.Reddit: mkClientParamsHeader :: ClientParams -> (HeaderName, ByteString)
+ Reddit.Types.Reddit: withHeaders :: Monad m => ([Header] -> [Header]) -> RedditT m a -> RedditT m a
- Reddit: Credentials :: Text -> Text -> LoginMethod
+ Reddit: Credentials :: Text -> Text -> ClientParams -> LoginMethod
- Reddit: data APIError a :: * -> *
+ Reddit: data APIError a
- Reddit: runReddit :: MonadIO m => Text -> Text -> RedditT m a -> m (Either (APIError RedditError) a)
+ Reddit: runReddit :: MonadIO m => Text -> Text -> ClientParams -> RedditT m a -> m (Either (APIError RedditError) a)
- Reddit.Login: login :: Monad m => Text -> Text -> RedditT m LoginDetails
+ Reddit.Login: login :: Monad m => Text -> Text -> ClientParams -> RedditT m LoginDetails
- Reddit.Types.Flair: FList :: [Flair] -> (Maybe UserID) -> (Maybe UserID) -> FList
+ Reddit.Types.Flair: FList :: [Flair] -> Maybe UserID -> Maybe UserID -> FList
- Reddit.Types.Reddit: RedditT :: (FreeT (RedditF m) m a) -> RedditT m a
+ Reddit.Types.Reddit: RedditT :: FreeT (RedditF m) m a -> RedditT m a

Files

README.md view
@@ -1,9 +1,10 @@-reddit for haskell [![Build Status](https://travis-ci.org/intolerable/reddit.svg?branch=master)](https://travis-ci.org/intolerable/reddit)+reddit for haskell [![CircleCI](https://circleci.com/gh/intolerable/reddit.svg?style=svg)](https://circleci.com/gh/intolerable/reddit) ---  A Haskell library for interacting with the [Reddit API](http://reddit.com/api). -### A couple of examples+A couple of examples+---  Let's get all the posts from the frontpage of Reddit and write a summary of each of them to the console: @@ -67,8 +68,14 @@  `cabal test test-io` -The `test` test suite will run the tests that don't rely on doing any IO, but the `test-io` should be used too to ensure that IO functions do what they're supposed to do. If you want to run the IO suite, add a file `test.cfg` to the `reddit/` directory, containing (one field per line):+The `test` test suite will run the tests that don't rely on doing any IO, but the `test-io` should be used too to ensure that IO functions do what they're supposed to do. If you want to run the IO suite, add a file `test_config.yaml` to the `reddit/` directory, like this: -- a reddit username-- a reddit password-- a subreddit name (the user should be a moderator for the subredit)+```+username: my_reddit_username+password: my_reddit_password+subreddit: test_subreddit # A subreddit that the user has moderator access to+client_id: MY_CLIENT_ID # Your reddit app client ID+client_secret: MY_CLIENT_SECRET # Your reddit app client secret+```++Your app client and secret can be found in [your reddit preferences](https://reddit.com/prefs/apps/)
reddit.cabal view
@@ -1,11 +1,11 @@--- This file has been generated from package.yaml by hpack version 0.20.0.+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.34.4. -- -- see: https://github.com/sol/hpack------ hash: 6d0965ca6f31dbbd95562eb8028178c6142b8dc8d08ddaf93ef114725e63d3fd  name:           reddit-version:        0.2.3.0+version:        0.3.0.0 synopsis:       Library for interfacing with Reddit's API description:    A library for interfacing with Reddit''s API in Haskell. Handles logins, rate-limiting and converting to and from JSON responses. Supports most user-facing Reddit API functions, as well as some moderator endpoints. Check out the readme at <https://github.com/intolerable/reddit>.                 Contributions are welcome.@@ -18,39 +18,17 @@ license:        BSD2 license-file:   LICENSE build-type:     Simple-cabal-version:  >= 1.10- extra-source-files:     README.md     test/data/aboutMe_example.json-    test/data/getUserComments_example.json     test/data/getUserInfo_example.json+    test/data/getUserComments_example.json  source-repository head   type: git   location: https://github.com/intolerable/reddit  library-  hs-source-dirs:-      src-  default-extensions: FlexibleInstances GADTs GeneralizedNewtypeDeriving LambdaCase OverloadedStrings-  ghc-options: -Wall -fno-warn-dodgy-imports-  build-depends:-      aeson >=0.9 && <1.3-    , api-builder >=0.10 && <0.16-    , base >=4.6 && <4.12-    , bytestring ==0.10.*-    , data-default-class >=0.0.1 && <0.2-    , free >=4 && <6-    , http-client >=0.4.30 && <0.6-    , http-client-tls >=0.2 && <0.4-    , http-types >=0.8 && <0.13-    , network ==2.6.*-    , text ==1.*-    , time >=1.5 && <1.9-    , transformers >=0.4 && <0.6-    , unordered-containers >=0.2.5 && <0.3-    , vector >=0.10 && <0.13   exposed-modules:       Reddit       Reddit.Actions@@ -102,15 +80,55 @@       Reddit.Types.Empty       Reddit.Utilities       Paths_reddit+  hs-source-dirs:+      src+  default-extensions:+      FlexibleInstances+      GADTs+      GeneralizedNewtypeDeriving+      LambdaCase+      OverloadedStrings+      OverloadedStrings+  ghc-options: -Wall -Wcompat -fno-warn-dodgy-imports+  build-depends:+      aeson >=2+    , api-builder >=0.10+    , base >=4.6+    , bytestring >=0.10.8+    , data-default-class >=0.0.1+    , free >=4+    , http-client >=0.4.30+    , http-client-tls >=0.2+    , http-types >=0.8+    , network >=2.6+    , text >=1.2+    , time >=1.5+    , transformers >=0.4+    , unordered-containers >=0.2.5+    , vector >=0.10   default-language: Haskell2010  test-suite test   type: exitcode-stdio-1.0   main-is: Spec.hs+  other-modules:+      Reddit.Types.CommentSpec+      Reddit.Types.ListingSpec+      Reddit.Types.PostSpec+      Reddit.Types.SubredditSpec+      Reddit.Types.UserSpec+      Reddit.Types.WikiSpec+      Paths_reddit   hs-source-dirs:       test-  default-extensions: FlexibleInstances GADTs GeneralizedNewtypeDeriving LambdaCase OverloadedStrings-  ghc-options: -Wall+  default-extensions:+      FlexibleInstances+      GADTs+      GeneralizedNewtypeDeriving+      LambdaCase+      OverloadedStrings+      OverloadedStrings+  ghc-options: -Wall -Wcompat   build-depends:       Cabal >=1.16.0     , aeson@@ -122,24 +140,34 @@     , text     , time     , transformers-  other-modules:-      Reddit.Types.CommentSpec-      Reddit.Types.ListingSpec-      Reddit.Types.PostSpec-      Reddit.Types.SubredditSpec-      Reddit.Types.UserSpec-      Reddit.Types.WikiSpec-      Paths_reddit   default-language: Haskell2010  test-suite test-anon   type: exitcode-stdio-1.0   main-is: Spec.hs+  other-modules:+      Reddit.Actions.CaptchaSpec+      Reddit.Actions.CommentSpec+      Reddit.Actions.FlairSpec+      Reddit.Actions.MessageSpec+      Reddit.Actions.PostSpec+      Reddit.Actions.SearchSpec+      Reddit.Actions.SubredditSpec+      Reddit.Actions.UserSpec+      Utils+      Paths_reddit   hs-source-dirs:       test-anon       test-utils-  default-extensions: FlexibleInstances GADTs GeneralizedNewtypeDeriving LambdaCase OverloadedStrings Rank2Types-  ghc-options: -Wall+  default-extensions:+      FlexibleInstances+      GADTs+      GeneralizedNewtypeDeriving+      LambdaCase+      OverloadedStrings+      OverloadedStrings+      Rank2Types+  ghc-options: -Wall -Wcompat   build-depends:       Cabal >=1.16.0     , api-builder@@ -150,33 +178,42 @@     , text     , time     , transformers+  default-language: Haskell2010++test-suite test-io+  type: exitcode-stdio-1.0+  main-is: Spec.hs   other-modules:+      ConfigLoad       Reddit.Actions.CaptchaSpec       Reddit.Actions.CommentSpec       Reddit.Actions.FlairSpec       Reddit.Actions.MessageSpec       Reddit.Actions.PostSpec-      Reddit.Actions.SearchSpec       Reddit.Actions.SubredditSpec       Reddit.Actions.UserSpec       Utils       Paths_reddit-  default-language: Haskell2010--test-suite test-io-  type: exitcode-stdio-1.0-  main-is: Spec.hs   hs-source-dirs:       test-io       test-utils-  default-extensions: FlexibleInstances GADTs GeneralizedNewtypeDeriving LambdaCase OverloadedStrings Rank2Types-  ghc-options: -Wall+  default-extensions:+      FlexibleInstances+      GADTs+      GeneralizedNewtypeDeriving+      LambdaCase+      OverloadedStrings+      OverloadedStrings+      Rank2Types+      RecordWildCards+  ghc-options: -Wall -Wcompat   build-depends:       Cabal >=1.16.0     , api-builder     , base ==4.*     , bytestring     , data-default-class+    , directory     , hspec     , http-client     , http-client-tls@@ -185,15 +222,4 @@     , time     , transformers     , yaml-  other-modules:-      ConfigLoad-      Reddit.Actions.CaptchaSpec-      Reddit.Actions.CommentSpec-      Reddit.Actions.FlairSpec-      Reddit.Actions.MessageSpec-      Reddit.Actions.PostSpec-      Reddit.Actions.SubredditSpec-      Reddit.Actions.UserSpec-      Utils-      Paths_reddit   default-language: Haskell2010
src/Reddit.hs view
@@ -33,7 +33,7 @@ import Data.ByteString.Char8 (ByteString) import Data.Default.Class import Data.Maybe (fromMaybe, isNothing)-import Data.Monoid+import Data.Semigroup ((<>)) import Data.Text (Text) import Data.Text.Encoding (encodeUtf8) import Data.Version@@ -41,6 +41,7 @@ import Network.HTTP.Client import Network.HTTP.Client.TLS import Network.HTTP.Types+import Prelude hiding ((<>)) import qualified Data.ByteString.Char8 as BS  import qualified Paths_reddit@@ -79,7 +80,7 @@ -- | Should we log in to Reddit? If so, should we use a stored set of credentials --   or get a new fresh set? data LoginMethod = Anonymous -- ^ Don't login, instead use an anonymous account-                 | Credentials Text Text -- ^ Login using the specified username and password+                 | Credentials Text Text ClientParams-- ^ Login using the specified username and password                  | StoredDetails LoginDetails -- ^                  --   Login using a stored set of credentials. Usually the best way to get                  --   these is to do @'runRedditAnon' $ 'login' user pass@.@@ -91,8 +92,8 @@ --   for 'RedditOptions': rate limiting enabled, default manager, login via username and password, and --   the default user-agent. You should change the user agent if you're making anything more complex than --   a basic script, since Reddit's API policy says that you should have a uniquely identifiable user agent.-runReddit :: MonadIO m => Text -> Text -> RedditT m a -> m (Either (APIError RedditError) a)-runReddit user pass = runRedditWith def { loginMethod = Credentials user pass }+runReddit :: MonadIO m => Text -> Text -> ClientParams -> RedditT m a -> m (Either (APIError RedditError) a)+runReddit user pass cp = runRedditWith def { loginMethod = Credentials user pass cp }  -- | Run a 'Reddit' action (or a 'RedditT' transformer action). This uses the default logged-out settings, so --   you won't be able to do anything that requires authentication (like checking messages or making a post).@@ -119,7 +120,7 @@   loginCreds <- case lm of     Anonymous -> return $ Right Nothing     StoredDetails ld -> return $ Right $ Just ld-    Credentials user pass -> liftM (fmap Just) $ interpretIO (RedditState loginBaseURL rl manager [] Nothing) $ login user pass+    Credentials user pass cp -> liftM (fmap Just) $ interpretIO (RedditState loginBaseURL rl manager [] Nothing) $ login user pass cp   case loginCreds of     Left (err, _) -> return $ Left (err, Just reddit)     Right lds ->@@ -136,6 +137,12 @@           return $ Left (err, Just $ resume >>= RedditT . n)         Left (err, Nothing) -> return $ Left (err, Nothing)         Right res -> interpretIO rstate $ RedditT $ n res+    Free (WithHeaders hf x n) -> do+      interpretIO (rstate { extraHeaders = hf (extraHeaders rstate)}) x >>= \case+        Left (err, Just resume) ->+          return $ Left (err, Just $ resume >>= RedditT . n)+        Left (err, Nothing) -> return $ Left (err, Nothing)+        Right res -> interpretIO rstate $ RedditT $ n res     Free (FailWith x) -> return $ Left (x, Nothing)     Free (Nest x n) ->       interpretIO rstate $ RedditT $ wrap $ NestResuming x (n . dropResume)@@ -179,7 +186,7 @@   RedditState { currentBaseURL :: Text               , rateLimit :: Bool               , connMgr :: Manager-              , _extraHeaders :: [Header]+              , extraHeaders :: [Header]               , _creds :: Maybe LoginDetails }  customUAWarning :: MonadIO m => m ()
src/Reddit/Actions/User.hs view
@@ -85,5 +85,5 @@ setUserFlair :: Monad m => SubredditName -> Username -> Text -> Text -> RedditT m () setUserFlair r u txt cls =   if Text.length txt > 64-    then fail "Flair text too long!"+    then failWith $ APIError FlairTooLong     else nothing $ runRoute $ Route.setUserFlair r u txt cls
src/Reddit/Login.hs view
@@ -13,12 +13,13 @@                              , "passwd" =. pass ]                              "POST" -getLoginDetails :: Monad m => Text -> Text -> RedditT m LoginDetails-getLoginDetails user pass = receiveRoute $ loginRoute user pass+getLoginDetails :: Monad m => Text -> Text -> ClientParams -> RedditT m LoginDetails+getLoginDetails user pass cp = withHeaders (mkClientParamsHeader cp :) $ receiveRoute $ loginRoute user pass  -- | Make a login request with the given username and password. login :: Monad m       => Text -- ^ Username to login with       -> Text -- ^ Password to login with+      -> ClientParams       -> RedditT m LoginDetails login = getLoginDetails
src/Reddit/Types/Empty.hs view
@@ -5,7 +5,7 @@ import Data.Aeson.Types import Data.Monoid import Prelude-import qualified Data.HashMap.Strict as Hash+import qualified Data.Aeson.KeyMap as KeyMap  -- | More specific @void@ for forcing a @Empty@ @FromJSON@ instance nothing :: Monad m => m Empty -> m ()@@ -16,7 +16,7 @@  instance FromJSON Empty where   parseJSON (Object o) =-    if Hash.null o+    if KeyMap.null o       then return Empty       else do         errs <- (o .: "json") >>= (.: "errors") :: Parser [Value]
src/Reddit/Types/Error.hs view
@@ -25,6 +25,7 @@                  | LinkDeleted                  | BadSubredditName                  | TooManyRequests+                 | FlairTooLong                  deriving (Show, Eq)  instance FromJSON RedditError where
src/Reddit/Types/Listing.hs view
@@ -5,7 +5,7 @@ import Control.Applicative import Data.Aeson import Data.Traversable-import Data.Monoid+import Data.Semigroup import Network.API.Builder.Query import Prelude @@ -32,9 +32,12 @@ instance Functor (Listing t) where   fmap f (Listing b a x) = Listing b a (fmap f x) -instance Ord t => Monoid (Listing t a) where-  mappend (Listing a b cs) (Listing d e fs) =+instance Ord t => Semigroup (Listing t a) where+  Listing a b cs <> Listing d e fs =     Listing (max a d) (min b e) (cs <> fs)++instance Ord t => Monoid (Listing t a) where+  mappend = (<>)   mempty = Listing Nothing Nothing []  instance (FromJSON t, FromJSON a) => FromJSON (Listing t a) where
src/Reddit/Types/Reddit.hs view
@@ -7,6 +7,7 @@   , nest   , failWith   , withBaseURL+  , withHeaders   , Modhash(..)   , LoginDetails(..)   , POSTWrapped(..)@@ -14,9 +15,12 @@   , RateLimits(RateLimits, should, info)   , RateLimitInfo(..)   , headersToRateLimitInfo+  , ClientParams(..)+  , mkClientParamsHeader   , mainBaseURL   , loginBaseURL-  , addAPIType ) where+  , addAPIType+  ) where  import Reddit.Types.Error @@ -35,6 +39,7 @@ import Prelude import Text.Read (readMaybe) import qualified Data.ByteString.Char8 as BS+import qualified Data.Text.Encoding as Text  type Reddit a = RedditT IO a @@ -45,6 +50,7 @@   ReceiveRoute :: Receivable b => Route -> (b -> a) -> RedditF m a   RunRoute :: FromJSON b => Route -> (b -> a) -> RedditF m a   WithBaseURL :: Text -> RedditT m b -> (b -> a) -> RedditF m a+  WithHeaders :: ([Header] -> [Header]) -> RedditT m b -> (b -> a) -> RedditF m a  instance Functor (RedditF m) where   fmap _ (FailWith x) = FailWith x@@ -53,6 +59,7 @@   fmap f (ReceiveRoute r x) = ReceiveRoute r (fmap f x)   fmap f (RunRoute r x) = RunRoute r (fmap f x)   fmap f (WithBaseURL u a x) = WithBaseURL u a (fmap f x)+  fmap f (WithHeaders hf a x) = WithHeaders hf a (fmap f x)  newtype RedditT m a = RedditT (FreeT (RedditF m) m a)   deriving (Functor, Applicative, Monad)@@ -75,6 +82,9 @@ withBaseURL :: Monad m => Text -> RedditT m a -> RedditT m a withBaseURL u f = RedditT $ liftF $ WithBaseURL u f id +withHeaders :: Monad m => ([Header] -> [Header]) -> RedditT m a -> RedditT m a+withHeaders u f = RedditT $ liftF $ WithHeaders u f id+ failWith :: Monad m => APIError RedditError -> RedditT m a failWith = RedditT . liftF . FailWith @@ -87,7 +97,7 @@   parseJSON _ = mempty  data LoginDetails = LoginDetails Modhash CookieJar-  deriving (Show, Eq)+  deriving (Show)  instance Receivable LoginDetails where   receive x = do@@ -120,6 +130,12 @@         rlResetTime' = fmap (\s -> addUTCTime (fromIntegral s) now) rlResetTime         extract s = lookup s hs >>= readMaybe . BS.unpack         trimap f (a, b, c) = (f a, f b, f c)++data ClientParams = ClientParams Text Text+  deriving (Show, Read, Eq, Ord)++mkClientParamsHeader :: ClientParams -> (HeaderName, BS.ByteString)+mkClientParamsHeader (ClientParams i s) = ("Authorization", Text.encodeUtf8 $ i <> ":" <> s)  addAPIType :: Route -> Route addAPIType (Route fs ps m) = Route fs ("api_type" =. ("json" :: Text) : ps) m
test-anon/Reddit/Actions/CommentSpec.hs view
@@ -29,11 +29,11 @@       Left _ -> expectationFailure "something failed"    it "should fail if we try to get an invalid comment" $ do-    res <- runAnon $ getCommentInfo (CommentID "garbage")+    res <- runAnon $ getCommentInfo (CommentID "!!!!!!!")     res `shouldSatisfy` isLeft    it "shouldn't be able to get a list of comment IDs where some are invalid" $ do-    res <- runAnon $ getCommentsInfo [CommentID "c60o0iw", CommentID "garbage"]+    res <- runAnon $ getCommentsInfo [CommentID "c60o0iw", CommentID "!!!!!!!"]     res `shouldSatisfy` isLeft    it "should be able to get mass comment IDs" $ do
test-io/ConfigLoad.hs view
@@ -1,49 +1,124 @@ module ConfigLoad where +import Control.Applicative+import Data.List+import Data.Semigroup (Semigroup(..)) import Data.Text (Text) import Data.Yaml import Network.HTTP.Client import Network.HTTP.Client.TLS import Reddit import Reddit.Login+import System.Directory+import System.Environment import System.Exit import Utils+import qualified Data.Text as Text  newtype RunReddit = RunReddit   { run :: forall a. Reddit a -> IO (Either (APIError RedditError) a) } -newtype TestConfig = TestConfig (Maybe Config)+data Opts =+  Opts { optsUsername :: Maybe Text+       , optsPassword :: Maybe Text+       , optsSubreddit :: Maybe Text+       , optsClientId :: Maybe Text+       , optsClientSecret :: Maybe Text+       }   deriving (Show, Eq, Ord) -instance FromJSON TestConfig where-  parseJSON = withObject "TestConfig" $ \ o ->-    TestConfig <$> o .:? "config"+instance Semigroup Opts where+  Opts u1 p1 s1 ci1 cs1 <> Opts u2 p2 s2 ci2 cs2 =+    Opts (u2 <|> u1)+         (p2 <|> p1)+         (s2 <|> s1)+         (ci2 <|> ci1)+         (cs2 <|> cs1) +instance Monoid Opts where+  mappend = (<>)+  mempty = Opts mempty mempty mempty mempty mempty++instance FromJSON Opts where+  parseJSON = withObject "Opts" $ \ o ->+    Opts <$> o .:? "username"+         <*> o .:? "password"+         <*> o .:? "subreddit"+         <*> o .:? "client_id"+         <*> o .:? "client_secret"++getEnvOpts :: IO Opts+getEnvOpts =+  Opts <$> lookupEnv' "REDDIT_USERNAME"+       <*> lookupEnv' "REDDIT_PASSWORD"+       <*> lookupEnv' "REDDIT_SUBREDDIT"+       <*> lookupEnv' "REDDIT_CLIENT_ID"+       <*> lookupEnv' "REDDIT_CLIENT_SECRET"+    where+      lookupEnv' x = fmap (fmap Text.pack) (lookupEnv x)+ data Config =-  Config { tcUsername :: Text-         , tcPassword :: Text-         , tcSubreddit :: Text+  Config { cfgUsername :: Text+         , cfgPassword :: Text+         , cfgSubreddit :: Text+         , cfgClientId :: Text+         , cfgClientSecret :: Text          }   deriving (Show, Eq, Ord) -instance FromJSON Config where-  parseJSON = withObject "Config" $ \ o ->-    Config <$> o .: "username"-           <*> o .: "password"-           <*> o .: "subreddit"+newtype Validation e a = Validation (Either e a)+  deriving (Show, Read, Eq, Ord, Functor) +instance Semigroup e => Applicative (Validation e) where+  pure = Validation . Right++  Validation a <*> Validation b =+    Validation $ case (a, b) of+      (Left ea, Left eb) -> Left (ea <> eb)+      (Left ea, Right _) -> Left ea+      (Right _, Left eb) -> Left eb+      (Right f, Right x) -> Right (f x)++optionsToConfig :: Opts -> Validation [String] Config+optionsToConfig Opts{..} =+  Config <$> get "optsUsername was missing" optsUsername+         <*> get "optsPassword was missing" optsPassword+         <*> get "optsSubreddit was missing" optsSubreddit+         <*> get "optsClientId was missing" optsClientId+         <*> get "optsClientSecret was missing" optsClientSecret+    where+      get :: String -> Maybe a -> Validation [String] a+      get _ (Just x) = pure x+      get msg Nothing = Validation (Left [msg])+ loadConfig :: IO (RunReddit, Username, SubredditName) loadConfig = do-  decodeFileEither "test_config.yaml" >>= \case-    Left err -> do-      print err-      exitFailure-    Right (TestConfig Nothing) ->-      putStrLn "Warning: missing config section, skipping authorized tests"-      exitSuccess-    Right (TestConfig (Just (Config user pass sub))) -> do+  jsonOpts <- do+    let configFilePath = "test_config.yaml"+    doesFileExist configFilePath >>= \case+      False -> do+        putStrLn "Warning: missing config, skipping authorized tests"+        pure mempty+      True ->+        decodeFileEither "test_config.yaml" >>= \case+          Left err -> do+            print err+            exitFailure+          Right opts -> pure opts+  envOpts <- getEnvOpts++  case optionsToConfig (jsonOpts <> envOpts) of+    Validation (Left errs) ->+      error $+        intercalate "\n" $+          [ "Options were missing, got:"+          , "  " <> show (jsonOpts <> envOpts)+          , "Errors:"+          , intercalate "\n" $ map ("  " <>) errs+          ]+    Validation (Right (Config user pass sub ci cs)) -> do       manager <- newManager tlsManagerSettings-      res <- runAnon $ login user pass+      res <- runAnon $ login user pass (ClientParams ci cs)       case res of         Left err -> do           print err
test-io/Reddit/Actions/CommentSpec.hs view
@@ -32,11 +32,11 @@       Left _ -> expectationFailure "something failed"    it "should fail if we try to get an invalid comment" $ do-    res <- run reddit $ getCommentInfo (CommentID "garbage")+    res <- run reddit $ getCommentInfo (CommentID "!!!!!!!")     res `shouldSatisfy` isLeft    it "shouldn't be able to get a list of comment IDs where some are invalid" $ do-    res <- run reddit $ getCommentsInfo [CommentID "c60o0iw", CommentID "garbage"]+    res <- run reddit $ getCommentsInfo [CommentID "c60o0iw", CommentID "!!!!!!!"]     res `shouldSatisfy` isLeft    it "should be able to get mass comment IDs" $ do