packages feed

smsaero (empty) → 0.1

raw patch · 8 files changed

+360/−0 lines, 8 filesdep +aesondep +basedep +eithersetup-changed

Dependencies added: aeson, base, either, servant, servant-client, text, time

Files

+ LICENSE view
@@ -0,0 +1,28 @@+Copyright (c) 2015, GetShopTV+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 smsaero nor the names of its+  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 HOLDER 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.+
+ README.md view
@@ -0,0 +1,24 @@+# smsaero++[![Build Status](https://travis-ci.org/GetShopTV/smsaero.svg?branch=master)](https://travis-ci.org/GetShopTV/smsaero)++SMSAero API and HTTP client based on servant library.++## Usage++Import `SMSAero` and `Control.Monad.Trans.Either` module to interact with SMSAero:++```+>>> :s -XOverloadedStrings+>>> import SMSAero+>>> import Control.Monad.Trans.Either+>>> let credentials = SMSAeroAuth "user@example.com" "md5-password-hash"+>>> runEitherT $ smsAeroBalance credentials+Right (ResponseOK (BalanceResponse 10.0))+```++## Contributing++Contributions and bug reports are welcome!++*GetShopTV Team*
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ smsaero.cabal view
@@ -0,0 +1,34 @@+name:                smsaero+version:             0.1+synopsis:            SMSAero API and HTTP client based on servant library.+description:         Please see README.md+homepage:            https://github.com/GetShopTV/smsaero+license:             BSD3+license-file:        LICENSE+author:              Nickolay Kudasov+maintainer:          nickolay@getshoptv.com+-- copyright:           +category:            Web+build-type:          Simple+extra-source-files:  README.md+cabal-version:       >=1.10++library+  hs-source-dirs:      src+  exposed-modules:+    SMSAero+    SMSAero.API+    SMSAero.Client+    SMSAero.Utils+  build-depends:       base >= 4.7 && < 5+                     , either+                     , servant        == 0.4.*+                     , servant-client+                     , aeson+                     , text+                     , time+  default-language:    Haskell2010++source-repository head+  type:     git+  location: https://github.com/GetShopTV/smsaero.git
+ src/SMSAero.hs view
@@ -0,0 +1,7 @@+module SMSAero (+  module SMSAero.API,+  module SMSAero.Client+) where++import SMSAero.API+import SMSAero.Client
+ src/SMSAero/API.hs view
@@ -0,0 +1,194 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+module SMSAero.API where++import Data.Aeson+import Data.Proxy++import Data.Time (UTCTime)+import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)++import Data.Text (Text)+import qualified Data.Text as Text++import Control.Applicative+import GHC.TypeLits (Symbol, KnownSymbol)+import Text.Read (readMaybe)++import Servant.API+import Servant.Client++-- | Content type for SMSAero JSON answer (it has JSON body but "text/plain" Content-Type).+data SmsAeroJson++instance Accept SmsAeroJson where+  contentType _ = contentType (Proxy :: Proxy PlainText)++instance FromJSON a => MimeUnrender SmsAeroJson a where+  mimeUnrender _ = mimeUnrender (Proxy :: Proxy JSON)++-- | Like 'QueryParam', but always required.+data RequiredQueryParam (sym :: Symbol) a++instance (HasClient sub, KnownSymbol sym, ToText a) => HasClient (RequiredQueryParam sym a :> sub) where+  type Client (RequiredQueryParam sym a :> sub) = a -> Client sub+  clientWithRoute _ req baseurl param = clientWithRoute (Proxy :: Proxy (QueryParam sym a :> sub)) req baseurl (Just param)++-- | SMSAero sender's signature. This is used for the "from" field.+newtype Signature = Signature { getSignature :: Text } deriving (Show, FromJSON, ToText)++-- | SMSAero sent message id.+newtype MessageId = MessageId Integer deriving (Show, FromJSON, ToText)++-- | SMSAero authentication data.+data SMSAeroAuth = SMSAeroAuth+  { authUser      :: Text   -- ^ Username.+  , authPassword  :: Text   -- ^ MD5 hash of a password.+  }++-- | Phone number.+newtype Phone = Phone { getPhone :: Integer } deriving (Show, ToText)++-- | Date.+newtype SMSAeroDate = SMSAeroDate { getSMSAeroDate :: UTCTime } deriving (Show)++instance ToText SMSAeroDate where+  toText (SMSAeroDate dt) = Text.pack (show (utcTimeToPOSIXSeconds dt))++-- | SMSAero authentication credentials.+data RequireAuth++instance HasClient sub => HasClient (RequireAuth :> sub) where+  type Client (RequireAuth :> sub) = SMSAeroAuth -> Client sub++  clientWithRoute _ req baseurl SMSAeroAuth{..} =+    clientWithRoute+      (Proxy :: Proxy (RequiredQueryParam "user"     Text :>+                       RequiredQueryParam "password" Text :>+                       sub))+      req+      baseurl+      authUser+      authPassword++-- | Implicit parameter that tells SMSAero to respond with JSON.+data AnswerJson++instance HasClient sub => HasClient (AnswerJson :> sub) where+    type Client (AnswerJson :> sub) = Client sub+    clientWithRoute _ req baseurl = clientWithRoute (Proxy :: Proxy (RequiredQueryParam "answer" Text :> sub)) req baseurl "json"++-- | Regular SMSAero GET API.+type SmsAeroGet a = Get '[SmsAeroJson] (SmsAeroResponse a)++-- | SMSAero API.+type SMSAeroAPI = RequireAuth :> AnswerJson :>+      ("send"     :> SendApi+  :<|> "status"   :> StatusApi+  :<|> "balance"  :> SmsAeroGet BalanceResponse+  :<|> "senders"  :> SmsAeroGet SendersResponse+  :<|> "sign"     :> SmsAeroGet SignResponse)++-- | SMSAero API to send a message.+type SendApi =+  RequiredQueryParam "to"   Phone       :>+  RequiredQueryParam "text" Text        :>+  RequiredQueryParam "from" Signature   :>+  QueryParam "date" SMSAeroDate :>+  SmsAeroGet SendResponse++-- | SMSAero API to check message status.+type StatusApi = RequiredQueryParam "id" MessageId :> SmsAeroGet StatusResponse++-- | Every SMSAero response is either rejected or provides some info.+data SmsAeroResponse a+  = ResponseOK a        -- ^ Some useful payload.+  | ResponseReject Text -- ^ Rejection reason.+  deriving (Show)++-- | SMSAero response to a send request.+data SendResponse+  = SendAccepted MessageId  -- ^ Message accepted.+  | SendNoCredits           -- ^ No credits to send a message.+  deriving (Show)++-- | SMSAero response to a status request.+data StatusResponse+  = StatusDeliverySuccess   -- ^ Message is successfully delivered.+  | StatusDeliveryFailure   -- ^ Message delivery has failed.+  | StatusSmscSubmit        -- ^ Message submitted to SMSC.+  | StatusSmscReject        -- ^ Message rejected by SMSC.+  | StatusQueue             -- ^ Message queued.+  | StatusWaitStatus        -- ^ Wait for message status.+  deriving (Show)++-- | SMSAero response to a balance request.+-- This is a number of available messages to send.+newtype BalanceResponse = BalanceResponse Double deriving (Show)++-- | SMSAero response to a senders request.+-- This is just a list of available signatures.+newtype SendersResponse = SendersResponse [Signature] deriving (Show, FromJSON)++-- | SMSAero response to a sign request.+data SignResponse+  = SignApproved  -- ^ Signature is approved.+  | SignRejected  -- ^ Signature is rejected.+  | SignPending   -- ^ Signature is pending.+  deriving (Show)++instance FromJSON a => FromJSON (SmsAeroResponse a) where+  parseJSON (Object o) = do+    result :: Maybe Text <- o .:? "result"+    case result of+      Just "reject" -> ResponseReject <$> o .: "reason"+      _ -> ResponseOK <$> parseJSON (Object o)+  parseJSON json = ResponseOK <$> parseJSON json++instance FromJSON SendResponse where+  parseJSON (Object o) = do+    result :: Text <- o .: "result"+    case result of+      "accepted"    -> SendAccepted <$> o .: "id"+      "no credits"  -> pure SendNoCredits+      _ -> empty+  parseJSON _ = empty++instance FromJSON StatusResponse where+  parseJSON (Object o) = do+    result :: Text <- o .: "result"+    case result of+      "delivery success"  -> pure StatusDeliverySuccess+      "delivery failure"  -> pure StatusDeliveryFailure+      "smsc submit"       -> pure StatusSmscSubmit+      "smsc reject"       -> pure StatusSmscReject+      "queue"             -> pure StatusQueue+      "wait status"       -> pure StatusWaitStatus+      _ -> empty+  parseJSON _ = empty++instance FromJSON BalanceResponse where+  parseJSON (Object o) = do+    balance <- o .: "balance"+    case readMaybe balance of+      Just x  -> pure (BalanceResponse x)+      Nothing -> empty+  parseJSON _ = empty++instance FromJSON SignResponse where+  parseJSON (Object o) = do+    accepted :: Text <- o .: "accepted"+    case accepted of+      "approved" -> pure SignApproved+      "rejected" -> pure SignRejected+      "pending"  -> pure SignPending+      _ -> empty+  parseJSON _ = empty
+ src/SMSAero/Client.hs view
@@ -0,0 +1,38 @@+module SMSAero.Client where++import Control.Monad.Trans.Either++import Data.Proxy+import Data.Text (Text)++import Servant.API+import Servant.Client++import SMSAero.API+import SMSAero.Utils++-- | SMSAero client.+smsAeroClient :: Client SMSAeroAPI+smsAeroClient = client (Proxy :: Proxy SMSAeroAPI) host+  where+    host = BaseUrl Https "gate.smsaero.ru" 443++-- | Common SMSAero client type.+type SmsAero a = EitherT ServantError IO (SmsAeroResponse a)++-- | Send a message.+smsAeroSend    :: SMSAeroAuth -> Phone -> Text -> Signature -> Maybe SMSAeroDate -> SmsAero SendResponse+-- | Check status of a previously sent message.+smsAeroStatus  :: SMSAeroAuth -> MessageId -> SmsAero StatusResponse+-- | Check balance.+smsAeroBalance :: SMSAeroAuth -> SmsAero BalanceResponse+-- | Check the list of available sender signatures.+smsAeroSenders :: SMSAeroAuth -> SmsAero SendersResponse+-- | Acquire a new signature.+smsAeroSign    :: SMSAeroAuth -> SmsAero SignResponse+(smsAeroSend    :<|>+ smsAeroStatus  :<|>+ smsAeroBalance :<|>+ smsAeroSenders :<|>+ smsAeroSign) = distributeClient smsAeroClient+
+ src/SMSAero/Utils.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+module SMSAero.Utils where++import Servant.API.Alternative++-- | Distribute a client looking like+--+-- @+-- a -> (b :<|> ... :<|> c)+-- @+--+-- into+--+-- @+-- (a -> b) :<|> ...  :<|> (a -> c)+-- @+--+-- This is useful to bring authentication credentials to+-- individual client endpoint queries.+class DistributiveClient client client' where+  distributeClient :: client -> client'++instance DistributiveClient (a -> b) (a -> b) where+  distributeClient = id++instance (DistributiveClient (a -> b) b', DistributiveClient (a -> c) c') => DistributiveClient (a -> (b :<|> c)) (b' :<|> c') where+  distributeClient client = distributeClient (aleft <$> client) :<|> distributeClient (aright <$> client)+    where+      aleft  (l :<|> _) = l+      aright (_ :<|> r) = r