docusign-client (empty) → 0.0.1
raw patch · 8 files changed
+624/−0 lines, 8 filesdep +aesondep +basedep +base64-bytestring
Dependencies added: aeson, base, base64-bytestring, bytestring, data-default, docusign-base, exceptions, http-client, http-client-tls, http-types, servant-client, text, uuid
Files
- LICENSE +30/−0
- docusign-client.cabal +45/−0
- src/DocuSign/Client.hs +143/−0
- src/DocuSign/Client/Authentication.hs +47/−0
- src/DocuSign/Client/Configuration.hs +49/−0
- src/DocuSign/Client/Types.hs +163/−0
- src/DocuSign/Client/Types/Conversion.hs +34/−0
- src/DocuSign/Client/Types/Parsing.hs +113/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Capital Match (c) 2018++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Author name here nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ docusign-client.cabal view
@@ -0,0 +1,45 @@+name: docusign-client+version: 0.0.1+synopsis: Client bindings for the DocuSign API+description: DocuSign is an electronic signature technology and digital transaction+ management. This is the client API.+category: Business+author: Jonathan Knowles <mail@jonathanknowles.net>+maintainer: dev@capital-match.com+license-file: LICENSE+license: BSD3+build-type: Simple+cabal-version: >= 1.10++source-repository head+ type: git+ location: https://github.com/capital-match/docusign-client++library+ exposed-modules:+ DocuSign.Client+ DocuSign.Client.Configuration+ DocuSign.Client.Types+ DocuSign.Client.Types.Conversion+ DocuSign.Client.Types.Parsing+ other-modules:+ DocuSign.Client.Authentication+ Paths_docusign_client+ hs-source-dirs:+ src+ build-depends:+ aeson+ -- base upper version bound is nonsense+ , base < 1000+ , base64-bytestring+ , bytestring+ , data-default+ , docusign-base >= 0.0.1+ , exceptions+ , http-client+ , http-client-tls+ , http-types+ , servant-client+ , text+ , uuid+ default-language: Haskell2010
+ src/DocuSign/Client.hs view
@@ -0,0 +1,143 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- | A simple client for communicating with a DocuSign server instance.++module DocuSign.Client+ ( DocuSignClient (..)+ , docuSignClient+ , runClient+ ) where++import DocuSign.Base.ContentTypes+import DocuSign.Client.Authentication+import DocuSign.Client.Types+import DocuSign.Client.Types.Conversion+import DocuSign.Client.Types.Parsing++import Control.Monad.IO.Class ( liftIO )+import Data.Aeson ( encode )+import Data.ByteString ( ByteString )+import Data.Default ( def )+import Data.List ( nub )+import Data.Text.Encoding ( decodeUtf8 )+import Network.HTTP.Client ( Request+ , managerModifyRequest+ , newManager+ , requestHeaders )+import Network.HTTP.Client.TLS ( tlsManagerSettings )+import Network.HTTP.Types.Header ( HeaderName )+import Servant.Client ( BaseUrl (..)+ , mkClientEnv+ , ClientM+ , Scheme (..)+ , ServantError+ , runClientM )++import qualified Data.ByteString.Base64 as Base64+import qualified Data.ByteString.Lazy as BL+import qualified Data.Text as T+import qualified DocuSign.Base as D+import qualified DocuSign.Base.Types as D+import qualified DocuSign.Client.Configuration as C++data DocuSignClient m = DocuSignClient {++ -- | List all accounts associated with the current set of credentials.+ listAccounts :: m [AccountInfo]++ -- | Post a set of documents to a single recipient for signing via email.+ , postDocumentsForEmailBasedSigning+ :: AccountId -> [Document] -> Envelope -> Recipient+ -> m EnvelopeId++ -- | Post a set of documents to a single recipient for signing via browser+ -- redirection.+ , postDocumentsForRedirectionBasedSigning+ :: AccountId -> [Document] -> Envelope -> Recipient+ -> (EnvelopeId -> PostSigningUri)+ -> m (EnvelopeId, SigningUri)++ -- | Fetch a document in its current state, regardless of whether it has+ -- or has not been signed.+ , fetchDocument :: AccountId -> EnvelopeId -> DocumentId -> m PDF+ }++docuSignClient :: DocuSignClient ClientM+docuSignClient = DocuSignClient++ { listAccounts =+ parseM =<< loginInformationGetLoginInformation+ Nothing Nothing Nothing Nothing++ , postDocumentsForEmailBasedSigning = postDocuments EmailBasedSigning++ , postDocumentsForRedirectionBasedSigning = \aid ds e r p -> do+ eid <- postDocuments RedirectionBasedSigning aid ds e r+ url <- generateSigningLink aid eid (recipientClientUserId r) r (RedirectionOptions $ p eid)+ pure (eid, url)++ , fetchDocument = \aid eid did ->+ documentsGetDocument (convert aid) (convert eid) (convert did)+ Nothing Nothing Nothing Nothing Nothing Nothing Nothing+ }+ where+ D.DocuSignClient {..} = D.docuSignClient++ postDocuments sm aid ds Envelope {..} Recipient {..} =+ parseM =<< envelopesPostEnvelopes (convert aid) Nothing Nothing Nothing def+ { D.envelopeDefinitionEmailSubject = Just envelopeSubject+ , D.envelopeDefinitionEmailBlurb = Just envelopeMessage+ , D.envelopeDefinitionStatus = Just "sent"+ , D.envelopeDefinitionDocuments = Just $ flip fmap ds $ \Document {..} ->+ def { D.documentDocumentBase64 = Just $ decodeUtf8 $ Base64.encode+ $ toBytes documentContent+ , D.documentDocumentId = Just $ convert documentId+ , D.documentName = Just documentName }+ , D.envelopeDefinitionRecipients = Just def+ { D.envelopeRecipientsSigners = Just+ [ def { D.signerClientUserId = case sm of+ EmailBasedSigning -> Nothing+ RedirectionBasedSigning ->+ Just $ unUserId recipientClientUserId+ , D.signerEmail = Just $ unEmailAddress recipientEmailAddress+ , D.signerName = Just recipientName+ , D.signerRecipientId = Just "1"+ , D.signerRoutingOrder = Just "1"+ , D.signerTabs = flip fmap recipientSignatureAnchorText $ \a ->+ def { D.envelopeRecipientTabsSignHereTabs = Just [+ def { D.signHereAnchorString = Just a } ] } } ] } }++ generateSigningLink aid eid uid Recipient {..} RedirectionOptions {..} =+ parseM =<< viewsPostEnvelopeRecipientView (convert aid) (convert eid) def+ { D.recipientViewRequestAuthenticationMethod = Just "email"+ , D.recipientViewRequestClientUserId = Just $ unUserId uid+ , D.recipientViewRequestEmail = Just $ unEmailAddress recipientEmailAddress+ , D.recipientViewRequestRecipientId = Just "1"+ , D.recipientViewRequestReturnUrl = Just $ unUri postSigningRedirectionUri+ , D.recipientViewRequestUserName = Just recipientName }++runClient :: C.Config -> ClientM a -> IO (Either ServantError a)+runClient config client = do+ m <- liftIO $ newManager (tlsManagerSettings {managerModifyRequest = addHeaders})+ runClientM client $ mkClientEnv m (baseUrlFromConfig config)++ where++ addHeaders :: Request -> IO Request+ addHeaders r = pure $ r { requestHeaders = nub (authenticationHeader : requestHeaders r)}++ authenticationHeader :: (HeaderName, ByteString)+ authenticationHeader =+ ( authenticationHeaderKey+ , BL.toStrict $ encode $ authenticationHeaderFromConfig config)++ authenticationHeaderFromConfig :: C.Config -> AuthenticationHeader+ authenticationHeaderFromConfig (C.Config (C.AccountConfig _ k u p) _) =+ AuthenticationHeader k u p++ baseUrlFromConfig :: C.Config -> BaseUrl+ baseUrlFromConfig (C.Config _ s) =+ BaseUrl Https (T.unpack $ C.serverHost s) (C.serverPort s) "/restapi"+
+ src/DocuSign/Client/Authentication.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}++module DocuSign.Client.Authentication+ ( AuthenticationHeader (..)+ , authenticationHeaderKey+ ) where++import Data.Aeson ( FromJSON+ , ToJSON+ , defaultOptions+ , genericParseJSON+ , genericToJSON+ , parseJSON+ , toJSON )+import Data.UUID ( UUID )+import Data.Text ( Text )+import DocuSign.Base.Fields ( modifyFieldLabel )+import GHC.Generics ( Generic )+import Network.HTTP.Types.Header ( HeaderName )++data AuthenticationHeader = AuthenticationHeader+ { authenticationHeaderIntegratorKey :: UUID+ , authenticationHeaderUsername :: Text+ , authenticationHeaderPassword :: Text+ } deriving (Show, Eq, Generic)++-- In the JSON serialization of an AuthenticationHeader, field names must start+-- with a capital letter, which is against the normal convention. Hence we must+-- define custom implementations of FromJSON and ToJSON:++instance FromJSON AuthenticationHeader where+ parseJSON = genericParseJSON+ $ modifyFieldLabel (dropLength "authenticationHeader")+ $ defaultOptions++instance ToJSON AuthenticationHeader where+ toJSON = genericToJSON+ $ modifyFieldLabel (dropLength "authenticationHeader")+ $ defaultOptions++authenticationHeaderKey :: HeaderName+authenticationHeaderKey = "X-DocuSign-Authentication"++dropLength :: String -> String -> String+dropLength = drop . length+
+ src/DocuSign/Client/Configuration.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE DeriveGeneric #-}++module DocuSign.Client.Configuration+ ( Config (..)+ , AccountConfig (..)+ , ServerConfig (..)+ ) where++import Data.Text ( Text )+import Data.UUID ( UUID )+import GHC.Generics ( Generic )++import DocuSign.Client.Types+import DocuSign.Base.Fields++import qualified Data.Aeson as J++data Config = Config+ { configAccount :: AccountConfig+ , configServer :: ServerConfig+ } deriving (Generic, Read, Show)++instance J.FromJSON Config where+ parseJSON = J.genericParseJSON (removeFieldLabelPrefix "config")+instance J.ToJSON Config where+ toJSON = J.genericToJSON (removeFieldLabelPrefix "config")++data AccountConfig = AccountConfig+ { accountId :: AccountId+ , accountKey :: UUID+ , accountUsername :: Text+ , accountPassword :: Text+ } deriving (Generic, Read, Show)++instance J.FromJSON AccountConfig where+ parseJSON = J.genericParseJSON (removeFieldLabelPrefix "account")+instance J.ToJSON AccountConfig where+ toJSON = J.genericToJSON (removeFieldLabelPrefix "account")++data ServerConfig = ServerConfig+ { serverHost :: Text+ , serverPort :: Int+ } deriving (Generic, Read, Show)++instance J.FromJSON ServerConfig where+ parseJSON = J.genericParseJSON (removeFieldLabelPrefix "server")+instance J.ToJSON ServerConfig where+ toJSON = J.genericToJSON (removeFieldLabelPrefix "server")+
+ src/DocuSign/Client/Types.hs view
@@ -0,0 +1,163 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++module DocuSign.Client.Types+ ( AccountId+ , AccountName+ , DocumentId+ , EmailAddress+ , EnvelopeId+ , PostSigningUri+ , RecipientId+ , SigningUri+ , Uri+ , UserId+ , UserName++ , mkAccountId+ , mkAccountName+ , mkDocumentId+ , mkEmailAddress+ , mkEnvelopeId+ , mkRecipientId+ , mkUri+ , mkUserId+ , mkUserName++ , unAccountId+ , unAccountName+ , unDocumentId+ , unEmailAddress+ , unEnvelopeId+ , unRecipientId+ , unUri+ , unUserId+ , unUserName++ , AccountInfo (..)+ , Document (..)+ , Envelope (..)+ , Recipient (..)+ , RedirectionOptions (..)+ , SigningEvent (..)+ , SigningMethod (..)+ ) where++import Control.Exception ( assert )+import Data.Aeson ( FromJSON+ , ToJSON )+import Data.Data ( Data )+import Data.Text ( Text )+import Data.Typeable ( Typeable )+import Data.UUID ( UUID )+import DocuSign.Base.ContentTypes ( PDF )++-- Type aliases.++-- | Represents a single-use signing link.+type SigningUri = Uri++-- | Represents a post-signing redirection target.+type PostSigningUri = Uri++-- Simple boxed value types.++newtype AccountId = AccountId { unAccountId :: Integer }+ deriving (Enum, Eq, FromJSON, Integral, Num, Ord, Read, Real, Show, ToJSON, Typeable)++mkAccountId :: Integer -> AccountId+mkAccountId i = assert (i > 0) $ AccountId i++newtype AccountName = AccountName { unAccountName :: Text }+ deriving (Eq, FromJSON, Read, Show, ToJSON, Typeable)++mkAccountName :: Text -> AccountName+mkAccountName = AccountName++newtype DocumentId = DocumentId { unDocumentId :: Integer }+ deriving (Data, Enum, Eq, FromJSON, Integral, Num, Ord, Read, Real, Show, ToJSON, Typeable)++mkDocumentId :: Integer -> DocumentId+mkDocumentId i = assert (i > 0) $ DocumentId i++newtype EmailAddress = EmailAddress { unEmailAddress :: Text }+ deriving (Eq, FromJSON, Read, Show, ToJSON, Typeable)++mkEmailAddress :: Text -> EmailAddress+mkEmailAddress = EmailAddress++newtype EnvelopeId = EnvelopeId { unEnvelopeId :: UUID }+ deriving (Data, Eq, FromJSON, Read, Show, ToJSON, Typeable)++mkEnvelopeId :: UUID -> EnvelopeId+mkEnvelopeId = EnvelopeId++newtype RecipientId = RecipientId { unRecipientId :: Text }+ deriving (Eq, FromJSON, Read, Show, ToJSON, Typeable)++mkRecipientId :: Text -> RecipientId+mkRecipientId = RecipientId++newtype Uri = Uri { unUri :: Text }+ deriving (Eq, FromJSON, Read, Show, ToJSON, Typeable)++mkUri :: Text -> Uri+mkUri = Uri++newtype UserId = UserId { unUserId :: Text }+ deriving (Eq, FromJSON, Read, Show, ToJSON, Typeable)++mkUserId :: Text -> UserId+mkUserId = UserId++newtype UserName = UserName { unUserName :: Text }+ deriving (Eq, FromJSON, Read, Show, ToJSON, Typeable)++mkUserName :: Text -> UserName+mkUserName = UserName++-- Sum types.++data SigningEvent+ = SigningCancelled+ | SigningCompleted+ | SigningDeclined+ | SigningException+ | SigningFaxPending+ | SigningIdCheckFailed+ | SigningSessionExpired+ | SigningTokenExpired+ | SigningViewingCompleted+ deriving (Eq, Show, Typeable)++data SigningMethod+ = EmailBasedSigning+ | RedirectionBasedSigning++-- Record types.++data AccountInfo = AccountInfo+ { accountId :: AccountId+ , accountIsDefault :: Bool+ , accountName :: AccountName+ , accountUserId :: UserId+ , accountUserName :: UserName }++data Document = Document+ { documentContent :: PDF+ , documentId :: DocumentId+ , documentName :: Text }++data Envelope = Envelope+ { envelopeSubject :: Text+ , envelopeMessage :: Text }++data Recipient = Recipient+ { recipientClientUserId :: UserId+ , recipientEmailAddress :: EmailAddress+ , recipientName :: Text+ , recipientSignatureAnchorText :: Maybe Text }++newtype RedirectionOptions = RedirectionOptions+ { postSigningRedirectionUri :: Uri }+
+ src/DocuSign/Client/Types/Conversion.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++{-| This module contains conversion operations that are guaranteed not to fail.++ Such conversions are typically used when preparing messages to be sent to a+ DocuSign server instance, and convert from types exposed by the user-facing+ high-level client API to types consumed by the lower-level REST API.+-}++module DocuSign.Client.Types.Conversion where++import DocuSign.Client.Types++import Data.Text ( Text )++import qualified Data.Text as T+import qualified Data.UUID as U++-- | A class for conversions that are guaranteed not to fail.+class Convert a b where+ convert :: a -> b++instance Convert AccountId Text where+ convert = T.pack . show . unAccountId++instance Convert DocumentId Text where+ convert = T.pack . show . unDocumentId++instance Convert EnvelopeId Text where+ convert = U.toText . unEnvelopeId+
+ src/DocuSign/Client/Types/Parsing.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}++{-| This module contains conversion operations that have the potential to fail.++ Such conversions are typically used when processing messages received from+ a DocuSign server instance, which may return values that are incomplete.++ Such messages typically include record instances for which only a subset of+ values are present. The precise subset of values included often depends on+ which particular operation was invoked, or on the current server state.++ The knowledge of when to expect that values will be present (and when to+ expect that they will be missing) is encapsulated in this module.+-}++module DocuSign.Client.Types.Parsing where++import DocuSign.Client.Types++import Control.Exception ( Exception )+import Control.Monad ( join )+import Control.Monad.Catch ( MonadThrow+ , throwM )+import Type.Reflection ( TypeRep+ , typeRep )+import Data.Text ( Text )+import Data.Text.Read ( decimal )+import Data.Typeable ( Typeable )+import Data.UUID ( UUID )++import qualified Data.Text as T+import qualified Data.UUID as U+import qualified DocuSign.Base.Types as D++-- | A class for conversions that have the potential to fail.+class Parse a b where+ parse :: a -> Maybe b++-- | Parse a value monadically.+parseM :: forall a b m .+ MonadThrow m =>+ Parse a b =>+ Show a =>+ Typeable a =>+ Typeable b => a -> m b+parseM a = maybe (throwM (parseFailure a :: ParseFailure a b)) pure $ parse a++-- | Thrown when the base API fails to return a value that was expected.+data ParseFailure a b = ParseFailure+ { sourceValue :: a+ , sourceType :: TypeRep a+ , targetType :: TypeRep b }+ deriving Show++parseFailure :: Typeable a => Typeable b => a -> ParseFailure a b+parseFailure a = ParseFailure a typeRep typeRep++instance (Show a, Typeable a, Typeable b) => Exception (ParseFailure a b)++instance Parse D.Authentication [AccountInfo] where+ parse D.Authentication {..} =+ join $ fmap (traverse parse) authenticationLoginAccounts++instance Parse Text AccountId where+ parse = either (const Nothing) (Just . fst) . decimal++instance Parse D.LoginAccount AccountInfo where+ parse D.LoginAccount {..} =+ AccountInfo+ <$> (parse =<< loginAccountAccountId)+ <*> (parse =<< loginAccountIsDefault)+ <*> (mkAccountName <$> loginAccountName )+ <*> (mkUserId <$> loginAccountUserId )+ <*> (mkUserName <$> loginAccountUserName )++instance Parse Text UUID where+ parse = U.fromText++instance Parse Text EnvelopeId where+ parse = fmap mkEnvelopeId . parse++instance Parse D.EnvelopeSummary EnvelopeId where+ parse D.EnvelopeSummary {..} =+ parse =<< envelopeSummaryEnvelopeId++instance Parse D.EnvelopeViews Uri where+ parse D.EnvelopeViews {..} =+ mkUri <$> envelopeViewsUrl++instance Parse Text Bool where+ parse t = case T.unpack $ T.toLower t of+ 't' : _ -> Just True+ 'f' : _ -> Just False+ _ -> Nothing++instance Parse Text SigningEvent where+ parse = \case+ "cancel" -> Just SigningCancelled+ "decline" -> Just SigningDeclined+ "exception" -> Just SigningException+ "fax_pending" -> Just SigningFaxPending+ "id_check_failed" -> Just SigningIdCheckFailed+ "session_timeout" -> Just SigningSessionExpired+ "signing_complete" -> Just SigningCompleted+ "ttl_expired" -> Just SigningTokenExpired+ "viewing_complete" -> Just SigningViewingCompleted+ _ -> Nothing+