packages feed

gerrit (empty) → 0.1.0.0

raw patch · 18 files changed

+1527/−0 lines, 18 filesdep +aesondep +basedep +bytestring

Dependencies added: aeson, base, bytestring, containers, directory, gerrit, http-client, http-client-tls, tasty, tasty-hunit, text

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Changelog++## 0.1.0.0 (2021-05-30)++- Basic Gerrit API data types.
+ LICENSE view
@@ -0,0 +1,201 @@+                                 Apache License+                           Version 2.0, January 2004+                        http://www.apache.org/licenses/++   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++   1. Definitions.++      "License" shall mean the terms and conditions for use, reproduction,+      and distribution as defined by Sections 1 through 9 of this document.++      "Licensor" shall mean the copyright owner or entity authorized by+      the copyright owner that is granting the License.++      "Legal Entity" shall mean the union of the acting entity and all+      other entities that control, are controlled by, or are under common+      control with that entity. For the purposes of this definition,+      "control" means (i) the power, direct or indirect, to cause the+      direction or management of such entity, whether by contract or+      otherwise, or (ii) ownership of fifty percent (50%) or more of the+      outstanding shares, or (iii) beneficial ownership of such entity.++      "You" (or "Your") shall mean an individual or Legal Entity+      exercising permissions granted by this License.++      "Source" form shall mean the preferred form for making modifications,+      including but not limited to software source code, documentation+      source, and configuration files.++      "Object" form shall mean any form resulting from mechanical+      transformation or translation of a Source form, including but+      not limited to compiled object code, generated documentation,+      and conversions to other media types.++      "Work" shall mean the work of authorship, whether in Source or+      Object form, made available under the License, as indicated by a+      copyright notice that is included in or attached to the work+      (an example is provided in the Appendix below).++      "Derivative Works" shall mean any work, whether in Source or Object+      form, that is based on (or derived from) the Work and for which the+      editorial revisions, annotations, elaborations, or other modifications+      represent, as a whole, an original work of authorship. For the purposes+      of this License, Derivative Works shall not include works that remain+      separable from, or merely link (or bind by name) to the interfaces of,+      the Work and Derivative Works thereof.++      "Contribution" shall mean any work of authorship, including+      the original version of the Work and any modifications or additions+      to that Work or Derivative Works thereof, that is intentionally+      submitted to Licensor for inclusion in the Work by the copyright owner+      or by an individual or Legal Entity authorized to submit on behalf of+      the copyright owner. For the purposes of this definition, "submitted"+      means any form of electronic, verbal, or written communication sent+      to the Licensor or its representatives, including but not limited to+      communication on electronic mailing lists, source code control systems,+      and issue tracking systems that are managed by, or on behalf of, the+      Licensor for the purpose of discussing and improving the Work, but+      excluding communication that is conspicuously marked or otherwise+      designated in writing by the copyright owner as "Not a Contribution."++      "Contributor" shall mean Licensor and any individual or Legal Entity+      on behalf of whom a Contribution has been received by Licensor and+      subsequently incorporated within the Work.++   2. Grant of Copyright License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      copyright license to reproduce, prepare Derivative Works of,+      publicly display, publicly perform, sublicense, and distribute the+      Work and such Derivative Works in Source or Object form.++   3. Grant of Patent License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      (except as stated in this section) patent license to make, have made,+      use, offer to sell, sell, import, and otherwise transfer the Work,+      where such license applies only to those patent claims licensable+      by such Contributor that are necessarily infringed by their+      Contribution(s) alone or by combination of their Contribution(s)+      with the Work to which such Contribution(s) was submitted. If You+      institute patent litigation against any entity (including a+      cross-claim or counterclaim in a lawsuit) alleging that the Work+      or a Contribution incorporated within the Work constitutes direct+      or contributory patent infringement, then any patent licenses+      granted to You under this License for that Work shall terminate+      as of the date such litigation is filed.++   4. Redistribution. You may reproduce and distribute copies of the+      Work or Derivative Works thereof in any medium, with or without+      modifications, and in Source or Object form, provided that You+      meet the following conditions:++      (a) You must give any other recipients of the Work or+          Derivative Works a copy of this License; and++      (b) You must cause any modified files to carry prominent notices+          stating that You changed the files; and++      (c) You must retain, in the Source form of any Derivative Works+          that You distribute, all copyright, patent, trademark, and+          attribution notices from the Source form of the Work,+          excluding those notices that do not pertain to any part of+          the Derivative Works; and++      (d) If the Work includes a "NOTICE" text file as part of its+          distribution, then any Derivative Works that You distribute must+          include a readable copy of the attribution notices contained+          within such NOTICE file, excluding those notices that do not+          pertain to any part of the Derivative Works, in at least one+          of the following places: within a NOTICE text file distributed+          as part of the Derivative Works; within the Source form or+          documentation, if provided along with the Derivative Works; or,+          within a display generated by the Derivative Works, if and+          wherever such third-party notices normally appear. The contents+          of the NOTICE file are for informational purposes only and+          do not modify the License. You may add Your own attribution+          notices within Derivative Works that You distribute, alongside+          or as an addendum to the NOTICE text from the Work, provided+          that such additional attribution notices cannot be construed+          as modifying the License.++      You may add Your own copyright statement to Your modifications and+      may provide additional or different license terms and conditions+      for use, reproduction, or distribution of Your modifications, or+      for any such Derivative Works as a whole, provided Your use,+      reproduction, and distribution of the Work otherwise complies with+      the conditions stated in this License.++   5. Submission of Contributions. Unless You explicitly state otherwise,+      any Contribution intentionally submitted for inclusion in the Work+      by You to the Licensor shall be under the terms and conditions of+      this License, without any additional terms or conditions.+      Notwithstanding the above, nothing herein shall supersede or modify+      the terms of any separate license agreement you may have executed+      with Licensor regarding such Contributions.++   6. Trademarks. This License does not grant permission to use the trade+      names, trademarks, service marks, or product names of the Licensor,+      except as required for reasonable and customary use in describing the+      origin of the Work and reproducing the content of the NOTICE file.++   7. Disclaimer of Warranty. Unless required by applicable law or+      agreed to in writing, Licensor provides the Work (and each+      Contributor provides its Contributions) on an "AS IS" BASIS,+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+      implied, including, without limitation, any warranties or conditions+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+      PARTICULAR PURPOSE. You are solely responsible for determining the+      appropriateness of using or redistributing the Work and assume any+      risks associated with Your exercise of permissions under this License.++   8. Limitation of Liability. In no event and under no legal theory,+      whether in tort (including negligence), contract, or otherwise,+      unless required by applicable law (such as deliberate and grossly+      negligent acts) or agreed to in writing, shall any Contributor be+      liable to You for damages, including any direct, indirect, special,+      incidental, or consequential damages of any character arising as a+      result of this License or out of the use or inability to use the+      Work (including but not limited to damages for loss of goodwill,+      work stoppage, computer failure or malfunction, or any and all+      other commercial damages or losses), even if such Contributor+      has been advised of the possibility of such damages.++   9. Accepting Warranty or Additional Liability. While redistributing+      the Work or Derivative Works thereof, You may choose to offer,+      and charge a fee for, acceptance of support, warranty, indemnity,+      or other liability obligations and/or rights consistent with this+      License. However, in accepting such obligations, You may act only+      on Your own behalf and on Your sole responsibility, not on behalf+      of any other Contributor, and only if You agree to indemnify,+      defend, and hold each Contributor harmless for any liability+      incurred by, or claims asserted against, such Contributor by reason+      of your accepting any such warranty or additional liability.++   END OF TERMS AND CONDITIONS++   APPENDIX: How to apply the Apache License to your work.++      To apply the Apache License to your work, attach the following+      boilerplate notice, with the fields enclosed by brackets "[]"+      replaced with your own identifying information. (Don't include+      the brackets!)  The text should be enclosed in the appropriate+      comment syntax for the file format. We also recommend that a+      file or class name and description of purpose be included on the+      same "printed page" as the copyright notice for easier+      identification within third-party archives.++   Copyright [yyyy] [name of copyright owner]++   Licensed under the Apache License, Version 2.0 (the "License");+   you may not use this file except in compliance with the License.+   You may obtain a copy of the License at++       http://www.apache.org/licenses/LICENSE-2.0++   Unless required by applicable law or agreed to in writing, software+   distributed under the License is distributed on an "AS IS" BASIS,+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+   See the License for the specific language governing permissions and+   limitations under the License.
+ README.md view
@@ -0,0 +1,20 @@+# gerrit++[![Hackage](https://img.shields.io/hackage/v/gerrit.svg?logo=haskell)](https://hackage.haskell.org/package/gerrit)+[![Apache-2.0 license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)++A client library for [gerrit](https://www.gerritcodereview.com/).++## Features++- JSON decoder for API endpoints and events stream.+- HTTP client helper functions.++## Contribute++Run tests:++```+$ cabal build+$ cabal test+```
+ gerrit.cabal view
@@ -0,0 +1,72 @@+cabal-version:       2.4+name:                gerrit+version:             0.1.0.0+synopsis:            A gerrit client library+description:+    Gerrit is a client library to interface with https://www.gerritcodereview.com/+    .+    Use this library to query and post to gerrit REST API.+    .+homepage:            https://github.com/softwarefactory-project/gerrit-haskell#readme+bug-reports:         https://github.com/softwarefactory-project/gerrit-haskell/issues+license:             Apache-2.0+license-file:        LICENSE+author:              Tristan de Cacqueray+maintainer:          tdecacqu@redhat.com+copyright:           2020 Red Hat+category:            Development+build-type:          Simple+extra-doc-files:     CHANGELOG.md+                     README.md+extra-source-files:  test/data/*.json+tested-with:         GHC == 8.10.4++source-repository head+  type:                git+  location:            https://github.com/softwarefactory-project/gerrit-haskell.git++common common-options+  build-depends:       base                   >= 4.11.0.0 && < 5+                     , aeson                  >= 1.0.0.0  && < 1.6+                     , bytestring             < 0.11+  ghc-options:         -Wall+                       -Wcompat+                       -Widentities+                       -Wincomplete-uni-patterns+                       -Wincomplete-record-updates+                       -Wredundant-constraints+  if impl(ghc >= 8.2)+    ghc-options:       -fhide-source-paths+  if impl(ghc >= 8.4)+    ghc-options:       -Wmissing-export-lists+                       -Wpartial-fields+  if impl(ghc >= 8.10)+    ghc-options:       -Wunused-packages++  default-language:    Haskell2010++library+  import:              common-options+  build-depends:       containers             >= 0.5.8.0  && < 0.7+                     , http-client            >= 0.5.0    && < 0.8+                     , http-client-tls        >= 0.2.0    && < 0.4+                     , text                   >= 0.11.1.0 && < 1.3++  hs-source-dirs:      src+  exposed-modules:     Gerrit+                     , Gerrit.Data+                     , Gerrit.Event+  other-modules:       Gerrit.Client++test-suite gerrit-test+  import:              common-options+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             Spec.hs+  build-depends:       directory+                     , gerrit+                     , tasty                      >= 0.11.2   && < 1.5+                     , tasty-hunit                >= 0.10     && < 0.11+  ghc-options:         -threaded+                       -rtsopts+                       -with-rtsopts=-N
+ src/Gerrit.hs view
@@ -0,0 +1,96 @@+{-# LANGUAGE OverloadedStrings #-}++-- | This module contains the gerrit client library+module Gerrit+  ( -- * Client+    GerritClient,+    withClient,++    -- * Api+    getVersion,+    getChange,+    queryChanges,+    postReview,++    -- * Main data types+    GerritVersion (..),+    GerritQuery (..),+    GerritChange (..),+    GerritChangeStatus (..),+    ReviewResult (..),++    -- * Convenient functions+    changeUrl,+    hasLabel,+  )+where++import Control.Exception (try)+import qualified Data.Map as M+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import Gerrit.Client+import Gerrit.Data+import Network.HTTP.Client (HttpException)++-- | Return the url of a 'GerritChange'+changeUrl :: GerritClient -> GerritChange -> T.Text+changeUrl client change = baseUrl client <> T.pack (show (number change))++-- | Get the server version+getVersion :: GerritClient -> IO GerritVersion+getVersion = gerritGet "config/server/version"++-- | Search for changes+queryChanges :: Int -> [GerritQuery] -> GerritClient -> IO [GerritChange]+queryChanges count queries = gerritGet ("changes/?" <> queryString)+  where+    queryString = T.intercalate "&" [changeString, countString, option]+    changeString = "q=" <> T.intercalate "+" (map queryText queries)+    countString = "n=" <> T.pack (show count)+    option = "o=CURRENT_REVISION&o=DETAILED_LABELS"++getChange :: Int -> GerritClient -> IO (Maybe GerritChange)+getChange changeNumber client = do+  res <-+    try+      ( gerritGet ("changes/" <> T.pack (show changeNumber) <> "?o=CURRENT_REVISION&o=DETAILED_LABELS") client+      ) ::+      IO (Either HttpException GerritChange)+  pure $ case res of+    Right change -> Just change+    Left _err -> Nothing++-- | Post a review+postReview ::+  -- | The change to review+  GerritChange ->+  -- | A message+  Text ->+  -- | A label+  Text ->+  -- | A vote+  Int ->+  -- | The client+  GerritClient ->+  -- | Returns the ReviewResult+  IO ReviewResult+postReview change message label value' = gerritPost urlPath review+  where+    urlPath = "changes/" <> changeId <> "/revisions/" <> revHash <> "/review"+    changeId = Gerrit.Data.id change+    revHash = fromMaybe "" (Gerrit.Data.current_revision change)+    review =+      ReviewInput+        { riMessage = Just message,+          riLabels = Just (M.fromList [(label, value')])+        }++-- | Check if a gerrit change as a label+hasLabel :: T.Text -> Int -> GerritChange -> Bool+hasLabel label labelValue change = case M.lookup label (labels change) of+  Just gerritLabel ->+    (> 0) $+      length $ filter (\vote -> fromMaybe 0 (value vote) == labelValue) (Gerrit.Data.all gerritLabel)+  _ -> False
+ src/Gerrit/Client.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- | This module contains the internal gerrit REST client+module Gerrit.Client+  ( GerritClient (baseUrl),+    withClient,+    gerritGet,+    gerritPost,+  )+where++import Data.Aeson (FromJSON, ToJSON, eitherDecode, encode)+import qualified Data.ByteString.Lazy as BSL+import Data.Maybe (fromMaybe)+import Data.Text (Text, unpack)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Network.HTTP.Client+import Network.HTTP.Client.TLS (tlsManagerSettings)+import System.Environment (lookupEnv)++-- | The GerritClient record, use 'withClient' to create+data GerritClient = GerritClient+  { baseUrl :: Text,+    manager :: Manager,+    auth :: Maybe (Text, Text)+  }++-- | Create the 'GerritClient'+withClient ::+  -- | The gerrit api url+  Text ->+  -- | A username (password is read from GERRIT_PASSWORD environment)+  Maybe Text ->+  -- | The callback+  (GerritClient -> IO ()) ->+  -- | withClient performs the IO+  IO ()+withClient url username callBack =+  do+    manager <- newManager tlsManagerSettings+    auth <- case username of+      Just user -> do+        pass <- lookupEnv "GERRIT_PASSWORD"+        pure $ Just (user, T.pack $ fromMaybe "" pass)+      _ -> pure Nothing+    callBack (GerritClient {..})+  where+    baseUrl = T.dropWhileEnd (== '/') url <> "/"++gerritDecode :: (FromJSON a, Applicative f) => Response BSL.ByteString -> f a+gerritDecode response = case eitherDecode $ BSL.drop 5 $ responseBody response of+  Left err -> error $ "Decoding of " <> show (responseBody response) <> " failed with: " <> err+  Right a -> pure a++gerritPost :: (ToJSON a, FromJSON b) => Text -> a -> GerritClient -> IO b+gerritPost path postData GerritClient {..} =+  do+    initRequest <- case auth of+      Just (user, pass) ->+        applyBasicAuth (T.encodeUtf8 user) (T.encodeUtf8 pass)+          <$> parseUrlThrow (unpack $ baseUrl <> "a/" <> path)+      Nothing -> parseUrlThrow (unpack $ baseUrl <> path)+    let request =+          initRequest+            { method = "POST",+              requestHeaders = requestHeaders initRequest <> [("Content-Type", "application/json; charset=UTF-8")],+              requestBody = RequestBodyLBS $ encode postData+            }+    response <- httpLbs request manager+    gerritDecode response++gerritGet :: (FromJSON a) => Text -> GerritClient -> IO a+gerritGet path GerritClient {..} =+  do+    request <- parseUrlThrow (unpack $ baseUrl <> path)+    response <- httpLbs request manager+    gerritDecode response
+ src/Gerrit/Data.hs view
@@ -0,0 +1,168 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}++-- | This module contains the gerrit data type+module Gerrit.Data+  ( -- * Main data types+    GerritVersion (..),+    GerritQuery (..),+    GerritChangeStatus (..),+    GerritChange (..),+    GerritRevision (..),+    GerritLabel (..),+    GerritAccount (..),+    GerritLabelVote (..),+    GerritDetailedLabelVote (..),+    GerritDetailedLabel (..),+    ReviewResult (..),+    ReviewInput (..),++    -- * Convenient functions+    queryText,+  )+where++import Data.Aeson+import Data.Char (isUpper, toLower)+import qualified Data.Map as M+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)++newtype GerritVersion = GerritVersion Text+  deriving (Show, Generic)+  deriving anyclass (FromJSON)++-- https://gerrit-review.googlesource.com/Documentation/json.html+data GerritRevisionKind = REWORK | TRIVIAL_REBASE | MERGE_FIRST_PARENT_UPDATE | NO_CODE_CHANGE | NO_CHANGE+  deriving (Eq, Show, Generic, FromJSON)++data GerritChangeStatus = NEW | MERGED | ABANDONED | DRAFT+  deriving (Eq, Show, Generic, FromJSON)++data GerritLabelVote = REJECTED | APPROVED | DISLIKED | RECOMMENDED+  deriving (Eq, Show, Ord, Generic)++-- We use a custom parseJSON to decode Label Vote as lowercase+instance FromJSON GerritLabelVote where+  parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = map toLower}++instance FromJSONKey GerritLabelVote where+  fromJSONKey = genericFromJSONKey defaultJSONKeyOptions {keyModifier = map toLower}++-- | Modify record attribute to match json schema+-- Remove the prefix and use snakecase+customParseJSON :: String -> Options+customParseJSON prefix = defaultOptions {fieldLabelModifier = recordToJson}+  where+    recordToJson = updateCase . drop (length prefix)+    updateCase [] = []+    updateCase (x : xs) = toLower x : updateCase' xs+    updateCase' [] = []+    updateCase' (x : xs)+      | isUpper x = '_' : toLower x : updateCase' xs+      | otherwise = x : updateCase' xs++-- https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html+data ReviewResult = ReviewResult+  { rrLabels :: Maybe (M.Map Text Int),+    rrReady :: Maybe Int+  }+  deriving (Eq, Show, Ord, Generic)++instance FromJSON ReviewResult where+  parseJSON = genericParseJSON $ customParseJSON "rr"++instance ToJSON ReviewResult where+  toJSON = genericToJSON $ customParseJSON "rr"++data ReviewInput = ReviewInput+  { riMessage :: Maybe Text,+    riLabels :: Maybe (M.Map Text Int)+  }+  deriving (Eq, Show, Ord, Generic)++instance FromJSON ReviewInput where+  parseJSON = genericParseJSON $ customParseJSON "ri"++instance ToJSON ReviewInput where+  toJSON = genericToJSON $ (customParseJSON "ri") {omitNothingFields = True}++-- https://gerrit-review.googlesource.com/Documentation/user-search.html+data GerritQuery+  = Status GerritChangeStatus+  | Owner Text+  | CommitMessage Text+  | Project Text+  | ChangeId Text++-- | Convert a GerritQuery object to the search terms+queryText :: GerritQuery -> Text+queryText (Status stat) = "status:" <> T.toLower (T.pack $ show stat)+queryText (Owner owner) = "owner:" <> owner+queryText (CommitMessage message) = "message:" <> message+queryText (Project project') = "project:" <> project'+queryText (ChangeId changeId) = "change:" <> changeId++data GerritRevision = GerritRevision+  { ref :: Text,+    kind :: GerritRevisionKind+  }+  deriving (Show, Generic, FromJSON)++newtype GerritAccount = GerritAccount+  { unused_account_id :: Int+  }+  deriving (Show, Generic)++-- We use a cusom parseJSON to decode `_account_id` as `account_id`+instance FromJSON GerritAccount where+  parseJSON = genericParseJSON aesonOptions++newtype GerritLabel+  = GerritLabel (M.Map GerritLabelVote GerritAccount)+  deriving (Show, Generic)+  deriving anyclass (FromJSON)++data GerritDetailedLabelVote = GerritDetailedLabelVote+  { value :: Maybe Int,+    account_id :: Int+  }+  deriving (Show, Generic)++-- We use a cusom parseJSON to decode record field properly+instance FromJSON GerritDetailedLabelVote where+  parseJSON = genericParseJSON aesonOptions++data GerritDetailedLabel = GerritDetailedLabel+  { all :: [GerritDetailedLabelVote],+    default_value :: Int+  }+  deriving (Show, Generic, FromJSON)++data GerritChange = GerritChange+  { id :: Text,+    project :: Text,+    branch :: Text,+    subject :: Text,+    status :: GerritChangeStatus,+    mergeable :: Maybe Bool,+    revisions :: M.Map Text (Maybe GerritRevision),+    current_revision :: Maybe Text,+    number :: Int,+    labels :: M.Map Text GerritDetailedLabel+  }+  deriving (Show, Generic)++-- We use a cusom parseJSON to decode `_number` as `number`+instance FromJSON GerritChange where+  parseJSON = genericParseJSON aesonOptions++aesonOptions :: Options+aesonOptions = defaultOptions {fieldLabelModifier = recordToJson}+  where+    recordToJson "number" = "_number"+    recordToJson "account_id" = "_account_id"+    recordToJson n = n
+ src/Gerrit/Event.hs view
@@ -0,0 +1,448 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- | Gerrit stream event data type+module Gerrit.Event+  ( -- * Main event data types+    EventType (..),+    Event (..),++    -- * Common data types+    User (..),+    PatchSet (..),+    Change (..),+    Ref (..),++    -- * Individual event data types+    AssigneeChanged (..),+    ChangeAbandoned (..),+    ChangeDeleted (..),+    ChangeMerged (..),+    ChangeRestored (..),+    CommentAdded (..),+    HashtagsChanged (..),+    ProjectCreated (..),+    PatchsetCreated (..),+    RefUpdated (..),+    ReviewerAdded (..),+    ReviewerDeleted (..),+    TopicChanged (..),+    WorkInProgressStateChanged (..),+    PrivateStateChanged (..),+    VoteDeleted (..),++    -- * Convenient functions+    getChange,+    getUser,+    getEventType,+    eventName,+  )+where++import Control.Monad (mzero)+import Data.Aeson+  ( FromJSON (..),+    Options (fieldLabelModifier),+    Value (Object, String),+    defaultOptions,+    genericParseJSON,+    (.:),+  )+import Data.Char (toLower)+import Data.Text (Text)+import qualified Data.Text as Text+import GHC.Generics (Generic)++aesonOption :: Text -> Options+aesonOption prefix = defaultOptions {fieldLabelModifier = recordToJson}+  where+    recordToJson = updateCase . drop (Text.length prefix)+    updateCase [] = []+    updateCase (x : xs) = toLower x : xs++-------------------------------------------------------------------------------+-- Shared types+-------------------------------------------------------------------------------+data User = User+  { userName :: Maybe Text,+    userEmail :: Maybe Text,+    userUsername :: Maybe Text+  }+  deriving (Show, Eq, Generic)++instance FromJSON User where+  parseJSON = genericParseJSON $ aesonOption "user"++data PatchSet = PatchSet+  { patchSetNumber :: Int,+    patchSetRef :: Text,+    patchSetUploader :: User,+    patchsetAuthor :: User+  }+  deriving (Show, Eq, Generic)++instance FromJSON PatchSet where+  parseJSON = genericParseJSON $ aesonOption "patchSet"++data Change = Change+  { changeProject :: Text,+    changeBranch :: Text,+    changeSubject :: Text,+    changeUrl :: Text,+    changeOwner :: User+  }+  deriving (Show, Eq, Generic)++instance FromJSON Change where+  parseJSON = genericParseJSON $ aesonOption "change"++data Ref = Ref+  { refOldRev :: Text,+    refNewRev :: Text,+    refRefName :: Text,+    refProject :: Text+  }+  deriving (Show, Eq, Generic)++instance FromJSON Ref where+  parseJSON = genericParseJSON $ aesonOption "ref"++-------------------------------------------------------------------------------+-- Individual Event data types+-------------------------------------------------------------------------------+data AssigneeChanged = AssigneeChanged+  { assigneeChangedChange :: Change,+    assigneeChangedChanger :: User+  }+  deriving (Show, Eq, Generic)++data ChangeAbandoned = ChangeAbandoned+  { changeAbandonedProject :: Text,+    changeAbandonedAbandoner :: User,+    changeAbandonedChange :: Change,+    changeAbandonedPatchSet :: PatchSet+  }+  deriving (Show, Eq, Generic)++data ChangeDeleted = ChangeDeleted+  { changeDeletedChange :: Change,+    changeDeletedDeleter :: User+  }+  deriving (Show, Eq, Generic)++data ChangeMerged = ChangeMerged+  { changeMergedProject :: Text,+    changeMergedSubmitter :: User,+    changeMergedChange :: Change,+    changeMergedPatchSet :: PatchSet+  }+  deriving (Show, Eq, Generic)++data ChangeRestored = ChangeRestored+  { changeRestoredChange :: Change,+    changeRestoredPatchSet :: PatchSet,+    changeRestoredRestorer :: User,+    changeRestoredReason :: Text+  }+  deriving (Show, Eq, Generic)++data CommentAdded = CommentAdded+  { commentAddedChange :: Change,+    commentAddedPatchSet :: PatchSet,+    commentAddedAuthor :: User+  }+  deriving (Show, Eq, Generic)++data HashtagsChanged = HashtagsChanged+  { hashtagsChangedChange :: Change,+    hashtagsChangedEditor :: User+  }+  deriving (Show, Eq, Generic)++data ProjectCreated = ProjectCreated+  { projectCreatedProjectName :: Text,+    projectCreatedHeadName :: Text+  }+  deriving (Show, Eq, Generic)++data PatchsetCreated = PatchsetCreated+  { patchsetCreatedUploader :: User,+    patchsetCreatedChange :: Change,+    patchsetCreatedPatchSet :: PatchSet+  }+  deriving (Show, Eq, Generic)++data RefUpdated = RefUpdated+  { refUpdatedSubmitter :: User,+    refUpdatedRefUpdate :: Ref+  }+  deriving (Show, Eq, Generic)++data ReviewerAdded = ReviewerAdded+  { reviewerAddedChange :: Change,+    reviewerAddedPatchSet :: PatchSet,+    reviewerAddedAdder :: User+  }+  deriving (Show, Eq, Generic)++data ReviewerDeleted = ReviewerDeleted+  { reviewerDeletedChange :: Change,+    reviewerDeletedPatchSet :: PatchSet,+    reviewerDeletedRemover :: User+  }+  deriving (Show, Eq, Generic)++data TopicChanged = TopicChanged+  { topicChangedChange :: Change,+    topicChangedChanger :: User,+    topicChangedOldTopic :: Text+  }+  deriving (Show, Eq, Generic)++data WorkInProgressStateChanged = WorkInProgressStateChanged+  { workInProgressStateChangedChange :: Change,+    workInProgressStateChangedPatchSet :: PatchSet,+    workInProgressStateChangedChanger :: User+  }+  deriving (Show, Eq, Generic)++data PrivateStateChanged = PrivateStateChanged+  { privateStateChangedChange :: Change,+    privateStateChangedPatchSet :: PatchSet,+    privateStateChangedChanger :: User+  }+  deriving (Show, Eq, Generic)++data VoteDeleted = VoteDeleted+  { voteDeletedChange :: Change,+    voteDeletedPatchSet :: PatchSet,+    voteDeletedRemover :: User+  }+  deriving (Show, Eq, Generic)++-------------------------------------------------------------------------------+-- Events FromJSON instances+-------------------------------------------------------------------------------+instance FromJSON AssigneeChanged where+  parseJSON = genericParseJSON $ aesonOption "assigneeChanged"++instance FromJSON ChangeAbandoned where+  parseJSON = genericParseJSON $ aesonOption "changeAbandoned"++instance FromJSON ChangeDeleted where+  parseJSON = genericParseJSON $ aesonOption "changeDeleted"++instance FromJSON ChangeMerged where+  parseJSON = genericParseJSON $ aesonOption "changeMerged"++instance FromJSON ChangeRestored where+  parseJSON = genericParseJSON $ aesonOption "changeRestored"++instance FromJSON CommentAdded where+  parseJSON = genericParseJSON $ aesonOption "commentAdded"++instance FromJSON HashtagsChanged where+  parseJSON = genericParseJSON $ aesonOption "hashtagsChanged"++instance FromJSON ProjectCreated where+  parseJSON = genericParseJSON $ aesonOption "projectCreated"++instance FromJSON PatchsetCreated where+  parseJSON = genericParseJSON $ aesonOption "patchsetCreated"++instance FromJSON RefUpdated where+  parseJSON = genericParseJSON $ aesonOption "refUpdated"++instance FromJSON ReviewerAdded where+  parseJSON = genericParseJSON $ aesonOption "reviewerAdded"++instance FromJSON ReviewerDeleted where+  parseJSON = genericParseJSON $ aesonOption "reviewerDeleted"++instance FromJSON TopicChanged where+  parseJSON = genericParseJSON $ aesonOption "topicChanged"++instance FromJSON WorkInProgressStateChanged where+  parseJSON = genericParseJSON $ aesonOption "workInProgressStateChanged"++instance FromJSON PrivateStateChanged where+  parseJSON = genericParseJSON $ aesonOption "privateStateChanged"++instance FromJSON VoteDeleted where+  parseJSON = genericParseJSON $ aesonOption "voteDeleted"++-- | The event "type" data type+data EventType+  = AssigneeChangedEvent+  | ChangeAbandonedEvent+  | ChangeDeletedEvent+  | ChangeMergedEvent+  | ChangeRestoredEvent+  | CommentAddedEvent+  | DroppedOutputEvent+  | HashtagsChangedEvent+  | ProjectCreatedEvent+  | PatchsetCreatedEvent+  | RefUpdatedEvent+  | ReviewerAddedEvent+  | ReviewerDeletedEvent+  | TopicChangedEvent+  | WorkInProgressStateChangedEvent+  | PrivateStateChangedEvent+  | VoteDeletedEvent+  deriving (Show, Eq)++instance FromJSON EventType where+  parseJSON (String v) = case v of+    "assignee-changed" -> pure AssigneeChangedEvent+    "change-abandoned" -> pure ChangeAbandonedEvent+    "change-deleted" -> pure ChangeDeletedEvent+    "change-merged" -> pure ChangeMergedEvent+    "change-restored" -> pure ChangeRestoredEvent+    "comment-added" -> pure CommentAddedEvent+    "dropped-output" -> pure DroppedOutputEvent+    "hashtags-changed" -> pure HashtagsChangedEvent+    "project-created" -> pure ProjectCreatedEvent+    "patchset-created" -> pure PatchsetCreatedEvent+    "ref-updated" -> pure RefUpdatedEvent+    "reviewer-added" -> pure ReviewerAddedEvent+    "reviewer-deleted" -> pure ReviewerDeletedEvent+    "topic-changed" -> pure TopicChangedEvent+    "wip-state-changed" -> pure WorkInProgressStateChangedEvent+    "private-state-changed" -> pure PrivateStateChangedEvent+    "vote-deleted" -> pure VoteDeletedEvent+    _ -> mzero+  parseJSON _ = mzero++-- | The main event union data type+data Event+  = EventAssigneeChanged AssigneeChanged+  | EventChangeAbandoned ChangeAbandoned+  | EventChangeDeleted ChangeDeleted+  | EventChangeMerged ChangeMerged+  | EventChangeRestored ChangeRestored+  | EventCommentAdded CommentAdded+  | EventDroppedOutput+  | EventHashtagsChanged HashtagsChanged+  | EventProjectCreated ProjectCreated+  | EventPatchsetCreated PatchsetCreated+  | EventRefUpdated RefUpdated+  | EventReviewerAdded ReviewerAdded+  | EventReviewerDeleted ReviewerDeleted+  | EventTopicChanged TopicChanged+  | EventWorkInProgressStateChanged WorkInProgressStateChanged+  | EventPrivateStateChanged PrivateStateChanged+  | EventVoteDeleted VoteDeleted+  deriving (Show, Eq)++instance FromJSON Event where+  parseJSON o@(Object v) = do+    eType <- v .: "type"+    case eType of+      AssigneeChangedEvent -> EventAssigneeChanged <$> parseJSON o+      ChangeAbandonedEvent -> EventChangeAbandoned <$> parseJSON o+      ChangeDeletedEvent -> EventChangeDeleted <$> parseJSON o+      ChangeMergedEvent -> EventChangeMerged <$> parseJSON o+      ChangeRestoredEvent -> EventChangeRestored <$> parseJSON o+      CommentAddedEvent -> EventCommentAdded <$> parseJSON o+      DroppedOutputEvent -> pure EventDroppedOutput+      HashtagsChangedEvent -> EventHashtagsChanged <$> parseJSON o+      ProjectCreatedEvent -> EventProjectCreated <$> parseJSON o+      PatchsetCreatedEvent -> EventPatchsetCreated <$> parseJSON o+      RefUpdatedEvent -> EventRefUpdated <$> parseJSON o+      ReviewerAddedEvent -> EventReviewerAdded <$> parseJSON o+      ReviewerDeletedEvent -> EventReviewerDeleted <$> parseJSON o+      TopicChangedEvent -> EventTopicChanged <$> parseJSON o+      WorkInProgressStateChangedEvent -> EventWorkInProgressStateChanged <$> parseJSON o+      PrivateStateChangedEvent -> EventPrivateStateChanged <$> parseJSON o+      VoteDeletedEvent -> EventVoteDeleted <$> parseJSON o+  parseJSON _ = mzero++-------------------------------------------------------------------------------+-- Utilities function+-------------------------------------------------------------------------------++-- | Get the associated change+getChange :: Event -> Maybe Change+getChange event = case event of+  EventAssigneeChanged AssigneeChanged {..} -> Just assigneeChangedChange+  EventChangeAbandoned ChangeAbandoned {..} -> Just changeAbandonedChange+  EventChangeDeleted ChangeDeleted {..} -> Just changeDeletedChange+  EventChangeMerged ChangeMerged {..} -> Just changeMergedChange+  EventChangeRestored ChangeRestored {..} -> Just changeRestoredChange+  EventCommentAdded CommentAdded {..} -> Just commentAddedChange+  EventDroppedOutput -> Nothing+  EventHashtagsChanged HashtagsChanged {..} -> Just hashtagsChangedChange+  EventProjectCreated _ -> Nothing+  EventPatchsetCreated PatchsetCreated {..} -> Just patchsetCreatedChange+  EventRefUpdated _ -> Nothing+  EventReviewerAdded ReviewerAdded {..} -> Just reviewerAddedChange+  EventReviewerDeleted ReviewerDeleted {..} -> Just reviewerDeletedChange+  EventTopicChanged TopicChanged {..} -> Just topicChangedChange+  EventWorkInProgressStateChanged WorkInProgressStateChanged {..} -> Just workInProgressStateChangedChange+  EventPrivateStateChanged PrivateStateChanged {..} -> Just privateStateChangedChange+  EventVoteDeleted VoteDeleted {..} -> Just voteDeletedChange++-- | Get the associated author+getUser :: Event -> Maybe User+getUser event = case event of+  EventAssigneeChanged AssigneeChanged {..} -> Just assigneeChangedChanger+  EventChangeAbandoned ChangeAbandoned {..} -> Just changeAbandonedAbandoner+  EventChangeDeleted ChangeDeleted {..} -> Just changeDeletedDeleter+  EventChangeMerged ChangeMerged {..} -> Just changeMergedSubmitter+  EventChangeRestored ChangeRestored {..} -> Just changeRestoredRestorer+  EventCommentAdded CommentAdded {..} -> Just commentAddedAuthor+  EventDroppedOutput -> Nothing+  EventHashtagsChanged HashtagsChanged {..} -> Just hashtagsChangedEditor+  EventProjectCreated _ -> Nothing+  EventPatchsetCreated PatchsetCreated {..} -> Just patchsetCreatedUploader+  EventRefUpdated _ -> Nothing+  EventReviewerAdded ReviewerAdded {..} -> Just reviewerAddedAdder+  EventReviewerDeleted ReviewerDeleted {..} -> Just reviewerDeletedRemover+  EventTopicChanged TopicChanged {..} -> Just topicChangedChanger+  EventWorkInProgressStateChanged WorkInProgressStateChanged {..} -> Just workInProgressStateChangedChanger+  EventPrivateStateChanged PrivateStateChanged {..} -> Just privateStateChangedChanger+  EventVoteDeleted VoteDeleted {..} -> Just voteDeletedRemover++-- | Get the 'EventType' back from an 'Event'+getEventType :: Event -> EventType+getEventType event = case event of+  EventAssigneeChanged _ -> AssigneeChangedEvent+  EventChangeAbandoned _ -> ChangeAbandonedEvent+  EventChangeDeleted _ -> ChangeDeletedEvent+  EventChangeMerged _ -> ChangeMergedEvent+  EventChangeRestored _ -> ChangeRestoredEvent+  EventCommentAdded _ -> CommentAddedEvent+  EventDroppedOutput -> DroppedOutputEvent+  EventHashtagsChanged _ -> HashtagsChangedEvent+  EventProjectCreated _ -> ProjectCreatedEvent+  EventPatchsetCreated _ -> PatchsetCreatedEvent+  EventRefUpdated _ -> RefUpdatedEvent+  EventReviewerAdded _ -> ReviewerAddedEvent+  EventReviewerDeleted _ -> ReviewerDeletedEvent+  EventTopicChanged _ -> TopicChangedEvent+  EventWorkInProgressStateChanged _ -> WorkInProgressStateChangedEvent+  EventPrivateStateChanged _ -> PrivateStateChangedEvent+  EventVoteDeleted _ -> VoteDeletedEvent++-- | Get the text representation suitable for event subscription.+eventName :: EventType -> Text+eventName eventType = case eventType of+  AssigneeChangedEvent -> "assignee-changed"+  ChangeAbandonedEvent -> "change-abandoned"+  ChangeDeletedEvent -> "change-deleted"+  ChangeMergedEvent -> "change-merged"+  ChangeRestoredEvent -> "change-restored"+  CommentAddedEvent -> "comment-added"+  DroppedOutputEvent -> "dropped-output"+  HashtagsChangedEvent -> "hashtags-changed"+  ProjectCreatedEvent -> "project-created"+  PatchsetCreatedEvent -> "patchset-created"+  RefUpdatedEvent -> "ref-updated"+  ReviewerAddedEvent -> "reviewer-added"+  ReviewerDeletedEvent -> "reviewer-deleted"+  TopicChangedEvent -> "topic-changed"+  WorkInProgressStateChangedEvent -> "wip-state-changed"+  PrivateStateChangedEvent -> "private-state-changed"+  VoteDeletedEvent -> "vote-deleted"
+ test/Spec.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import Data.Aeson (decode, encode)+import Data.ByteString.Lazy (ByteString)+import qualified Data.ByteString.Lazy as BSL+import Data.Maybe (fromJust)+import Gerrit+import Gerrit.Data as Gerrit+import qualified Gerrit.Event as Event+import System.Directory (listDirectory)+import Test.Tasty+import Test.Tasty.HUnit++main :: IO ()+main =+  do+    files <- listDirectory "./test/data"+    dataFiles <- traverse (BSL.readFile . ("./test/data/" <>)) files+    withClient "http://example.com/r" Nothing $ \client ->+      defaultMain (tests (zip files dataFiles) client)++tests :: [(FilePath, ByteString)] -> GerritClient -> TestTree+tests dataFiles client = testGroup "Tests" $ [unitTests] <> encodingTests dataFiles client++unitTests :: TestTree+unitTests =+  testGroup+    "unitTests"+    [ testCase "queryString status" $+        assertEqual "Query string is valid" "status:new" (queryText (Status NEW))+    ]++encodingTests :: [(FilePath, ByteString)] -> GerritClient -> [TestTree]+encodingTests dataFiles client =+  [ testGroup+      "FromJSON"+      [ testCase "Test GerritChange.json" $+          assertBool "GerritChange is decoded" $+            isChange $ decode $ getRaw "GerritChange.json",+        testCase "Test EventPatchsetCreated.json" $+          assertBool "EventPatchsetCreated is decoded" $+            isEventPatchsetCreated $ decode $ getRaw "EventPatchsetCreated.json",+        testCase "Test EventChangeMerged.json" $+          assertBool "EventChangeMerged is decoded" $+            isChangeMergedEvent $ decode $ getRaw "EventChangeMerged.json",+        testCase "Test EventCommentAdded.json" $+          assertBool "EventCommentAdded is decoded" $+            isCommentEvent $ decode $ getRaw "EventCommentAdded.json",+        testCase "Test EventChangeAbandoned.json" $+          assertBool "EventChangeAbandoned is decoded" $+            isAbandonedEvent $ decode $ getRaw "EventChangeAbandoned.json",+        testCase "Test EventProjectCreated.json" $+          assertBool "EventProjectCreated is decoded" $+            isEventProjectCreated $ decode $ getRaw "EventProjectCreated.json",+        testCase "Test EventRefUpdated.json" $+          assertBool "EventRefUpdated is decoded" $+            isEventRefUpdated $ decode $ getRaw "EventRefUpdated.json",+        testCase "Test ReviewResult.json" $+          assertBool "ReviewResult is decoded" $+            isReviewResult $ decode $ getRaw "ReviewResult.json",+        testCase "Test changeUrl works" $+          assertEqual "Change url is correct" "http://example.com/r/18839" testGetUrl,+        testCase "Test hasLabel works" $+          assertBool "Label not found" testIsApproved+      ],+    testGroup+      "toJSON"+      [ testCase "Test ReviewInput.json" $+          assertEqual "Review is encoded" (getRaw "ReviewInput.json") (prettyEncode reviewInput)+      ]+  ]+  where+    prettyEncode obj = encode obj <> "\n"+    reviewInput = Gerrit.ReviewInput (Just "Thanks!") Nothing+    getRaw :: FilePath -> ByteString+    getRaw fp = fromJust $ lookup fp dataFiles+    isReviewResult :: Maybe ReviewResult -> Bool+    isReviewResult = \case+      Just _ -> True+      Nothing -> False+    isChangeMergedEvent = \case+      Just (Event.EventChangeMerged _) -> True+      _ -> False+    isEventPatchsetCreated = \case+      Just (Event.EventPatchsetCreated _) -> True+      _ -> False+    isCommentEvent = \case+      Just (Event.EventCommentAdded _) -> True+      _ -> False+    isAbandonedEvent = \case+      Just (Event.EventChangeAbandoned _) -> True+      _ -> False+    isEventProjectCreated = \case+      Just (Event.EventProjectCreated _) -> True+      _ -> False+    isEventRefUpdated = \case+      Just (Event.EventRefUpdated _) -> True+      _ -> False+    isChange :: Maybe GerritChange -> Bool+    isChange = \case+      Just _ -> True+      Nothing -> False+    testIsApproved = case decode (getRaw "GerritChange.json") of+      Just change -> hasLabel "Code-Review" 1 change+      Nothing -> False+    testGetUrl = case decode (getRaw "GerritChange.json") of+      Just change -> changeUrl client change+      Nothing -> "decode-failed"
+ test/data/EventChangeAbandoned.json view
@@ -0,0 +1,49 @@+{+  "abandoner": {+    "name": "Tristan de Cacqueray",+    "email": "tdecacqu@redhat.com",+    "username": "TristanCacqueray"+  },+  "reason": "Duplicated changeid",+  "patchSet": {+    "number": 1,+    "revision": "39ff07f7277ad4e85a94df1e130ec3e0dc55e9c7",+    "parents": ["b20838415c86314d73ff5bb5532b1a9a43d38043"],+    "ref": "refs/changes/16/21916/1",+    "uploader": {+      "name": "Tristan de Cacqueray",+      "email": "tdecacqu@redhat.com",+      "username": "TristanCacqueray"+    },+    "createdOn": 1622284534,+    "author": {+      "name": "Tristan de Cacqueray",+      "email": "tdecacqu@redhat.com",+      "username": "TristanCacqueray"+    },+    "kind": "REWORK",+    "sizeInsertions": 158,+    "sizeDeletions": 23+  },+  "change": {+    "project": "software-factory/gerritbot-matrix",+    "branch": "master",+    "id": "Ie795336f3b1814a7e3d65a33c391fe3f64472fac",+    "number": 21916,+    "subject": "Group related event to emit a single message",+    "owner": {+      "name": "Tristan de Cacqueray",+      "email": "tdecacqu@redhat.com",+      "username": "TristanCacqueray"+    },+    "url": "https://softwarefactory-project.io/r/c/software-factory/gerritbot-matrix/+/21916",+    "commitMessage": "Group related event to emit a single message\n\nFixes https://github.com/softwarefactory-project/gerritbot-matrix/issues/1\nChange-Id: If4b38b79a421b9e257bdf82077ab4c9801a3a447\n\nChange-Id: Ie795336f3b1814a7e3d65a33c391fe3f64472fac\n",+    "createdOn": 1622284534,+    "status": "ABANDONED"+  },+  "project": "software-factory/gerritbot-matrix",+  "refName": "refs/heads/master",+  "changeKey": { "id": "Ie795336f3b1814a7e3d65a33c391fe3f64472fac" },+  "type": "change-abandoned",+  "eventCreatedOn": 1622285180+}
+ test/data/EventChangeMerged.json view
@@ -0,0 +1,47 @@+{+  "submitter": { "name": "Zuul", "username": "zuul" },+  "newRev": "d7d5dc4a739aafcb0a3edfb758e15f878ad3292f",+  "patchSet": {+    "number": 2,+    "revision": "4cc63b82c3c0114d29b50e2593af21504aa10f57",+    "parents": ["e61b385d95d4a8969bc09ab9163b550da8f715a3"],+    "ref": "refs/changes/05/21905/2",+    "uploader": {+      "name": "Tristan de Cacqueray",+      "email": "tdecacqu@redhat.com",+      "username": "TristanCacqueray"+    },+    "createdOn": 1622229383,+    "author": {+      "name": "Tristan de Cacqueray",+      "email": "tdecacqu@redhat.com",+      "username": "TristanCacqueray"+    },+    "kind": "REWORK",+    "sizeInsertions": 201,+    "sizeDeletions": 1+  },+  "change": {+    "project": "software-factory/gerrit-haskell",+    "branch": "master",+    "id": "I7c926701b9e2b57b934917672bb1397851119ae0",+    "number": 21905,+    "subject": "Add Gerrit.Event data type",+    "owner": {+      "name": "Tristan de Cacqueray",+      "email": "tdecacqu@redhat.com",+      "username": "TristanCacqueray"+    },+    "url": "https://softwarefactory-project.io/r/c/software-factory/gerrit-haskell/+/21905",+    "commitMessage": "Add Gerrit.Event data type\n\nChange-Id: I7c926701b9e2b57b934917672bb1397851119ae0\n",+    "createdOn": 1622228651,+    "status": "MERGED"+  },+  "project": "software-factory/gerrit-haskell",+  "refName": "refs/heads/master",+  "changeKey": {+    "id": "I7c926701b9e2b57b934917672bb1397851119ae0"+  },+  "type": "change-merged",+  "eventCreatedOn": 1622233316+}
+ test/data/EventCommentAdded.json view
@@ -0,0 +1,70 @@+{+  "author": {+    "name": "Zuul CI",+    "email": "zuul@softwarefactory-project.io",+    "username": "zuul"+  },+  "approvals": [+    {+      "type": "Verified",+      "description": "Verified",+      "value": "0"+    },+    {+      "type": "Code-Review",+      "description": "Code-Review",+      "value": "0"+    },+    {+      "type": "Workflow",+      "description": "Workflow",+      "value": "0"+    }+  ],+  "comment": "Patch Set 3:\n\nStarting check jobs.",+  "patchSet": {+    "number": 3,+    "revision": "938356461cd9fcb470e195fd1ef1907c06db730a",+    "parents": [+      "e61b385d95d4a8969bc09ab9163b550da8f715a3"+    ],+    "ref": "refs/changes/05/21905/3",+    "uploader": {+      "name": "Tristan de Cacqueray",+      "email": "tdecacqu@redhat.com",+      "username": "TristanCacqueray"+    },+    "createdOn": 1622229429,+    "author": {+      "name": "Tristan de Cacqueray",+      "email": "tdecacqu@redhat.com",+      "username": "TristanCacqueray"+    },+    "kind": "REWORK",+    "sizeInsertions": 203,+    "sizeDeletions": 1+  },+  "change": {+    "project": "software-factory/gerrit-haskell",+    "branch": "master",+    "id": "I7c926701b9e2b57b934917672bb1397851119ae0",+    "number": 21905,+    "subject": "Add Gerrit.Event data type",+    "owner": {+      "name": "Tristan de Cacqueray",+      "email": "tdecacqu@redhat.com",+      "username": "TristanCacqueray"+    },+    "url": "https://softwarefactory-project.io/r/c/software-factory/gerrit-haskell/+/21905",+    "commitMessage": "Add Gerrit.Event data type\n\nChange-Id: I7c926701b9e2b57b934917672bb1397851119ae0\n",+    "createdOn": 1622228651,+    "status": "NEW"+  },+  "project": "software-factory/gerrit-haskell",+  "refName": "refs/heads/master",+  "changeKey": {+    "id": "I7c926701b9e2b57b934917672bb1397851119ae0"+  },+  "type": "comment-added",+  "eventCreatedOn": 1622229444+}
+ test/data/EventPatchsetCreated.json view
@@ -0,0 +1,50 @@+{+  "uploader": {+    "name": "Tristan de Cacqueray",+    "email": "tdecacqu@redhat.com",+    "username": "TristanCacqueray"+  },+  "patchSet": {+    "number": 2,+    "revision": "4cc63b82c3c0114d29b50e2593af21504aa10f57",+    "parents": ["e61b385d95d4a8969bc09ab9163b550da8f715a3"],+    "ref": "refs/changes/05/21905/2",+    "uploader": {+      "name": "Tristan de Cacqueray",+      "email": "tdecacqu@redhat.com",+      "username": "TristanCacqueray"+    },+    "createdOn": 1622229383,+    "author": {+      "name": "Tristan de Cacqueray",+      "email": "tdecacqu@redhat.com",+      "username": "TristanCacqueray"+    },+    "kind": "REWORK",+    "sizeInsertions": 201,+    "sizeDeletions": 1+  },+  "change": {+    "project": "software-factory/gerrit-haskell",+    "branch": "master",+    "id": "I7c926701b9e2b57b934917672bb1397851119ae0",+    "number": 21905,+    "subject": "Add Gerrit.Event data type",+    "owner": {+      "name": "Tristan de Cacqueray",+      "email": "tdecacqu@redhat.com",+      "username": "TristanCacqueray"+    },+    "url": "https://softwarefactory-project.io/r/c/software-factory/gerrit-haskell/+/21905",+    "commitMessage": "Add Gerrit.Event data type\n\nChange-Id: I7c926701b9e2b57b934917672bb1397851119ae0\n",+    "createdOn": 1622228651,+    "status": "NEW"+  },+  "project": "software-factory/gerrit-haskell",+  "refName": "refs/heads/master",+  "changeKey": {+    "id": "I7c926701b9e2b57b934917672bb1397851119ae0"+  },+  "type": "patchset-created",+  "eventCreatedOn": 1622229383+}
+ test/data/EventProjectCreated.json view
@@ -0,0 +1,6 @@+{+  "projectName": "software-factory/gerritbot-matrix",+  "headName": "refs/heads/master",+  "type": "project-created",+  "eventCreatedOn": 1622230530+}
+ test/data/EventRefUpdated.json view
@@ -0,0 +1,15 @@+{+  "submitter": {+    "name": "Tristan de Cacqueray",+    "email": "tdecacqu@redhat.com",+    "username": "TristanCacqueray"+  },+  "refUpdate": {+    "oldRev": "0000000000000000000000000000000000000000",+    "newRev": "aaf4e7f7437e010ba19ed259be6391a9a658dbaa",+    "refName": "refs/changes/05/21905/1",+    "project": "software-factory/gerrit-haskell"+  },+  "type": "ref-updated",+  "eventCreatedOn": 1622228651+}
+ test/data/GerritChange.json view
@@ -0,0 +1,84 @@+{+  "id": "software-factory%2Fgerrit-haskell~master~Ic6add806c49bccaf6e15d4ef1c62522d56eb9384",+  "project": "software-factory/gerrit-haskell",+  "branch": "master",+  "hashtags": [],+  "change_id": "Ic6add806c49bccaf6e15d4ef1c62522d56eb9384",+  "subject": "Initial summon",+  "status": "NEW",+  "created": "2020-07-15 19:39:55.000000000",+  "updated": "2020-07-15 23:01:14.000000000",+  "submit_type": "MERGE_IF_NECESSARY",+  "mergeable": true,+  "insertions": 295,+  "deletions": 0,+  "unresolved_comment_count": 0,+  "_number": 18839,+  "owner": {+    "_account_id": 7+  },+  "labels": {+    "Code-Review": {+      "all": [+        {+          "value": 1,+          "permitted_voting_range": {+            "min": -1,+            "max": 1+          },+          "_account_id": 458+        }+      ],+      "values": {+        "-2": "Do not submit",+        "-1": "I would prefer that you didn't submit this",+        " 0": "No score",+        "+1": "Looks good to me, but someone else must approve",+        "+2": "Looks good to me (core reviewer)"+      },+      "default_value": 0+    },+    "Verified": {+      "all": [+        {+          "value": 1,+          "date": "2020-07-17 13:02:15.000000000",+          "permitted_voting_range": {+            "min": -2,+            "max": 2+          },+          "_account_id": 458+        },+        {+          "_account_id": 42+        }+      ],+      "values": {+        "-2": "Fails",+        "-1": "Doesn't seem to work",+        " 0": "No score",+        "+1": "Works for me",+        "+2": "Verified"+      },+      "default_value": 0+    }+  },+  "current_revision": "155658085799464e8d198edb7c8bd62de9ec9ae6",+  "revisions": {+    "155658085799464e8d198edb7c8bd62de9ec9ae6": {+      "kind": "REWORK",+      "_number": 3,+      "created": "2020-07-15 23:01:02.000000000",+      "uploader": {+        "_account_id": 7+      },+      "ref": "refs/changes/39/18839/3",+      "fetch": {+        "anonymous http": {+          "url": "https://softwarefactory-project.io/r/software-factory/gerrit-haskell",+          "ref": "refs/changes/39/18839/3"+        }+      }+    }+  }+}
+ test/data/ReviewInput.json view
@@ -0,0 +1,1 @@+{"message":"Thanks!"}
+ test/data/ReviewResult.json view
@@ -0,0 +1,5 @@+{+  "labels": {+    "Code-Review": -1+  }+}