mondo (empty) → 0.1.0.0
raw patch · 8 files changed
+470/−0 lines, 8 filesdep +aesondep +authenticate-oauthdep +basesetup-changed
Dependencies added: aeson, authenticate-oauth, base, bytestring, http-client, http-client-tls, mtl, servant, servant-client, transformers
Files
- LICENSE +20/−0
- README.md +7/−0
- Setup.hs +2/−0
- mondo.cabal +90/−0
- src/Mondo.hs +32/−0
- src/Mondo/API.hs +139/−0
- src/Mondo/Types.hs +146/−0
- test/Example.hs +34/−0
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2016 Michael B. Gale + +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.
+ README.md view
@@ -0,0 +1,7 @@+# Haskell bindings for the Mondo API + +## Getting started + +There is some example code in `test/Example.hs`. The code assumes that a file named `token.txt` containing a valid OAuth token is in the same folder. + +To run the example, run `cabal repl` (if using a cabal sandbox) or `ghci Mondo.hs` (if not). Then load the example into the REPL with `:l test/Example.hs`. Finally, invoke the example with `withMondo token foo`.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple +main = defaultMain
+ mondo.cabal view
@@ -0,0 +1,90 @@+-- Initial mondo.cabal generated by cabal init. For further documentation, +-- see http://haskell.org/cabal/users-guide/ + +-- The name of the package. +name: mondo + +-- 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: Haskell bindings for the Mondo API + +-- A longer description of the package. +description: Provides Haskell bindings for the Mondo API. + +-- The license under which the package is released. +license: MIT + +-- The file containing the license text. +license-file: LICENSE + +-- The package author(s). +author: Michael B. Gale + +-- An email address to which users can send suggestions, bug reports, and +-- patches. +maintainer: michael.gale@cl.cam.ac.uk + +-- A copyright notice. +-- copyright: + +category: Web + +build-type: Simple + +-- Extra files to be distributed with the package, such as examples or a +-- README. +extra-source-files: README.md, + test/Example.hs + +-- Constraint on the version of Cabal needed to build this package. +cabal-version: >=1.10 + +source-repository head + type: git + location: https://github.com/mbg/mondo + +library + -- Modules exported by the library. + exposed-modules: Mondo + + -- Modules included in this library but not exported. + other-modules: Mondo.API, + Mondo.Types + + -- LANGUAGE extensions used by modules in this package. + -- other-extensions: + + -- Other library packages from which modules are imported. + build-depends: base >=4.8 && <4.9, + aeson >=0.11, + servant >=0.5, + servant-client >=0.5, + transformers >=0.4, + http-client >= 0.4, + http-client-tls >= 0.2, + authenticate-oauth >=1.5, + bytestring >=0.10, + mtl + + -- Directories containing source files. + hs-source-dirs: src + + -- Base language which the package is written in. + default-language: Haskell2010 + + default-extensions: TypeOperators, + DataKinds, + DeriveGeneric, + OverloadedStrings, + ScopedTypeVariables, + FlexibleInstances, + FlexibleContexts, + UndecidableInstances, + TypeFamilies
+ src/Mondo.hs view
@@ -0,0 +1,32 @@+-------------------------------------------------------------------------------- +-- Haskell bindings for the Mondo API -- +-- Written by Michael B. Gale (michael.gale@cl.cam.ac.uk) -- +-------------------------------------------------------------------------------- + +module Mondo ( + module Mondo.Types, + module Mondo.API +) where + +-------------------------------------------------------------------------------- + +import Control.Monad.IO.Class + +import qualified Data.ByteString.Internal as BS + +import Web.Authenticate.OAuth + +import Servant.API.BasicAuth +import Servant.Client (ServantError) + +import Mondo.Types +import Mondo.API + +-------------------------------------------------------------------------------- + +mondoAuth :: OAuth +mondoAuth = newOAuth { + oauthServerName = "https://auth.getmondo.co.uk/" +} + +--------------------------------------------------------------------------------
+ src/Mondo/API.hs view
@@ -0,0 +1,139 @@+-------------------------------------------------------------------------------- +-- Haskell bindings for the Mondo API -- +-- Written by Michael B. Gale (michael.gale@cl.cam.ac.uk) -- +-------------------------------------------------------------------------------- + +{-# LANGUAGE FunctionalDependencies #-} + +module Mondo.API where + +-------------------------------------------------------------------------------- + +import GHC.TypeLits + +import qualified Data.ByteString.Internal as BS +import Data.Proxy +import Data.Monoid + +import Control.Monad.Trans +import Control.Monad.Trans.Except +import Control.Monad.Trans.Reader + +import Network.HTTP.Client (Manager, newManager) +import Network.HTTP.Client.TLS + +import Servant.API hiding (addHeader) +import Servant.Common.Req (Req(..), addHeader) +import Servant.Client + +import Mondo.Types + +-------------------------------------------------------------------------------- + +data MondoAuth + +instance HasClient api => HasClient (MondoAuth :> api) where + type Client (MondoAuth :> api) = String -> Client api + + clientWithRoute Proxy req url mgr val = + clientWithRoute (Proxy :: Proxy api) (mondoAuthReq val req) url mgr + +mondoAuthReq :: String -> Req -> Req +mondoAuthReq token = addHeader "Authorization" ("Bearer " <> token) + +-------------------------------------------------------------------------------- + +data MondoAccount + +instance HasClient api => HasClient (MondoAccount :> api) where + type Client (MondoAccount :> api) = AccountID -> Client api + + clientWithRoute Proxy req url mgr val = + clientWithRoute (Proxy :: Proxy api) (addHeader "account_id" val req) url mgr + +-------------------------------------------------------------------------------- + +-- | A type representing the Mondo API. +type MondoAPI = + MondoAuth :> "accounts" :> Get '[JSON] AccountsResponse + :<|> MondoAuth :> "balance" :> QueryParam "account_id" AccountID :> Get '[JSON] Balance + :<|> MondoAuth :> "transactions" :> Capture "transaction_id" String :> Get '[JSON] TransactionResponse + :<|> MondoAuth :> "transactions" :> QueryParam "account_id" AccountID :> Get '[JSON] Transactions + + +mondoAPI :: Proxy MondoAPI +mondoAPI = Proxy + +-- | The URL of the Mondo API. +mondoURL :: BaseUrl +mondoURL = BaseUrl Https "api.getmondo.co.uk" 443 "/" + +-------------------------------------------------------------------------------- + +type Mondo = ReaderT String (ReaderT Manager (ExceptT ServantError IO)) + +withMondo :: String -> Mondo a -> IO (Either ServantError a) +withMondo tkn m = do + manager <- newManager tlsManagerSettings + runExceptT (runReaderT (runReaderT m tkn) manager) + +credential :: Mondo String +credential = ask + +manager :: Mondo Manager +manager = lift ask + +-------------------------------------------------------------------------------- + +api = client mondoAPI mondoURL + +getAccounts :: Mondo AccountsResponse +getAccounts = do + mgr <- manager + tkn <- credential + let + fn = getNth (Proxy :: Proxy 0) $ api mgr + lift $ lift $ fn tkn + +getBalance :: String -> Mondo Balance +getBalance acc = do + mgr <- manager + tkn <- credential + let + fn = getNth (Proxy :: Proxy 1) $ api mgr + lift $ lift $ fn tkn (Just acc) + +getTransaction :: String -> Mondo Transaction +getTransaction tr = do + mgr <- manager + tkn <- credential + let + fn = getNth (Proxy :: Proxy 2) $ api mgr + lift $ lift (transaction <$> fn tkn tr) + +getTransactions :: AccountID -> Mondo Transactions +getTransactions acc = do + mgr <- manager + tkn <- credential + let + fn = getNth (Proxy :: Proxy 3) $ api mgr + lift $ lift $ fn tkn (Just acc) + +-------------------------------------------------------------------------------- + +class GetNth (n :: Nat) a b | n a -> b where + getNth :: Proxy n -> a -> b + +instance x ~ y => GetNth 0 x y where + getNth _ x = x + +instance {-# OVERLAPPING #-} + GetNth 0 (x :<|> y) x where + getNth _ (x :<|> _) = x + +instance + (GetNth (n-1) x y) => GetNth n (a :<|> x) y where + getNth _ (_ :<|> x) = getNth (Proxy :: Proxy (n-1)) x + + +--------------------------------------------------------------------------------
+ src/Mondo/Types.hs view
@@ -0,0 +1,146 @@+-------------------------------------------------------------------------------- +-- Haskell bindings for the Mondo API -- +-- Written by Michael B. Gale (michael.gale@cl.cam.ac.uk) -- +-------------------------------------------------------------------------------- + +module Mondo.Types where + +-------------------------------------------------------------------------------- + +import GHC.Generics + +import Control.Monad (mzero) + +import Data.Aeson + +-------------------------------------------------------------------------------- + +type AccountID = String + +-------------------------------------------------------------------------------- + +data AccountsResponse = AccountsResponse { + accounts :: [Account] +} deriving (Show, Generic) + +instance FromJSON AccountsResponse + +data Account = Account { + id :: AccountID, + description :: String, + created :: String +} deriving (Show, Generic) + +accountID :: Account -> String +accountID (Account x _ _) = x + +instance FromJSON Account + +data Balance = Balance { + balance :: Integer, + currency :: String, + spend_today :: Integer +} deriving (Show, Generic) + +instance FromJSON Balance + +-- | Enumerates reasons which cause transactions to be declined. +data DeclineReason + = InsufficientFunds + | CardInactive + | CardBlocked + | Other + deriving Show + +instance FromJSON DeclineReason where + parseJSON (String "INSUFFICIENT_FUNDS") = pure InsufficientFunds + parseJSON (String "CARD_INACTIVE") = pure CardInactive + parseJSON (String "CARD_BLOCKED") = pure CardBlocked + parseJSON (String "OTHER") = pure Other + parseJSON _ = mzero + +data Address = Address { + addrAddress :: String, + addrCity :: String, + addrCountry :: String, + addrLatitude :: String, + addrLongitude :: String, + addrPostcode :: String, + addrRegion :: String +} deriving Show + +instance FromJSON Address where + parseJSON (Object v) = + Address <$> v .: "address" + <*> v .: "city" + <*> v .: "country" + <*> v .: "latitude" + <*> v .: "longitude" + <*> v .: "postcode" + <*> v .: "region" + +data Merchant = Merchant { + merchantAddress :: Maybe Address, + merchantCreated :: Maybe String, + merchantGroupID :: Maybe String, + merchantID :: String, + merchantLogo :: Maybe String, + merchantEmoji :: Maybe String, + merchantName :: Maybe String, + merchantCategory :: Maybe String +} deriving Show + +instance FromJSON Merchant where + parseJSON (Object v) = + Merchant <$> v .: "address" + <*> v .: "created" + <*> v .: "group_id" + <*> v .: "id" + <*> v .: "logo" + <*> v .: "emoji" + <*> v .: "name" + <*> v .: "category" + +data Transactions = Transactions { + transactions :: [Transaction] +} deriving (Show, Generic) + +instance FromJSON Transactions + +data TransactionResponse = TransactionResponse { + transaction :: Transaction +} deriving (Show, Generic) + +instance FromJSON TransactionResponse + +-- TODO: add metadata + +data Transaction = Transaction { + transactionAccountBalance :: Integer, + transactionAmount :: Integer, + transactionCreated :: String, + transactionCurrency :: String, + transactionDescription :: String, + transactionID :: String, + transactionDeclineReason :: Maybe DeclineReason, + transactionIsLoad :: Bool, + transactionSettled :: Bool, + transactionCategory :: Maybe String, + transactionMerchant :: Merchant +} deriving Show + +instance FromJSON Transaction where + parseJSON (Object v) = + Transaction <$> v .: "account_balance" + <*> v .: "amount" + <*> v .: "created" + <*> v .: "currency" + <*> v .: "description" + <*> v .: "id" + <*> v .: "decline_reason" + <*> v .: "is_load" + <*> v .: "settled" + <*> v .: "category" + <*> v .: "merchant" + +--------------------------------------------------------------------------------
+ test/Example.hs view
@@ -0,0 +1,34 @@+-------------------------------------------------------------------------------- +-- Haskell bindings for the Mondo API -- +-- Written by Michael B. Gale (michael.gale@cl.cam.ac.uk) -- +-------------------------------------------------------------------------------- + +module Example where + +-------------------------------------------------------------------------------- + +import Control.Monad.IO.Class + +import System.IO.Unsafe + +import Mondo + +-------------------------------------------------------------------------------- + +token :: String +token = head $ lines $ unsafePerformIO $ readFile "test/token.txt" + +-------------------------------------------------------------------------------- + +foo :: Mondo () +foo = do + AccountsResponse [acc] <- getAccounts + liftIO $ print acc + + r2 <- getBalance (accountID acc) + liftIO $ print r2 + + r3 <- getTransactions (accountID acc) + liftIO $ print r3 + +--------------------------------------------------------------------------------