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 +202/−0
- Setup.hs +2/−0
- atlassian-connect-descriptor.cabal +128/−0
- dist/build/test-descriptorStub/test-descriptorStub-tmp/test-descriptorStub.hs +5/−0
- src/Data/Connect/AesonHelpers.hs +33/−0
- src/Data/Connect/BaseTypes.hs +77/−0
- src/Data/Connect/Conditions.hs +192/−0
- src/Data/Connect/Descriptor.hs +206/−0
- src/Data/Connect/Lifecycle.hs +47/−0
- src/Data/Connect/Modules.hs +144/−0
- src/Data/Connect/OrphanInstances.hs +13/−0
- src/Data/Connect/Scopes.hs +43/−0
- src/Data/Connect/Webhooks.hs +152/−0
- tests/AssertionHelpers.hs +8/−0
- tests/ConditionsTest.hs +107/−0
- tests/DescriptorTest.hs +94/−0
- tests/ModulesTest.hs +59/−0
- tests/Test.hs +18/−0
- tests/Util.hs +25/−0
- tests/ValueExtractors.hs +45/−0
+ 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"