packages feed

bugzilla-redhat (empty) → 0.3.0

raw patch · 11 files changed

+1556/−0 lines, 11 filesdep +aesondep +basedep +blaze-builder

Dependencies added: aeson, base, blaze-builder, bugzilla-redhat, bytestring, connection, containers, http-conduit, http-types, iso8601-time, resourcet, text, time, transformers, unordered-containers, vector

Files

+ ChangeLog.md view
@@ -0,0 +1,13 @@+# bugzilla-redhat version history++## 0.3.0 (2020-08-05)+- a fork of Seth Fowler's [bugzilla library](https://hackage.haskell.org/package/bugzilla)+- updated to build with Stackage LTS > 5 up to LTS 16 and Nightly+- minor API tweaks needed for redhat.bugzilla.com v5 (e3b424e2)+- expose BugzillaToken, newBzRequest, intAsText+- drop closeBugzillaContext and withBugzillaContext+- add evalSearchExpr+- hlint fixes++## bugzilla 0.2 and older+See Seth Fowler's original changelog.orig file.
+ LICENSE view
@@ -0,0 +1,27 @@+Copyright (c) 2014, Seth Fowler+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++* Redistributions of source code must retain the above copyright notice, this+  list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright notice,+  this list of conditions and the following disclaimer in the documentation+  and/or other materials provided with the distribution.++* Neither the name of the {organization} nor the names of its+  contributors may be used to endorse or promote products derived from+  this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,12 @@+hsbugzilla+==========++[![Build Status](https://travis-ci.org/sethfowler/hsbugzilla.png?branch=master)](https://travis-ci.org/sethfowler/hsbugzilla)++A Haskell interface to the Bugzilla native REST API.++Relevant links:++- The Bugzilla [native REST API](https://wiki.mozilla.org/BMO/REST).+- The (obsolete) [non-native REST API](https://wiki.mozilla.org/Bugzilla%3aREST_API).+- ["Interfacing with RESTful JSON APIs"](https://www.fpcomplete.com/school/to-infinity-and-beyond/competition-winners/interfacing-with-restful-json-apis) on School of Haskell.
+ bugzilla-redhat.cabal view
@@ -0,0 +1,71 @@+name:                bugzilla-redhat+version:             0.3.0+synopsis:            A Haskell interface to the Bugzilla native REST API+description:         This package is designed to provide an easy-to-use, typesafe+                     interface to querying Bugzilla from Haskell.+                     .+                     This is a friendly fork of Seth Fowler's library,+                     with minor updates and API tweaks needed for+                     bugzilla.redhat.com.+homepage:            https://github.com/juhp/hsbugzilla+license:             BSD3+license-file:        LICENSE+author:              Seth Fowler <mark.seth.fowler@gmail.com>+maintainer:          Jens Petersen <juhpetersen@gmail.com>+copyright:           2014 Seth Fowler+                     2020 Jens Petersen+category:            Web+build-type:          Simple+extra-source-files:  README.md+                     ChangeLog.md+                     changelog.orig+cabal-version:       >=1.10++source-repository head+  type:     git+  location: https://github.com/juhp/hsbugzilla.git++flag BuildDemo+  description:  Build the 'bugzilla' demo program+  default:      False++library+  ghc-options:         -Wall+  exposed-modules:     Web.Bugzilla.RedHat,+                       Web.Bugzilla.RedHat.Search+  other-modules:       Web.Bugzilla.RedHat.Internal.Network,+                       Web.Bugzilla.RedHat.Internal.Search,+                       Web.Bugzilla.RedHat.Internal.Types+  build-depends:       base >=4.6 && <4.15,+                       aeson >=0.7 && <1.5,+                       blaze-builder >=0.3 && <0.5,+                       bytestring >=0.10 && <0.11,+                       connection >=0.2 && <0.4,+                       containers >=0.5 && <0.7,+                       http-conduit >=2.0 && <2.4,+                       http-types >=0.8 && <0.13,+                       iso8601-time >=0.1 && <1.6,+                       resourcet >=0.4 && <1.3,+                       text >=0.11 && <1.3,+                       time >=1.4 && <1.10,+                       transformers >=0.3 && <0.6,+                       unordered-containers >=0.2 && <0.3,+                       vector >= 0.10 && <0.13+  hs-source-dirs:      src+  default-language:    Haskell2010++executable bugzilla+  if flag(BuildDemo)+    buildable:         True+  else+    buildable:         False++  ghc-options:         -Wall+  main-is:             BugzillaDemo.hs+  build-depends:       base >=4.6 && <4.15,+                       bugzilla-redhat,+                       containers >=0.5 && <0.7,+                       text >=0.11 && <1.3,+                       time >=1.4 && <1.10+  hs-source-dirs:      demo+  default-language:    Haskell2010
+ changelog.orig view
@@ -0,0 +1,18 @@+0.2.1.1++	* Tightened the version bounds for data-default. The old bounds weren't+	accurate.++0.2.1.0++	* Added searchBugs' and searchBugsWithLimit', variations which return+	only a list of BugIds. These are more suitable for polling than the+	existing functions.++0.2.0.0++	* History events now have a sequential id, just like comments.++	* When using an anonymous session, Bugzilla does not return email+	addresses for users.  To accomodate this, the User type now stores email+	addresses as type 'Maybe UserEmail'.
+ demo/BugzillaDemo.hs view
@@ -0,0 +1,166 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++import Control.Exception (bracket)+import Control.Monad+import Data.Maybe+import qualified Data.Text as T+import Data.Time.Clock (diffUTCTime)+import System.Environment (getArgs)+import System.IO++import Web.Bugzilla+import Web.Bugzilla.Search++main :: IO ()+main = dispatch Nothing Nothing =<< getArgs++dispatch :: Maybe UserEmail -> Maybe BugzillaServer -> [String] -> IO ()+dispatch Nothing s ("--login" : user : as)    = dispatch (Just $ T.pack user) s as+dispatch l Nothing ("--server" : server : as) = dispatch l (Just $ T.pack server) as+dispatch l s ["--assigned-to", user]          = withBz l s $ doAssignedTo (T.pack user)+dispatch l s ["--assigned-to-brief", user]    = withBz l s $ doAssignedToBrief (T.pack user)+dispatch l s ["--requests", user]             = withBz l s $ doRequests (T.pack user)+dispatch l s ["--history", bug, n]            = withBz l s $ doHistory (read bug) (read n)+dispatch _ _ _                                = usage++usage :: IO ()+usage = hPutStrLn stderr "Connection options:"+     >> hPutStrLn stderr "  --server [domain name] - REQUIRED. The Bugzilla server to access."+     >> hPutStrLn stderr "  --login [user email]   - The user to log in with."+     >> hPutStrLn stderr ""+     >> hPutStrLn stderr "Bugzilla queries:"+     >> hPutStrLn stderr "  --assigned-to [user email] - List bugs assigned to the user."+     >> hPutStrLn stderr "  --assigned-to [user email] - List bugs assigned to the user."+     >> hPutStrLn stderr "  --requests [user email]    - List requests for the user."+     >> hPutStrLn stderr "  --history [bug number] [n] - List the most recent 'n' changes to the bug."++withBz :: Maybe UserEmail -> Maybe BugzillaServer -> (BugzillaSession -> IO ()) -> IO ()+withBz mLogin mServer f = do+  let server = case mServer of+                 Just s  -> s+                 Nothing -> error "Please specify a server with '--server'"+  ctx <- newBugzillaContext server+  case mLogin of+    Just login -> do hPutStrLn stderr "Enter password: "+                     password <- T.pack <$> withEcho False getLine+                     mSession <- loginSession ctx login password+                     case mSession of+                       Just session -> do hPutStrLn stderr "Login successful."+                                          f session+                       Nothing      -> do hPutStrLn stderr "Login failed. Falling back to anonymous session."+                                          f $ anonymousSession ctx+    Nothing -> f $ anonymousSession ctx+++doAssignedTo :: UserEmail -> BugzillaSession -> IO ()+doAssignedTo user session = do+    let search = AssignedToField .==. user+    bugs <- searchBugs session search+    mapM_ showBug bugs+  where+    showBug Bug {..} = putStrLn $ show bugId ++ ": " ++ show bugSummary+                                 ++ " [" ++ T.unpack bugStatus ++ ": " ++ T.unpack bugResolution ++ "] Updated: "+                                 ++ show bugLastChangeTime++doAssignedToBrief :: UserEmail -> BugzillaSession -> IO ()+doAssignedToBrief user session = do+    let search = AssignedToField .==. user+    bugs <- searchBugs' session search+    mapM_ print bugs++doRequests :: UserEmail -> BugzillaSession -> IO ()+doRequests user session = do+    let needinfoSearch = FlagRequesteeField .==. user .&&. FlagsField `contains` "needinfo"+    needinfoBugs <- searchBugs session needinfoSearch+    mapM_ showNeedinfo needinfoBugs++    let reviewSearch = FlagRequesteeField .==. user .&&.+                       (FlagsField `contains` "review" .||. FlagsField `contains` "feedback")+    reviewBugs <- map bugId <$> searchBugs session reviewSearch+    forM_ reviewBugs $ \rBugId -> do+      attachments <- getAttachments session rBugId+      mapM_ showReview $ filter (any hasReviewFlag . attachmentFlags) attachments+      mapM_ showFeedback $ filter (any hasFeedbackFlag . attachmentFlags) attachments++  where+    showNeedinfo Bug {..} = do+      let flags = filter hasNeedinfoFlag $ fromMaybe [] bugFlags+      forM_ flags $ \flag ->+        putStrLn $ "[NEEDINFO] " ++ show bugId ++ ": " ++ show bugSummary+                ++ " (" ++ show (flagSetter flag) ++ " " ++ show (flagCreationDate flag) ++ ")"++    showReview Attachment {..} =+      putStrLn $ "[REVIEW] " ++ show attachmentBugId ++ ": " ++ show attachmentSummary+              ++ " (" ++ show attachmentCreator ++ " " ++ show attachmentCreationTime ++ ")"++    showFeedback Attachment {..} =+      putStrLn $ "[FEEDBACK] " ++ show attachmentBugId ++ ": " ++ show attachmentSummary+              ++ " (" ++ show attachmentCreator ++ " " ++ show attachmentCreationTime ++ ")"++    hasNeedinfoFlag f = flagRequestee f == Just user && flagName f == "needinfo"+    hasReviewFlag f   = flagRequestee f == Just user && flagName f == "review"+    hasFeedbackFlag f = flagRequestee f == Just user && flagName f == "feedback"++doHistory :: BugId -> Int -> BugzillaSession -> IO ()+doHistory bug count session = do+    comments <- getComments session bug+    history <- getHistory session bug+    recentEventsRev <- takeRecent count (reverse comments)+                                        (reverse $ historyEvents history)+    mapM_ putStrLn (reverse recentEventsRev)+  where+    takeRecent :: Int -> [Comment] -> [HistoryEvent] -> IO [String]+    takeRecent 0 _ _ = return []+    takeRecent n (c:cs) (e:es)+      | commentCreationTime c `diffUTCTime` historyEventTime e >= 0 = (:) <$> showComment c+                                                                          <*> takeRecent (n - 1) cs (e:es)+      | otherwise                                                   = (:) <$> showEvent e+                                                                          <*> takeRecent (n - 1) (c:cs) es+    takeRecent n cs@(_:_) [] = mapM showComment $ take n cs+    takeRecent n [] es@(_:_) = mapM showEvent $ take n es+    takeRecent _ [] []       = return []++    -- FIXME: showComment and showEvent will call getUser for the same+    -- user over and over again. You should never do this in a real application.+    showComment Comment {..} = do+      user <- getUser session commentCreator+      let commentUserRealName = maybe commentCreator userRealName user+      let commentUserEmail = fromMaybe commentCreator $ userEmail =<< user+      return $ "(Comment " ++ show commentCount ++ ") " ++ T.unpack commentUserRealName+            ++ " <" ++ T.unpack commentUserEmail ++ "> " ++ show commentCreationTime+            ++ "\n" ++ (unlines . map ("  " ++) . lines . T.unpack $ commentText)++    showEvent HistoryEvent {..} = do+      user <- getUser session historyEventUser+      let eventUserRealName = maybe historyEventUser userRealName user+      let eventUserEmail = fromMaybe historyEventUser $ userEmail =<< user+      return $ "(Event " ++ show historyEventId ++ ") " ++ T.unpack eventUserRealName+            ++ " <" ++ T.unpack eventUserEmail ++ ">\n"+            ++ concatMap showChange historyEventChanges++    showChange (TextFieldChange f (Modification r a aid)) = showChange' f r a aid+    showChange (ListFieldChange f (Modification r a aid)) = showChange' f r a aid+    showChange (IntFieldChange f (Modification r a aid))  = showChange' f r a aid+    showChange (TimeFieldChange f (Modification r a aid)) = showChange' f r a aid+    showChange (BoolFieldChange f (Modification r a aid)) = showChange' f r a aid++    showChange' f r a aid = "  " ++ showField f ++ ": "+                         ++ showMod r ++ " -> " ++ showMod a+                         ++ showAid aid ++ "\n"++    showField = T.unpack . fieldName++    showMod :: Show a => Maybe a -> String+    showMod (Just v) = show v+    showMod Nothing  = "___"++    showAid :: Maybe AttachmentId -> String+    showAid (Just aid) = " (Attachment " ++ show aid ++ ")"+    showAid Nothing    = ""++withEcho :: Bool -> IO a -> IO a+withEcho echo action =+    bracket (hGetEcho stdin)+            (hSetEcho stdin)+            (const $ hSetEcho stdin echo >> action)
+ src/Web/Bugzilla/RedHat.hs view
@@ -0,0 +1,240 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}++-- | This package is designed to provide an easy-to-use, typesafe+--   interface to querying Bugzilla from Haskell.+--+--   A very simple program using this package might look like this:+--+-- >   ctx <- newBugzillaContext "bugzilla.example.org"+-- >   let session = anonymousSession ctx+-- >       user = "me@example.org"+-- >       query = AssignedToField .==. user .&&.+-- >               FlagRequesteeField .==. user .&&.+-- >               (FlagsField `contains` "review" .||. FlagsField `contains` "feedback")+-- >   bugs <- searchBugs session query+-- >   mapM_ (putStrLn . show . bugSummary) bugs+--+--   There's a somewhat more in-depth demo program included with the+--   source code to this package.+module Web.Bugzilla.RedHat+( -- * Connecting to Bugzilla+  newBugzillaContext+, loginSession+, anonymousSession++, BugzillaServer+, BugzillaContext+, BugzillaSession (..)+, BugzillaToken (..)++  -- * Querying Bugzilla+, searchBugs+, searchBugs'+, searchBugsWithLimit+, searchBugsWithLimit'+, getBug+, getAttachment+, getAttachments+, getComments+, getHistory+, searchUsers+, getUser+, getUserById+, newBzRequest+, intAsText++, BugId+, AttachmentId+, CommentId+, UserId+, EventId+, FlagId+, FlagType+, UserEmail+, Field (..)+, User (..)+, Flag (..)+, Bug (..)+, Attachment (..)+, Comment (..)+, History (..)+, HistoryEvent (..)+, Change (..)+, Modification (..)+, fieldName++, BugzillaException (..)+) where++import Control.Exception (throw, try)+import Control.Monad.IO.Class (liftIO)+import qualified Data.Text as T+import Network.Connection (TLSSettings(..))+import Network.HTTP.Conduit (mkManagerSettings, newManager)++import Web.Bugzilla.RedHat.Internal.Network+import Web.Bugzilla.RedHat.Internal.Search+import Web.Bugzilla.RedHat.Internal.Types++-- | Creates a new 'BugzillaContext', suitable for connecting to the+--   provided server. You should try to reuse 'BugzillaContext's+--   whenever possible, because creating them is expensive.+newBugzillaContext :: BugzillaServer -> IO BugzillaContext+newBugzillaContext server = do+  let settings = mkManagerSettings (TLSSettingsSimple True False False) Nothing+  manager <- liftIO $ newManager settings+  return $ BugzillaContext server manager++-- | Attempts to create a logged-in 'BugzillaSession' using the+--   provided username and password. Returns 'Nothing' if login+--   fails.+loginSession :: BugzillaContext -> UserEmail -> T.Text -> IO (Maybe BugzillaSession)+loginSession ctx user password = do+  let loginQuery = [("login", Just user),+                    ("password", Just password)]+      session = anonymousSession ctx+      req = newBzRequest session ["login"] loginQuery+  eToken <- try $ sendBzRequest session req+  return $ case eToken of+             Left (BugzillaAPIError 300 _) -> Nothing+             Left e                        -> throw e+             Right token                   -> Just $ LoginSession ctx token++-- | Creates an anonymous 'BugzillaSession'. Note that some content+--   will be hidden by Bugzilla when you make queries in this state.+anonymousSession :: BugzillaContext -> BugzillaSession+anonymousSession = AnonymousSession++intAsText :: Int -> T.Text+intAsText = T.pack . show++-- | Searches Bugzilla and returns a list of 'Bug's. The 'SearchExpression'+-- can be constructed conveniently using the operators in "Web.Bugzilla.Search".+searchBugs :: BugzillaSession -> SearchExpression -> IO [Bug]+searchBugs session search = do+  let searchQuery = evalSearchExpr search+      req = newBzRequest session ["bug"] searchQuery+  (BugList bugs) <- sendBzRequest session req+  return bugs++-- | Like 'searchBugs', but returns a list of 'BugId's. You can+-- retrieve the 'Bug' for each 'BugId' using 'getBug'. The combination+-- of 'searchBugs'' and 'getBug' is much less efficient than+-- 'searchBugs'. 'searchBugs'' is suitable for cases where you won't need to call+-- 'getBug' most of the time - for example, polling to determine whether the+-- set of bugs returned by a query has changed.+searchBugs' :: BugzillaSession -> SearchExpression -> IO [BugId]+searchBugs' session search = do+  let fieldsQuery = [("include_fields", Just "id")]+      searchQuery = evalSearchExpr search+      req = newBzRequest session ["bug"] (fieldsQuery ++ searchQuery)+  (BugIdList bugs) <- sendBzRequest session req+  return bugs++-- | Search Bugzilla and returns a limited number of results. You can+--   call this repeatedly and use 'offset' to retrieve the results of+--   a large query incrementally. Note that most Bugzillas won't+--   return all of the results for a very large query by default, but+--   you can request this by calling 'searchBugsWithLimit' with 0 for+--   the limit.+searchBugsWithLimit :: BugzillaSession+                    -> Int  -- ^ The maximum number of results to return.+                    -> Int  -- ^ The offset from the first result to start from.+                    -> SearchExpression+                    -> IO [Bug]+searchBugsWithLimit session limit offset search = do+  let limitQuery = [("limit", Just $ intAsText limit),+                    ("offset", Just $ intAsText offset)]+      searchQuery = evalSearchExpr search+      req = newBzRequest session ["bug"] (limitQuery ++ searchQuery)+  (BugList bugs) <- sendBzRequest session req+  return bugs++-- | Like 'searchBugsWithLimit', but returns a list of 'BugId's. See+-- 'searchBugs'' for more discussion.+searchBugsWithLimit' :: BugzillaSession+                     -> Int  -- ^ The maximum number of results to return.+                     -> Int  -- ^ The offset from the first result to start from.+                     -> SearchExpression+                     -> IO [BugId]+searchBugsWithLimit' session limit offset search = do+  let fieldsQuery = [("include_fields", Just "id")]+      limitQuery = [("limit", Just $ intAsText limit),+                    ("offset", Just $ intAsText offset)]+      searchQuery = evalSearchExpr search+      req = newBzRequest session ["bug"] (fieldsQuery ++ limitQuery ++ searchQuery)+  (BugIdList bugs) <- sendBzRequest session req+  return bugs++-- | Retrieve a bug by bug number.+getBug :: BugzillaSession -> BugId -> IO (Maybe Bug)+getBug session bid = do+  let req = newBzRequest session ["bug", intAsText bid] []+  (BugList bugs) <- sendBzRequest session req+  case bugs of+    [bug] -> return $ Just bug+    []    -> return Nothing+    _     -> throw $ BugzillaUnexpectedValue+                     "Request for a single bug returned multiple bugs"++-- | Retrieve a bug by attachment number.+getAttachment :: BugzillaSession -> AttachmentId -> IO (Maybe Attachment)+getAttachment session aid = do+  let req = newBzRequest session ["bug", "attachment", intAsText aid] []+  (AttachmentList as) <- sendBzRequest session req+  case as of+    [a] -> return $ Just a+    []  -> return Nothing+    _   -> throw $ BugzillaUnexpectedValue+                   "Request for a single attachment returned multiple attachments"++-- | Get all attachments for a bug.+getAttachments :: BugzillaSession -> BugId -> IO [Attachment]+getAttachments session bid = do+  let req = newBzRequest session ["bug", intAsText bid, "attachment"] []+  (AttachmentList as) <- sendBzRequest session req+  return as++-- | Get all comments for a bug.+getComments :: BugzillaSession -> BugId -> IO [Comment]+getComments session bid = do+  let req = newBzRequest session ["bug", intAsText bid, "comment"] []+  (CommentList as) <- sendBzRequest session req+  return as++-- | Get the history for a bug.+getHistory :: BugzillaSession -> BugId -> IO History+getHistory session bid = do+  let req = newBzRequest session ["bug", intAsText bid, "history"] []+  sendBzRequest session req++-- | Search user names and emails using a substring search.+searchUsers :: BugzillaSession -> T.Text -> IO [User]+searchUsers session text = do+  let req = newBzRequest session ["user"] [("match", Just text)]+  (UserList users) <- sendBzRequest session req+  return users++-- | Get a user by email.+getUser :: BugzillaSession -> UserEmail -> IO (Maybe User)+getUser session user = do+  let req = newBzRequest session ["user", user] []+  (UserList users) <- sendBzRequest session req+  case users of+    [u] -> return $ Just u+    []  -> return Nothing+    _   -> throw $ BugzillaUnexpectedValue+                   "Request for a single user returned multiple users"++-- | Get a user by user ID.+getUserById :: BugzillaSession -> UserId -> IO (Maybe User)+getUserById session uid = do+  let req = newBzRequest session ["user", intAsText uid] []+  (UserList users) <- sendBzRequest session req+  case users of+    [u] -> return $ Just u+    []  -> return Nothing+    _   -> throw $ BugzillaUnexpectedValue+                   "Request for a single user returned multiple users"
+ src/Web/Bugzilla/RedHat/Internal/Network.hs view
@@ -0,0 +1,115 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE OverloadedStrings #-}++module Web.Bugzilla.RedHat.Internal.Network+( BugzillaServer+, BugzillaContext (..)+, BugzillaToken (..)+, BugzillaSession (..)+, BugzillaException (..)+, QueryPart+, Request+, requestUrl+, newBzRequest+, sendBzRequest+) where++import Blaze.ByteString.Builder (toByteString)+#if !MIN_VERSION_base(4,8,0)+import Control.Applicative ((<$>), (<*>))+#endif+import Control.Exception (Exception, throw)+import Control.Monad (mzero)+import Control.Monad.IO.Class (liftIO)+import Control.Monad.Trans.Resource (runResourceT)+import Data.Aeson+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+#if !MIN_VERSION_base(4,11,0)+import Data.Monoid ((<>))+#endif+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import Data.Typeable+import Network.HTTP.Conduit (Manager, Request(..), Response(..), defaultRequest, host, httpLbs, path, queryString, secure)+import Network.HTTP.Types.URI (QueryText, encodePathSegments, renderQueryText)++type BugzillaServer  = T.Text++-- | Holds information about a 'BugzillaServer' and manages outgoing+-- connections. You can use 'newBugzillaContext' or+-- 'withBugzillaContext' to create one.+data BugzillaContext = BugzillaContext+  { bzServer  :: BugzillaServer+  , bzManager :: Manager+  }++newtype BugzillaToken = BugzillaToken T.Text++instance FromJSON BugzillaToken where+  parseJSON (Object v) = BugzillaToken <$> v .: "token"+  parseJSON _          = mzero++-- | A session for Bugzilla queries. Use 'anonymousSession' and+-- 'loginSession', as appropriate, to create one.+data BugzillaSession = AnonymousSession BugzillaContext+                     | LoginSession BugzillaContext BugzillaToken++bzContext :: BugzillaSession -> BugzillaContext+bzContext (AnonymousSession ctx) = ctx+bzContext (LoginSession ctx _)   = ctx++data BugzillaException+  = BugzillaJSONParseError String+  | BugzillaAPIError Int String+  | BugzillaUnexpectedValue String+    deriving (Show, Typeable)+instance Exception BugzillaException++type QueryPart = (T.Text, Maybe T.Text)++requestUrl :: Request -> B.ByteString+requestUrl req = "https://" <> host req <> path req <> queryString req++sslRequest :: Request+sslRequest =+  defaultRequest {+    secure = True,+    port   = 443+  }++newBzRequest :: BugzillaSession -> [T.Text] -> QueryText -> Request+newBzRequest session methodParts query =+    sslRequest {+      host        = TE.encodeUtf8 . bzServer . bzContext $ session,+      path        = toByteString $ encodePathSegments $ "rest" : methodParts,+      queryString = toByteString $ renderQueryText True queryWithToken+    }+  where+    queryWithToken = case session of+                       AnonymousSession _                   -> query+                       LoginSession _ (BugzillaToken token) -> ("token", Just token) : query++data BzError = BzError Int String+               deriving (Eq, Show)++instance FromJSON BzError where+  parseJSON (Object v) = BzError <$> v .: "code"+                                 <*> v .: "message"+  parseJSON _          = mzero++handleError :: String -> BL.ByteString -> IO b+handleError parseError body = do+  let mError = eitherDecode body+  case mError of+    Left _                   -> throw $ BugzillaJSONParseError parseError+    Right (BzError code msg) -> throw $ BugzillaAPIError code msg++sendBzRequest :: FromJSON a => BugzillaSession -> Request -> IO a+sendBzRequest session req = runResourceT $ do+  response <- liftIO $ httpLbs req . bzManager . bzContext $ session+  let mResult = eitherDecode $ responseBody response+  case mResult of+    Left msg      -> liftIO $ handleError msg (responseBody response)+    Right decoded -> return decoded
+ src/Web/Bugzilla/RedHat/Internal/Search.hs view
@@ -0,0 +1,88 @@+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Operators which can be used to construct queries for Bugzilla.+--   These operators are intended to be typesafe: you should not be+--   able to construct a query that causes Bugzilla to return an+--   error. If you *are* able to construct an erroneous query, please+--   report a bug.+module Web.Bugzilla.RedHat.Internal.Search+( FieldType+, SearchTerm (..)+, SearchExpression (..)+, evalSearchExpr+) where++import Data.List+import qualified Data.Text as T+import Data.Time.Clock (UTCTime(..))+import Data.Time.ISO8601 (formatISO8601)++import Web.Bugzilla.RedHat.Internal.Network+import Web.Bugzilla.RedHat.Internal.Types++class FieldType a where fvAsText :: a -> T.Text++instance FieldType T.Text where fvAsText = id+instance FieldType Int where fvAsText = T.pack . show+instance FieldType UTCTime where fvAsText = T.pack . formatISO8601++instance FieldType Bool where+  fvAsText True  = "true"+  fvAsText False = "false"++instance FieldType a => FieldType [a] where+  fvAsText = T.intercalate "," . map fvAsText++data SearchTerm where+  UnaryOp  :: FieldType a => T.Text -> Field a -> SearchTerm+  BinaryOp :: (FieldType a, FieldType b) => T.Text -> Field a -> b -> SearchTerm++-- | A Boolean expression which can be used to query Bugzilla.+data SearchExpression+  = And [SearchExpression]+  | Or [SearchExpression]+  | Not SearchExpression+  | Term SearchTerm++taggedQueryPart :: Int -> Char -> T.Text -> QueryPart+taggedQueryPart t k v = (T.cons k . T.pack . show $ t, Just v)++termQuery :: FieldType b => Int -> Field a -> T.Text -> b -> [QueryPart]+termQuery t f o v = [taggedQueryPart t 'f' (searchFieldName f),+                     taggedQueryPart t 'o' o,+                     taggedQueryPart t 'v' (fvAsText v)]++evalSearchTerm :: Int -> SearchTerm -> [QueryPart]+evalSearchTerm t (UnaryOp op field)          = termQuery t field op ("" :: T.Text)+evalSearchTerm t (BinaryOp op field val)     = termQuery t field op val++evalSearchExpr :: SearchExpression -> [QueryPart]+evalSearchExpr e = snd $ evalSearchExpr' 1 e+  where+    evalExprGroup :: Int -> [SearchExpression] -> (Int, [QueryPart])+    evalExprGroup t es =+      let (subExprT, subExprQs) = foldl' evalSubExpr (t + 1, []) es+          qs = taggedQueryPart t 'f' "OP" :+               taggedQueryPart subExprT 'f' "CP" :+               subExprQs+      in (subExprT + 1, qs)++    evalSubExpr :: (Int, [QueryPart]) -> SearchExpression -> (Int, [QueryPart])+    evalSubExpr (t, qs) expr = let (nextT, qs') = evalSearchExpr' t expr+                               in  (nextT, qs ++ qs')++    evalSearchExpr' :: Int -> SearchExpression -> (Int, [QueryPart])+    evalSearchExpr' t (And es) = evalExprGroup t es++    evalSearchExpr' t (Or es) =+      let (groupT, groupQs) = evalExprGroup t es+          qs = taggedQueryPart t 'j' "OR" : groupQs+      in (groupT + 1, qs)++    evalSearchExpr' t (Not es) =+      let (groupT, groupQs) = evalSearchExpr' t es+          qs = taggedQueryPart t 'n' "1" : groupQs+      in (groupT + 1, qs)++    evalSearchExpr' t (Term term) = (t + 1, evalSearchTerm t term)
+ src/Web/Bugzilla/RedHat/Internal/Types.hs view
@@ -0,0 +1,685 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ViewPatterns #-}++module Web.Bugzilla.RedHat.Internal.Types+( BugId+, AttachmentId+, CommentId+, UserId+, EventId+, FlagId+, FlagType+, UserEmail+, Field (..)+, User (..)+, UserList (..)+, Flag (..)+, Bug (..)+, BugList (..)+, BugIdList (..)+, Attachment (..)+, AttachmentList (..)+, Comment (..)+, CommentList (..)+, History (..)+, HistoryEvent (..)+, Change (..)+, Modification (..)+, fieldName+, searchFieldName+) where++#if !MIN_VERSION_base(4,8,0)+import Control.Applicative (pure, (<$>), (<*>))+#endif+import Control.Monad (mzero)+import Data.Aeson+#if MIN_VERSION_aeson(1,0,0)+import Data.Aeson.Text+#else+import Data.Aeson.Encode+#endif+import Data.Aeson.Types+import qualified Data.HashMap.Strict as H+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as TLB+import qualified Data.Text.Read as TR+import qualified Data.Vector as V+import Data.Time.Clock (UTCTime(..))++type BugId        = Int+type AttachmentId = Int+type CommentId    = Int+type UserId       = Int+type EventId      = Int+type FlagId       = Int+type FlagType     = Int+type UserEmail    = T.Text++-- | A field which you can search by using 'Web.Bugzilla.searchBugs' or track+--   changes to using 'Web.Bugzilla.getHistory'. To get a human-readable name for+--   a field, use 'fieldName'.+data Field a where+  AliasField                    :: Field [T.Text]         -- Alias+  AssignedToField               :: Field UserEmail        -- Assignee+  AttachmentCreatorField        :: Field UserEmail        -- Attachment creator+  AttachmentDataField           :: Field T.Text           -- Attachment data+  AttachmentDescriptionField    :: Field T.Text           -- Attachment description+  AttachmentFilenameField       :: Field T.Text           -- Attachment filename+  AttachmentIsObsoleteField     :: Field Bool             -- Attachment is obsolete+  AttachmentIsPatchField        :: Field Bool             -- Attachment is patch+  AttachmentIsPrivateField      :: Field Bool             -- Attachment is private+  AttachmentMimetypeField       :: Field T.Text           -- Attachment mime type+  BlocksField                   :: Field Int              -- Blocks+  BugIdField                    :: Field Int              -- Bug ID+  CcField                       :: Field UserEmail        -- CC+  CcListAccessibleField         :: Field Bool             -- CC list accessible+  ClassificationField           :: Field T.Text           -- Classification+  CommentField                  :: Field T.Text           -- Comment+  CommentIsPrivateField         :: Field T.Text           -- Comment is private+  CommentTagsField              :: Field T.Text           -- Comment Tags+  CommenterField                :: Field UserEmail        -- Commenter+  ComponentField                :: Field [T.Text]         -- Component+  ContentField                  :: Field T.Text           -- Content+  CreationDateField             :: Field UTCTime          -- Creation date+  DaysElapsedField              :: Field Int              -- Days since bug changed+  DependsOnField                :: Field Int              -- Depends on+  EverConfirmedField            :: Field Bool             -- Ever confirmed+  FlagRequesteeField            :: Field UserEmail        -- Flag Requestee+  FlagSetterField               :: Field UserEmail        -- Flag Setter+  FlagsField                    :: Field T.Text           -- Flags+  GroupField                    :: Field T.Text           -- Group+  KeywordsField                 :: Field [T.Text]         -- Keywords+  ChangedField                  :: Field UTCTime          -- Changed+  CommentCountField             :: Field Int              -- Number of Comments+  OperatingSystemField          :: Field T.Text           -- OS+  HardwareField                 :: Field T.Text           -- Hardware+  PriorityField                 :: Field T.Text           -- Priority+  ProductField                  :: Field T.Text           -- Product+  QaContactField                :: Field UserEmail        -- QA Contact+  ReporterField                 :: Field UserEmail        -- Reporter+  ReporterAccessibleField       :: Field Bool             -- Reporter accessible+  ResolutionField               :: Field T.Text           -- Resolution+  RestrictCommentsField         :: Field Bool             -- Restrict Comments+  SeeAlsoField                  :: Field T.Text           -- See Also+  SeverityField                 :: Field T.Text           -- Severity+  StatusField                   :: Field T.Text           -- Status+  WhiteboardField               :: Field T.Text           -- Whiteboard+  SummaryField                  :: Field T.Text           -- Summary+  TagsField                     :: Field T.Text           -- Tags+  TargetMilestoneField          :: Field T.Text           -- Target Milestone+  TimeSinceAssigneeTouchedField :: Field Int              -- Time Since Assignee Touched+  BugURLField                   :: Field T.Text           -- URL+  VersionField                  :: Field T.Text           -- Version+  VotesField                    :: Field T.Text           -- Votes+  CustomField                   :: T.Text -> Field T.Text -- (Custom fields)++instance Eq (Field a) where+  (CustomField a) == (CustomField b) = a == b+  (CustomField _) == _               = False+  _ == (CustomField _)               = False+  a == b                             = searchFieldName a == searchFieldName b++instance Show (Field a) where+  show AliasField                    = "AliasField"+  show AssignedToField               = "AssignedToField"+  show AttachmentCreatorField        = "AttachmentCreatorField"+  show AttachmentDataField           = "AttachmentDataField"+  show AttachmentDescriptionField    = "AttachmentDescriptionField"+  show AttachmentFilenameField       = "AttachmentFilenameField"+  show AttachmentIsObsoleteField     = "AttachmentIsObsoleteField"+  show AttachmentIsPatchField        = "AttachmentIsPatchField"+  show AttachmentIsPrivateField      = "AttachmentIsPrivateField"+  show AttachmentMimetypeField       = "AttachmentMimetypeField"+  show BlocksField                   = "BlocksField"+  show BugIdField                    = "BugIdField"+  show CcField                       = "CcField"+  show CcListAccessibleField         = "CcListAccessibleField"+  show ClassificationField           = "ClassificationField"+  show CommentField                  = "CommentField"+  show CommentIsPrivateField         = "CommentIsPrivateField"+  show CommentTagsField              = "CommentTagsField"+  show CommenterField                = "CommenterField"+  show ComponentField                = "ComponentField"+  show ContentField                  = "ContentField"+  show CreationDateField             = "CreationDateField"+  show DaysElapsedField              = "DaysElapsedField"+  show DependsOnField                = "DependsOnField"+  show EverConfirmedField            = "EverConfirmedField"+  show FlagRequesteeField            = "FlagRequesteeField"+  show FlagSetterField               = "FlagSetterField"+  show FlagsField                    = "FlagsField"+  show GroupField                    = "GroupField"+  show KeywordsField                 = "KeywordsField"+  show ChangedField                  = "ChangedField"+  show CommentCountField             = "CommentCountField"+  show OperatingSystemField          = "OperatingSystemField"+  show HardwareField                 = "HardwareField"+  show PriorityField                 = "PriorityField"+  show ProductField                  = "ProductField"+  show QaContactField                = "QaContactField"+  show ReporterField                 = "ReporterField"+  show ReporterAccessibleField       = "ReporterAccessibleField"+  show ResolutionField               = "ResolutionField"+  show RestrictCommentsField         = "RestrictCommentsField"+  show SeeAlsoField                  = "SeeAlsoField"+  show SeverityField                 = "SeverityField"+  show StatusField                   = "StatusField"+  show WhiteboardField               = "WhiteboardField"+  show SummaryField                  = "SummaryField"+  show TagsField                     = "TagsField"+  show TargetMilestoneField          = "TargetMilestoneField"+  show TimeSinceAssigneeTouchedField = "TimeSinceAssigneeTouchedField"+  show BugURLField                   = "BugURLField"+  show VersionField                  = "VersionField"+  show VotesField                    = "VotesField"+  show (CustomField name)            = "CustomField " ++ show name++-- | Provides a human-readable name for a 'Field'.+fieldName :: Field a -> T.Text+fieldName AliasField                    = "Alias"+fieldName AssignedToField               = "Assigned to"+fieldName AttachmentCreatorField        = "Attachment creator"+fieldName AttachmentDataField           = "Attachment data"+fieldName AttachmentDescriptionField    = "Attachment description"+fieldName AttachmentFilenameField       = "Attachment filename"+fieldName AttachmentIsObsoleteField     = "Attachment is obsolete"+fieldName AttachmentIsPatchField        = "Attachment is patch"+fieldName AttachmentIsPrivateField      = "Attachment is private"+fieldName AttachmentMimetypeField       = "Attachment MIME type"+fieldName BlocksField                   = "Blocks"+fieldName BugIdField                    = "BugId"+fieldName CcField                       = "CC"+fieldName CcListAccessibleField         = "CC list is accessible"+fieldName ClassificationField           = "Classification"+fieldName CommentField                  = "Comment"+fieldName CommentIsPrivateField         = "Comment is private"+fieldName CommentTagsField              = "Comment tags"+fieldName CommenterField                = "Commenter"+fieldName ComponentField                = "Component"+fieldName ContentField                  = "Content"+fieldName CreationDateField             = "Creation date"+fieldName DaysElapsedField              = "Days elapsed"+fieldName DependsOnField                = "Depends on"+fieldName EverConfirmedField            = "Ever confirmed"+fieldName FlagRequesteeField            = "Flag requestee"+fieldName FlagSetterField               = "Flag setter"+fieldName FlagsField                    = "Flags"+fieldName GroupField                    = "Group"+fieldName KeywordsField                 = "Keywords"+fieldName ChangedField                  = "Changed"+fieldName CommentCountField             = "Comment count"+fieldName OperatingSystemField          = "Operating system"+fieldName HardwareField                 = "Hardware"+fieldName PriorityField                 = "Priority"+fieldName ProductField                  = "Product"+fieldName QaContactField                = "QA contact"+fieldName ReporterField                 = "Reporter"+fieldName ReporterAccessibleField       = "Reporter accessible"+fieldName ResolutionField               = "Resolution"+fieldName RestrictCommentsField         = "Restrict comments"+fieldName SeeAlsoField                  = "See also"+fieldName SeverityField                 = "Severity"+fieldName StatusField                   = "Status"+fieldName WhiteboardField               = "Whiteboard"+fieldName SummaryField                  = "Summary"+fieldName TagsField                     = "Tags"+fieldName TargetMilestoneField          = "Target milestone"+fieldName TimeSinceAssigneeTouchedField = "Time since assignee touched"+fieldName BugURLField                   = "Bug URL"+fieldName VersionField                  = "Version"+fieldName VotesField                    = "Votes"+fieldName (CustomField name)            = T.concat ["Custom field \"", T.pack (show name), "\""]++searchFieldName :: Field a -> T.Text+searchFieldName AliasField                    = "alias"+searchFieldName AssignedToField               = "assigned_to"+searchFieldName AttachmentCreatorField        = "attachments.submitter"+searchFieldName AttachmentDataField           = "attach_data.thedata"+searchFieldName AttachmentDescriptionField    = "attachments.description"+searchFieldName AttachmentFilenameField       = "attachments.filename"+searchFieldName AttachmentIsObsoleteField     = "attachments.isobsolete"+searchFieldName AttachmentIsPatchField        = "attachments.ispatch"+searchFieldName AttachmentIsPrivateField      = "attachments.isprivate"+searchFieldName AttachmentMimetypeField       = "attachments.mimetype"+searchFieldName BlocksField                   = "blocked"+searchFieldName BugIdField                    = "bug_id"+searchFieldName CcField                       = "cc"+searchFieldName CcListAccessibleField         = "cclist_accessible"+searchFieldName ClassificationField           = "classification"+searchFieldName CommentField                  = "longdesc"+searchFieldName CommentIsPrivateField         = "longdescs.isprivate"+searchFieldName CommentTagsField              = "comment_tag"+searchFieldName CommenterField                = "commenter"+searchFieldName ComponentField                = "component"+searchFieldName ContentField                  = "content"+searchFieldName CreationDateField             = "creation_ts"+searchFieldName DaysElapsedField              = "days_elapsed"+searchFieldName DependsOnField                = "dependson"+searchFieldName EverConfirmedField            = "everconfirmed"+searchFieldName FlagRequesteeField            = "requestees.login_name"+searchFieldName FlagSetterField               = "setters.login_name"+searchFieldName FlagsField                    = "flagtypes.name"+searchFieldName GroupField                    = "bug_group"+searchFieldName KeywordsField                 = "keywords"+searchFieldName ChangedField                  = "delta_ts"+searchFieldName CommentCountField             = "longdescs.count"+searchFieldName OperatingSystemField          = "op_sys"+searchFieldName HardwareField                 = "rep_platform"+searchFieldName PriorityField                 = "priority"+searchFieldName ProductField                  = "product"+searchFieldName QaContactField                = "qa_contact"+searchFieldName ReporterField                 = "reporter"+searchFieldName ReporterAccessibleField       = "reporter_accessible"+searchFieldName ResolutionField               = "resolution"+searchFieldName RestrictCommentsField         = "restrict_comments"+searchFieldName SeeAlsoField                  = "see_also"+searchFieldName SeverityField                 = "bug_severity"+searchFieldName StatusField                   = "bug_status"+searchFieldName WhiteboardField               = "status_whiteboard"+searchFieldName SummaryField                  = "short_desc"+searchFieldName TagsField                     = "tag"+searchFieldName TargetMilestoneField          = "target_milestone"+searchFieldName TimeSinceAssigneeTouchedField = "owner_idle_time"+searchFieldName BugURLField                   = "bug_file_loc"+searchFieldName VersionField                  = "version"+searchFieldName VotesField                    = "votes"+searchFieldName (CustomField name)            = name++-- | A Bugzilla user.+data User = User+  { userId       :: !UserId+  , userEmail    :: Maybe UserEmail+  , userName     :: T.Text+  , userRealName :: T.Text+  } deriving (Eq, Ord, Show)++instance FromJSON User where+  parseJSON (Object v) =+    User <$> v .: "id"+         <*> v .:? "email"+         <*> v .: "name"+         <*> v .: "real_name"+  parseJSON _ = mzero++newtype UserList = UserList [User]+                deriving (Eq, Show)++instance FromJSON UserList where+  parseJSON (Object v) = UserList <$> v .: "users"+  parseJSON _          = mzero++-- | Flags, which may be set on an attachment or on a bug directly.+data Flag = Flag+  { flagId               :: !FlagId+  , flagTypeId           :: !FlagType+  , flagName             :: T.Text+  , flagSetter           :: UserEmail+  , flagStatus           :: T.Text+  , flagCreationDate     :: UTCTime+  , flagModificationDate :: UTCTime+  , flagRequestee        :: Maybe UserEmail+  } deriving (Eq, Ord, Show)++instance FromJSON Flag where+  parseJSON (Object v) =+    Flag <$> v .: "id"+         <*> v .: "type_id"+         <*> v .: "name"+         <*> v .: "setter"+         <*> v .: "status"+         <*> v .: "creation_date"+         <*> v .: "modification_date"+         <*> v .:? "requestee"+  parseJSON _ = mzero++-- | A Bugzilla bug.+data Bug = Bug+  { bugId                  :: !BugId+  , bugAlias               :: Maybe [T.Text]+  , bugAssignedTo          :: UserEmail+  , bugAssignedToDetail    :: User+  , bugBlocks              :: [BugId]+  , bugCc                  :: [UserEmail]+  , bugCcDetail            :: [User]+  , bugClassification      :: T.Text+  , bugComponent           :: [T.Text]+  , bugCreationTime        :: UTCTime+  , bugCreator             :: UserEmail+  , bugCreatorDetail       :: User+  , bugDependsOn           :: [BugId]+  , bugDupeOf              :: Maybe BugId+  , bugFlags               :: Maybe [Flag]+  , bugGroups              :: [T.Text]+  , bugIsCcAccessible      :: Bool+  , bugIsConfirmed         :: Bool+  , bugIsCreatorAccessible :: Bool+  , bugIsOpen              :: Bool+  , bugKeywords            :: [T.Text]+  , bugLastChangeTime      :: UTCTime+  , bugOpSys               :: T.Text+  , bugPlatform            :: T.Text+  , bugPriority            :: T.Text+  , bugProduct             :: T.Text+  , bugQaContact           :: UserEmail+  , bugResolution          :: T.Text+  , bugSeeAlso             :: [T.Text]+  , bugSeverity            :: T.Text+  , bugStatus              :: T.Text+  , bugSummary             :: T.Text+  , bugTargetMilestone     :: T.Text+  , bugUrl                 :: T.Text+  , bugVersion             :: [T.Text]+  , bugWhiteboard          :: T.Text+  , bugCustomFields        :: H.HashMap T.Text T.Text+  } deriving (Eq, Show)++instance FromJSON Bug where+  parseJSON (Object v) =+      Bug <$> v .: "id"+          <*> v .:? "alias"+          <*> v .: "assigned_to"+          <*> v .: "assigned_to_detail"+          <*> v .: "blocks"+          <*> v .: "cc"+          <*> v .: "cc_detail"+          <*> v .: "classification"+          <*> v .: "component"+          <*> v .: "creation_time"+          <*> v .: "creator"+          <*> v .: "creator_detail"+          <*> v .: "depends_on"+          <*> v .:? "dupe_of"+          <*> v .:? "flags"+          <*> v .: "groups"+          <*> v .: "is_cc_accessible"+          <*> v .: "is_confirmed"+          <*> v .: "is_creator_accessible"+          <*> v .: "is_open"+          <*> v .: "keywords"+          <*> v .: "last_change_time"+          <*> v .: "op_sys"+          <*> v .: "platform"+          <*> v .: "priority"+          <*> v .: "product"+          <*> v .: "qa_contact"+          <*> v .: "resolution"+          <*> v .: "see_also"+          <*> v .: "severity"+          <*> v .: "status"+          <*> v .: "summary"+          <*> v .: "target_milestone"+          <*> v .: "url"+          <*> v .: "version"+          <*> v .: "whiteboard"+          <*> pure (customFields v)+  parseJSON _ = mzero++customFields :: Object -> H.HashMap T.Text T.Text+customFields = H.map stringifyCustomFields+             . H.filterWithKey filterCustomFields+  where+    stringifyCustomFields :: Value -> T.Text+    stringifyCustomFields (String t) = t+    stringifyCustomFields v          = T.concat+                                     . TL.toChunks+                                     . TLB.toLazyText+                                     . encodeToTextBuilder+                                     . toJSON+                                     $ v++    filterCustomFields k _ = "cf_" `T.isPrefixOf` k++newtype BugList = BugList [Bug]+               deriving (Eq, Show)++instance FromJSON BugList where+  parseJSON (Object v) = BugList <$> v .: "bugs"+  parseJSON _          = mzero++newtype BugIdList = BugIdList [BugId]+                 deriving (Eq, Show)++instance FromJSON BugIdList where+  parseJSON (Object v) = do+    bugs <- v .: "bugs"+    bugIds <- mapM (.: "id") bugs+    return $ BugIdList bugIds+  parseJSON _          = mzero++-- | An attachment to a bug.+data Attachment = Attachment+  { attachmentId             :: !AttachmentId+  , attachmentBugId          :: !BugId+  , attachmentFileName       :: T.Text+  , attachmentSummary        :: T.Text+  , attachmentCreator        :: UserEmail+  , attachmentIsPrivate      :: Bool+  , attachmentIsObsolete     :: Bool+  , attachmentIsPatch        :: Bool+  , attachmentFlags          :: [Flag]+  , attachmentCreationTime   :: UTCTime+  , attachmentLastChangeTime :: UTCTime+  , attachmentContentType    :: T.Text+  , attachmentSize           :: !Int+  , attachmentData           :: T.Text+  } deriving (Eq, Show)++instance FromJSON Attachment where+  parseJSON (Object v) =+    Attachment <$> v .: "id"+               <*> v .: "bug_id"+               <*> v .: "file_name"+               <*> v .: "summary"+               <*> v .: "creator"+               <*> (fromNumericBool <$> v .: "is_private")+               <*> (fromNumericBool <$> v .: "is_obsolete")+               <*> (fromNumericBool <$> v .: "is_patch")+               <*> v .: "flags"+               <*> v .: "creation_time"+               <*> v .: "last_change_time"+               <*> v .: "content_type"+               <*> v .: "size"+               <*> v .: "data"+  parseJSON _ = mzero++fromNumericBool :: Int -> Bool+fromNumericBool 0 = False+fromNumericBool _ = True++newtype AttachmentList = AttachmentList [Attachment]+                      deriving (Eq, Show)++instance FromJSON AttachmentList where+  parseJSON (Object v) = do+    attachmentsVal <- v .: "attachments"+    bugsVal <- v .: "bugs"+    case (attachmentsVal, bugsVal) of+      (Object (H.toList -> [(_, as)]), _) -> AttachmentList . (:[]) <$> parseJSON as+      (_, Object (H.toList -> [(_, as)])) -> AttachmentList <$> parseJSON as+      _                                   -> mzero+  parseJSON _ = mzero++-- | A bug comment. To display these the way Bugzilla does, you'll+-- need to call 'getUser' and use the 'userRealName' for each user.+data Comment = Comment+  { commentId           :: !CommentId+  , commentBugId        :: !BugId+  , commentAttachmentId :: Maybe AttachmentId+  , commentCount        :: !Int+  , commentText         :: T.Text+  , commentCreator      :: UserEmail+  , commentCreationTime :: UTCTime+  , commentIsPrivate    :: Bool+  } deriving (Eq, Show)++instance FromJSON Comment where+  parseJSON (Object v) =+    Comment <$> v .: "id"+            <*> v .: "bug_id"+            <*> v .: "attachment_id"+            <*> v .: "count"+            <*> v .: "text"+            <*> v .: "creator"+            <*> v .: "creation_time"+            <*> v .: "is_private"+  parseJSON _ = mzero++newtype CommentList = CommentList [Comment]+                   deriving (Eq, Show)++instance FromJSON CommentList where+  parseJSON (Object v) = do+    bugsVal <- v .: "bugs"+    case bugsVal of+      Object (H.toList -> [(_, cs)]) ->+        do comments <- withObject "comments" (.: "comments") cs+           withArray "comment list" (\a -> CommentList <$> parseJSON (addCount a)) comments+      _ -> mzero+  parseJSON _ = mzero++-- Note that we make the (possibly unwise) assumption that Bugzilla+-- returns the comments in order. If it turns out that's not true, we+-- can always sort by their 'id' to ensure correct results.+addCount :: V.Vector Value -> Value+addCount vs = Array $ V.zipWith addCount' (V.enumFromN 0 $ V.length vs) vs+ where+   addCount' :: Int -> Value -> Value+   addCount' c (Object v) = Object $ H.insert "count" (Number $ fromIntegral c) v+   addCount' _ v          = v++-- | History information for a bug.+data History = History+  { historyBugId   :: !BugId+  , historyEvents  :: [HistoryEvent]+  } deriving (Eq, Show)++instance FromJSON History where+  parseJSON (Object v) = do+    bugsVal <- v .: "bugs"+    case bugsVal of+      Array (V.toList -> [history]) ->+        withObject "history"+                   (\h -> History <$> h .: "id"+                                  <*> parseHistoryEvents h)+                   history+      _ -> mzero+  parseJSON _ = mzero++parseHistoryEvents :: Object -> Parser [HistoryEvent]+parseHistoryEvents h = do+  events <- h .: "history"+  withArray "event list" (parseJSON . addCount) events++-- | An event in a bug's history.+data HistoryEvent = HistoryEvent+  { historyEventId      :: EventId   -- ^ A sequential event id.+  , historyEventTime    :: UTCTime   -- ^ When the event occurred.+  , historyEventUser    :: UserEmail -- ^ Which user was responsible.+  , historyEventChanges :: [Change]  -- ^ All the changes which are+                                     --   part of this event.+  } deriving (Eq, Show)++instance FromJSON HistoryEvent where+  parseJSON (Object v) =+    HistoryEvent <$> v .: "count"+                 <*> v .: "when"+                 <*> v .: "who"+                 <*> v .: "changes"+  parseJSON _ = mzero++-- | A single change which is part of an event. Different constructors+--   are used according to the type of the field. The 'Modification'+--   describes the value of the field before and after the change.+data Change+  = TextFieldChange (Field T.Text) (Modification T.Text)+  | ListFieldChange (Field [T.Text]) (Modification [T.Text])+  | IntFieldChange (Field Int) (Modification Int)+  | TimeFieldChange (Field UTCTime) (Modification UTCTime)+  | BoolFieldChange (Field Bool) (Modification Bool)+    deriving (Eq, Show)++instance FromJSON Change where+  parseJSON (Object v) = do+    changedField <- v .: "field_name"+    case changedField of+      "alias"                  -> ListFieldChange AliasField <$> parseModification v+      "assigned_to"            -> TextFieldChange AssignedToField <$> parseModification v+      "attachments.submitter"  -> TextFieldChange AttachmentCreatorField <$> parseModification v+      "attach_data.thedata"    -> TextFieldChange AttachmentDataField <$> parseModification v+      "attachments.description"-> TextFieldChange AttachmentDescriptionField <$> parseModification v+      "attachments.filename"   -> TextFieldChange AttachmentFilenameField <$> parseModification v+      "attachments.isobsolete" -> BoolFieldChange AttachmentIsObsoleteField <$> parseModification v+      "attachments.ispatch"    -> BoolFieldChange AttachmentIsPatchField <$> parseModification v+      "attachments.isprivate"  -> BoolFieldChange AttachmentIsPrivateField <$> parseModification v+      "attachments.mimetype"   -> TextFieldChange AttachmentMimetypeField <$> parseModification v+      "blocks"                 -> IntFieldChange BlocksField <$> parseModification v+      "bug_id"                 -> IntFieldChange BugIdField <$> parseModification v+      "cc"                     -> TextFieldChange CcField <$> parseModification v+      "is_cc_accessible"       -> BoolFieldChange CcListAccessibleField <$> parseModification v+      "classification"         -> TextFieldChange ClassificationField <$> parseModification v+      "component"              -> ListFieldChange ComponentField <$> parseModification v+      "content"                -> TextFieldChange ContentField <$> parseModification v+      "creation_time"          -> TimeFieldChange CreationDateField <$> parseModification v+      "days_elapsed"           -> IntFieldChange DaysElapsedField <$> parseModification v+      "depends_on"             -> IntFieldChange DependsOnField <$> parseModification v+      "everconfirmed"          -> BoolFieldChange EverConfirmedField <$> parseModification v+      "flagtypes.name"         -> TextFieldChange FlagsField <$> parseModification v+      "bug_group"              -> TextFieldChange GroupField <$> parseModification v+      "keywords"               -> ListFieldChange KeywordsField <$> parseModification v+      "op_sys"                 -> TextFieldChange OperatingSystemField <$> parseModification v+      "platform"               -> TextFieldChange HardwareField <$> parseModification v+      "priority"               -> TextFieldChange PriorityField <$> parseModification v+      "product"                -> TextFieldChange ProductField <$> parseModification v+      "qa_contact"             -> TextFieldChange QaContactField <$> parseModification v+      "reporter"               -> TextFieldChange ReporterField <$> parseModification v+      "reporter_accessible"    -> BoolFieldChange ReporterAccessibleField <$> parseModification v+      "resolution"             -> TextFieldChange ResolutionField <$> parseModification v+      "restrict_comments"      -> BoolFieldChange RestrictCommentsField <$> parseModification v+      "see_also"               -> TextFieldChange SeeAlsoField <$> parseModification v+      "severity"               -> TextFieldChange SeverityField <$> parseModification v+      "status"                 -> TextFieldChange StatusField <$> parseModification v+      "whiteboard"             -> TextFieldChange WhiteboardField <$> parseModification v+      "summary"                -> TextFieldChange SummaryField <$> parseModification v+      "tag"                    -> TextFieldChange TagsField <$> parseModification v+      "target_milestone"       -> TextFieldChange TargetMilestoneField <$> parseModification v+      "url"                    -> TextFieldChange BugURLField <$> parseModification v+      "version"                -> TextFieldChange VersionField <$> parseModification v+      "votes"                  -> TextFieldChange VotesField <$> parseModification v+      name                     -> TextFieldChange (CustomField name) <$> parseModification v+  parseJSON _ = mzero++-- | A description of how a field changed during a 'HistoryEvent'.+data (Eq a, Show a) => Modification a = Modification+  { modRemoved      :: Maybe a+  , modAdded        :: Maybe a+  , modAttachmentId :: Maybe AttachmentId+  } deriving (Eq, Show)++parseModification :: (FromJSON a, Eq b, Show b, ToModification a b) => Object -> Parser (Modification b)+parseModification v = Modification <$> (toMod =<< v .: "removed")+                                   <*> (toMod =<< v .: "added")+                                   <*> v .:? "attachment_id"++class ToModification a b | b -> a where toMod :: a -> Parser (Maybe b)+instance ToModification T.Text T.Text where toMod = return . Just+instance ToModification UTCTime UTCTime where toMod = return . Just++instance ToModification T.Text Int where+  toMod v | v == "" = return Nothing+          | otherwise = case TR.decimal v of+                          Left _       -> mzero+                          Right (i, _) -> return $ Just i++instance ToModification T.Text Bool where+  toMod v | v == "0"  = return $ Just False+          | v == "1"  = return $ Just True+          | otherwise = mzero++instance ToModification T.Text [T.Text] where+  toMod v = return . Just $ T.splitOn ", " v
+ src/Web/Bugzilla/RedHat/Search.hs view
@@ -0,0 +1,121 @@+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}++module Web.Bugzilla.RedHat.Search+(+  -- * Search operators+  (.==.)+, (./=.)+, (.<.)+, (.<=.)+, (.>.)+, (.>=.)+, (.=~.)+, (./=~.)+, equalsAny+, contains+, containsCase+, containsAny+, containsAll+, changedBefore+, changedAfter+, changedFrom+, changedTo+, changedBy+, contentMatches+, isEmpty+, (.&&.)+, (.||.)+, not'++  -- * Search expressions+, Field (..)+, SearchExpression+, evalSearchExpr+) where++import qualified Data.Text as T+import Data.Time.Clock (UTCTime(..))++import Web.Bugzilla.RedHat.Internal.Search+import Web.Bugzilla.RedHat.Internal.Types++(.==.) :: FieldType a => Field a -> a -> SearchExpression+(.==.) = (Term .) . BinaryOp "equals"+infix 4 .==.++(./=.) :: FieldType a => Field a -> a -> SearchExpression+(./=.) = (Term .) . BinaryOp "notequals"+infix 4 ./=.++(.<.) :: FieldType a => Field a -> a -> SearchExpression+(.<.) = (Term .) . BinaryOp "lessthan"+infix 4 .<.++(.<=.) :: FieldType a => Field a -> a -> SearchExpression+(.<=.) = (Term .) . BinaryOp "lessthaneq"+infix 4 .<=.++(.>.) :: FieldType a => Field a -> a -> SearchExpression+(.>.) = (Term .) . BinaryOp "greaterthan"+infix 4 .>.++(.>=.) :: FieldType a => Field a -> a -> SearchExpression+(.>=.) = (Term .) . BinaryOp "greaterthaneq"+infix 4 .>=.++(.=~.) :: FieldType a => Field a -> a -> SearchExpression+(.=~.) = (Term .) . BinaryOp "regexp"++(./=~.) :: FieldType a => Field a -> a -> SearchExpression+(./=~.) = (Term .) . BinaryOp "notregexp"++equalsAny :: FieldType a => Field a -> [a] -> SearchExpression+equalsAny = (Term .) . BinaryOp "anyexact"++contains :: Field T.Text -> T.Text -> SearchExpression+contains = (Term .) . BinaryOp "substring"++containsCase :: Field T.Text -> T.Text -> SearchExpression+containsCase = (Term .) . BinaryOp "casesubstring"++containsAny :: Field T.Text -> [T.Text] -> SearchExpression+containsAny = (Term .) . BinaryOp "anywordssubstr"++containsAll :: Field T.Text -> [T.Text] -> SearchExpression+containsAll = (Term .) . BinaryOp "allwordssubstr"++changedBefore :: FieldType a => Field a -> UTCTime -> SearchExpression+changedBefore = (Term .) . BinaryOp "changedbefore"++changedAfter :: FieldType a => Field a -> UTCTime -> SearchExpression+changedAfter = (Term .) . BinaryOp "changedafter"++changedFrom :: FieldType a => Field a -> a -> SearchExpression+changedFrom = (Term .) . BinaryOp "changedfrom"++changedTo :: FieldType a => Field a -> a -> SearchExpression+changedTo = (Term .) . BinaryOp "changedto"++changedBy :: FieldType a => Field a -> UserEmail -> SearchExpression+changedBy = (Term .) . BinaryOp "changedby"++contentMatches :: T.Text -> SearchExpression+contentMatches = Term . BinaryOp "matches" ContentField++isEmpty :: FieldType a => Field a -> SearchExpression+isEmpty = Term . UnaryOp "isempty"++(.&&.) :: SearchExpression -> SearchExpression -> SearchExpression+(.&&.) a (And as) = And (a:as)+(.&&.) a b        = And [a, b]+infixr 3 .&&.++(.||.) :: SearchExpression -> SearchExpression -> SearchExpression+(.||.) a (Or as) = Or (a:as)+(.||.) a b       = Or [a, b]+infixr 2 .||.++not' :: SearchExpression -> SearchExpression+not' (Not a) = a+not' a       = Not a