libjenkins 0.2.0.0 → 0.3.0.0
raw patch · 23 files changed
+1434/−877 lines, 23 filesdep +hspecdep +http-clientdep +libjenkinsdep −data-defaultdep ~basedep ~bytestringdep ~transformers
Dependencies added: hspec, http-client, libjenkins
Dependencies removed: data-default
Dependency ranges changed: base, bytestring, transformers
Files
- CHANGELOG.md +15/−2
- bench/Concurrency.hs +60/−0
- bench/README.md +14/−0
- example/discover.hs +29/−0
- example/grep-jobs.hs +41/−0
- example/rename-jobs.hs +72/−0
- example/running-jobs-count.hs +30/−0
- examples/Discover.hs +0/−29
- examples/Jobs.hs +0/−73
- examples/Rename.hs +0/−81
- libjenkins.cabal +32/−15
- src/Jenkins/REST.hs +0/−201
- src/Jenkins/REST/Internal.hs +0/−114
- src/Jenkins/REST/Lens.hs +0/−56
- src/Jenkins/REST/Method.hs +0/−278
- src/Jenkins/Rest.hs +125/−0
- src/Jenkins/Rest/Internal.hs +228/−0
- src/Jenkins/Rest/Method.hs +302/−0
- src/Network/HTTP/Conduit/Lens.hs +359/−0
- test/Doctest.hs +28/−0
- test/Jenkins/RestSpec.hs +98/−0
- test/Spec.hs +1/−0
- tests/Doctests.hs +0/−28
CHANGELOG.md view
@@ -1,9 +1,22 @@+0.3.0.0+=======++ * `restart` does not send requests to `$jenkins_url/restart` anymore. Instead, it calls+ `$jenkins_url/safe-restart` which waits running jobs to complete. New `forceRestart` function+ does now what `restart` did before++ * Massive refactoring++ * More optics in `Network.HTTP.Conduit.Lens`++ * Add `overallLoad` and `computer` REST API methods shortcuts+ 0.2.0.0 ======= - * Move onto http-conduit 2.0 API+ * Move onto http-conduit 2.0 API. 0.1.0.0 ======= -Initial release. REST and Discovery APIs support.+ * Initial release. REST and Discovery APIs support.
+ bench/Concurrency.hs view
@@ -0,0 +1,60 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-name-shadowing #-}+-- | Concurrency benchmark+--+-- The benchmark does the folllowing:+--+-- * it asks Jenkins instance for accessible jobs+--+-- * it sends queries to get jobs descriptions+--+-- * it prints all descriptions+--+-- The benchmark can be run sequentially or concurrently+--+-- See @bench/README.md@ for usage instructions+module Main (main) where++import Control.Lens -- lens+import Control.Lens.Aeson -- lens-aeson+import qualified Data.ByteString.Char8 as B -- bytestring+import Data.Text (Text) -- text+import Jenkins.Rest -- libjenkins+import System.Environment (getArgs) -- base+import System.Exit (exitFailure) -- base+import System.IO (hPutStrLn, stderr) -- base+++type Aggregate a b = (a -> Jenkins b) -> [a] -> Jenkins [b]++main :: IO ()+main = do+ m:host:port:user:pass:_ <- getArgs+ ds <- descriptions (aggregate m) $+ ConnectInfo host (read port) (B.pack user) (B.pack pass)+ case ds of+ Result ds -> mapM_ print ds+ Disconnect -> die "disconnect!"+ Error e -> die (show e)+ where+ die message = do+ hPutStrLn stderr message+ exitFailure++ aggregate :: String -> Aggregate a b+ aggregate "concurrent" = (concurrentlys .) . map+ aggregate "sequential" = mapM+ aggregate _ = error "Unknown mode"++descriptions+ :: Aggregate Text (Maybe Text)+ -> ConnectInfo+ -> IO (Result HttpException [Maybe Text])+descriptions aggregate settings = runJenkins settings $ do+ res <- get (json -?- "tree" -=- "jobs[name]")+ aggregate describe (res ^.. key "jobs".values.key "name"._String)++describe :: Text -> Jenkins (Maybe Text)+describe name = do+ desc <- get (job name `as` json -?- "tree" -=- "description")+ return (desc ^? key "description"._String)
+ bench/README.md view
@@ -0,0 +1,14 @@+# Benchmarks++## `Concurrent.hs`++ ./bench/Concurrency (concurrent|sequential) HOST PORT USERNAME PASSWORD++Measures the concurrency impact on time API queries take.++Concurrency is the most useful if you:++ * Do not use any kind of "proxy" servers (apache2, nginx, etc) and _especially_ https+ * Try to run queries from the nearest machine possible: network latency matters++Under these conditions, expect to get ~2x speedup from using `concurrent` mode on Jenkins server with ~500 jobs.
+ example/discover.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Discover Jenkins server on the network+module Main where++import Data.Monoid ((<>), mempty) -- base+import Data.Text (Text) -- text+import qualified Data.Text as T -- text+import qualified Data.Text.IO as T -- text+import Jenkins.Discover -- libjenkins+import System.Exit (exitFailure) -- base+++main :: IO ()+main = do+ -- send discover broadcast with 100ms timeout+ discoveries <- discover 100000+ case discoveries of+ -- no Jenkins responded+ [] -> exitFailure+ -- pretty print responses+ _ -> mapM_ (T.putStrLn . pretty) discoveries++-- | Pretty print Jenkins discovery responses+pretty :: Discover -> Text+pretty x = T.unwords $+ ["Jenkins", version x] ++ maybe mempty (return . between "(" ")") (server_id x) ++ ["at", url x]+ where+ between l r t = l <> t <> r+
+ example/grep-jobs.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+-- | Grep Jenkins instance jobs with Perl regex+module Main (main) where++import Control.Lens -- lens+import Control.Exception.Lens -- lens+import Control.Lens.Aeson (key, values, _String) -- lens-aeson+import Control.Monad -- base+import Data.String (fromString) -- base+import Data.Text (Text) -- text+import qualified Data.Text as Text -- text+import qualified Data.Text.IO as Text -- text+import Jenkins.Rest -- libjenkins+import System.Environment (getArgs) -- base+import System.Exit.Lens -- lens+import System.Process (readProcessWithExitCode) -- process+import Text.Printf (printf) -- base+++main :: IO ()+main = do+ user:pswd:host:port:regex:_ <- getArgs+ jobs <- grep regex (ConnectInfo host (read port) (fromString user) (fromString pswd))+ case jobs of+ [] -> throwingM _ExitFailure 1+ _ -> mapM_ Text.putStrLn jobs++-- | Filter matching job names+grep :: String -> ConnectInfo -> IO [Text]+grep regex conn = do+ jobs <- runJenkins conn $+ get (json -?- "tree" -=- "jobs[name]") <&> \res -> res ^.. key "jobs".values.key "name"._String+ filterM (match regex) (jobs ^.. _Result.folded)++-- | Match job name again Perl regex+match :: String -> Text -> IO Bool+match regex name = do+ (exitcode, _, _) <-+ readProcessWithExitCode "perl" ["-n", "-e", printf "/%s/ or die" regex] (Text.unpack name)+ return $ has _ExitSuccess exitcode
+ example/rename-jobs.hs view
@@ -0,0 +1,72 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+-- | Rename jobs matching supplied pattern+module Main where++import Control.Lens -- lens+import Control.Lens.Aeson -- lens-aeson+import Control.Monad (when) -- base+import qualified Data.ByteString.Char8 as B -- bytestring+import Data.Foldable (for_) -- base+import Data.Function (fix) -- base+import Data.Text (Text) -- text+import qualified Data.Text as T -- text+import qualified Data.Text.IO as T -- text+import Jenkins.Rest -- libjenkins+import System.Environment (getArgs) -- base+import System.Exit (exitFailure) -- base+import System.IO (hPutStrLn, stderr) -- base++{-# ANN module ("HLint: ignore Use camelCase" :: String) #-}+++-- | Program options+data Options = Options+ { settings :: ConnectInfo+ , old :: Text+ , new :: Text+ }+++main :: IO ()+main = do+ -- more useful help on error+ host:port:user:pass:o:n:_ <- getArgs+ let opts = Options (ConnectInfo host (read port) (B.pack user) (B.pack pass)) (T.pack o) (T.pack n)+ res <- rename opts+ case res of+ Result _ -> T.putStrLn "Done."+ -- disconnected for some reason+ Disconnect -> die "disconnect!"+ -- something bad happened, show it!+ Error e -> die (show e)+ where+ die message = do+ hPutStrLn stderr message+ exitFailure++-- | Prompt to rename all jobs matching pattern+rename :: Options -> IO (Result HttpException ())+rename (Options { settings, old, new }) = runJenkins settings $ do+ -- get jobs names from jenkins "root" API+ res <- get (json -?- "tree" -=- "jobs[name]")+ let jobs = res ^.. key "jobs".values.key "name"._String+ for_ jobs rename_job+ where+ rename_job :: Text -> Jenkins ()+ rename_job name = when (old `T.isInfixOf` name) $ do+ let name' = (old `T.replace` new) name+ -- prompt for every matching job+ yes <- prompt $ T.unwords ["Rename", name, "to", name', "? [y/n]"]+ when yes $+ -- if user agrees then voodoo comes+ post_ (job name -/- "doRename" -?- "newName" -=- name')++ -- asks user until she enters 'y' or 'n'+ prompt message = io . fix $ \loop -> do+ T.putStrLn message+ res <- T.getLine+ case T.toUpper res of+ "Y" -> return True+ "N" -> return False+ _ -> loop
+ example/running-jobs-count.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Count running jobs on Jenkins instance+--+-- Usage: count-running-jobs HOST PORT USER APITOKEN+--+-- Uses an awful hack, that is inspecting the job ball color. Jenkins sets+-- it to "blue_anime", meaning "animated blue ball" if job is running+module Main (main) where++import Control.Lens -- lens+import Control.Lens.Aeson -- lens-aeson+import Data.ByteString.Lazy (ByteString) -- bytestring+import Data.String (fromString) -- bytestring+import Jenkins.Rest -- libjenkins+import System.Environment (getArgs) -- base+import Text.Printf (printf) -- base+++main :: IO ()+main = do+ host:port:user:apiToken:_ <- getArgs+ let creds = ConnectInfo host (read port) (fromString user) (fromString apiToken)+ jobs <- runJenkins creds getJobs+ printf "Running jobs count: %d\n" (lengthOf (_Result.running) jobs)++getJobs :: Jenkins ByteString+getJobs = get (json -?- "tree" -=- "jobs[color]")++running :: Fold ByteString ()+running = key "jobs".values.key "color"._String.only "blue_anime"
− examples/Discover.hs
@@ -1,29 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Discover Jenkins server on the network-module Main where--import Data.Monoid ((<>), mempty) -- base-import Data.Text (Text) -- text-import qualified Data.Text as T -- text-import qualified Data.Text.IO as T -- text-import Jenkins.Discover -- libjenkins-import System.Exit (exitFailure) -- base---main :: IO ()-main = do- -- send discover broadcast with 100ms timeout- discoveries <- discover 100000- case discoveries of- -- no Jenkins responded- [] -> exitFailure- -- pretty print responses- _ -> mapM_ (T.putStrLn . pretty) discoveries---- | Pretty print Jenkins discovery responses-pretty :: Discover -> Text-pretty x = T.unwords $- ["Jenkins", version x] ++ maybe mempty (return . between "(" ")") (server_id x) ++ ["at", url x]- where- between l r t = l <> t <> r-
− examples/Jobs.hs
@@ -1,73 +0,0 @@-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedStrings #-}--- | Show jobs status-module Main where--import Control.Lens -- lens-import Control.Lens.Aeson -- lens-aeson-import qualified Data.ByteString.Char8 as B -- bytestring-import Data.Text (Text) -- text-import qualified Data.Text.IO as T -- text-import Jenkins.REST hiding (render) -- libjenkins-import Options.Applicative -- optparse-applicative-import System.Console.ANSI -- ansi-terminal-import System.Exit (exitFailure) -- base--{-# ANN module ("HLint: Use camelCase" :: String) #-}----- | Job name and status-data Job = Job- { name :: Text- , color :: Color- }---main :: IO ()-main = do- -- more useful help on error- opts <- customExecParser (prefs showHelpOnError) options- -- get all jobs (colored)- jobs <- colorized_jobs opts- case jobs of- -- render them- Right js -> mapM_ render js- -- something bad happened, show it!- Left e -> do- print e- exitFailure---- get jobs names from jenkins "root" API-colorized_jobs :: Settings -> IO (Either Disconnect [Job])-colorized_jobs settings = runJenkins settings $ do- res <- get (json -?- "tree" -=- "jobs[name]")- let jobs = res ^.. key "jobs"._Array.each.key "name"._String- concurrentlys (map colorize jobs)---- get jobs colors as they appear on web UI-colorize :: Text -> Jenkins Job-colorize name = do- res <- get (job name `as` json -?- "tree" -=- "color")- return . Job name $ case res ^? key "color" of- -- but sane- Just "red" -> Red- Just "blue" -> Green- _ -> Yellow---- render colored job (assumes ANSI terminal)-render :: Job -> IO ()-render Job { name, color } = do- setSGR [SetColor Foreground Dull color]- T.putStrLn name- setSGR []----- | Quite a trivial jenkins settings parser-options :: ParserInfo Settings-options = info (helper <*> parser) fullDesc- where- parser = Settings- <$> (Host <$> strOption (long "host"))- <*> (Port <$> option (long "port"))- <*> (User . B.pack <$> strOption (long "user"))- <*> (APIToken . B.pack <$> strOption (long "token" <> long "password"))
− examples/Rename.hs
@@ -1,81 +0,0 @@-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedStrings #-}--- | Rename jobs matching supplied pattern-module Main where--import Control.Lens -- lens-import Control.Lens.Aeson -- lens-aeson-import Control.Monad (when) -- base-import qualified Data.ByteString.Char8 as B -- bytestring-import Data.Foldable (for_) -- base-import Data.Function (fix) -- base-import Data.Text (Text) -- text-import qualified Data.Text as T -- text-import qualified Data.Text.IO as T -- text-import Jenkins.REST -- libjenkins-import Options.Applicative -- optparse-applicative-import System.Exit (exitFailure) -- base--{-# ANN module ("HLint: Use camelCase" :: String) #-}---main :: IO ()-main = do- -- more useful help on error- opts <- customExecParser (prefs showHelpOnError) options- res <- rename opts- case res of- Right () -> T.putStrLn "Done."- -- something bad happened, show it!- Left e -> do- print e- exitFailure---- | Prompt to rename all jobs matching pattern-rename :: Options -> IO (Either Disconnect ())-rename (Options { settings, old, new }) = runJenkins settings $ do- -- get jobs names from jenkins "root" API- res <- get (json -?- "tree" -=- "jobs[name]")- let jobs = res ^.. key "jobs"._Array.each.key "name"._String- for_ jobs rename_job- where- rename_job :: Text -> Jenkins ()- rename_job name = when (old `T.isInfixOf` name) $ do- let name' = (old `T.replace` new) name- -- prompt for every matching job- yes <- prompt $ T.unwords ["Rename", name, "to", name', "? [y/n]"]- when yes $- -- if user agrees then voodoo comes- post_ (job name -/- "doRename" -?- "newName" -=- name')-- -- asks user until she enters 'y' or 'n'- prompt message = io . fix $ \loop -> do- T.putStrLn message- res <- T.getLine- case T.toUpper res of- "Y" -> return True- "N" -> return False- _ -> loop----- | Program options-data Options = Options- { settings :: Settings- , old :: Text- , new :: Text- }---- | Quite a trivial options parser-options :: ParserInfo Options-options = info (helper <*> parser) fullDesc- where- parser = Options- <$> parse_settings- <*> nullOption (reader (return . T.pack) <> long "old")- <*> nullOption (reader (return . T.pack) <> long "new")-- parse_settings = Settings- <$> (Host <$> strOption (long "host"))- <*> (Port <$> option (long "port"))- <*> (User . B.pack <$> strOption (long "user"))- <*> (APIToken . B.pack <$> strOption (long "token" <> long "password"))
libjenkins.cabal view
@@ -1,5 +1,5 @@ name: libjenkins-version: 0.2.0.0+version: 0.3.0.0 synopsis: Jenkins API interface description: Jenkins API interface. It supports REST and Discovery APIs license: BSD3@@ -11,9 +11,12 @@ extra-source-files: README.md CHANGELOG.md- examples/Jobs.hs- examples/Rename.hs- examples/Discover.hs+ bench/Concurrency.hs+ bench/README.md+ example/grep-jobs.hs+ example/rename-jobs.hs+ example/discover.hs+ example/running-jobs-count.hs cabal-version: >= 1.10 library@@ -21,17 +24,17 @@ hs-source-dirs: src exposed-modules: Jenkins.Discover- Jenkins.REST- Jenkins.REST.Internal- Jenkins.REST.Lens- Jenkins.REST.Method+ Jenkins.Rest+ Jenkins.Rest.Internal+ Jenkins.Rest.Method+ Network.HTTP.Conduit.Lens build-depends: async >= 2.0- , base >= 4.5 && < 5+ , base >= 4.6 && < 5 , bytestring >= 0.9 , conduit >= 1.0- , data-default >= 0.5 , free >= 4.1+ , http-client >= 0.2.0.2 , http-conduit >= 2.0 && < 2.1 , http-types >= 0.8 , lens >= 3.9@@ -42,17 +45,31 @@ , xml-conduit >= 1.1 , xml-lens >= 0.1 -test-suite doctests+test-suite doctest default-language: Haskell2010 type: exitcode-stdio-1.0 build-depends:- base == 4.*+ base == 4.* , directory , doctest , filepath- hs-source-dirs: tests- main-is: Doctests.hs+ hs-source-dirs: test+ main-is: Doctest.hs +test-suite spec+ default-language: Haskell2010+ type: exitcode-stdio-1.0+ build-depends:+ base == 4.*+ , bytestring+ , hspec+ , libjenkins+ , transformers+ hs-source-dirs: test+ main-is: Spec.hs+ other-modules:+ Jenkins.RestSpec+ source-repository head type: git location: https://github.com/supki/libjenkins@@ -60,4 +77,4 @@ source-repository this type: git location: https://github.com/supki/libjenkins- tag: 0.2.0.0+ tag: 0.3.0.0
− src/Jenkins/REST.hs
@@ -1,201 +0,0 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE TemplateHaskell #-}--- | Jenkins REST API interface-module Jenkins.REST- ( -- * Types- Jenkins, Disconnect(..)- , Settings(..), Host(..), Port(..), User(..), APIToken(..), Password- , Request- -- * Jenkins queries construction- , get, post, post_, concurrently, io, disconnect, with- -- ** Jenkins method construction- , module Jenkins.REST.Method- -- ** Little helpers- , concurrentlys, concurrentlys_, postXML, reload, restart- -- * Jenkins queries execution- , runJenkins, runJenkinsP- -- ** Lenses and prisms- , jenkins_host, jenkins_port, jenkins_user, jenkins_api_token, jenkins_password- , _Host, _Port, _User, _APIToken, _Password- -- * Usable @http-conduit@ 'Request' type API- , module Jenkins.REST.Lens- ) where--import Control.Applicative ((<$))-import Control.Exception (handle)-import Control.Lens-import Control.Monad.IO.Class (MonadIO(..))-import Control.Monad.Trans.Maybe (MaybeT(..))-import Control.Monad.Trans.Reader (ReaderT, runReaderT)-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as BL-import Data.Data (Data, Typeable)-import Data.Default (Default(..))-import Data.Monoid (mempty)-import Data.String (IsString)-import GHC.Generics (Generic)-import Network.HTTP.Conduit (Request, HttpException, withManager, applyBasicAuth, parseUrl)-import Text.XML (Document, renderLBS)--import Jenkins.REST.Internal-import qualified Jenkins.REST.Lens-import Jenkins.REST.Lens as L-import Jenkins.REST.Method--{-# ANN module ("HLint: Use const" :: String) #-}----- | @GET@ query-get :: Method Complete f -> Jenkins BL.ByteString-get m = liftJ $ Get m id-{-# INLINE get #-}---- | @POST@ query (with payload)-post :: (forall f. Method Complete f) -> BL.ByteString -> Jenkins ()-post m body = liftJ $ Post m body (\_ -> ())-{-# INLINE post #-}---- | @POST@ query (without payload)-post_ :: (forall f. Method Complete f) -> Jenkins ()-post_ m = post m mempty-{-# INLINE post_ #-}---- | Do both queries 'concurrently'-concurrently :: Jenkins a -> Jenkins b -> Jenkins (a, b)-concurrently ja jb = liftJ $ Conc ja jb (,)-{-# INLINE concurrently #-}---- | Lift arbitrary 'IO' action-io :: IO a -> Jenkins a-io = liftIO-{-# INLINE io #-}---- | Disconnect from Jenkins------ Following queries won't be executed-disconnect :: Jenkins a-disconnect = liftJ Dcon-{-# INLINE disconnect #-}---- | Make custom local changes to 'Request'-with :: (Request -> Request) -> Jenkins a -> Jenkins a-with f j = liftJ $ With f j id-{-# INLINE with #-}----- * Convenience---- | @POST@ job's @config.xml@ (in @xml-conduit@ format)-postXML :: (forall f. Method Complete f) -> Document -> Jenkins ()-postXML m =- with (requestHeaders <>~ [("Content-Type", "text/xml")]) . post m . renderLBS def-{-# INLINE postXML #-}---- | Do a list of queries 'concurrently'-concurrentlys :: [Jenkins a] -> Jenkins [a]-concurrentlys = foldr go (return [])- where- go x xs = do- (y, ys) <- concurrently x xs- return (y : ys)-{-# INLINE concurrentlys #-}---- | Do a list of queries 'concurrently' ignoring their results-concurrentlys_ :: [Jenkins a] -> Jenkins ()-concurrentlys_ = foldr (\x xs -> () <$ concurrently x xs) (return ())-{-# INLINE concurrentlys_ #-}---- | Reload jenkins configuration from disk-reload :: Jenkins a-reload = do- post_ "reload"- disconnect-{-# INLINE reload #-}---- | Restart jenkins-restart :: Jenkins a-restart = do- post_ "restart"- disconnect-{-# INLINE restart #-}----- | Jenkins settings------ '_jenkins_api_token' might as well be the user's password, Jenkins--- does not make any distinction between these concepts-data Settings = Settings- { _jenkins_host :: Host- , _jenkins_port :: Port- , _jenkins_user :: User- , _jenkins_api_token :: APIToken- } deriving (Show, Eq, Typeable, Data, Generic)--instance Default Settings where- def = Settings- { _jenkins_host = "http://example.com"- , _jenkins_port = 80- , _jenkins_user = "anonymous"- , _jenkins_api_token = "secret"- }---- | Jenkins host address-newtype Host = Host { unHost :: String }- deriving (Show, Eq, Typeable, Data, Generic, IsString)---- | Jenkins port-newtype Port = Port { unPort :: Int }- deriving (Show, Eq, Typeable, Data, Generic, Num)---- | Jenkins user-newtype User = User { unUser :: B.ByteString }- deriving (Show, Eq, Typeable, Data, Generic, IsString)---- | Jenkins user API token-newtype APIToken = APIToken { unAPIToken :: B.ByteString }- deriving (Show, Eq, Typeable, Data, Generic, IsString)---- | Jenkins user password-type Password = APIToken--makeLenses ''Settings--jenkins_password :: Lens' Settings Password-jenkins_password = jenkins_api_token-{-# INLINE jenkins_password #-}--makePrisms ''Host-makePrisms ''Port-makePrisms ''User-makePrisms ''APIToken--_Password :: Prism' Password B.ByteString-_Password = _APIToken-{-# INLINE _Password #-}---data Disconnect =- Disconnect- | JenkinsException HttpException- deriving (Show, Typeable, Generic)---- | Communicate with Jenkins REST API------ Catches 'HttpException's thrown by @http-conduit@, but--- does not catch exceptions thrown by embedded 'IO' actions-runJenkins :: Settings -> Jenkins a -> IO (Either Disconnect a)-runJenkins (Settings (Host h) (Port p) (User u) (APIToken t)) jenk =- handle (return . Left . JenkinsException) $- withManager $ \manager -> do- req <- liftIO $ parseUrl h- let req' = req- & L.port .~ p- & L.responseTimeout .~ Just (20 * 1000000)- res <- runReaderT (runMaybeT (runJenkinsIO manager jenk)) (applyBasicAuth u t req')- return $ maybe (Left Disconnect) Right res
− src/Jenkins/REST/Internal.hs
@@ -1,114 +0,0 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE NoMonoLocalBinds #-}-{-# LANGUAGE NoMonomorphismRestriction #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE TypeFamilies #-}-module Jenkins.REST.Internal where--import Control.Applicative (Applicative(..))-import Control.Concurrent.Async (concurrently)-import Control.Exception (toException)-import Control.Lens-import Control.Monad (join)-import Control.Monad.Free.Church (F, iterM, liftF)-import Control.Monad.IO.Class (MonadIO(..))-import Control.Monad.Trans.Class (lift)-import Control.Monad.Trans.Control (MonadTransControl(..))-import Control.Monad.Trans.Reader (ReaderT, ask, local)-import Control.Monad.Trans.Maybe (MaybeT, mapMaybeT)-import qualified Data.ByteString.Lazy as BL-import Data.Conduit (ResourceT)-import Network.HTTP.Conduit-import Network.HTTP.Types (Status(..))--import Jenkins.REST.Lens as L-import Jenkins.REST.Method----- | Jenkins REST API composable queries-newtype Jenkins a = Jenkins { unJenkins :: F JenkinsF a }- deriving (Functor, Applicative, Monad)--instance MonadIO Jenkins where- liftIO = liftJ . IO- {-# INLINE liftIO #-}---- | 'JenkinsF' terms-data JenkinsF a where- Get :: Method Complete f -> (BL.ByteString -> a) -> JenkinsF a- Post :: (forall f. Method Complete f) -> BL.ByteString -> (BL.ByteString -> a) -> JenkinsF a- Conc :: Jenkins a -> Jenkins b -> (a -> b -> c) -> JenkinsF c- IO :: IO a -> JenkinsF a- With :: (Request -> Request) -> Jenkins b -> (b -> a) -> JenkinsF a- Dcon :: JenkinsF a--instance Functor JenkinsF where- fmap f (Get m g) = Get m (f . g)- fmap f (Post m body g) = Post m body (f . g)- fmap f (Conc m n g) = Conc m n (\a b -> f (g a b))- fmap f (IO a) = IO (fmap f a)- fmap f (With h j g) = With h j (f . g)- fmap _ Dcon = Dcon- {-# INLINE fmap #-}---- | Lift 'JenkinsF' query to the 'Jenkins' query language-liftJ :: JenkinsF a -> Jenkins a-liftJ = Jenkins . liftF-{-# INLINE liftJ #-}---runJenkinsIO- :: Manager- -> Jenkins a- -> MaybeT (ReaderT Request (ResourceT IO)) a-runJenkinsIO manager = runJenkinsP (jenkinsIO manager)-{-# INLINE runJenkinsIO #-}---- | Generic Jenkins REST API queries interpreter------ Particularly useful for testing (with @m ≡ 'Identity'@)-runJenkinsP :: Monad m => (JenkinsF (m a) -> m a) -> Jenkins a -> m a-runJenkinsP go = iterM go . unJenkins-{-# INLINE runJenkinsP #-}--jenkinsIO- :: Manager- -> JenkinsF (MaybeT (ReaderT Request (ResourceT IO)) a)- -> MaybeT (ReaderT Request (ResourceT IO)) a-jenkinsIO manager = go where- go (Get m next) = do- req <- lift ask- let req' = req- & L.path %~ (`slash` render m)- & L.method .~ "GET"- bs <- lift . lift $ httpLbs req' manager- next (responseBody bs)- go (Post m body next) = do- req <- lift ask- let req' = req- & L.path %~ (`slash` render m)- & L.method .~ "POST"- & L.requestBody .~ RequestBodyLBS body- & L.redirectCount .~ 0- & L.checkStatus .~ \s@(Status st _) hs cookie_jar ->- if 200 <= st && st < 400- then Nothing- else Just . toException $ StatusCodeException s hs cookie_jar- res <- lift . lift $ httpLbs req' manager- next (responseBody res)- go (Conc jenka jenkb next) = do- (a, b) <- liftWith $ \run' -> liftWith $ \run'' -> liftWith $ \run''' ->- let run :: Jenkins t -> IO (StT ResourceT (StT (ReaderT Request) (StT MaybeT t)))- run = run''' . run'' . run' . runJenkinsIO manager- in concurrently (run jenka) (run jenkb)- c <- restoreT . restoreT . restoreT $ return a- d <- restoreT . restoreT . restoreT $ return b- next c d- go (IO action) = join (liftIO action)- go (With f jenk next) = do- res <- mapMaybeT (local f) (runJenkinsIO manager jenk)- next res- go Dcon = fail "disconnect"
− src/Jenkins/REST/Lens.hs
@@ -1,56 +0,0 @@--- | Lenses for 'Network.HTTP.Conduit' types-module Jenkins.REST.Lens- ( -- * 'Request' lenses- method, secure- , host, port- , path, queryString, requestBody, requestHeaders- , redirectCount, checkStatus, responseTimeout- ) where--import Control.Applicative ((<$>))-import Control.Exception (SomeException)-import Control.Lens-import Data.ByteString (ByteString)-import qualified Network.HTTP.Conduit as H-import qualified Network.HTTP.Types as H----- | HTTP request method, eg GET, POST.-method :: Lens' H.Request H.Method-method f req = (\m' -> req { H.method = m' }) <$> f (H.method req)---- | Whether to use HTTPS (ie, SSL).-secure :: Lens' H.Request Bool-secure f req = (\s' -> req { H.secure = s' }) <$> f (H.secure req)--host :: Lens' H.Request ByteString-host f req = (\h' -> req { H.host = h' }) <$> f (H.host req)--port :: Lens' H.Request Int-port f req = (\p' -> req { H.port = p' }) <$> f (H.port req)---- | Everything from the host to the query string.-path :: Lens' H.Request ByteString-path f req = (\p' -> req { H.path = p' }) <$> f (H.path req)--queryString :: Lens' H.Request ByteString-queryString f req = (\qs' -> req { H.queryString = qs' }) <$> f (H.queryString req)--requestBody :: Lens' H.Request H.RequestBody-requestBody f req = (\rb' -> req { H.requestBody = rb' }) <$> f (H.requestBody req)--requestHeaders :: Lens' H.Request H.RequestHeaders-requestHeaders f req = (\rh' -> req { H.requestHeaders = rh' }) <$> f (H.requestHeaders req)---- | How many redirects to follow when getting a resource. 0 means follow no redirects. Default value: 10.-redirectCount :: Lens' H.Request Int-redirectCount f req = (\rc' -> req { H.redirectCount = rc' }) <$> f (H.redirectCount req)---- | Check the status code. Note that this will run after all redirects are performed.--- Default: return a StatusCodeException on non-2XX responses.-checkStatus :: Lens' H.Request (H.Status -> H.ResponseHeaders -> H.CookieJar -> Maybe SomeException)-checkStatus f req = (\cs' -> req { H.checkStatus = cs' }) <$> f (H.checkStatus req)---- | Number of microseconds to wait for a response. If Nothing, will wait indefinitely. Default: 5 seconds.-responseTimeout :: Lens' H.Request (Maybe Int)-responseTimeout f req = (\rt' -> req { H.responseTimeout = rt' }) <$> f (H.responseTimeout req)
− src/Jenkins/REST/Method.hs
@@ -1,278 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE KindSignatures #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE StandaloneDeriving #-}-{-# LANGUAGE UndecidableInstances #-}--- | Jenkins REST API method construction-module Jenkins.REST.Method- ( -- * Types- Method, Type(..), Format, As- -- * Method construction- , text, int, (-?-), (-/-), (-=-), (-&-), query- , as, JSONy(..), XMLy(..), Pythony(..)- -- ** Helpers- , job, build, view, queue- -- * Rendering- , render, slash- ) where--import Data.ByteString (ByteString)-#if defined(__GLASGOW_HASKELL__) && (__GLASGOW_HASKELL__ < 706)-import Data.ByteString.Char8 ()-#endif-import qualified Data.ByteString as B-import Data.Data (Data, Typeable)-import Data.Monoid (Monoid(..), (<>))-import Data.String (IsString(..))-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import Data.Text (Text)-import GHC.Generics (Generic)-import Network.URI (escapeURIChar, isUnreserved)---- $setup--- >>> :set -XOverloadedStrings---infix 1 :~?, -?--infix 3 :~@, `as`-infix 7 :~=, -=--infixr 5 :~/, -/-, :~&, -&---- | Jenkins RESTFul API method encoding-data Method :: Type -> Format -> * where- Empty :: Method t f- Text :: Text -> Method Complete f- (:~/) :: Method Complete f -> Method Complete f -> Method Complete f- (:~@) :: Method Complete f -> As f -> Method Complete f- (:~=) :: Text -> Maybe Text -> Method Query f- (:~&) :: Method Query f -> Method Query f -> Method Query f- (:~?) :: Method Complete f -> Method Query f -> Method Complete f--deriving instance Show (As f) => Show (Method t f)---- | Only to support number literals-instance Num (Method Complete f) where- (+) = error "Method.(+): not supposed to be used"- (*) = error "Method.(*): not supposed to be used"- abs = error "Method.abs: not supposed to be used"- signum = error "Method.signum: not supposed to be used"- fromInteger = fromString . show--instance IsString (Method Complete f) where- fromString = Text . T.pack--instance IsString (Method Query f) where- fromString str = T.pack str :~= Nothing---- | Method types-data Type = Query | Complete- deriving (Show, Eq, Typeable, Data, Generic)---- | Response formats-data Format = JSON | XML | Python- deriving (Show, Eq, Typeable, Data, Generic)---- | Response format singleton type-data As :: Format -> * where- AsJSON :: As JSON- AsXML :: As XML- AsPython :: As Python--deriving instance Show (As f)-deriving instance Eq (As f)---- | Convert 'Text' to 'Method'-text :: Text -> Method Complete f-text = Text---- | Convert 'Integer' to 'Method'-int :: Integer -> Method Complete f-int = fromInteger---- | Append 2 paths-(-/-) :: Method Complete f -> Method Complete f -> Method Complete f-(-/-) = (:~/)---- | Append 2 queries-(-&-) :: Method Query f -> Method Query f -> Method Query f-(-&-) = (:~&)---- | Make a query-(-=-) :: Text -> Text -> Method Query f-x -=- y = x :~= Just y---- | Choose response format-as :: Method Complete f -> As f -> Method Complete f-as = (:~@)---- | JSON response format-class JSONy t where- json :: t JSON--instance JSONy As where- json = AsJSON--instance t ~ Complete => JSONy (Method t) where- json = "" `as` json---- | XML response format-class XMLy t where- xml :: t XML--instance XMLy As where- xml = AsXML--instance t ~ Complete => XMLy (Method t) where- xml = "" `as` xml---- | Python response format-class Pythony t where- python :: t Python--instance Pythony As where- python = AsPython--instance t ~ Complete => Pythony (Method t) where- python = "" `as` python---- | Append path and query-(-?-) :: Method Complete f -> Method Query f -> Method Complete f-(-?-) = (:~?)---- | list-to-query combinator------ >>> render (query [("foo", Nothing), ("bar", Just "baz"), ("quux", Nothing)])--- "foo&bar=baz&quux"------ >>> render (query [])--- ""-query :: [(Text, Maybe Text)] -> Method Query f-query [] = Empty-query xs = foldr1 (:~&) (map (uncurry (:~=)) xs)----- | Render 'Method' to something that can be sent over the wire------ >>> render ("" `as` xml)--- "api/xml"------ >>> render xml--- "api/xml"------ >>> render ("job" -/- 7 `as` xml)--- "job/7/api/xml"------ >>> render ("job" -/- 7 `as` xml)--- "job/7/api/xml"------ >>> render ("job" -/- 7 `as` json)--- "job/7/api/json"------ >>> render (text "restart")--- "restart"------ >>> render ("job" -?- "name" -=- "foo" -&- "title" -=- "bar")--- "job?name=foo&title=bar"------ >>> render ("job" -?- "name" -&- "title" -=- "bar")--- "job?name&title=bar"------ >>> render ("job" -/- 7 `as` json -?- "name" -&- "title" -=- "bar")--- "job/7/api/json?name&title=bar"------ >>> render ("job" -/- "ДМИТРИЙ" `as` xml)--- "job/%D0%94%D0%9C%D0%98%D0%A2%D0%A0%D0%98%D0%99/api/xml"-render :: Method t f -> ByteString-render Empty = ""-render (Text s) = renderText s-render (x :~/ y) = render x `slash` render y-render (x :~@ f) =- let prefix = render x- postfix = renderFormat f- in if B.null prefix then "api" `slash` postfix else prefix `slash` "api" `slash` postfix-render (x :~= Just y) = renderText x `equals` renderText y-render (x :~= Nothing) = renderText x-render (x :~& y) = render x `ampersand` render y-render (x :~? y) = render x `question` render y--renderFormat :: IsString s => As f -> s-renderFormat AsJSON = "json"-renderFormat AsXML = "xml"-renderFormat AsPython = "python"---- | Render unicode text as query string------ >>> renderText "foo-bar-baz"--- "foo-bar-baz"------ >>> renderText "foo bar baz"--- "foo%20bar%20baz"------ >>> renderText "ДМИТРИЙ МАЛИКОВ"--- "%D0%94%D0%9C%D0%98%D0%A2%D0%A0%D0%98%D0%99%20%D0%9C%D0%90%D0%9B%D0%98%D0%9A%D0%9E%D0%92"-renderText :: Text -> ByteString-renderText = T.encodeUtf8 . T.concatMap (T.pack . escapeURIChar isUnreserved)---- | Insert \"\/\" between two 'String'-like things and concat them.-slash :: (IsString m, Monoid m) => m -> m -> m-slash = insert "/"---- | Insert \"=\" between two 'String'-like things and concat them.-equals :: (IsString m, Monoid m) => m -> m -> m-equals = insert "="---- | Insert \"&\" between two 'String'-like things and concat them.-ampersand :: (IsString m, Monoid m) => m -> m -> m-ampersand = insert "&"---- | Insert \"?\" between two 'String'-like things and concat them.-question :: (IsString m, Monoid m) => m -> m -> m-question = insert "?"---- | Insert 'String'-like thing between two 'String'-like things and concat them.------ >>> "foo" `slash` "bar"--- "foo/bar"------ >>> "" `ampersand` "foo"--- "&foo"------ >>> "foo" `question` ""--- "foo?"-insert :: (IsString m, Monoid m) => m -> m -> m -> m-insert t x y = x <> t <> y----- | Job API method------ >>> render (job "name" `as` json)--- "job/name/api/json"-job :: Text -> Method Complete f-job name = "job" -/- text name---- | Job build API method------ >>> render (build "name" 4 `as` json)--- "job/name/4/api/json"-build :: Integral a => Text -> a -> Method Complete f-build name num = "job" -/- text name -/- int (toInteger num)---- | View API method------ >>> render (view "name" `as` xml)--- "view/name/api/xml"-view :: Text -> Method Complete f-view name = "view" -/- text name---- | Queue API method------ >>> render (queue `as` python)--- "queue/api/python"-queue :: Method Complete f-queue = "queue"
+ src/Jenkins/Rest.hs view
@@ -0,0 +1,125 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+-- | Jenkins REST API interface+module Jenkins.Rest+ ( -- * Query Jenkins+ runJenkins, ConnectInfo(..), defaultConnectInfo, Jenkins, Result(..)+ -- ** Combinators+ , get, post, post_, concurrently, io, disconnect, with+ -- ** Method+ , module Jenkins.Rest.Method+ -- ** Convenience+ , postXML, concurrentlys, concurrentlys_, reload, restart, forceRestart+ -- * Lensy things+ , jenkinsUrl, jenkinsPort, jenkinsUser, jenkinsApiToken, jenkinsPassword+ , _Error, _Disconnect, _Result+ -- * Type reexports+ , Request, HttpException+ ) where++import Control.Applicative ((<$))+import Data.Foldable (Foldable, foldr)+import Control.Lens+import Control.Monad.IO.Class (MonadIO(..))+import Data.ByteString.Lazy (ByteString)+import Data.Monoid (mempty)+import Network.HTTP.Conduit (Request, HttpException)+import Prelude hiding (foldr)+import Text.XML (Document, renderLBS, def)++import Jenkins.Rest.Internal+import Jenkins.Rest.Method+import Network.HTTP.Conduit.Lens++{-# ANN module ("HLint: ignore Use const" :: String) #-}+++-- | @GET@ query+get :: Method Complete f -> Jenkins ByteString+get m = liftJ $ Get m id+{-# INLINE get #-}++-- | @POST@ query (with a payload)+post :: (forall f. Method Complete f) -> ByteString -> Jenkins ()+post m body = liftJ $ Post m body (\_ -> ())+{-# INLINE post #-}++-- | @POST@ query (without payload)+post_ :: (forall f. Method Complete f) -> Jenkins ()+post_ m = post m mempty+{-# INLINE post_ #-}++-- | Do both queries 'concurrently'+concurrently :: Jenkins a -> Jenkins b -> Jenkins (a, b)+concurrently ja jb = liftJ $ Conc ja jb (,)+{-# INLINE concurrently #-}++-- | Lift arbitrary 'IO' action+io :: IO a -> Jenkins a+io = liftIO+{-# INLINE io #-}++-- | Disconnect from Jenkins+--+-- Any following queries won't be executed+disconnect :: Jenkins a+disconnect = liftJ Dcon+{-# INLINE disconnect #-}++-- | Make local changes to the 'Request'+with :: (Request -> Request) -> Jenkins a -> Jenkins a+with f j = liftJ $ With f j id+{-# INLINE with #-}+++-- | @POST@ job's @config.xml@ (or any other xml, really) in @xml-conduit@ format+postXML :: (forall f. Method Complete f) -> Document -> Jenkins ()+postXML m =+ with (requestHeaders <>~ [("Content-Type", "text/xml")]) . post m . renderLBS def+{-# INLINE postXML #-}++-- | Send a list of queries 'concurrently'+concurrentlys :: Foldable f => f (Jenkins a) -> Jenkins [a]+concurrentlys = foldr go (return [])+ where+ go x xs = do+ (y, ys) <- concurrently x xs+ return (y : ys)+{-# INLINE concurrentlys #-}++-- | Send a list of queries 'concurrently' ignoring their results+--+-- /Note/: exceptions are still raised+concurrentlys_ :: Foldable f => f (Jenkins a) -> Jenkins ()+concurrentlys_ = foldr (\x xs -> () <$ concurrently x xs) (return ())+{-# INLINE concurrentlys_ #-}++-- | Reload jenkins configuration from disk+--+-- Calls @/reload@ and disconnects+reload :: Jenkins a+reload = do+ post_ "reload"+ disconnect+{-# INLINE reload #-}++-- | Restart jenkins safely+--+-- Calls @/safeRestart@ and disconnects+--+-- @/safeRestart@ allows all running jobs to complete+restart :: Jenkins a+restart = do+ post_ "safeRestart"+ disconnect+{-# INLINE restart #-}++-- | Force jenkins to restart without waiting for running jobs to finish+--+-- Calls @/restart@ and disconnects+forceRestart :: Jenkins a+forceRestart = do+ post_ "restart"+ disconnect+{-# INLINE forceRestart #-}
+ src/Jenkins/Rest/Internal.hs view
@@ -0,0 +1,228 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+-- | Jenkins REST API interface internals+module Jenkins.Rest.Internal where++import Control.Applicative+import Control.Concurrent.Async (concurrently)+import Control.Exception (Exception, try, toException)+import Control.Lens+import Control.Monad+import Control.Monad.Free.Church (F, iterM, liftF)+import Control.Monad.IO.Class (MonadIO(..))+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Control (MonadTransControl(..))+import Control.Monad.Trans.Reader (ReaderT, runReaderT, ask, local)+import Control.Monad.Trans.Maybe (MaybeT(..), mapMaybeT)+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import Data.Conduit (ResourceT)+import Data.Data (Data, Typeable)+import GHC.Generics (Generic)+import Network.HTTP.Conduit+import Network.HTTP.Types (Status(..))++import Jenkins.Rest.Method+import qualified Network.HTTP.Conduit.Lens as L+++-- | Jenkins REST API query sequence description+newtype Jenkins a = Jenkins { unJenkins :: F JenkinsF a }+ deriving (Functor, Applicative, Monad)++instance MonadIO Jenkins where+ liftIO = liftJ . IO+ {-# INLINE liftIO #-}++-- | Jenkins REST API query+data JenkinsF a where+ Get :: Method Complete f -> (BL.ByteString -> a) -> JenkinsF a+ Post :: (forall f. Method Complete f) -> BL.ByteString -> (BL.ByteString -> a) -> JenkinsF a+ Conc :: Jenkins a -> Jenkins b -> (a -> b -> c) -> JenkinsF c+ IO :: IO a -> JenkinsF a+ With :: (Request -> Request) -> Jenkins b -> (b -> a) -> JenkinsF a+ Dcon :: JenkinsF a++instance Functor JenkinsF where+ fmap f (Get m g) = Get m (f . g)+ fmap f (Post m body g) = Post m body (f . g)+ fmap f (Conc m n g) = Conc m n (\a b -> f (g a b))+ fmap f (IO a) = IO (fmap f a)+ fmap f (With h j g) = With h j (f . g)+ fmap _ Dcon = Dcon+ {-# INLINE fmap #-}++-- | Lift 'JenkinsF' to 'Jenkins'+liftJ :: JenkinsF a -> Jenkins a+liftJ = Jenkins . liftF+{-# INLINE liftJ #-}++-- | The result of Jenkins REST API queries+data Result e v =+ Error e -- ^ Exception @e@ was thrown while querying+ | Disconnect -- ^ The client was explicitly disconnected+ | Result v -- ^ Querying successfully finished the with value @v@+ deriving (Show, Eq, Ord, Typeable, Data, Generic)++-- | Query Jenkins API using 'Jenkins' description+--+-- Successful result is either 'Disconnect' or @ 'Result' v @+--+-- If 'HttpException' was thrown by @http-conduit@, 'runJenkins' catches it+-- and wraps in 'Error'. Other exceptions are /not/ catched+runJenkins :: ConnectInfo -> Jenkins a -> IO (Result HttpException a)+runJenkins (ConnectInfo h p user token) jenk =+ fmap result . try . withManager $ \manager -> do+ req <- liftIO $ parseUrl h+ let req' = req+ & L.port .~ p+ & L.responseTimeout .~ Just (20 * 1000000)+ runReaderT (runMaybeT (iterJenkinsIO manager jenk)) (applyBasicAuth user token req')+ where+ result :: Either e (Maybe v) -> Result e v+ result (Left e) = Error e+ result (Right Nothing) = Disconnect+ result (Right (Just val)) = Result val++-- | A prism into Jenkins error+_Error :: Prism (Result e a) (Result e' a) e e'+_Error = prism Error $ \case+ Error e -> Right e+ Disconnect -> Left Disconnect+ Result a -> Left (Result a)+{-# INLINE _Error #-}++-- | A prism into disconnect+_Disconnect :: Prism' (Result e a) ()+_Disconnect = prism' (\_ -> Disconnect) $ \case+ Disconnect -> Just ()+ _ -> Nothing+{-# INLINE _Disconnect #-}++-- | A prism into result+_Result :: Prism (Result e a) (Result e b) a b+_Result = prism Result $ \case+ Error e -> Left (Error e)+ Disconnect -> Left Disconnect+ Result a -> Right a+{-# INLINE _Result #-}++-- | Interpret 'JenkinsF' AST in 'IO'+iterJenkinsIO+ :: Manager+ -> Jenkins a+ -> MaybeT (ReaderT Request (ResourceT IO)) a+iterJenkinsIO manager = iterJenkins (interpreter manager)+{-# INLINE iterJenkinsIO #-}++-- | Tear down 'JenkinsF' AST with a 'JenkinsF'-algebra+iterJenkins :: Monad m => (JenkinsF (m a) -> m a) -> Jenkins a -> m a+iterJenkins go = iterM go . unJenkins+{-# INLINE iterJenkins #-}++-- | 'JenkinsF' AST interpreter+interpreter+ :: Manager+ -> JenkinsF (MaybeT (ReaderT Request (ResourceT IO)) a)+ -> MaybeT (ReaderT Request (ResourceT IO)) a+interpreter manager = go where+ go (Get m next) = do+ req <- lift ask+ let req' = req+ & L.path %~ (`slash` render m)+ & L.method .~ "GET"+ bs <- lift . lift $ httpLbs req' manager+ next (responseBody bs)+ go (Post m body next) = do+ req <- lift ask+ let req' = req+ & L.path %~ (`slash` render m)+ & L.method .~ "POST"+ & L.requestBody .~ RequestBodyLBS body+ & L.redirectCount .~ 0+ & L.checkStatus .~ \s@(Status st _) hs cookie_jar ->+ if 200 <= st && st < 400+ then Nothing+ else Just . toException $ StatusCodeException s hs cookie_jar+ res <- lift . lift $ httpLbs req' manager+ next (responseBody res)+ go (Conc jenka jenkb next) = do+ (a, b) <- liftWith $ \run' -> liftWith $ \run'' -> liftWith $ \run''' ->+ let+ run :: Jenkins t -> IO (StT ResourceT (StT (ReaderT Request) (StT MaybeT t)))+ run = run''' . run'' . run' . iterJenkinsIO manager+ in+ concurrently (run jenka) (run jenkb)+ c <- restoreT . restoreT . restoreT $ return a+ d <- restoreT . restoreT . restoreT $ return b+ next c d+ go (IO action) = join (liftIO action)+ go (With f jenk next) = do+ res <- mapMaybeT (local f) (iterJenkinsIO manager jenk)+ next res+ go Dcon = mzero+++-- | Jenkins connection settings+--+-- '_jenkinsApiToken' may be user's password, Jenkins+-- does not make any distinction between these concepts+data ConnectInfo = ConnectInfo+ { _jenkinsUrl :: String -- ^ Jenkins URL, e.g. @http:\/\/example.com\/jenkins@+ , _jenkinsPort :: Int -- ^ Jenkins port, e.g. @8080@+ , _jenkinsUser :: B.ByteString -- ^ Jenkins user, e.g. @jenkins@+ , _jenkinsApiToken :: B.ByteString -- ^ Jenkins user API token+ } deriving (Show, Eq, Typeable, Data, Generic)++-- | Default Jenkins connection settings+--+-- @+-- defaultConnectInfo = ConnectInfo+-- { _jenkinsUrl = \"http:\/\/example.com\/jenkins\"+-- , _jenkinsPort = 8080+-- , _jenkinsUser = \"jenkins\"+-- , _jenkinsApiToken = \"\"+-- }+-- @+defaultConnectInfo :: ConnectInfo+defaultConnectInfo = ConnectInfo+ { _jenkinsUrl = "http://example.com/jenkins"+ , _jenkinsPort = 8080+ , _jenkinsUser = "jenkins"+ , _jenkinsApiToken = ""+ }++-- | A lens into Jenkins URL+jenkinsUrl :: Lens' ConnectInfo String+jenkinsUrl f req = (\u' -> req { _jenkinsUrl = u' }) <$> f (_jenkinsUrl req)+{-# INLINE jenkinsUrl #-}++-- | A lens into Jenkins port+jenkinsPort :: Lens' ConnectInfo Int+jenkinsPort f req = (\p' -> req { _jenkinsPort = p' }) <$> f (_jenkinsPort req)+{-# INLINE jenkinsPort #-}++-- | A lens into Jenkins user+jenkinsUser :: Lens' ConnectInfo B.ByteString+jenkinsUser f req = (\u' -> req { _jenkinsUser = u' }) <$> f (_jenkinsUser req)+{-# INLINE jenkinsUser #-}++-- | A lens into Jenkins user API token+jenkinsApiToken :: Lens' ConnectInfo B.ByteString+jenkinsApiToken f req = (\a' -> req { _jenkinsApiToken = a' }) <$> f (_jenkinsApiToken req)+{-# INLINE jenkinsApiToken #-}++-- | A lens into Jenkins password+--+-- @+-- jenkinsPassword = jenkinsApiToken+-- @+jenkinsPassword :: Lens' ConnectInfo B.ByteString+jenkinsPassword = jenkinsApiToken+{-# INLINE jenkinsPassword #-}
+ src/Jenkins/Rest/Method.hs view
@@ -0,0 +1,302 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving #-}+-- | Jenkins REST API method construction+module Jenkins.Rest.Method+ ( -- * Types+ Method+ , Type(..)+ , Format+ , As+ -- * Method construction+ , text, int+ , (-?-), (-/-), (-=-), (-&-)+ , query+ , as+ , JSONy(..)+ , XMLy(..)+ , Pythony(..)+ -- * Shortcuts+ , job+ , build+ , view+ , queue+ , overallLoad+ , computer+ -- * Rendering+ , render+ , slash+ ) where++import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import Data.Data (Data, Typeable)+import Data.Monoid (Monoid(..), (<>))+import Data.String (IsString(..))+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Data.Text (Text)+import GHC.Generics (Generic)+import Network.URI (escapeURIChar, isUnreserved)++-- $setup+-- >>> :set -XOverloadedStrings+++infix 1 :~?, -?-+infix 3 :~@, `as`+infix 7 :~=, -=-+infixr 5 :~/, -/-, :~&, -&-++-- | Jenkins RESTFul API method encoding+data Method :: Type -> Format -> * where+ Empty :: Method t f+ Text :: Text -> Method Complete f+ (:~/) :: Method Complete f -> Method Complete f -> Method Complete f+ (:~@) :: Method Complete f -> As f -> Method Complete f+ (:~=) :: Text -> Maybe Text -> Method Query f+ (:~&) :: Method Query f -> Method Query f -> Method Query f+ (:~?) :: Method Complete f -> Method Query f -> Method Complete f++deriving instance Show (As f) => Show (Method t f)++-- | Only to support number literals+instance t ~ Complete => Num (Method t f) where+ (+) = error "Method.(+): not supposed to be used"+ (*) = error "Method.(*): not supposed to be used"+ abs = error "Method.abs: not supposed to be used"+ signum = error "Method.signum: not supposed to be used"+ fromInteger = fromString . show++instance IsString (Method Complete f) where+ fromString = Text . T.pack++instance IsString (Method Query f) where+ fromString str = T.pack str :~= Nothing++-- | Method types+data Type = Query | Complete+ deriving (Show, Eq, Typeable, Data, Generic)++-- | Response formats+data Format = JSON | XML | Python+ deriving (Show, Eq, Typeable, Data, Generic)++-- | Response format singleton type+data As :: Format -> * where+ AsJSON :: As JSON+ AsXML :: As XML+ AsPython :: As Python++deriving instance Show (As f)+deriving instance Eq (As f)++-- | Convert 'Text' to 'Method'+text :: Text -> Method Complete f+text = Text++-- | Convert 'Integer' to 'Method'+int :: Integer -> Method Complete f+int = fromInteger++-- | Combine 2 paths+(-/-) :: Method Complete f -> Method Complete f -> Method Complete f+(-/-) = (:~/)++-- | Combine 2 queries+(-&-) :: Method Query f -> Method Query f -> Method Query f+(-&-) = (:~&)++-- | Make a field-value pair+(-=-) :: Text -> Text -> Method Query f+x -=- y = x :~= Just y++-- | Choose response format+as :: Method Complete f -> As f -> Method Complete f+as = (:~@)++-- | JSON response format+class JSONy t where+ json :: t JSON++instance JSONy As where+ json = AsJSON++instance t ~ Complete => JSONy (Method t) where+ json = "" `as` json++-- | XML response format+class XMLy t where+ xml :: t XML++instance XMLy As where+ xml = AsXML++instance t ~ Complete => XMLy (Method t) where+ xml = "" `as` xml++-- | Python response format+class Pythony t where+ python :: t Python++instance Pythony As where+ python = AsPython++instance t ~ Complete => Pythony (Method t) where+ python = "" `as` python++-- | Combine path and query+(-?-) :: Method Complete f -> Method Query f -> Method Complete f+(-?-) = (:~?)++-- | List-to-query convenience combinator+--+-- >>> render (query [("foo", Nothing), ("bar", Just "baz"), ("quux", Nothing)])+-- "foo&bar=baz&quux"+--+-- >>> render (query [])+-- ""+query :: [(Text, Maybe Text)] -> Method Query f+query [] = Empty+query xs = foldr1 (:~&) (map (uncurry (:~=)) xs)+++-- | Render 'Method' to something that can be sent over the wire+--+-- >>> render ("" `as` xml)+-- "api/xml"+--+-- >>> render xml+-- "api/xml"+--+-- >>> render ("job" -/- 7 `as` xml)+-- "job/7/api/xml"+--+-- >>> render ("job" -/- 7 `as` xml)+-- "job/7/api/xml"+--+-- >>> render ("job" -/- 7 `as` json)+-- "job/7/api/json"+--+-- >>> render (text "restart")+-- "restart"+--+-- >>> render ("job" -?- "name" -=- "foo" -&- "title" -=- "bar")+-- "job?name=foo&title=bar"+--+-- >>> render ("job" -?- "name" -&- "title" -=- "bar")+-- "job?name&title=bar"+--+-- >>> render ("job" -/- 7 `as` json -?- "name" -&- "title" -=- "bar")+-- "job/7/api/json?name&title=bar"+--+-- >>> render ("job" -/- "ДМИТРИЙ" `as` xml)+-- "job/%D0%94%D0%9C%D0%98%D0%A2%D0%A0%D0%98%D0%99/api/xml"+render :: Method t f -> ByteString+render Empty = ""+render (Text s) = renderText s+render (x :~/ y) = render x `slash` render y+render (x :~@ f) =+ let prefix = render x+ postfix = renderFormat f+ in if B.null prefix then "api" `slash` postfix else prefix `slash` "api" `slash` postfix+render (x :~= Just y) = renderText x `equals` renderText y+render (x :~= Nothing) = renderText x+render (x :~& y) = render x `ampersand` render y+render (x :~? y) = render x `question` render y++renderFormat :: IsString s => As f -> s+renderFormat AsJSON = "json"+renderFormat AsXML = "xml"+renderFormat AsPython = "python"++-- | Render unicode text as a query string+--+-- >>> renderText "foo-bar-baz"+-- "foo-bar-baz"+--+-- >>> renderText "foo bar baz"+-- "foo%20bar%20baz"+--+-- >>> renderText "ДМИТРИЙ МАЛИКОВ"+-- "%D0%94%D0%9C%D0%98%D0%A2%D0%A0%D0%98%D0%99%20%D0%9C%D0%90%D0%9B%D0%98%D0%9A%D0%9E%D0%92"+renderText :: Text -> ByteString+renderText = T.encodeUtf8 . T.concatMap (T.pack . escapeURIChar isUnreserved)++-- | Insert \"\/\" between two 'String'-like things and concatenate everything.+slash :: (IsString m, Monoid m) => m -> m -> m+slash = insert "/"++-- | Insert \"=\" between two 'String'-like things and concatenate everything.+equals :: (IsString m, Monoid m) => m -> m -> m+equals = insert "="++-- | Insert \"&\" between two 'String'-like things and concatenate everything.+ampersand :: (IsString m, Monoid m) => m -> m -> m+ampersand = insert "&"++-- | Insert \"?\" between two 'String'-like things and concatenate everything.+question :: (IsString m, Monoid m) => m -> m -> m+question = insert "?"++-- | Insert 'String'-like thing between two 'String'-like things and concatenate everything.+--+-- >>> "foo" `slash` "bar"+-- "foo/bar"+--+-- >>> "" `ampersand` "foo"+-- "&foo"+--+-- >>> "foo" `question` ""+-- "foo?"+insert :: (IsString m, Monoid m) => m -> m -> m -> m+insert t x y = x <> t <> y+++-- | Job API method+--+-- >>> render (job "name" `as` json)+-- "job/name/api/json"+job :: Text -> Method Complete f+job name = "job" -/- text name++-- | Job build API method+--+-- >>> render (build "name" 4 `as` json)+-- "job/name/4/api/json"+build :: Integral a => Text -> a -> Method Complete f+build name num = "job" -/- text name -/- int (toInteger num)++-- | View API method+--+-- >>> render (view "name" `as` xml)+-- "view/name/api/xml"+view :: Text -> Method Complete f+view name = "view" -/- text name++-- | Queue API method+--+-- >>> render (queue `as` python)+-- "queue/api/python"+queue :: Method Complete f+queue = "queue"++-- | Statistics API method+--+-- >>> render (overallLoad `as` xml)+-- "overallLoad/api/xml"+overallLoad :: Method Complete f+overallLoad = "overallLoad"++-- | Node API method+--+-- >>> render (computer `as` python)+-- "computer/api/python"+computer :: Method Complete f+computer = "computer"
+ src/Network/HTTP/Conduit/Lens.hs view
@@ -0,0 +1,359 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+-- | Optics for @http-conduit@ types+module Network.HTTP.Conduit.Lens+ ( -- * 'Request' lenses+ method+ , secure+ , host+ , port+ , path+ , queryString+ , requestBody+ , requestHeaders+ , proxy+ , hostAddress+ , rawBody+ , decompress+ , redirectCount+ , checkStatus+ , responseTimeout+ , cookieJar+ , getConnectionWrapper+ -- * 'HttpException' prisms+ , AsHttpException(..)+ , _StatusCodeException+ , _InvalidUrlException+ , _TooManyRedirects+ , _UnparseableRedirect+ , _TooManyRetries+ , _HttpParserException+ , _HandshakeFailed+ , _OverlongHeaders+ , _ResponseTimeout+ , _FailedConnectionException+ , _ExpectedBlankAfter100Continue+ , _InvalidStatusLine+ , _InvalidHeader+ , _InternalIOException+ , _ProxyConnectException+ , _NoResponseDataReceived+ , _TlsException+ , _TlsNotSupported+ , _ResponseBodyTooShort+ , _InvalidChunkHeaders+ , _IncompleteHeaders+ ) where++import Control.Applicative+import Control.Exception (SomeException, IOException)+import Control.Exception.Lens (exception)+import Control.Lens+import Data.ByteString (ByteString)+import qualified Data.ByteString.Lazy as Lazy+import Data.Word (Word64)+import qualified Network.HTTP.Conduit as H+import qualified Network.HTTP.Client.Internal as H+import qualified Network.HTTP.Types as H+import Network.Socket (HostAddress)+++-- | 'H.method' lens+method :: Lens' H.Request H.Method+method f req = f (H.method req) <&> \m' -> req { H.method = m' }+{-# INLINE method #-}++-- | 'H.secure' lens+secure :: Lens' H.Request Bool+secure f req = f (H.secure req) <&> \s' -> req { H.secure = s' }+{-# INLINE secure #-}++-- | 'H.host' lens+host :: Lens' H.Request ByteString+host f req = f (H.host req) <&> \h' -> req { H.host = h' }+{-# INLINE host #-}++-- | 'H.port' lens+port :: Lens' H.Request Int+port f req = f (H.port req) <&> \p' -> req { H.port = p' }+{-# INLINE port #-}++-- | 'H.path' lens+path :: Lens' H.Request ByteString+path f req = f (H.path req) <&> \p' -> req { H.path = p' }+{-# INLINE path #-}++-- | 'H.queryString' lens+queryString :: Lens' H.Request ByteString+queryString f req = f (H.queryString req) <&> \qs' -> req { H.queryString = qs' }+{-# INLINE queryString #-}++-- | 'H.requestBody' lens+requestBody :: Lens' H.Request H.RequestBody+requestBody f req = f (H.requestBody req) <&> \rb' -> req { H.requestBody = rb' }+{-# INLINE requestBody #-}++-- | 'H.requestHeaders' lens+requestHeaders :: Lens' H.Request H.RequestHeaders+requestHeaders f req = f (H.requestHeaders req) <&> \rh' -> req { H.requestHeaders = rh' }+{-# INLINE requestHeaders #-}++-- | 'H.proxy'+proxy :: Lens' H.Request (Maybe H.Proxy)+proxy f req = f (H.proxy req) <&> \mp' -> req { H.proxy = mp' }+{-# INLINE proxy #-}++-- | 'H.hostAddress'+hostAddress :: Lens' H.Request (Maybe HostAddress)+hostAddress f req = f (H.hostAddress req) <&> \ha' -> req { H.hostAddress = ha' }+{-# INLINE hostAddress #-}++-- | 'H.rawBody'+rawBody :: Lens' H.Request Bool+rawBody f req = f (H.rawBody req) <&> \b' -> req { H.rawBody = b' }+{-# INLINE rawBody #-}++-- | 'H.decompress'+decompress :: Lens' H.Request (ByteString -> Bool)+decompress f req = f (H.decompress req) <&> \btb' -> req { H.decompress = btb' }+{-# INLINE decompress #-}++-- | 'H.redirectCount' lens+redirectCount :: Lens' H.Request Int+redirectCount f req = f (H.redirectCount req) <&> \rc' -> req { H.redirectCount = rc' }+{-# INLINE redirectCount #-}++-- | 'H.checkStatus' lens+checkStatus :: Lens' H.Request (H.Status -> H.ResponseHeaders -> H.CookieJar -> Maybe SomeException)+checkStatus f req = f (H.checkStatus req) <&> \cs' -> req { H.checkStatus = cs' }+{-# INLINE checkStatus #-}++-- | 'H.responseTimeout' lens+responseTimeout :: Lens' H.Request (Maybe Int)+responseTimeout f req = f (H.responseTimeout req) <&> \rt' -> req { H.responseTimeout = rt' }+{-# INLINE responseTimeout #-}++-- | 'H.cookieJar'+cookieJar :: Lens' H.Request (Maybe H.CookieJar)+cookieJar f req = f (H.cookieJar req) <&> \mcj' -> req { H.cookieJar = mcj' }+{-# INLINE cookieJar #-}++-- | 'H.getConnectionWrapper'+getConnectionWrapper+ :: Lens' H.Request+ ( Maybe Int+ -> H.HttpException+ -> IO (H.ConnRelease, H.Connection, H.ManagedConn)+ -> IO (Maybe Int, (H.ConnRelease, H.Connection, H.ManagedConn))+ )+getConnectionWrapper f req =+ f (H.getConnectionWrapper req) <&> \wat' -> req { H.getConnectionWrapper = wat' }+{-# INLINE getConnectionWrapper #-}+++-- | @http-conduit@ exceptions+class AsHttpException p f t where+ -- | @http-conduit@ exceptions overloading+ _HttpException :: Overloaded' p f t H.HttpException++instance AsHttpException p f H.HttpException where+ _HttpException = id+ {-# INLINE _HttpException #-}++instance (Choice p, Applicative f) => AsHttpException p f SomeException where+ _HttpException = exception+ {-# INLINE _HttpException #-}++-- | 'H.StatusCodeException' exception+_StatusCodeException+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t (H.Status, H.ResponseHeaders, H.CookieJar)+_StatusCodeException = _HttpException . prism' (uncurry3 H.StatusCodeException) go where+ go (H.StatusCodeException s rh cj) = Just (s, rh, cj)+ go _ = Nothing+{-# INLINE _StatusCodeException #-}++-- | 'H.InvalidUrlException' exception+_InvalidUrlException+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t (String, String)+_InvalidUrlException = _HttpException . prism' (uncurry H.InvalidUrlException) go where+ go (H.InvalidUrlException s s') = Just (s, s')+ go _ = Nothing+{-# INLINE _InvalidUrlException #-}++-- | 'H.TooManyRedirects' exception+_TooManyRedirects+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t [H.Response Lazy.ByteString]+_TooManyRedirects = _HttpException . prism' H.TooManyRedirects go where+ go (H.TooManyRedirects rs) = Just rs+ go _ = Nothing+{-# INLINE _TooManyRedirects #-}++-- | 'H.UnparseableRedirect' exception+_UnparseableRedirect+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t (H.Response Lazy.ByteString)+_UnparseableRedirect = _HttpException . prism' H.UnparseableRedirect go where+ go (H.UnparseableRedirect r) = Just r+ go _ = Nothing+{-# INLINE _UnparseableRedirect #-}++-- | 'H.TooManyRetries' exception+_TooManyRetries+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t ()+_TooManyRetries = _HttpException . prism' (const H.TooManyRetries) go where+ go H.TooManyRetries = Just ()+ go _ = Nothing+{-# INLINE _TooManyRetries #-}++-- | 'H.HttpParserException' exception+_HttpParserException+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t String+_HttpParserException = _HttpException . prism' H.HttpParserException go where+ go (H.HttpParserException s) = Just s+ go _ = Nothing+{-# INLINE _HttpParserException #-}++-- | 'H.HandshakeFailed' exception+_HandshakeFailed+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t ()+_HandshakeFailed = _HttpException . prism' (const H.HandshakeFailed) go where+ go H.HandshakeFailed = Just ()+ go _ = Nothing+{-# INLINE _HandshakeFailed #-}++-- | 'H.OverlongHeaders' exception+_OverlongHeaders+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t ()+_OverlongHeaders = _HttpException . prism' (const H.OverlongHeaders) go where+ go H.OverlongHeaders = Just ()+ go _ = Nothing+{-# INLINE _OverlongHeaders #-}++-- | 'H.ResponseTimeout' exception+_ResponseTimeout+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t ()+_ResponseTimeout = _HttpException . prism' (const H.ResponseTimeout) go where+ go H.ResponseTimeout = Just ()+ go _ = Nothing+{-# INLINE _ResponseTimeout #-}++-- | 'H.FailedConnectionException' exception+_FailedConnectionException+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t (String, Int)+_FailedConnectionException = _HttpException . prism' (uncurry H.FailedConnectionException) go where+ go (H.FailedConnectionException s i) = Just (s, i)+ go _ = Nothing+{-# INLINE _FailedConnectionException #-}++-- | 'H.ExpectedBlankAfter100Continue' exception+_ExpectedBlankAfter100Continue+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t ()+_ExpectedBlankAfter100Continue =+ _HttpException . prism' (const H.ExpectedBlankAfter100Continue) go where+ go H.ExpectedBlankAfter100Continue = Just ()+ go _ = Nothing+{-# INLINE _ExpectedBlankAfter100Continue #-}++-- | 'H.InvalidStatusLine' exception+_InvalidStatusLine+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t ByteString+_InvalidStatusLine = _HttpException . prism' H.InvalidStatusLine go where+ go (H.InvalidStatusLine b) = Just b+ go _ = Nothing+{-# INLINE _InvalidStatusLine #-}++-- | 'H.InvalidHeader' exception+_InvalidHeader+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t ByteString+_InvalidHeader = _HttpException . prism' H.InvalidHeader go where+ go (H.InvalidHeader b) = Just b+ go _ = Nothing+{-# INLINE _InvalidHeader #-}++-- | 'H.InternalIOException' exception+_InternalIOException+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t IOException+_InternalIOException = _HttpException . prism' H.InternalIOException go where+ go (H.InternalIOException ioe) = Just ioe+ go _ = Nothing+{-# INLINE _InternalIOException #-}++-- | 'H.ProxyConnectException' exception+_ProxyConnectException+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t (ByteString, Int, Either ByteString H.HttpException)+_ProxyConnectException = _HttpException . prism' (uncurry3 H.ProxyConnectException) go where+ go (H.ProxyConnectException b i ebhe) = Just (b, i, ebhe)+ go _ = Nothing+{-# INLINE _ProxyConnectException #-}++-- | 'H.NoResponseDataReceived' exception+_NoResponseDataReceived+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t ()+_NoResponseDataReceived = _HttpException . prism' (const H.NoResponseDataReceived) go where+ go H.NoResponseDataReceived = Just ()+ go _ = Nothing+{-# INLINE _NoResponseDataReceived #-}++-- | 'H.TlsException' exception+_TlsException+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t SomeException+_TlsException = _HttpException . prism' H.TlsException go where+ go (H.TlsException se) = Just se+ go _ = Nothing+{-# INLINE _TlsException #-}++-- | 'H.TlsNotSupported' exception+_TlsNotSupported+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t ()+_TlsNotSupported = _HttpException . prism' (const H.TlsNotSupported) go where+ go H.TlsNotSupported = Just ()+ go _ = Nothing+{-# INLINE _TlsNotSupported #-}++-- | 'H.ResponseBodyTooShort' exception+_ResponseBodyTooShort+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t (Word64, Word64)+_ResponseBodyTooShort = _HttpException . prism' (uncurry H.ResponseBodyTooShort) go where+ go (H.ResponseBodyTooShort w w') = Just (w, w')+ go _ = Nothing+{-# INLINE _ResponseBodyTooShort #-}++-- | 'H.InvalidChunkHeaders' exception+_InvalidChunkHeaders+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t ()+_InvalidChunkHeaders = _HttpException . prism' (const H.InvalidChunkHeaders) go where+ go H.InvalidChunkHeaders = Just ()+ go _ = Nothing+{-# INLINE _InvalidChunkHeaders #-}++-- | 'H.IncompleteHeaders' exception+_IncompleteHeaders+ :: (AsHttpException p f t, Choice p, Applicative f)+ => Overloaded' p f t ()+_IncompleteHeaders = _HttpException . prism' (const H.IncompleteHeaders) go where+ go H.IncompleteHeaders = Just ()+ go _ = Nothing+{-# INLINE _IncompleteHeaders #-}++uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d+uncurry3 f (a, b, c) = f a b c+{-# INLINE uncurry3 #-}
+ test/Doctest.hs view
@@ -0,0 +1,28 @@+module Main where++import Control.Applicative ((<$>))+import Control.Monad (forM, liftM)+import Data.List (isSuffixOf)+import System.Directory (getDirectoryContents, doesDirectoryExist)+import System.FilePath ((</>))+import Test.DocTest (doctest)+++main :: IO ()+main = sources "src" >>= doctest++sources :: FilePath -> IO [FilePath]+sources root = do+ files <- contents root+ liftM concat . forM files $ \file -> do+ let path = root </> file+ is_dir <- doesDirectoryExist path+ case is_dir of+ True -> sources path+ _ | isSource path -> return [path]+ | otherwise -> return []+ where+ isSource path = any (`isSuffixOf` path) [".hs", ".lhs"]++contents :: FilePath -> IO [FilePath]+contents dir = filter (`notElem` [".", ".."]) <$> getDirectoryContents dir
+ test/Jenkins/RestSpec.hs view
@@ -0,0 +1,98 @@+{-# LANGUAGE OverloadedStrings #-}+module Jenkins.RestSpec (spec) where++import Control.Applicative+import Control.Monad.Trans.State (State, evalState, get, put)+import qualified Data.ByteString as Strict+import qualified Data.ByteString.Lazy as Lazy+import Data.Monoid (mempty)+import Test.Hspec+import qualified Jenkins.Rest as Rest+import Jenkins.Rest.Internal+++spec :: Spec+spec = do+ context "POST requests" $ do+ it "post_ sends POST request with empty body" $ do+ interpret $ do+ Rest.post_ "foo"+ Rest.post_ "bar"+ Rest.post_ "baz"+ `shouldBe`+ [QPost 0 "" "foo", QPost 1 "" "bar", QPost 2 "" "baz"]++ it "post sends POST request with non-empty body" $ do+ interpret $ do+ Rest.post "foo" "qux"+ Rest.post "bar" "quux"+ Rest.post "baz" "xyzzy"+ `shouldBe`+ [QPost 0 "qux" "foo", QPost 1 "quux" "bar", QPost 2 "xyzzy" "baz"]++ context "GET requests" $+ it "get sends GET requests" $ do+ interpret $ do+ Rest.get "foo"+ Rest.get "bar"+ Rest.get "baz"+ `shouldBe`+ [QGet 0 "foo", QGet 1 "bar", QGet 2 "baz"]+++ describe "reload" $+ it "calls $jenkins_url/reload with POST query and then disconnects" $ do+ interpret $ do+ Rest.reload+ Rest.post_ "foo"+ `shouldBe`+ [QPost 0 "" "reload", QDisconnect]++ describe "restart" $+ it "calls $jenkins_url/safeRestart with POST query and then disconnects" $ do+ interpret $ do+ Rest.restart+ Rest.post_ "bar"+ `shouldBe`+ [QPost 0 "" "safeRestart", QDisconnect]++ describe "forceRestart" $+ it "calls $jenkins_url/restart with POST query and then disconnects" $ do+ interpret $ do+ Rest.forceRestart+ Rest.post_ "baz"+ `shouldBe`+ [QPost 0 "" "restart", QDisconnect]+++data Query =+ QGet Int Strict.ByteString+ | QPost Int Lazy.ByteString Strict.ByteString+ | QDisconnect+ deriving (Show, Eq)++newtype Requests a = Requests [a]+ deriving (Show, Eq)++interpret :: Rest.Jenkins a -> [Query]+interpret adt = evalState (iterJenkins go ([] <$ adt)) (Requests [0..]) where+ go :: JenkinsF (State (Requests Int) [Query]) -> State (Requests Int) [Query]+ go (Get m n) = do+ r <- render QGet m+ fmap (r :) (n mempty)+ go (Post m body n) = do+ r <- render (\x y -> QPost x body y) m+ fmap (r :) (n mempty)+ go Dcon =+ return [QDisconnect]++render :: (a -> Strict.ByteString -> Query) -> Rest.Method f x -> State (Requests a) Query+render f m = do+ n <- next+ return $ f n (Rest.render m)++next :: State (Requests a) a+next = do+ Requests (x:xs) <- get+ put (Requests xs)+ return x
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
− tests/Doctests.hs
@@ -1,28 +0,0 @@-module Main where--import Control.Applicative ((<$>))-import Control.Monad (forM, liftM)-import Data.List (isSuffixOf)-import System.Directory (getDirectoryContents, doesDirectoryExist)-import System.FilePath ((</>))-import Test.DocTest (doctest)---main :: IO ()-main = sources "src" >>= doctest--sources :: FilePath -> IO [FilePath]-sources root = do- files <- contents root- liftM concat . forM files $ \file -> do- let path = root </> file- is_dir <- doesDirectoryExist path- case is_dir of- True -> sources path- _ | isSource path -> return [path]- | otherwise -> return []- where- isSource path = any (`isSuffixOf` path) [".hs", ".lhs"]--contents :: FilePath -> IO [FilePath]-contents dir = filter (`notElem` [".", ".."]) <$> getDirectoryContents dir