packages feed

zendesk-api (empty) → 0.1.0.0

raw patch · 7 files changed

+623/−0 lines, 7 filesdep +aesondep +basedep +bytestringsetup-changed

Dependencies added: aeson, base, bytestring, case-insensitive, conduit, connection, data-default, failure, http-client, http-client-tls, http-conduit, http-types, monad-logger, mtl, pem, template-haskell, text, time, tls, transformers, unordered-containers, x509, x509-store, x509-validation

Files

+ LICENSE view
@@ -0,0 +1,22 @@+The MIT License (MIT)++Copyright (c) <year> <copyright holders>++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.+
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Json.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE FlexibleInstances #-}++module Json+  ( jsonOptionsForPrefix+  , deriveJSON+  , deriveJSON_+  , deriveJSONOptions+  , deriveEnumJSON+  ) where++import Data.Aeson (FromJSON(..), ToJSON(..), Value(..))+import qualified Data.Aeson.TH as J (Options (..), defaultOptions, deriveJSON)+import Data.Char (toLower, isUpper)+import Data.Text as T (unpack)+import Language.Haskell.TH+import Language.Haskell.TH.Syntax (showName)++jsonOptionsForPrefix :: String -> J.Options+jsonOptionsForPrefix prefix =+  J.defaultOptions+    { J.fieldLabelModifier = underscoreIt . (drop $ length prefix) }++underscoreIt :: String -> String+underscoreIt s = go [] s+  where+    go :: String -> String -> String+    go [] (c:cs) = go [toLower c] cs+    go r (c:cs) | isUpper c = go (r ++ "_" ++ [(toLower c)]) cs+    go r (c:cs) = go (r ++ [c]) cs+    go r [] = r++deriveJSONOptions :: J.Options -> Name -> Q [Dec]+deriveJSONOptions = J.deriveJSON++deriveJSON :: Name -> Q [Dec]+deriveJSON name = deriveJSONOptions (jsonOptionsForPrefix (nameBase name)) name++deriveJSON_ :: Name -> Q [Dec]+deriveJSON_ name = deriveJSONOptions (jsonOptionsForPrefix ('_' : nameBase name)) name++deriveEnumJSON :: Name -> Q [Dec]+deriveEnumJSON name = [d|+  instance FromJSON $(conT name) where+    parseJSON (String s) = return $ read . T.unpack $ s+    parseJSON _ = fail $ "Invalid " ++ $(stringE (showName name))++  instance ToJSON $(conT name) where+    toJSON = toJSON . show+  |]
+ src/Zendesk.hs view
@@ -0,0 +1,161 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverlappingInstances #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE IncoherentInstances #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Zendesk+( runZendesk+, createUser+, getUsers+, getTickets+, getTicketFields+, ZendeskConfig(..)+, runZendeskT+) where++import Control.Applicative ((<$>), (<*>))+import Control.Exception (catches, SomeException(..), Handler(..))+import Control.Failure (Failure(..))+import Control.Monad (liftM, forM)+import Control.Monad.Reader (ReaderT (..), asks)+import Control.Monad.Error (ErrorT (..), MonadError(..), Error(..))+import Control.Monad.IO.Class (MonadIO(..))+import Control.Monad.Logger (MonadLogger(..), logDebug)+import Control.Monad.Trans (lift)+import Data.Aeson as J (eitherDecode, encode, FromJSON(..), withObject, withText, (.:), object, (.=), Value(..), Object)+import Data.ByteString.Lazy (ByteString)+import qualified Data.ByteString.Lazy as LBS (toStrict)+import Data.ByteString.Char8 as BS8 (pack, unpack)+import Data.CaseInsensitive (mk)+import Data.Default (Default(..))+import qualified Data.HashMap.Strict as M+import Data.Int (Int64)+import Data.Maybe (fromJust)+import Data.PEM (PEM(..), pemParseBS)+import Data.Text as T (pack, Text, unpack)+import Data.Text.Encoding as T (encodeUtf8)+import Data.Time.Clock (UTCTime(..))+import Data.Traversable (Traversable(..))+import Data.X509 (HashALG(..), decodeSignedCertificate)+import Data.X509.CertificateStore (CertificateStore, makeCertificateStore)+import Data.X509.Validation (ValidationChecks(..), validate, defaultHooks, defaultChecks)+import Network.Connection (TLSSettings(..))+import Network.HTTP.Client (getUri, applyBasicAuth)+import Network.HTTP.Client.TLS (mkManagerSettings)+import Network.HTTP.Conduit as HTTP ( httpLbs, parseUrl, withManagerSettings+                                    , HttpException(..)+                                    , Request(..), Response(..)+                                    , responseBody, RequestBody(..), requestBody+                                    , method, requestHeaders, checkStatus)+import Network.HTTP.Types.Status (Status(..))+import Network.TLS (Credential, Credentials(..), ClientParams(..), Shared(..), ClientHooks(..), Supported(..), defaultParamsClient, credentialLoadX509FromMemory)+import Network.TLS.Extra.Cipher (ciphersuite_strong)++import Json (deriveJSON, deriveJSON_, deriveEnumJSON)++import Data.Conduit (Source, yield)++import Zendesk.Common+import Zendesk.User++getTicketsUrl :: Monad m => ZendeskT m String+getTicketsUrl = do+  baseUrl <- asks zendeskUrl+  return $ baseUrl ++ "/api/v2/tickets.json"++getTicketFieldsUrl :: Monad m => ZendeskT m String+getTicketFieldsUrl = do+  baseUrl <- asks zendeskUrl+  return $ baseUrl ++ "/api/v2/ticket_fields.json"+++data TicketField = TicketField+  { ticketFieldId                  :: Maybe Int+  , ticketFieldUrl                 :: Maybe Text+  , ticketFieldType                :: Text+  , ticketFieldTitle               :: Text+  , ticketFieldDescription         :: Maybe Text+  , ticketFieldPosition            :: Maybe Int+  , ticketFieldActive              :: Maybe Bool+  , ticketFieldRequired            :: Maybe Bool+  , ticketFieldCollapsedForAgents  :: Maybe Bool+  , ticketFieldRegexpForValidation :: Maybe Text+  , ticketFieldTitleInPortal       :: Maybe Text+  , ticketFieldVisibleInPortal     :: Maybe Bool+  , ticketFieldEditableInPortal    :: Maybe Bool+  , ticketFieldRequiredInPortal    :: Maybe Bool+  , ticketFieldTag                 :: Maybe Text+  , ticketFieldCreatedAt           :: Maybe UTCTime+  , ticketFieldUpdatedAt           :: Maybe UTCTime+  --, ticketSystemFieldOptions       :: Maybe [Text] TODO Array of something+  --, ticketCustomFieldOptions       :: Maybe [Text] TODO Array of something+  , ticketFieldRemovable           :: Maybe Bool+  } deriving (Show)++data TicketFieldValue = TicketFieldValue+  { ticketFieldValueId    :: Int+  , ticketFieldValueValue :: Maybe Text+  } deriving (Show)++data Ticket = Ticket+  { ticketId :: Maybe Int+  , ticketUrl :: Maybe Text+  , ticketExternalId :: Maybe Text+  , ticketType :: Maybe Text+  , ticketSubject :: Maybe Text+  , ticketDescription :: Maybe Text+  , ticketPriority :: Maybe Text+  , ticketStatus :: Maybe Text+  , ticketRecipient :: Maybe Text+  , ticketRequesterId :: Int+  , ticketSubmitterId :: Maybe Int+  , ticketAssigneeId :: Maybe Int+  , ticketOrganizationId :: Maybe Int+  , ticketGroupId :: Maybe Int+  , ticketCollaboratorIds :: Maybe [Int]+  , ticketForumTopicId :: Maybe Int+  , ticketProblemId :: Maybe Int+  , ticketHasIncidents :: Maybe Bool+  , ticketDueAt :: Maybe Text+  , ticketTags :: Maybe [Text]+  , ticketVia :: Maybe Via+  , ticketCustomFields :: Maybe [TicketFieldValue]+  , ticketSatisfactionRaiting :: Maybe Object+  , ticketSharingAgreementIds :: Maybe [Int]+  , ticketFollowupIds :: Maybe [Int]+  , ticketTicketFormId :: Maybe Int+  , ticketBrandId :: Maybe Int+  , ticketCreatedAt :: Maybe UTCTime+  , ticketUpdatedAt :: Maybe UTCTime+  } deriving (Show)++data Via = Via+  { viaChannel :: Text+  , viaSource  :: Maybe Object+  } deriving (Show)++instance CollectionKey Ticket where+  collectionKey _ = "tickets"++instance CollectionKey TicketField where+  collectionKey _ = "ticket_fields"++deriveJSON ''Ticket+deriveJSON ''TicketField+deriveJSON ''TicketFieldValue+deriveJSON ''Via++getTickets :: (MonadIO m, MonadLogger m) => Source (ZendeskT m) Ticket+getTickets =+  getCollection =<< (Just `liftM` (T.pack `liftM` (lift getTicketsUrl)))++getTicketFields :: (MonadIO m, MonadLogger m) => Source (ZendeskT m) TicketField+getTicketFields =+  getCollection =<< (Just `liftM` (T.pack `liftM` (lift getTicketFieldsUrl)))
+ src/Zendesk/Common.hs view
@@ -0,0 +1,243 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverlappingInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE UndecidableInstances #-}++module Zendesk.Common+where++import Control.Applicative ((<$>), (<*>))++import Control.Exception (catches, SomeException(..), Handler(..))++import Control.Failure (Failure(..))++import Control.Monad (liftM, forM)++import Control.Monad.Error (ErrorT(..), MonadError(..), Error(..))+import Control.Monad.IO.Class (MonadIO(..))+import Control.Monad.Logger (MonadLogger(..), logDebug)+import Control.Monad.Reader (ReaderT (..), asks)+import Control.Monad.Trans (lift)++import Data.Aeson as J (eitherDecode, FromJSON(..), (.:), Value(..), Object)++import Data.ByteString.Char8 as BS8 (pack, unpack)+import Data.ByteString.Lazy (ByteString)++import Data.Conduit (Source, yield)++import Data.Default (Default(..))++import Data.Text as T (pack, Text, unpack)++import Data.X509 (HashALG(..))+import Data.X509.CertificateStore (CertificateStore)+import Data.X509.Validation (ValidationChecks(..), validate, defaultHooks, defaultChecks)++import Json (deriveJSON)++import Network.Connection (TLSSettings(..))++import Network.HTTP.Client (getUri, applyBasicAuth)+import Network.HTTP.Client.TLS (mkManagerSettings)+import Network.HTTP.Conduit as HTTP ( httpLbs, parseUrl, withManagerSettings+                                    , HttpException(..)+                                    , Request(..), Response(..)+                                    , responseBody, requestBody+                                    , requestHeaders, checkStatus)+import Network.HTTP.Types.Status (Status(..))++import Network.TLS ( Credential, ClientHooks(..), ClientParams(..)+                   , Credentials(..), Supported(..), defaultParamsClient+                   , Shared(..))+import Network.TLS.Extra.Cipher (ciphersuite_strong)++data ZendeskError = TimeoutError+                  | NoResourceError+                  | BadRequestError String+                  | NoFreeServersError String+                  | InternalError String+                  | UnknownError String++instance Error ZendeskError where+  strMsg = UnknownError++instance Show ZendeskError where+  show NoResourceError   = "Resource not found in datacenter"+  show TimeoutError      = "Datacenter unavailable"+  show (BadRequestError s)      = "Bad request: " ++ s+  show (NoFreeServersError s) = "Not enough free servers (" ++ s ++ ")"+  show (InternalError s) = "Internal error: " ++ s+  show (UnknownError s)  = s++runZendeskT :: ZendeskConfig -> ZendeskT m a -> m (Either ZendeskError a)+runZendeskT c f = runErrorT $ runReaderT f c++type ZendeskT m = ReaderT ZendeskConfig (ErrorT ZendeskError m)++data ZendeskConfig = ZendeskConfig+  { zendeskUrl :: String -- zendesk domain url+  , zendeskUsername :: String+  , zendeskPassword :: String+  , zendeskTLS :: Maybe ZendeskTLSConfig+  }++data ZendeskTLSConfig = ZendeskTLSConfig+  { tlsCredential :: Credential+  , tlsCertificateStore :: CertificateStore+  }++class CollectionKey e where+  collectionKey :: e -> Text++data Collection e = Collection+  { collectionElements :: [e]+  , collectionCount    :: Int+  , collectionNextPage :: Maybe Text+  , collectionPrevPage :: Maybe Text+  } deriving Show++instance (CollectionKey e, FromJSON e) => FromJSON (Collection e) where+  parseJSON (Object v) =+    Collection+          <$> v .: (collectionKey (undefined :: e))+          <*> v .: "count"+          <*> v .: "next_page"+          <*> v .: "previous_page"++showRequest :: Request -> String+showRequest x = unlines+  [ "Request {"+  , "  method = " ++ show (method x)+  , "  url = \"" ++ show (getUri x) ++ "\""+  , "  headers = " ++ show (requestHeaders x)+  -- , "  body = " ++ show (requestBody x)+  , "}"+  ]++showResponse :: Response ByteString -> String+showResponse x = unlines+  [ "Response {"+  , "  status = " ++ show (statusCode $ responseStatus x)+  , "  headers = " ++ show (responseHeaders x)+  , "  body = " ++ show (responseBody x)+  , "}"+  ]++errorResponseText :: HTTP.Response ByteString -> String+errorResponseText response =+  either (\_ -> show $ responseBody response) errorReportError $ eitherDecode . responseBody $ response++handleExceptionsAndResult :: MonadIO m => IO a -> ZendeskT m a+handleExceptionsAndResult monad = do+  res <- liftIO $ (fmap Right monad) `catches` [ Handler handleHttpException+                                               , Handler handleOtherExceptions+                                               ]+  case res of+    Left v -> throwError v+    Right x -> return x++decodeResponseBody :: (Monad m, ParseResponseBody a)+                   => Response ByteString -> m (Either ZendeskError a)+decodeResponseBody response+  | statusCode (responseStatus response) `elem` [200..299] =+    case parseBody . responseBody $ response of+      Left e -> return $ Left $ InternalError $ "Json parse error: " ++ e+      Right x -> return $ Right x+  | statusCode (responseStatus response) == 404 = return $ Left NoResourceError+  | statusCode (responseStatus response) == 400 = return $ Left $ BadRequestError $ errorResponseText response+  | statusCode (responseStatus response) == 402 = return $ Left $ NoFreeServersError $ errorResponseText response+  | statusCode (responseStatus response) == 502 = return $ Left $ TimeoutError+  | statusCode (responseStatus response) `elem` [500..599] = return $ Left $ InternalError $ errorResponseText response+  | otherwise = return $ Left $ UnknownError $ errorResponseText response++handleHttpException :: HttpException -> IO (Either ZendeskError a)+handleHttpException ResponseTimeout = return $ Left TimeoutError+handleHttpException e               = return $ Left $ UnknownError $ show e++handleOtherExceptions :: SomeException -> IO (Either ZendeskError a)+handleOtherExceptions e = return $ Left $ UnknownError $ show e++instance Monad m => Failure HttpException (ZendeskT m) where+  failure e = throwError $ InternalError $ show e++class Monad m => MonadZendesk m where+  runZendesk :: ZendeskT m a -> m (Either ZendeskError a)++runRequest :: (MonadIO m, MonadLogger m, ParseResponseBody a)+           => Request -> ZendeskT m a+runRequest request = do+  username <- BS8.pack `liftM` (asks zendeskUsername)+  password <- BS8.pack `liftM` (asks zendeskPassword)+  let request' = applyBasicAuth username password $ request { checkStatus = \_ _ _ -> Nothing }++  $logDebug $ T.pack $ "Sending request to zendesk: " ++ (showRequest request')++  mTLSConfig <- asks zendeskTLS+  let tlsSettings = case mTLSConfig of+                      Nothing -> def+                      Just tlsConfig ->+                        let params = defaultParamsClient (BS8.unpack $ host request') ""+                            validationChecks = defaultChecks+                                                 { checkCAConstraints = False+                                                 , checkLeafV3 = False+                                                 , checkFQHN = False+                                                 }+                         in TLSSettings $ params+                              { clientShared = (clientShared params)+                                                 { sharedCredentials = Credentials [tlsCredential tlsConfig]+                                                 , sharedCAStore = tlsCertificateStore tlsConfig+                                                 }+                              , clientHooks = def { onCertificateRequest = \_ -> return . Just $ tlsCredential tlsConfig+                                                  , onServerCertificate = validate HashSHA256 defaultHooks validationChecks+                                                  }+                              , clientSupported = (clientSupported params)+                                                    { supportedCiphers = ciphersuite_strong }+                              }+++  let httpSettings = mkManagerSettings tlsSettings Nothing++  response <- handleExceptionsAndResult $ do+    withManagerSettings httpSettings $ httpLbs request'++  $logDebug $ T.pack $ "Got response from zendesk: " ++ (showResponse response)++  result <- decodeResponseBody response+  case result of+    Left v -> throwError v+    Right x -> return x++runRequestTo :: (MonadIO m, MonadLogger m, ParseResponseBody a)+             => String -> ZendeskT m a+runRequestTo url = parseUrl url >>= runRequest++getCollection :: (CollectionKey e, MonadIO m, MonadLogger m, FromJSON e)+              => Maybe Text -> Source (ZendeskT m) e+getCollection Nothing = return ()+getCollection (Just url) = do+  collection <- lift $ runRequestTo $ T.unpack url+  forM (collectionElements collection) yield+  getCollection $ collectionNextPage collection++data None = None+  deriving (Show, Eq)++class ParseResponseBody a where+  parseBody :: ByteString -> Either String a++instance ParseResponseBody None where+  parseBody _ = Right $ None++instance FromJSON a => ParseResponseBody a where+  parseBody = eitherDecode++data ErrorReport = ErrorReport { errorReportError :: String }+  deriving Show++deriveJSON ''ErrorReport
+ src/Zendesk/User.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module Zendesk.User+( createUser+, getUsers+, User(..)+) where++import Control.Monad (liftM)+import Control.Monad.IO.Class (MonadIO(..))+import Control.Monad.Logger (MonadLogger(..))+import Control.Monad.Reader (asks)+import Control.Monad.Trans (lift)+import Data.Aeson as J (encode, object, (.=), Value(..), Object)+import qualified Data.ByteString.Lazy as LBS (toStrict)+import Data.CaseInsensitive (mk)+import Data.Conduit (Source)+import Data.Text as T (pack, Text)+import Data.Time.Clock (UTCTime(..))++import Json (deriveJSON, deriveJSON_, deriveEnumJSON)+import Network.HTTP.Conduit as HTTP ( parseUrl+                                    , RequestBody(..), requestBody+                                    , method, requestHeaders)++import Zendesk.Common++data User = User+  { userId              :: Maybe Int+  , userUrl             :: Maybe Text+  , userName            :: Text+  , userCreatedAt       :: Maybe UTCTime+  , userUpdatedAt       :: Maybe UTCTime+  , userTimeZone        :: Maybe Text+  , userEmail           :: Text+  , userPhone           :: Maybe Text+  , userLocale          :: Maybe Text+  , userLocaleId        :: Maybe Int+  , userOrganizationId  :: Maybe Int+  , userRole            :: Text+  , userVerified        :: Maybe Bool+  , userPhoto           :: Maybe Attachment+  } deriving (Show)++data Attachment = Attachment+  { attachmentId          :: Int+  , attachmentFileName    :: Text+  , attachmentContentUrl  :: Text+  , attachmentContentType :: Text+  , attachmentSize        :: Int+  , attachmentThumbnails  :: Maybe [Attachment]+  } deriving (Show)++data UserReply = UserReply+  { userReplyUser :: User+  }++newtype Name = Name String++newtype Email = Email String++createUser :: (MonadIO m, MonadLogger m) => Name -> Email -> ZendeskT m User+createUser (Name name) (Email email) = do+  initRequest <- parseUrl =<< getUsersUrl+  let request = initRequest+                { method = "POST"+                , requestHeaders = [ (mk "Content-Type", "application/json") ]+                , requestBody = RequestBodyBS $ LBS.toStrict $ encode+                                  $ object ["user" .= CreateUserRequest name email]+                }+  userReplyUser `liftM` (runRequest request)++getUsers :: (MonadIO m, MonadLogger m) => Source (ZendeskT m) User+getUsers =+  getCollection =<< (Just `liftM` (T.pack `liftM` (lift getUsersUrl)))++getUsersUrl :: Monad m => ZendeskT m String+getUsersUrl = do+  baseUrl <- asks zendeskUrl+  return $ baseUrl ++ "/api/v2/users.json"++data CreateUserRequest = CreateUserRequest+  { createUserRequestName  :: String+  , createUserRequestEmail :: String+  } deriving (Show)++instance CollectionKey User where+  collectionKey _ = "users"++deriveJSON ''CreateUserRequest+deriveJSON ''User+deriveJSON ''UserReply+deriveJSON ''Attachment
+ zendesk-api.cabal view
@@ -0,0 +1,51 @@+name:                zendesk-api+version:             0.1.0.0+synopsis:            Zendesk API for Haskell programming language.+description:         This is a library for accessing zendesk api using haskell bindings.+homepage:            https://github.com/VictorDenisov/zendesk-api+license:             MIT+license-file:        LICENSE+author:              Victor Denisov+maintainer:          denisovenator@gmail.com+-- copyright:+category:            API+build-type:          Simple+-- extra-source-files:+cabal-version:       >=1.10++library+  exposed-modules:     Zendesk+  other-modules:       Json+                     , Zendesk.Common+                     , Zendesk.User+  -- other-extensions:+  build-depends:       aeson == 0.7.*+                     , base >=4.5 && <4.7+                     , bytestring == 0.10.0.2+                     , case-insensitive == 1.1.0.3+                     , conduit == 1.0.*+                     , connection == 0.2.*+                     , data-default == 0.5.*+                     , failure == 0.2.*+                     , http-client == 0.2.*+                     , http-client-tls == 0.2.*+                     , http-conduit == 2.0.0.3+                     , http-types == 0.8.3+                     , monad-logger == 0.3.*+                     , mtl == 2.1.2+                     , pem == 0.2.*+                     , text >= 0.11.3+                     , template-haskell == 2.8.0.0+                     , time == 1.4.0.1+                     , tls == 1.2.*+                     , transformers == 0.3.0.0+                     , unordered-containers == 0.2.*+                     , x509 == 1.4.*+                     , x509-store == 1.4.*+                     , x509-validation == 1.5.*+  hs-source-dirs:      src+  default-language:    Haskell2010++source-repository head+  type:         git+  location:     https://github.com/VictorDenisov/zendesk-api