packages feed

atlassian-connect-descriptor (empty) → 0.1.0.0

raw patch · 20 files changed

+1600/−0 lines, 20 filesdep +Cabaldep +HUnitdep +aesonsetup-changed

Dependencies added: Cabal, HUnit, aeson, base, cases, network, network-uri, scientific, text, time-units, unordered-containers, vector

Files

+ LICENSE view
@@ -0,0 +1,202 @@++                                 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.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ atlassian-connect-descriptor.cabal view
@@ -0,0 +1,128 @@+-- Initial atlassian-connect-descriptor.cabal generated by cabal init.  For+--  further documentation, see http://haskell.org/cabal/users-guide/++-- The name of the package.+name:                atlassian-connect-descriptor++-- The package version.  See the Haskell package versioning policy (PVP) +-- for standards guiding when and how versions should be incremented.+-- http://www.haskell.org/haskellwiki/Package_versioning_policy+-- PVP summary:      +-+------- breaking API changes+--                   | | +----- non-breaking API additions+--                   | | | +--- code changes with no API change+version:             0.1.0.0++-- A short (one-line) description of the package.+synopsis:            Code that helps you create a valid Atlassian Connect Descriptor.++-- A longer description of the package.+-- description:         ++-- The license under which the package is released.+license:             Apache-2.0++-- The file containing the license text.+license-file:        LICENSE++-- The package author(s).+author:              Robert Massaioli++-- An email address to which users can send suggestions, bug reports, and +-- patches.+maintainer:          rmassaioli@atlassian.com++-- A copyright notice.+-- copyright:           ++category:            Data++build-type:          Simple++-- Extra files to be distributed with the package, such as examples or a +-- README.+-- extra-source-files:  ++-- Constraint on the version of Cabal needed to build this package.+cabal-version:       >=1.20++flag network-uri+   description: Get Network.URI from the network-uri package+   default: True++library+  -- Modules exported by the library.+  exposed-modules:    Data.Connect.Descriptor+  +  -- Modules included in this library but not exported.+  other-modules:      Data.Connect.AesonHelpers+                      , Data.Connect.BaseTypes+                      , Data.Connect.Conditions+                      , Data.Connect.Lifecycle+                      , Data.Connect.Modules+                      , Data.Connect.OrphanInstances+                      , Data.Connect.Scopes+                      , Data.Connect.Webhooks+  +  -- LANGUAGE extensions used by modules in this package.+  -- other-extensions:    +  +  -- Other library packages from which modules are imported.+  build-depends:       base         >=4.6 && <5+                       , aeson      >  0.7.0.3 && < 0.8+                       , text       >= 0.11    && <= 1.2+                       , time-units == 1.*+                       , cases      == 0.1.*+                       , unordered-containers == 0.2.*++  if flag(network-uri)+     build-depends: network-uri >= 2.6, network >= 2.6+  else+     build-depends: network-uri < 2.6, network < 2.6+  +  -- Directories containing source files.+  hs-source-dirs:      src+  +  -- Base language which the package is written in.+  default-language:    Haskell2010+  ghc-options: -Wall++Test-Suite test-descriptor+  hs-source-dirs: tests, src+  type:        detailed-0.9++  test-module: Test+  other-modules: Util+                 , AssertionHelpers+                 , ValueExtractors+                 , ConditionsTest+                 , ModulesTest+                 , DescriptorTest+                 , Data.Connect.Descriptor+                 , Data.Connect.AesonHelpers+                 , Data.Connect.BaseTypes+                 , Data.Connect.Conditions+                 , Data.Connect.Lifecycle+                 , Data.Connect.Modules+                 , Data.Connect.OrphanInstances+                 , Data.Connect.Scopes+                 , Data.Connect.Webhooks++  build-depends:+    base+    , Cabal >= 1.20+    , HUnit+    , vector+    , aeson+    , scientific+    , text+    , time-units+    , cases+    , unordered-containers++  if flag(network-uri)+     build-depends: network-uri >= 2.6, network >= 2.6+  else+     build-depends: network-uri < 2.6, network < 2.6++  default-language:    Haskell2010+  ghc-options: -Wall
+ dist/build/test-descriptorStub/test-descriptorStub-tmp/test-descriptorStub.hs view
@@ -0,0 +1,5 @@+module Main ( main ) where+import Distribution.Simple.Test.LibV09 ( stubMain )+import Test ( tests )+main :: IO ()+main = stubMain tests
+ src/Data/Connect/AesonHelpers.hs view
@@ -0,0 +1,33 @@+module Data.Connect.AesonHelpers+   ( baseOptions+   , stripFieldNamePrefix+   , dropSuffixAndSnakeCase+   ) where++import qualified Cases            as CS+import           Data.Aeson.Types+import qualified Data.Char        as C+import qualified Data.List        as L+import           Data.Maybe       (fromMaybe)+import qualified Data.Text        as T++baseOptions :: Options+baseOptions = defaultOptions+   { omitNothingFields = True+   }++stripFieldNamePrefix :: String -> String -> String+stripFieldNamePrefix pre = lowerFirst . try (L.stripPrefix pre)++dropSuffixAndSnakeCase :: String -> String -> String+dropSuffixAndSnakeCase suffix = T.unpack . CS.process CS.lower CS.snake . T.pack . dropSuffix suffix++lowerFirst :: String -> String+lowerFirst (x : xs) = C.toLower x : xs+lowerFirst [] = []++dropSuffix :: String -> String -> String+dropSuffix suffix = reverse . try (L.stripPrefix . reverse $ suffix) . reverse++try :: (a -> Maybe a) -> a -> a+try f v = fromMaybe v (f v)
+ src/Data/Connect/BaseTypes.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE DeriveGeneric              #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings          #-}+module Data.Connect.BaseTypes where++import           Data.Aeson+import           Data.Aeson.Types+import           Data.Connect.AesonHelpers+import           Data.Connect.OrphanInstances ()+import           Data.Text+import qualified Data.Time.Units              as DTU+import           GHC.Generics+import qualified Network.URI                  as NU++-- | This data type represents a Key for a particular data type.+data Key t a = Key t deriving (Show, Eq, Generic)++-- | This data type represents an Atlassian Connect Add-on key.+data PluginKey = PluginKey Text deriving (Show, Eq, Generic)++-- | Represents a timeout in seconds.+newtype Timeout = Timeout DTU.Second deriving (Show, Eq, Enum, Num, Ord, Real, Integral)++-- | Represents the Vendor of the add-on; which will be you. Put your details in this structure.+data Vendor = Vendor+   { vendorName :: Name Vendor -- ^ Your name as a Vendor. Might be your personal name or your business name.+   , vendorUrl :: NU.URI -- ^ A URL to a website that represents you as a vendor.+   } deriving (Show, Eq, Generic)++-- | If your Atlassian Connect addon wants to perform any server side communication with the host product then you will+-- need to use authentication. Otherwise you should specify that you don't need authentication.+data Authentication = Authentication+   { authType :: AuthType -- ^ The authentication type that you wish to use.+   } deriving (Show, Eq, Generic)++-- | The authentication type that you wish to use in your Add-on.+data AuthType+   = Jwt -- ^ If you need to communicate with the host product then you will want to request JWT authentication.+   | None -- ^ If you do not need to communicate the host product then you should request None for authentication.+   deriving (Show, Eq, Generic)++-- | Represents an arbitrary icon. Potentially for an Atlassian Connect module or for the entire add-on itself.+data IconDetails = IconDetails+   { iconUrl    :: Text -- ^ The URI to the icon.+   , iconWidth  :: Maybe Integer -- ^ The width of the icon.+   , iconHeight :: Maybe Integer -- ^ The height of the icon.+   } deriving (Show, Generic)++instance ToJSON PluginKey++-- | Atlassian Connect descriptors contain many names: module names, add-on names, vendor names etc. We want to make sure+-- that these names don't get put in places that they do not belong. Or, if they do get moved around, they get moved around+-- specifically. We are just adding type saefty to names.+data Name a = Name Text deriving (Show, Eq, Generic)++instance ToJSON (Name PluginKey)+instance ToJSON (Name Vendor)++instance ToJSON IconDetails where+   toJSON = genericToJSON baseOptions+      { fieldLabelModifier = stripFieldNamePrefix "icon"+      }++instance ToJSON Vendor where+   toJSON = genericToJSON baseOptions+      { fieldLabelModifier = stripFieldNamePrefix "vendor"+      }++instance ToJSON Authentication where+   toJSON = genericToJSON baseOptions+      { fieldLabelModifier = stripFieldNamePrefix "auth"+      }++instance ToJSON AuthType where+   toJSON Jwt  = "jwt"+   toJSON None  = "none"
+ src/Data/Connect/Conditions.hs view
@@ -0,0 +1,192 @@+{-# LANGUAGE DeriveGeneric     #-}+{-# LANGUAGE OverloadedStrings #-}+module Data.Connect.Conditions+    ( Condition(..)+    , ConditionType(..)+    , ConditionSource(..)+    , JIRACondition(..)+    , ConfluenceCondition(..)+    , staticJiraCondition+    , staticConfluenceCondition+    , remoteCondition+    , invertCondition+    ) where++import           Data.Aeson+import           Data.Connect.AesonHelpers+import           Data.Connect.OrphanInstances ()+import           GHC.Generics+import qualified Data.HashMap.Strict as HM++-- | A 'Condition' can be placed on an Atlassian Connect Module to cause it to display or not based on the result it+-- returns. For example, you can choose not to show a WebPanel if the user viewing the page is not logged in. Conditions+-- are very useful in curating when your modules will appear to your users.+--+-- The Atlassian Connect documentation describes conditions fully:+-- <https://developer.atlassian.com/static/connect/docs/concepts/conditions.html>+data Condition+   -- | A single condition based on a source.+   = SingleCondition+   { conditionSource   :: ConditionSource -- ^ The source of this condition.+   , conditionInverted :: Bool -- ^ If you should invert the condition. For example, only show if user is NOT logged in.+   , conditionParams :: HM.HashMap String String -- ^ Extra parameters to pass with the condition to give it context.+   }+   -- | A condition that is the composition of one or more conditions. The 'ConditionType' decides the way in which the+   -- conditions are composed+   | CompositeCondition+   { subConditions :: [Condition] -- ^ The conditions that will be merged together.+   , conditionType :: ConditionType -- ^ The way in which the conditions will be merged together.+   }+   deriving (Show)++-- | Turn a standard JIRA Condition into a regular 'Condition'.+staticJiraCondition :: JIRACondition -> Condition+staticJiraCondition c = SingleCondition { conditionSource = StaticJIRACondition c, conditionInverted = False, conditionParams = HM.empty }++-- | Turn a standard Confluence Condition into a regular 'Condition'.+staticConfluenceCondition :: ConfluenceCondition -> Condition+staticConfluenceCondition c = SingleCondition { conditionSource = StaticConfluenceCondition c, conditionInverted = False, conditionParams = HM.empty }++-- | Given a URI that defines a remote condition convert it into a regular 'Condition'.+remoteCondition :: String -> Condition+remoteCondition conditionLocation = SingleCondition { conditionSource = RemoteCondition conditionLocation, conditionInverted = False, conditionParams = HM.empty }++-- | Invert the given condition.+invertCondition :: Condition -> Condition+invertCondition c@(SingleCondition {}) = c { conditionInverted = not . conditionInverted $ c }+-- One application of DeMorgans law for composite conditions.+invertCondition (CompositeCondition cs ct) = CompositeCondition (fmap invertCondition cs) (invertConditionType ct)++invertConditionType :: ConditionType -> ConditionType+invertConditionType AndCondition = OrCondition+invertConditionType OrCondition = AndCondition++instance ToJSON Condition where+   toJSON sc@(SingleCondition {}) = object+      [ "condition" .= conditionSource sc+      , "invert" .= conditionInverted sc+      , "params" .= conditionParams sc+      ]+   toJSON cc@(CompositeCondition {}) = object [ "conditions" .= subConditions cc, "type" .= conditionType cc]++-- | Composite Conditions can be joined together to behave as a single condition. The way that you can join them together+-- is decided by the condition type.+data ConditionType+   = AndCondition -- ^ The boolean intersection of the conditions.+   | OrCondition -- ^ The boolean union of the conditions.+   deriving (Eq, Show)++instance ToJSON ConditionType where+   toJSON AndCondition = String "AND"+   toJSON OrCondition  = String "OR"++-- | Conditions can be specified by the Host application or by the Atlassian Connect add-on itself. This means that the+-- source of the condition needs to be specified and that is what you can use this data type to do.+data ConditionSource+   -- | A static JIRA condition.+   = StaticJIRACondition        JIRACondition+   -- | A static Confluence condition.+   | StaticConfluenceCondition  ConfluenceCondition+   -- | A remote condition defined by your Atlassian Connect application.+   | RemoteCondition+      { remoteConditionPath :: String -- ^ The relative URI that you should hit in your Atlassian Connect application to+                                      -- get the condition result. This URI, when hit, should return a JSON response in+                                      -- the format:+                                      --+                                      -- > { "shouldDisplay": <true|false> }+      }+   deriving (Show, Eq)++instance ToJSON ConditionSource where+   toJSON (StaticJIRACondition x) = toJSON x+   toJSON (StaticConfluenceCondition x) = toJSON x+   toJSON (RemoteCondition x) = toJSON x++-- The JIRA Conditions have been taken from:+-- https://developer.atlassian.com/static/connect/docs/modules/fragment/single-condition.html+-- as of the following date: Tue 23 Sep 2014 08:45:49 EST+-- Please update the date above whenever you update these conditions.++-- | The conditions that have been provided by JIRA. Please see the single condition documentation for more details:+-- <https://developer.atlassian.com/static/connect/docs/modules/fragment/single-condition.html>+data JIRACondition+   = CanAttachFileToIssueJiraCondition+   | CanManageAttachmentsJiraCondition+   | FeatureFlagJiraCondition+   | HasIssuePermissionJiraCondition+   | HasProjectPermissionJiraCondition+   | HasSelectedProjectPermissionJiraCondition+   | HasSubTasksAvaliableJiraCondition+   | HasVotedForIssueJiraCondition+   | IsAdminModeJiraCondition+   | IsIssueAssignedToCurrentUserJiraCondition+   | IsIssueEditableJiraCondition+   | IsIssueReportedByCurrentUserJiraCondition+   | IsIssueUnresolvedJiraCondition+   | IsSubTaskJiraCondition+   | IsWatchingIssueJiraCondition+   | LinkingEnabledJiraCondition+   | SubTasksEnabledJiraCondition+   | TimeTrackingEnabledJiraCondition+   | UserHasIssueHistoryJiraCondition+   | UserIsAdminJiraCondition+   | UserIsLoggedInJiraCondition+   | UserIsProjectAdminJiraCondition+   | UserIsSysadminJiraCondition+   | UserIsTheLoggedInUserJiraCondition+   | VotingEnabledJiraCondition+   | WatchingEnabledJiraCondition+   deriving (Eq, Show, Generic)++instance ToJSON JIRACondition where+   toJSON = toJSON . dropSuffixAndSnakeCase "JiraCondition" . show++-- | The conditions that have been provided by Confluence. Please see the single condition documentation for more details:+-- <https://developer.atlassian.com/static/connect/docs/modules/fragment/single-condition.html>+data ConfluenceCondition+   = ActiveThemeConfluenceCondition+   | CanEditSpaceStylesConfluenceCondition+   | CanSignupConfluenceCondition+   | ContentHasAnyPermissionsSetConfluenceCondition+   | CreateContentConfluenceCondition+   | EmailAddressPublicConfluenceCondition+   | FavouritePageConfluenceCondition+   | FavouriteSpaceConfluenceCondition+   | FeatureFlagConfluenceCondition+   | FollowingTargetUserConfluenceCondition+   | HasAttachmentConfluenceCondition+   | HasBlogPostConfluenceCondition+   | HasPageConfluenceCondition+   | HasSpaceConfluenceCondition+   | HasTemplateConfluenceCondition+   | LatestVersionConfluenceCondition+   | NotPersonalSpaceConfluenceCondition+   | PrintableVersionConfluenceCondition+   | ShowingPageAttachmentsConfluenceCondition+   | SpaceFunctionPermissionConfluenceCondition+   | SpaceSidebarConfluenceCondition+   | TargetUserCanSetStatusConfluenceCondition+   | TargetUserHasPersonalBlogConfluenceCondition+   | TargetUserHasPersonalSpaceConfluenceCondition+   | ThreadedCommentsConfluenceCondition+   | TinyUrlSupportedConfluenceCondition+   | UserCanCreatePersonalSpaceConfluenceCondition+   | UserCanUpdateUserStatusConfluenceCondition+   | UserCanUseConfluenceConfluenceCondition+   | UserFavouritingTargetUserPersonalSpaceConfluenceCondition+   | UserHasPersonalBlogConfluenceCondition+   | UserHasPersonalSpaceConfluenceCondition+   | UserIsAdminConfluenceCondition+   | UserIsConfluenceAdministratorConfluenceCondition+   | UserIsLoggedInConfluenceCondition+   | UserIsSysadminConfluenceCondition+   | UserLoggedInEditableConfluenceCondition+   | UserWatchingPageConfluenceCondition+   | UserWatchingSpaceConfluenceCondition+   | UserWatchingSpaceForContentTypeConfluenceCondition+   | ViewingContentConfluenceCondition+   | ViewingOwnProfileConfluenceCondition+   deriving (Eq, Show, Generic)++instance ToJSON ConfluenceCondition where+   toJSON = toJSON . dropSuffixAndSnakeCase "ConfluenceCondition" . show
+ src/Data/Connect/Descriptor.hs view
@@ -0,0 +1,206 @@+{-# LANGUAGE DeriveGeneric     #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}++{-|+Module      : Data.Connect.Descriptor+Description : Defines a strictly typed Atlassian Connect add-on descriptor.+Copyright   : (c) Robert Massioli, 2014+License     : APACHE-2+Maintainer  : rmassaioli@atlassian.com+Stability   : experimental++This module provides the data types to let you write your own typesafe Atlassian Connect descriptor and it comes with+Aeson bindings so that you can easily convert into json: the format that the Atlassian Connect framework expects.++Atlassian Connect is a framework for writing Add-on's that can run inside the Atlassian Cloud products. You can find more+information from the Atlassian Connect documentation <https://developer.atlassian.com/static/connect/docs/guides/introduction.html>.++The plugin descriptor is defined by the 'Plugin' class. The end result of using this Haskell Module should be for you to+end up with a valid 'Plugin'. To turn your plugin into JSON that the Atlassian marketplace can accept just use the 'encode'+function from the Aeson library. For example, here in an example Atlassian Connect Descriptor:++> pluginToJsonString :: Plugin -> ByteString+> pluginToJsonString = encode+>+> exampleDescriptor :: Plugin+> exampleDescriptor = (pluginDescriptor (PluginKey "my-example-connect") baseURL (Authentication Jwt))+>     { pluginName = Just . Name $ "My Example Connect Addon"+>     , pluginDescription = Just "This is an example connect descriptor."+>     , vendor = Just $ Vendor (Name "Awesome Devs") (toURI "http://awesome-devs.com")+>     , lifecycle = Just defaultLifecycle+>     , modules = Just exampleModules+>     , enableLicensing = Just False+>     , links = fromList+>         [ ("documentation", toURI "http://awesome-devs.com/docs")+>         , ("source", toURI "http://bitbucket.org/awesome-devs/connect-addon")+>         ]+>     , scopes = Just [Read, Admin]+>     }+>+> exampleModules :: Modules+> exampleModules = Modules exampleJIRAModules emptyConfluenceModules+>+> exampleJIRAModules :: JIRAModules+> exampleJIRAModules = emptyJIRAModules+>     { jiraWebPanels =+>         [ WebPanel+>             { wpKey = "test-web-panel"+>             , wpName = Name "Test Web Panel"+>             , wpLocation = "some-location-in-jira"+>             , wpUrl = "/panel/location/for"+>             , wpConditions = [staticJiraCondition UserIsAdminJiraCondition]+>             }+>         ]+>     , jiraGeneralPages =+>         [ GeneralPage+>             { generalPageKey = "test-general-page"+>             , generalPageName = Name "Test General Page"+>             , generalPageLocation = Just "some-other-location-in-jira"+>             , generalPageWeight = Just 1234+>             , generalPageUrl = "/panel/general-page"+>             , generalPageIcon = Just IconDetails+>                 { iconUrl = "/static/path/to/icon.png"+>                 , iconWidth = Just 20+>                 , iconHeight = Just 40+>                 }+>             , generalPageConditions = [staticJiraCondition UserHasIssueHistoryJiraCondition]+>             }+>         ]+>     , jiraWebhooks =+>         [ Webhook+>             { webhookEvent = JiraIssueDeleted+>             , webhookUrl = "/webhook/handle-deletion"+>             }+>         ]+>     }++You can use this library to make your own. This library will experience change whenever the Atlassian Connect descriptor+changes. There are likely to be many breaking changes but we will keep track of them using the standard Haskell version+structure.+-}+module Data.Connect.Descriptor (+   -- * Atlassian Connect Add-on Descriptor+     Plugin(..)+   , pluginDescriptor+   -- * Basic Types+   , Name(..)+   , Key(..)+   , PluginKey(..)+   , Timeout(..)+   , IconDetails(..)+   -- * Authentication+   , Authentication(..)+   , AuthType(..)+   -- * Vendor Details+   , Vendor(..)+   -- * Lifecycle+   , Lifecycle(..)+   , emptyLifecycle+   , defaultLifecycle+   -- * Add-on Modules+   , Modules(..)+   , JIRAModules(..)+   , emptyJIRAModules+   , ConfluenceModules(..)+   , emptyConfluenceModules+   , WebPanel(..)+   , GeneralPage(..)+   -- ** Webhooks+   , Webhook(..)+   , WebhookEvent(..)+   -- * Module Conditions+   , Condition(..)+   , ConditionType(..)+   , ConditionSource(..)+   , remoteCondition+   , JIRACondition(..)+   , staticJiraCondition+   , ConfluenceCondition(..)+   , staticConfluenceCondition+   , invertCondition+   -- * Scopes (Permissions)+   , ProductScope(..)+   ) where++import           Data.Aeson+import           Data.Aeson.Types+import           Data.Connect.AesonHelpers+import           Data.Connect.BaseTypes+import           Data.Connect.Conditions+import           Data.Connect.Lifecycle+import           Data.Connect.Modules+import           Data.Connect.Scopes+import           Data.Connect.Webhooks+import qualified Data.HashMap.Strict       as HM+import           Data.Text+import           GHC.Generics+import           Network.URI++-- | A 'Plugin' is the end result of an Atlassian Connect descriptor. It is what you should provide to the Atlassian+-- Marketplace in order to register your plugin and it is what your Atlassian Cloud customers will download to install+-- your Add-on via the Atlassian UPM (Universal Plugin Manager). Only a very small number of fields are strictly required+-- to generate a valid Atlassian Connect plugin descriptor. Everything that is optional is marked by a maybe type.+--+-- Even though we provide documentation here you shoucd check the Atlassian Connect Descriptor documentation if you want+-- to get accurate information on the contents of a plugin: <https://developer.atlassian.com/static/connect/docs/modules/>+data Plugin = Plugin+   { pluginKey         :: PluginKey -- ^ Plugin keys are required. The important detail about this key is that it should+                                    -- be unique across the Atlassian Marketplace. For example, a good key might be+                                    -- @com.yourcompanyorpersonalname.youraddonname@ because it would be unique in the+                                    -- marketplace.+   , pluginBaseUrl     :: URI -- ^ Every plugin must specify a base url where all other relative URI's in the plugin will+                              -- call to in production. This is the url that the Atlassian Marketplace will query for your+                              -- descriptor and the url that your customers will come in on. This is especially important+                              -- for load balanced applications and will also likely be different in your staging and production+                              -- environments.+   , authentication    :: Authentication -- ^ The authentication type that you plugin requires. See 'Authentication' for more details.+   , pluginName        :: Maybe (Name Plugin) -- ^ While your add-on does not require a name it is strongly recommended+                                              -- as this is the human readable name that will appear in the UPM amongst other places.+   , pluginDescription :: Maybe Text -- ^ You should give your add-on a description. This description will appear in multiple+                                     -- locations on the Atlassian Marketplace and UPM and will be used to explain what your+                                     -- add-on does.+   , vendor            :: Maybe Vendor -- ^ You are the 'Vendor'. Put your details here!+   , lifecycle         :: Maybe Lifecycle -- ^ Atlassian Connect addon's have a lifecycle. Register your handlers for+                                          -- the 'Lifecycle' events here so that you can tell, for example, when your+                                          -- addon is installed or enabled.+   , modules           :: Maybe Modules -- ^ The modules that your Atlassian Connect add-on provides to the Cloud application. Look at the 'Modules' documentaiton for more information.+   , apiVersion        :: Maybe Text -- ^ Required if you wish to provide new versions of your addon to a subset of beta customers.+   , enableLicensing   :: Maybe Bool -- ^ If you are giving away a free add-on then you can set this to false, otherwise set it to true.+   , links             :: HM.HashMap Text URI -- ^ A collection of custom links that you wish to publish with your add-on. Like documentation or bug-tracking links.+   , scopes            :: Maybe [ProductScope] -- ^ The scopes that your add-on requires. See 'ProductScope' for more information.+   } deriving (Show, Generic)++instance ToJSON (Name Plugin)++instance ToJSON Plugin where+   toJSON = genericToJSON baseOptions+      { fieldLabelModifier = stripFieldNamePrefix "plugin"+      }++-- | A helper method to generate a bare-bones Atlassian Connect add-on by providing only the absolutely required fields.+-- You can then use Haskell record syntax to update the plugin with more details. For example:+--+-- > (pluginDescriptor (PluginKey . pack $ "com.company.mycoolplugin") (fromJust . parseURI $ "http://mycoolplugin.company.com") (Authentication Jwt))+-- >    { pluginName = Just . Name . pack $ "My Cool Plugin"+-- >    , pluginDescription = Just . pack $ "Chil and be cool, you have a plugin descriptor."+-- >    }+pluginDescriptor+   :: PluginKey -- ^ The key for your add-on.+   -> URI -- ^ The base url for your add-on.+   -> Authentication -- ^ The authentication that your add-on requires.+   -> Plugin -- ^ A bare-bones Atlassian Connect descriptor.+pluginDescriptor key' url' auth = Plugin+   { pluginKey = key'+   , pluginBaseUrl = url'+   , authentication = auth+   , pluginName = Nothing+   , pluginDescription = Nothing+   , vendor = Nothing+   , apiVersion = Nothing+   , modules = Nothing+   , enableLicensing = Nothing+   , lifecycle = Nothing+   , links = HM.empty+   , scopes = Nothing+   }
+ src/Data/Connect/Lifecycle.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE DeriveGeneric #-}+module Data.Connect.Lifecycle+    ( Lifecycle(..)+    , emptyLifecycle+    , defaultLifecycle+    ) where++import           Data.Aeson+import           Data.Connect.AesonHelpers+import           Data.Connect.OrphanInstances ()+import           GHC.Generics+import qualified Network.URI                  as NU++-- | Every Atlassian Connect add-on can be installed, uninstalled, enabled and disabled. These are known as 'Lifecycle'+-- events. These events will fire on each and every Cloud instance that your add-on is installed on. You can request in+-- your Atlassian Connect add-on descriptor to be alerted of lifecycle events. When the event fires, if you have+-- requested it, you will be given the details of the event in a JSON blob by the host application.+--+-- The lifecycle events are documented fully in the Atlassian Connect documentation:+-- <https://developer.atlassian.com/static/connect/docs/modules/lifecycle.html>+--+-- It is important to note that the installed event is particularily important to any Atlassian Connect add-on that+-- needs to use 'Jwt' auth tokens because the installed handler will come with the shared secret for your add-on on+-- that particular instance.+data Lifecycle = Lifecycle+   { installed   :: Maybe NU.URI -- ^ Potential relative URI to call every time an add-on is installed on an instance.+   , uninstalled :: Maybe NU.URI -- ^ Potential relative URI to call every time an add-on is uninstalled on an instance.+   , enabled     :: Maybe NU.URI -- ^ Potential relative URI to call every time an add-on is enabled on an instance.+   , disabled    :: Maybe NU.URI -- ^ Potential relative URI to call every time an add-on is disabled on an instance.+   } deriving (Show, Generic) -- TODO++instance ToJSON Lifecycle where+   toJSON = genericToJSON baseOptions++-- | The empty 'Lifecycle' allowing you to specify exactly which events you wish to handle with Haskell record syntax.+emptyLifecycle :: Lifecycle+emptyLifecycle = Lifecycle Nothing Nothing Nothing Nothing++-- | The default 'Lifecycle' where installed goes to /installed and so on and so forth for every lifecycle event. You+-- can choose to disclude certain events by 'Nothing' them out.+defaultLifecycle :: Lifecycle+defaultLifecycle = Lifecycle+   { installed = NU.parseRelativeReference "/installed"+   , uninstalled = NU.parseRelativeReference "/uninstalled"+   , enabled = NU.parseRelativeReference "/enabled"+   , disabled = NU.parseRelativeReference "/disabled"+   }
+ src/Data/Connect/Modules.hs view
@@ -0,0 +1,144 @@+{-# LANGUAGE DeriveGeneric     #-}+{-# LANGUAGE FlexibleInstances #-}+module Data.Connect.Modules+   ( Modules(..)+   , JIRAModules(..)+   , emptyJIRAModules+   , ConfluenceModules(..)+   , emptyConfluenceModules+   , WebPanel(..)+   , GeneralPage(..)+   ) where++import           Data.Aeson+import           Data.Aeson.Types+import           Data.Connect.AesonHelpers+import           Data.Connect.BaseTypes+import           Data.Connect.Conditions+import           Data.Connect.Webhooks+import qualified Data.HashMap.Strict       as HM+import qualified Data.Text                 as T+import           GHC.Generics++-- | 'Modules' are perhaps the most important part of your Atlassian Connect descriptor. They specify which parts of the+-- host application you wish to inject content into. They provide your entry point into the host application.+--+-- Atlassian Connect provides a large set of pre-defined entry points into the host application. Some of which are common+-- to every application and some of which are unique to the particular application that you are targeting:+--+-- * To see the JIRA modules: <https://developer.atlassian.com/static/connect/docs/modules/jira/index.html>+-- * To see the Confluence modules: <https://developer.atlassian.com/static/connect/docs/modules/confluence/index.html>+--+-- Note: One important point about modules: they must all have a key and that key must be unique inside the same Atlassian+-- Connect addon.+data Modules = Modules+   { jiraModules       :: JIRAModules -- ^ All of the JIRA Modules that you wish to define.+   , confluenceModules :: ConfluenceModules -- ^ All of the Confluence modules that you wish to define.+   } deriving (Show, Generic)++instance ToJSON Modules where+   toJSON modules = case (jm, cm) of+      (Object jiraObject, Object confluenceObject) -> Object $ HM.union jiraObject confluenceObject+      _ -> Null+      where+         jm = toJSON . jiraModules $ modules+         cm = toJSON . confluenceModules $ modules++-- TODO use Endo Modules to add modules for multiple different products to the modules list++-- | A collection of all of the JIRA Modules that you can define. For more documentation on which Modules are supported+-- the Atlassian Connect framework please see 'Modules'. You can also find more documentation on each of the modules.+data JIRAModules = JIRAModules+   { jiraWebPanels    :: [WebPanel]+   , jiraGeneralPages :: [GeneralPage]+   , jiraWebhooks     :: [Webhook]+   } deriving (Show, Generic)++instance ToJSON JIRAModules where+   toJSON = genericToJSON baseOptions++-- | A collection of all of the Confluence Modules that you can define. For more documentation on which Modules are supported+-- the Atlassian Connect framework please see 'Modules'. You can also find more documentation on each of the modules.+data ConfluenceModules = ConfluenceModules+   { confluenceWebPanels :: [WebPanel]+   } deriving (Show, Generic)++instance ToJSON ConfluenceModules where+   toJSON = genericToJSON baseOptions++-- | Empty JIRA Modules; useful when you only want to define a few modules via Haskell record syntax.+emptyJIRAModules :: JIRAModules+emptyJIRAModules = JIRAModules [] [] []++-- | Empty Confluence Modules; useful when you only want to define a few modules via Haskell record syntax.+emptyConfluenceModules :: ConfluenceModules+emptyConfluenceModules = ConfluenceModules []++-- | A 'WebPanel' is an injectable segment of the host application that you can place content inside. Currently the+-- WebPanel has the same structure for both JIRA and Confluence but, potentially, that could change in the future.+-- You can read their Atlassian Connect documentation here:+--+-- * JIRA Web panels: <https://developer.atlassian.com/static/connect/docs/modules/jira/web-panel.html>+-- * Confluence Web panels: <https://developer.atlassian.com/static/connect/docs/modules/confluence/web-panel.html>+--+-- Here is what an example Hello World web panel might look like:+--+-- > helloWorldWebPanel = WebPanel+-- >    { wpKey = "hello-world"+-- >    , wpName = Name "Hello world!"+-- >    , wpUrl = "/panel/show-hello-world"+-- >    , wpLocation = "atl.jira.view.issue.right.context"+-- >    , wpConditions = [staticJiraCondition UserIsLoggedInJiraCondition]+-- >    }+-- >    where+-- >       toURI = fromJust . parseRelativeReference+--+-- WebPanels are a great way to inject your add-on's content into the host application.+data WebPanel = WebPanel+   { wpKey        :: T.Text -- ^ The add-on unique key for this module.+   , wpName       :: Name WebPanel -- ^ The name of this panel, likely to appear in the User Interface.+   , wpUrl        :: T.Text -- ^ The relative URI that the host product will hit to get HTML content.+   , wpLocation   :: T.Text -- ^ The location that this content should be injected in the host product.+   , wpConditions :: [Condition] -- ^ The 'Condition's that need to be met for this module to be displayed.+   } deriving (Show, Generic)++instance ToJSON WebPanel where+   toJSON = genericToJSON baseOptions+      { fieldLabelModifier = stripFieldNamePrefix "wp"+      }++-- | A 'GeneralPage' makes your add-on take a large section of screen realestate, with the intention of displaying+-- your own page with no other distracting content. This is very useful for pages like Configuration screens or+-- Statistics pages where you really want the user to be fully working with your add-on inside the host product.+--+-- General pages, like Web Panels, are common to JIRA and Confluence and share the same json format. However they have+-- separate documentation:+--+-- * JIRA General Page: <https://developer.atlassian.com/static/connect/docs/modules/jira/general-page.html>+-- * Confluence General Page: <https://developer.atlassian.com/static/connect/docs/modules/confluence/general-page.html>+--+-- Even though, at this point in time, the documentation looks identical.+data GeneralPage = GeneralPage+   { generalPageKey        :: T.Text -- ^ The add-on unique key for this module.+   , generalPageName       :: Name GeneralPage -- ^ The name of this General Page. Likely to be used in the page title.+   , generalPageUrl        :: T.Text -- ^ The relative URI that the host product will hit to get the HTML content for the page.+   , generalPageLocation   :: Maybe T.Text -- ^ The location for this General Page to display; see the docs for your options.+   , generalPageIcon       :: Maybe IconDetails -- ^ The optional icon to use for this general page.+   , generalPageWeight     :: Maybe Integer -- ^ Determines the order that this item appears in any menu or list.+                                            -- Lower numbers mean that it will appear higher in the list.+   , generalPageConditions :: [Condition] -- ^ The 'Condition's that need to be met for this module to be displayed.+   } deriving (Show, Generic)++instance ToJSON GeneralPage where+   toJSON = genericToJSON baseOptions+      { fieldLabelModifier = stripFieldNamePrefix "generalPage"+      }++instance ToJSON (Name WebPanel) where+   toJSON = nameToValue++instance ToJSON (Name GeneralPage) where+   toJSON = nameToValue++nameToValue :: Name a -> Value+nameToValue (Name name) = object [ T.pack "value" .= name ]
+ src/Data/Connect/OrphanInstances.hs view
@@ -0,0 +1,13 @@+module Data.Connect.OrphanInstances where++import           Control.Monad (mzero)+import           Data.Aeson+import qualified Data.Text     as T+import qualified Network.URI   as NU++instance FromJSON NU.URI where+   parseJSON (String uriString) = maybe mzero return (NU.parseURI . T.unpack $ uriString)+   parseJSON _ = mzero++instance ToJSON NU.URI where+   toJSON = String . T.pack . show
+ src/Data/Connect/Scopes.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE DeriveGeneric     #-}+{-# LANGUAGE OverloadedStrings #-}+module Data.Connect.Scopes+    ( ProductScope(..)+    ) where++import           Control.Monad (mzero)+import           Data.Aeson+import           GHC.Generics++-- | Scopes are an Atlassian Connect concept that declare how much access your addon requires to any give Cloud instance.+-- These scopes can be thought of as permissions are are well documented:+-- <https://developer.atlassian.com/static/connect/docs/scopes/scopes.html>+--+-- It is important to note that these scopes only give you restricted access to certain REST resources. You can not query+-- any REST url as you would with an Atlassian Server plugin. The restricted set of REST resources per application can be+-- found in the Atlassian Connect documentation.+data ProductScope+  = Read -- ^ The read scope means that you can pull data from the Cloud application.+  | Write -- ^ The write scope gives you the same access as a regular user of the Atlassian connect application.+  | Delete -- ^ The delete scope is required if you want to perform potentially destructive operations on data.+  | ProjectAdmin -- ^ A JIRA specific scope. Lets your add-on administer a project in JIRA.+  | SpaceAdmin -- ^ A Confluence specific scope. Lets your add-on administer a space in Confluence.+  | Admin -- ^ Gives your Atlassian Connect add-on administrative rights to the Cloud instance. (But NOT system+          -- administrator permission. Happily you cannot request that level of access.)+  deriving (Show, Eq, Generic)++instance ToJSON ProductScope where+   toJSON Read         = "read"+   toJSON Write        = "write"+   toJSON Delete       = "delete"+   toJSON ProjectAdmin = "project_admin"+   toJSON SpaceAdmin   = "space_admin"+   toJSON Admin        = "admin"++instance FromJSON ProductScope where+  parseJSON (String "read")             = return Read+  parseJSON (String "write")            = return Write+  parseJSON (String "delete")           = return Delete+  parseJSON (String "project_admin")    = return ProjectAdmin+  parseJSON (String "space_admin")      = return SpaceAdmin+  parseJSON (String "admin")            = return Admin+  parseJSON _                           = mzero
+ src/Data/Connect/Webhooks.hs view
@@ -0,0 +1,152 @@+{-# LANGUAGE DeriveGeneric     #-}+{-# LANGUAGE OverloadedStrings #-}+module Data.Connect.Webhooks+   ( Webhook(..)+   , WebhookEvent(..)+   ) where++import           Data.Aeson+import           Data.Aeson.Types+import           Data.Connect.AesonHelpers+import           Data.Connect.OrphanInstances ()+import qualified Data.Text                    as T+import           GHC.Generics++-- | When users of the host application perform updates your Atlassian Connect add-on will not be alerted /unless/+-- it listens to the 'WebhookEvent's coming from that application. Webhooks are the way to close the issue recency loop+-- in the Atlassian products. It is important to note that Webhooks are 'best effort' and that there is no guarantee+-- that the webhook will make it to your Atlassian Connect application.+--+-- The Atlassian connect webhook documentation explains this in more detail:+-- <https://developer.atlassian.com/static/connect/docs/modules/jira/webhook.html>+data Webhook = Webhook+   { webhookEvent :: WebhookEvent -- ^ The event that you want your Atlassian Connect add-on to watch.+   , webhookUrl   :: T.Text -- ^ The relative URI that you wish to handle the webhook response.+   } deriving (Show, Generic)++instance ToJSON Webhook where+   toJSON = genericToJSON baseOptions+      { fieldLabelModifier = stripFieldNamePrefix "webhook"+      }++-- | The webhook event that you wish to watch from your Atlassian Connect add-on.+data WebhookEvent+   = ConnectAddonDisabled+   | ConnectAddonEnabled+   | JiraWebhookPostFunction+   | JiraIssueCreated+   | JiraIssueDeleted+   | JiraIssueUpdated+   | JiraWorklogUpdated+   | JiraPluginEnabled+   | JiraPluginsUpgraded+   | JiraRemoteIssueLinkAggregateClearedEvent+   | JiraRemoteWorkflowPostFunction+   | ConfluenceAttachmentCreated+   | ConfluenceAttachmentRemoved+   | ConfluenceAttachmentUpdated+   | ConfluenceAttachmentViewed+   | ConfluenceBlogCreated+   | ConfluenceBlogRemoved+   | ConfluenceBlogRestored+   | ConfluenceBlogTrashed+   | ConfluenceBlogUpdated+   | ConfluenceBlogViewed+   | ConfluenceCacheStatisticsChanged+   | ConfluenceCommentCreated+   | ConfluenceCommentRemoved+   | ConfluenceCommentUpdated+   | ConfluenceContentPermissionsUpdated+   | ConfluenceLabelAdded+   | ConfluenceLabelCreated+   | ConfluenceLabelDeleted+   | ConfluenceLabelRemoved+   | ConfluenceLogin+   | ConfluenceLoginFailed+   | ConfluenceLogout+   | ConfluencePageChildrenReordered+   | ConfluencePageCreated+   | ConfluencePageMoved+   | ConfluencePageRemoved+   | ConfluencePageRestored+   | ConfluencePageTrashed+   | ConfluencePageUpdated+   | ConfluencePageViewed+   | ConfluenceSearchPerformed+   | ConfluenceSpaceCreated+   | ConfluenceSpaceLogoUpdated+   | ConfluenceSpacePermissionsUpdated+   | ConfluenceSpaceRemoved+   | ConfluenceSpaceUpdated+   | ConfluenceStatusCleared+   | ConfluenceStatusCreated+   | ConfluenceStatusRemoved+   | ConfluenceUserCreated+   | ConfluenceUserDeactivated+   | ConfluenceUserFollowed+   | ConfluenceUserReactivated+   | ConfluenceUserRemoved+   | ConfluenceGroupCreated+   | ConfluenceGroupRemoved+   | ServerUpgraded+   deriving (Show)++instance ToJSON WebhookEvent where+   toJSON ConnectAddonDisabled = String "connect_addon_disabled"+   toJSON ConnectAddonEnabled = String "connect_addon_enabled"+   toJSON JiraWebhookPostFunction = String "jira-webhook-post-function"+   toJSON JiraIssueCreated = String "jira:issue_created"+   toJSON JiraIssueDeleted = String "jira:issue_deleted"+   toJSON JiraIssueUpdated = String "jira:issue_updated"+   toJSON JiraWorklogUpdated = String "jira:worklog_updated"+   toJSON JiraPluginEnabled = String "plugin_enabled"+   toJSON JiraPluginsUpgraded = String "plugins_upgraded"+   toJSON JiraRemoteIssueLinkAggregateClearedEvent = String "remote_issue_link_aggregate_cleared_event"+   toJSON JiraRemoteWorkflowPostFunction = String "remote_workflow_post_function"+   toJSON ConfluenceAttachmentCreated = "attachment_created"+   toJSON ConfluenceAttachmentRemoved = "attachment_removed"+   toJSON ConfluenceAttachmentUpdated = "attachment_updated"+   toJSON ConfluenceAttachmentViewed = "attachment_viewed"+   toJSON ConfluenceBlogCreated = "blog_created"+   toJSON ConfluenceBlogRemoved = "blog_removed"+   toJSON ConfluenceBlogRestored = "blog_restored"+   toJSON ConfluenceBlogTrashed = "blog_trashed"+   toJSON ConfluenceBlogUpdated = "blog_updated"+   toJSON ConfluenceBlogViewed = "blog_viewed"+   toJSON ConfluenceCacheStatisticsChanged = "cache_statistics_changed"+   toJSON ConfluenceCommentCreated = "comment_created"+   toJSON ConfluenceCommentRemoved = "comment_removed"+   toJSON ConfluenceCommentUpdated = "comment_updated"+   toJSON ConfluenceContentPermissionsUpdated = "content_permissions_updated"+   toJSON ConfluenceLabelAdded = "label_added"+   toJSON ConfluenceLabelCreated = "label_created"+   toJSON ConfluenceLabelDeleted = "label_deleted"+   toJSON ConfluenceLabelRemoved = "label_removed"+   toJSON ConfluenceLogin = "login"+   toJSON ConfluenceLoginFailed = "login_failed"+   toJSON ConfluenceLogout = "logout"+   toJSON ConfluencePageChildrenReordered = "page_children_reordered"+   toJSON ConfluencePageCreated = "page_created"+   toJSON ConfluencePageMoved = "page_moved"+   toJSON ConfluencePageRemoved = "page_removed"+   toJSON ConfluencePageRestored = "page_restored"+   toJSON ConfluencePageTrashed = "page_trashed"+   toJSON ConfluencePageUpdated = "page_updated"+   toJSON ConfluencePageViewed = "page_viewed"+   toJSON ConfluenceSearchPerformed = "search_performed"+   toJSON ConfluenceSpaceCreated = "space_created"+   toJSON ConfluenceSpaceLogoUpdated = "space_logo_updated"+   toJSON ConfluenceSpacePermissionsUpdated = "space_permissions_updated"+   toJSON ConfluenceSpaceRemoved = "space_removed"+   toJSON ConfluenceSpaceUpdated = "space_updated"+   toJSON ConfluenceStatusCleared = "status_cleared"+   toJSON ConfluenceStatusCreated = "status_created"+   toJSON ConfluenceStatusRemoved = "status_removed"+   toJSON ConfluenceUserCreated = "user_created"+   toJSON ConfluenceUserDeactivated = "user_deactivated"+   toJSON ConfluenceUserFollowed = "user_followed"+   toJSON ConfluenceUserReactivated = "user_reactivated"+   toJSON ConfluenceUserRemoved = "user_removed"+   toJSON ConfluenceGroupCreated = "group_created"+   toJSON ConfluenceGroupRemoved = "group_removed"+   toJSON ServerUpgraded = String "server_upgraded"
+ tests/AssertionHelpers.hs view
@@ -0,0 +1,8 @@+module AssertionHelpers where++import           Test.HUnit++isEqualTo :: (Eq a, Show a) => IO a -> a -> Assertion+isEqualTo actual expected = do+    value <- actual+    value @?= expected
+ tests/ConditionsTest.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE OverloadedStrings #-}+module ConditionsTest (conditionsTests) where++import           AssertionHelpers+import           Control.Monad+import           Data.Aeson+import           Data.Connect.Conditions+import qualified Data.Text               as T+import           Test.HUnit+import           ValueExtractors+import qualified Data.HashMap.Strict as HM++conditionsTests :: Test+conditionsTests = TestList+    [ invertConditionSimple+    , invertConditionAndType+    , invertConditionOrType+    , invertConditionNested+    , jiraConditionJsonCorrect+    , confluenceConditionJsonCorrect+    , remoteConditionJsonCorrect+    , compositeAndConditionConnect+    , conditionParamsTest+    ]++-- Inverting conditions works correctly+invertConditionSimple :: Test+invertConditionSimple = TestCase $ do+    let originalCondition = staticJiraCondition UserIsAdminJiraCondition+    (not . conditionInverted $ originalCondition) @? "Expected the condition to not be inverted on creation."+    let invertedCondition = invertCondition originalCondition+    conditionInverted invertedCondition @? "Expected the condition to be inverted."++threeChildConditions :: [Condition]+threeChildConditions =+    [ invertCondition $ staticJiraCondition UserHasIssueHistoryJiraCondition+    , staticJiraCondition IsWatchingIssueJiraCondition+    , staticJiraCondition HasVotedForIssueJiraCondition+    ]++invertConditionAndType :: Test+invertConditionAndType = TestCase $ do+    let invertedCondition = invertCondition $ CompositeCondition threeChildConditions AndCondition+    OrCondition @=? conditionType invertedCondition+    [False, True, True] @=? (fmap conditionInverted . subConditions $ invertedCondition)++invertConditionOrType :: Test+invertConditionOrType = TestCase $ do+    let invertedCondition = invertCondition $ CompositeCondition threeChildConditions OrCondition+    AndCondition @=? conditionType invertedCondition+    [False, True, True] @=? (fmap conditionInverted . subConditions $ invertedCondition)++invertConditionNested :: Test+invertConditionNested = TestCase $ do+    let child1 = invertCondition $ CompositeCondition threeChildConditions OrCondition+    let child2 = CompositeCondition threeChildConditions OrCondition+    let child3 = CompositeCondition threeChildConditions AndCondition+    let invertedCondition = invertCondition $ CompositeCondition [child1, child2, child3] AndCondition+    OrCondition @=? conditionType invertedCondition+    [OrCondition, AndCondition, OrCondition] @=? (fmap conditionType . subConditions $ invertedCondition)+    [[True, False, False], [False, True, True], [False, True, True]] @=? (fmap (fmap conditionInverted . subConditions) . subConditions $ invertedCondition)++-- JIRA Conditions come out correctly+jiraConditionJsonCorrect :: Test+jiraConditionJsonCorrect = TestCase $ do+    let condition = staticJiraCondition UserIsAdminJiraCondition+    let jsonValue = toJSON condition+    isObject jsonValue @? "Expect the result to be an object"+    (getString =<< get "condition" jsonValue) `isEqualTo` "user_is_admin"+    (return . not =<< getBool =<< get "invert" jsonValue) @? "Expected the condition to not be inverted"++-- Confluence Conditions come out correctly+confluenceConditionJsonCorrect :: Test+confluenceConditionJsonCorrect = TestCase $ do+    let condition = invertCondition $ staticConfluenceCondition HasAttachmentConfluenceCondition+    let jsonValue = toJSON condition+    isObject jsonValue @? "Expect the result to be an object"+    (getString =<< get "condition" jsonValue) `isEqualTo` "has_attachment"+    (getBool =<< get "invert" jsonValue) @? "Expect the condition to be inverted"++-- Remote conditions come out correctly+remoteConditionJsonCorrect :: Test+remoteConditionJsonCorrect = TestCase $ do+    let remoteUrl = "/rest/condition/is-awesome"+    let condition = invertCondition $ remoteCondition remoteUrl+    let jsonValue = toJSON condition+    isObject jsonValue @? "Expect the remote condition to come out as an object."+    (getString =<< get "condition" jsonValue) `isEqualTo` T.pack remoteUrl+    (getBool =<< get "invert" jsonValue) @? "Expect the condition to be inverted"++-- CompositeCondition's are rendered correctly+compositeAndConditionConnect :: Test+compositeAndConditionConnect = TestCase $ do+    let condition = CompositeCondition threeChildConditions AndCondition+    let jsonValue = toJSON condition+    isObject jsonValue @? "Expect the conditions in an object."+    conditions <- getArray =<< get "conditions" jsonValue+    sequence (fmap (getString <=< get "condition") conditions) `isEqualTo` ["user_has_issue_history","is_watching_issue","has_voted_for_issue"]+    get "type" jsonValue `isEqualTo` "AND"++conditionParamsTest :: Test+conditionParamsTest = TestCase $ do+    let condition = staticJiraCondition UserIsAdminJiraCondition+    let conditionWithParams = condition { conditionParams = HM.fromList [("param-one", "value-one"), ("param-two", "value-two")] }+    let jv = toJSON conditionWithParams+    (getString =<< get "param-one" =<< get "params" jv) `isEqualTo` "value-one"+    (getString =<< get "param-two" =<< get "params" jv) `isEqualTo` "value-two"
+ tests/DescriptorTest.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE OverloadedStrings #-}+module DescriptorTest (descriptorTests) where++import           AssertionHelpers+import           Control.Applicative+import           Data.Aeson+import           Data.Connect.Descriptor+import           Data.HashMap.Strict+import           Data.Maybe              (fromJust)+import qualified Data.Text               as T+import           Network.URI+import           Test.HUnit+import           ValueExtractors++toURI :: String -> URI+toURI = fromJust . parseURI++baseURL :: URI+baseURL = toURI "http://my-example.connect.com"++exampleDescriptor1 :: Plugin+exampleDescriptor1 = (pluginDescriptor (PluginKey "my-example-connect") baseURL (Authentication Jwt))+    { pluginName = Just . Name $ "My Example Connect Addon"+    , pluginDescription = Just "This is an example connect descriptor."+    , vendor = Just $ Vendor (Name "Awesome Devs") (toURI "http://awesome-devs.com")+    , lifecycle = Just defaultLifecycle+    , modules = Just exampleModules1+    , enableLicensing = Just False+    , links = fromList+        [ ("documentation", toURI "http://awesome-devs.com/docs")+        , ("source", toURI "http://bitbucket.org/awesome-devs/connect-addon")+        ]+    , scopes = Just [Read, Admin]+    }++exampleModules1 :: Modules+exampleModules1 = Modules exampleJIRAModules emptyConfluenceModules++exampleJIRAModules :: JIRAModules+exampleJIRAModules = emptyJIRAModules+    { jiraWebPanels =+        [ WebPanel+            { wpKey = "test-web-panel"+            , wpName = Name "Test Web Panel"+            , wpLocation = "some-location-in-jira"+            , wpUrl = "/panel/location/for"+            , wpConditions = [staticJiraCondition UserIsAdminJiraCondition]+            }+        ]+    , jiraGeneralPages =+        [ GeneralPage+            { generalPageKey = "test-general-page"+            , generalPageName = Name "Test General Page"+            , generalPageLocation = Just "some-other-location-in-jira"+            , generalPageWeight = Just 1234+            , generalPageUrl = "/panel/general-page"+            , generalPageIcon = Just IconDetails+                { iconUrl = "/static/path/to/icon.png"+                , iconWidth = Just 20+                , iconHeight = Just 40+                }+            , generalPageConditions = [staticJiraCondition UserHasIssueHistoryJiraCondition]+            }+        ]+    , jiraWebhooks =+        [ Webhook+            { webhookEvent = JiraIssueDeleted+            , webhookUrl = "/webhook/handle-deletion"+            }+        ]+    }++descriptorTests :: Test+descriptorTests = TestList+    [ basicDescriptorTest+    ]++basicDescriptorTest :: Test+basicDescriptorTest = TestCase $ do+    let jv = toJSON exampleDescriptor1+    isObject jv @? "Expect the descriptor to be one big json object."+    (getString =<< get "key" jv) `isEqualTo` "my-example-connect"+    (getString =<< get "baseUrl" jv) `isEqualTo` (T.pack . show $ baseURL)+    (getString =<< get "type" =<< get "authentication" jv) `isEqualTo` "jwt"+    (getString =<< get "name" jv) `isEqualTo` "My Example Connect Addon"+    (getString =<< get "description" jv) `isEqualTo` "This is an example connect descriptor."+    (getString =<< get "name" =<< get "vendor" jv) `isEqualTo` "Awesome Devs"+    (getString =<< get "url" =<< get "vendor" jv) `isEqualTo` "http://awesome-devs.com"+    (isObject <$> get "lifecycle" jv) @? "Expected the lifecycle to be an object."+    (isObject <$> get "modules" jv) @? "Expected it to be an array of modules."+    (getString =<< get "documentation" =<< get "links" jv) `isEqualTo` "http://awesome-devs.com/docs"+    (getString =<< get "source" =<< get "links" jv) `isEqualTo` "http://bitbucket.org/awesome-devs/connect-addon"+    (getArray =<< get "scopes" jv) `isEqualTo` ["read", "admin"]+    (not <$> (getBool =<< get "enableLicensing" jv)) @? "Expected licensing to be disabled."
+ tests/ModulesTest.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE OverloadedStrings #-}+module ModulesTest (moduleTests) where++import           AssertionHelpers+import           Control.Applicative+import           Data.Aeson+import           Data.Connect.BaseTypes+import           Data.Connect.Conditions (JIRACondition (..),+                                          staticJiraCondition)+import           Data.Connect.Modules+import           Test.HUnit+import           ValueExtractors++moduleTests :: Test+moduleTests = TestList+    [ testWebPanelCorrectFormat+    , testGeneralPageCorrectFormat+    ]++testWebPanelCorrectFormat :: Test+testWebPanelCorrectFormat = TestCase $ do+    let wp = WebPanel+              { wpKey = "web-panel-key"+              , wpName = Name "My web panel"+              , wpUrl = "/panel/my-panel?issue_key={issue.key}"+              , wpLocation = "atl.jira.view.issue.right.context"+              , wpConditions = [staticJiraCondition UserIsAdminJiraCondition]+              }+    let jv = toJSON wp+    isObject jv @? "Expected the web panel to be an object"+    (getString =<< get "key" jv) `isEqualTo` "web-panel-key"+    (getString =<< get "value" =<< get "name" jv) `isEqualTo` "My web panel"+    (getString =<< get "url" jv) `isEqualTo` "/panel/my-panel?issue_key={issue.key}"+    (getString =<< get "location" jv) `isEqualTo` "atl.jira.view.issue.right.context"+    (isArray <$> get "conditions" jv) @? "Expected the conditions to be an array"++testGeneralPageCorrectFormat :: Test+testGeneralPageCorrectFormat = TestCase $ do+    let gp = GeneralPage+              { generalPageKey = "general-page-key"+              , generalPageName = Name "General Page Name"+              , generalPageUrl = "/panel/page-panel-name?page_id={page.id}"+              , generalPageLocation = Just "some-confluence-location"+              , generalPageIcon = Just $ IconDetails "/path/to/icon" (Just 10) (Just 20)+              , generalPageWeight = Just 10000+              , generalPageConditions = [staticJiraCondition IsIssueReportedByCurrentUserJiraCondition]+              }+    let jv = toJSON gp+    isObject jv @? "Expected the web panel to be an object"+    get "key" jv `isEqualTo` "general-page-key"+    (get "value" =<< get "name" jv) `isEqualTo` "General Page Name"+    get "url" jv `isEqualTo` "/panel/page-panel-name?page_id={page.id}"+    get "location" jv `isEqualTo` "some-confluence-location"+    (isObject <$> get "icon" jv) @? "Expected an icon to be present"+    (get "url" =<< get "icon" jv) `isEqualTo` "/path/to/icon"+    (getNumber =<< get "width" =<< get "icon" jv) `isEqualTo` 10+    (getNumber =<< get "height" =<< get "icon" jv) `isEqualTo` 20+    (getNumber =<< get "weight" jv) `isEqualTo` 10000+    (isArray <$> get "conditions" jv) @? "Expected the conditions to be present."
+ tests/Test.hs view
@@ -0,0 +1,18 @@+module Test where++import           ConditionsTest+import           DescriptorTest+import qualified Distribution.TestSuite as TS+import           ModulesTest+import qualified Test.HUnit             as HU+import           Util++hunitTests :: HU.Test+hunitTests = HU.TestList+    [ HU.TestLabel "Condition Tests" conditionsTests+    , HU.TestLabel "Module Tests" moduleTests+    , HU.TestLabel "Descriptor Tests" descriptorTests+    ]++tests :: IO [TS.Test]+tests = return [ TS.Test (hunitTestInstance hunitTests) ]
+ tests/Util.hs view
@@ -0,0 +1,25 @@+module Util+    ( hunitTestInstance+    ) where++import qualified Distribution.TestSuite as TS+import qualified Test.HUnit             as HU++runHUnitTests :: HU.Test -> IO TS.Progress+runHUnitTests tests = do+   (HU.Counts cases tried errors failures) <- HU.runTestTT tests+   return $ if errors > 0+      then TS.Finished $ TS.Error "There were errors in the HUnit tests"+      else if failures > 0+         then TS.Finished $ TS.Fail "There were failures in the HUnit tests"+         else TS.Finished TS.Pass++hunitTestInstance :: HU.Test -> TS.TestInstance+hunitTestInstance hTests = TS.TestInstance+    { TS.run = runHUnitTests hTests+    , TS.name = "HUnit Tests"+    , TS.tags = ["hunit"]+    , TS.options = []+    , TS.setOption = \_ _ -> Right (hunitTestInstance hTests)+    }+
+ tests/ValueExtractors.hs view
@@ -0,0 +1,45 @@+module ValueExtractors where++import           Data.Aeson+import qualified Data.HashMap.Strict as M+import qualified Data.Scientific     as S+import qualified Data.Text           as T+import qualified Data.Vector         as V++get :: T.Text -> Value -> IO Value+get key (Object o) = maybe notFound return $ M.lookup key o+    where+        notFound = fail $ "Could not find a value in object with key: " ++ T.unpack key+get key _ = fail $ "The value was not an object when looking for: " ++ T.unpack key++getArray :: Value -> IO [Value]+getArray (Array v) = return . V.toList $ v+getArray v = fail $ "Value is not an array. It is a: " ++ valueAsString v++getString :: Value -> IO T.Text+getString (String s) = return s+getString v = fail $ "Value is not a string value. It is a: " ++ valueAsString v++getBool :: Value -> IO Bool+getBool (Bool b) = return b+getBool v = fail $ "Value is not a boolean. It is a: " ++ valueAsString v++isObject :: Value -> Bool+isObject (Object _) = True+isObject _ = False++isArray :: Value -> Bool+isArray (Array _) = True+isArray _ = False++getNumber :: Value -> IO S.Scientific+getNumber (Number n) = return n+getNumber v = fail $ "Value is not a number. It is a: " ++ valueAsString v++valueAsString :: Value -> String+valueAsString (Object _) = "Object"+valueAsString (Array _) = "Array"+valueAsString (Bool _) = "Bool"+valueAsString (String _) = "String"+valueAsString (Number _) = "Number"+valueAsString Null = "Null"