{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE InstanceSigs #-}
module Data.Api.Types where
import Control.Exception.Safe
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Reader (MonadReader (..), ReaderT (..))
import Data.Aeson
import Data.Aeson.TH
import Data.Api.TestByteStrings
import Data.ByteString.Lazy (ByteString)
import Data.Fixed
import Data.Functor.Identity
import qualified Data.Map as M
import Data.Maybe (isJust, isNothing)
import Data.Monoid ((<>))
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time (Day)
import Lens.Micro
import Lens.Micro.TH
import Network.HTTP.Conduit
import Text.Casing (camel, quietSnake)
data Environment
= Sandbox
| Development
| Production deriving (Ord, Eq)
$(deriveFromJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 11 }) ''Environment)
instance Show Environment where
show Sandbox = "sandbox"
show Development = "development"
show Production = "production"
class Monad m => PlaidHttp m where
executePost :: ToJSON a => Text -> a -> m ByteString
executeGet :: Text -> m ByteString
newtype PlaidError =
PlaidError Text
deriving Show
deriving anyclass Exception
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 10 }) ''PlaidError)
newtype AccessToken =
AccessToken
{ unAccessToken :: Text
} deriving (Eq, Show)
instance ToJSON AccessToken where
toJSON AccessToken {..} = String unAccessToken
instance FromJSON AccessToken where
parseJSON = withText "AccessToken" $ \t ->
return $ AccessToken t
newtype PublicToken =
PublicToken
{ unPublicToken :: Text
} deriving (Eq, Show)
instance ToJSON PublicToken where
toJSON PublicToken {..} = String unPublicToken
instance FromJSON PublicToken where
parseJSON = withText "PublicToken" $ \t ->
return $ PublicToken t
newtype ClientId =
ClientId
{ unClientId :: Text
} deriving (Eq, Show)
instance ToJSON ClientId where
toJSON ClientId {..} = String unClientId
instance FromJSON ClientId where
parseJSON = withText "ClientId" $ \t ->
return $ ClientId t
newtype Secret =
Secret
{ unSecret :: Text
} deriving (Eq, Show)
instance ToJSON Secret where
toJSON Secret {..} = String unSecret
instance FromJSON Secret where
parseJSON = withText "Secret" $ \t ->
return $ Secret t
newtype InstitutionId =
InstitutionId
{ unInstitutionId :: Text
} deriving (Eq, Show)
instance ToJSON InstitutionId where
toJSON InstitutionId {..} = String unInstitutionId
instance FromJSON InstitutionId where
parseJSON = withText "InstitutionId" $ \t ->
return $ InstitutionId t
newtype Url a = Url { unUrl :: Text }
instance ToJSON (Url a) where
toJSON Url {..} = String unUrl
instance FromJSON (Url a) where
parseJSON = withText "Url" $ \t ->
return $ Url t
type PublicKey = Text
type AccessKey = Text
type PlaidProduct = Text
type Institution = Text
type AccountNumber = Text
type SortCode = Text
type AccountId = Text
type Iban = Text
type Bic = Text
type RequestId = Text
data AuthGet
data GetBalance
data PublicTokenCreate
data PlaidTokenExchange
data PlaidTransactionsGet
data PlaidIdentityGet
data PlaidIncomeGet
data PlaidOptions =
PlaidOptions
{ _plaidOptionsWebHook :: Text
, _plaidOptionsOverrideUsername :: Text
, _plaidOptionsOverridePassword :: Text
} deriving (Eq, Show)
makeLenses ''PlaidOptions
$(deriveFromJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 13 }) ''PlaidOptions)
instance ToJSON PlaidOptions where
toJSON b =
object [ "webhook" .= (b ^. plaidOptionsWebHook)
, "override_username" .= (b ^. plaidOptionsOverrideUsername)
, "override_password" .= (b ^. plaidOptionsOverridePassword)
]
data PlaidPaginationOptions =
PlaidPaginationOptions
{ _plaidPaginationOptionsCount :: Int
, _plaidPaginationOptionsOffset :: Int
} deriving (Eq, Show)
makeLenses ''PlaidPaginationOptions
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 23 }) ''PlaidPaginationOptions)
defPaginationOptions :: PlaidPaginationOptions
defPaginationOptions =
PlaidPaginationOptions
{ _plaidPaginationOptionsCount = 100
, _plaidPaginationOptionsOffset = 0
}
data PlaidEnv =
PlaidEnv
{ _plaidEnvPublicKey :: PublicKey
, _plaidEnvClientId :: ClientId
, _plaidEnvSecret :: Secret
, _plaidEnvEnvironment :: Environment
} deriving (Eq, Show)
makeLenses ''PlaidEnv
$(deriveFromJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 8 }) ''PlaidEnv)
data PlaidBody a
= PlaidBody
{ _plaidBodyEnv :: PlaidEnv
, _plaidBodyPublicToken :: Maybe PublicToken
, _plaidBodyAccessToken :: Maybe AccessToken
, _plaidBodyOptions :: Maybe PlaidOptions
, _plaidBodyPaginationOptions :: Maybe PlaidPaginationOptions
, _plaidBodyInstitutionId :: Maybe InstitutionId
, _plaidBodyInitialProducts :: Maybe [PlaidProduct]
, _plaidBodyStartDate :: Maybe Day
, _plaidBodyEndDate :: Maybe Day
} deriving (Eq, Show)
makeLenses ''PlaidBody
instance ToJSON (PlaidBody GetBalance) where
toJSON b =
if isNothing (b ^. plaidBodyOptions)
then
object [ "client_id" .= (b ^. plaidBodyEnv . plaidEnvClientId)
, "secret" .= (b ^. plaidBodyEnv . plaidEnvSecret)
, "access_token" .= toJSON (b ^. plaidBodyAccessToken)
]
else
object [ "client_id" .= (b ^. plaidBodyEnv . plaidEnvClientId)
, "secret" .= (b ^. plaidBodyEnv . plaidEnvSecret)
, "access_token" .= (b ^. plaidBodyAccessToken)
, "options" .= (b ^. plaidBodyOptions)
]
instance ToJSON (PlaidBody PublicTokenCreate) where
toJSON b =
if isNothing (b ^. plaidBodyOptions)
then
object [ "institution_id" .= (b ^. plaidBodyInstitutionId)
, "public_key" .= (b ^. plaidBodyEnv . plaidEnvPublicKey)
, "initial_products" .= (b ^. plaidBodyInitialProducts)
]
else
object [ "institution_id" .= (b ^. plaidBodyInstitutionId)
, "public_key" .= (b ^. plaidBodyEnv . plaidEnvPublicKey)
, "initial_products" .= (b ^. plaidBodyInitialProducts)
, "options" .= toJSON (b ^. plaidBodyOptions)
]
instance ToJSON (PlaidBody AuthGet) where
toJSON b =
object [ "client_id" .= (b ^. plaidBodyEnv . plaidEnvClientId)
, "secret" .= (b ^. plaidBodyEnv . plaidEnvSecret)
, "access_token" .= (b ^. plaidBodyAccessToken)
]
instance ToJSON (PlaidBody PlaidTokenExchange) where
toJSON b =
object [ "client_id" .= (b ^. plaidBodyEnv . plaidEnvClientId)
, "secret" .= (b ^. plaidBodyEnv . plaidEnvSecret)
, "public_token" .= (b ^. plaidBodyPublicToken)
]
instance ToJSON (PlaidBody PlaidTransactionsGet) where
toJSON b =
object $ [ "client_id" .= (b ^. plaidBodyEnv . plaidEnvClientId)
, "secret" .= (b ^. plaidBodyEnv . plaidEnvSecret)
, "access_token" .= (b ^. plaidBodyAccessToken)
, "start_date" .= (b ^. plaidBodyStartDate)
, "end_date" .= (b ^. plaidBodyEndDate)
] <> perhapsSendObject
where
perhapsSendObject =
["options" .= toJSON (b ^. plaidBodyPaginationOptions) | isJust (b ^. plaidBodyPaginationOptions)]
instance ToJSON (PlaidBody PlaidIdentityGet) where
toJSON b =
object [ "client_id" .= (b ^. plaidBodyEnv . plaidEnvClientId)
, "secret" .= (b ^. plaidBodyEnv . plaidEnvSecret)
, "access_token" .= (b ^. plaidBodyAccessToken)
]
instance ToJSON (PlaidBody PlaidIncomeGet) where
toJSON b =
object [ "client_id" .= (b ^. plaidBodyEnv . plaidEnvClientId)
, "secret" .= (b ^. plaidBodyEnv . plaidEnvSecret)
, "access_token" .= (b ^. plaidBodyAccessToken)
]
newtype Plaid a =
Plaid
{ unPlaid :: ReaderT PlaidEnv IO a
} deriving newtype ( Functor
, Applicative
, Monad
, MonadReader PlaidEnv
, MonadIO
, MonadThrow
)
newtype PlaidTest a =
PlaidTest
{ unPlaidTest :: Identity a
} deriving newtype ( Functor
, Applicative
, Monad
)
runPlaid :: PlaidEnv -> Plaid a -> IO a
runPlaid env = flip runReaderT env . unPlaid
runTestPlaid :: PlaidTest a -> a
runTestPlaid = runIdentity . unPlaidTest
data CurrencyCode
= USD
| EUR
deriving (Eq, Show)
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 12 }) ''CurrencyCode)
data Balance =
Balance
{ balanceAvailable :: Double
, balanceCurrent :: Double
, balanceLimit :: Maybe Double
, balanceIsoCurrencyCode :: CurrencyCode
, balanceUnofficialCurrencyCode :: Maybe CurrencyCode
} deriving (Eq, Show)
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 7 }) ''Balance)
data Account =
Account
{ accountAccountId :: Text
, accountBalances :: Balance
, accountMask :: Text
, accountName :: Text
, accountOfficialName :: Text
, accountSubtype :: Text
, accountType :: Text
} deriving (Eq, Show)
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 7 }) ''Account)
data Ach =
Ach
{ achAccount :: AccountNumber
, achAccountId :: AccountId
, achRouting :: Text
, achWireRouting :: Text
} deriving (Eq, Show)
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 3 }) ''Ach)
data Error =
Error
{ errorType :: Text
, errorCode :: Text
, errorMessage :: Text
, displayMessage :: Maybe Text
} deriving (Eq, Show)
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 5 }) ''Error)
data Item =
Item
{ itemAvailableProducts :: [Text]
, itemBilledProducts :: [Text]
, itemError :: Maybe Error
, itemInstitutionId :: InstitutionId
, itemItemId :: Text
, itemWebhook :: Maybe Text
} deriving (Eq, Show)
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 4 }) ''Item)
data Eft =
Eft
{ eftAccount :: AccountNumber
, eftAccountId :: AccountId
, eftInstitution :: Institution
, eftBranch :: Text
} deriving (Eq, Show)
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 3 }) ''Eft)
data International =
International
{ internationalAccountId :: AccountId
, internationalBic :: Bic
, internationalIban :: Iban
} deriving (Eq, Show)
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 13 }) ''International)
data Bacs =
Bacs
{ bacsAccount :: AccountNumber
, bacsAccountId :: AccountId
, bacsSortCode :: SortCode
} deriving (Eq, Show)
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 4 }) ''Bacs)
data Numbers =
Numbers
{ numbersAch :: [Ach]
, numbersEft :: [Eft]
, numbersInternational :: [International]
, numbersBacs :: [Bacs]
} deriving (Eq, Show)
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 7 }) ''Numbers)
data TransactionType
= Digital
| Place
| Special
| Unresolved
deriving (Eq, Show)
instance ToJSON TransactionType where
toJSON Digital = String "digital"
toJSON Place = String "place"
toJSON Special = String "special"
toJSON Unresolved = String "unresolved"
instance FromJSON TransactionType where
parseJSON "digital" = pure Digital
parseJSON "place" = pure Place
parseJSON "special" = pure Special
parseJSON "unresolved" = pure Unresolved
parseJSON _ = mempty
data TransactionLocation =
TransactionLocation
{ transactionLocationAddress :: Maybe Text
, transactionLocationCity :: Maybe Text
, transactionLocationRegion :: Maybe Text
, transactionLocationPostalCode :: Maybe Text
, transactionLocationPostalCountry :: Maybe Text
, transactionLocationPostalLat :: Maybe Double
, transactionLocationPostalLon :: Maybe Double
} deriving (Eq, Show)
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 19 }) ''TransactionLocation)
data PaymentMeta =
PaymentMeta
{ paymentMetaReferenceNumber :: Maybe Text
, paymentMetaPpdId :: Maybe Text
, paymentMetaPayee :: Maybe Text
} deriving (Eq, Show)
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 11 }) ''PaymentMeta)
data Transaction =
Transaction
{ transactionAccountId :: AccountId
, transactionAmount :: Double
, transactionIsoCurrencyCode :: CurrencyCode
, transactionUnofficialCurrencyCode :: Maybe CurrencyCode
, transactionCategory :: Maybe [Text]
, transactionCategoryId :: Maybe Text
, transactionTransactionType :: Maybe TransactionType
, transactionName :: Text
, transactionDate :: Day
, transactionLocation :: TransactionLocation
, transactionPending :: Bool
, transactionPaymentMeta :: PaymentMeta
, transactionPendingTransactionId :: Maybe Text
, transactionAccountOwner :: Maybe Text
} deriving (Eq, Show)
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 11 }) ''Transaction)
data PlaidTransactionsGetResponse =
PlaidTransactionsGetResponse
{ _plaidTransactionsGetResponseAccounts :: [Account]
, _plaidTransactionsGetResponseTransactions :: [Transaction]
, _plaidTransactionsItem :: Item
, _plaidTransactionsTotalTransactions :: Int
, _plaidTransactionsRequestId :: Text
} deriving (Eq, Show)
makeLenses ''PlaidTransactionsGetResponse
$(deriveToJSON (defaultOptions { fieldLabelModifier = camel . drop 29 }) ''PlaidTransactionsGetResponse)
instance FromJSON PlaidTransactionsGetResponse where
parseJSON = withObject "PlaidTransactionsGetResponse" $ \o ->
PlaidTransactionsGetResponse
<$> o .: "accounts"
<*> o .: "transactions"
<*> o .: "item"
<*> o .: "total_transactions"
<*> o .: "request_id"
data PlaidAuthGetResponse =
PlaidAuthGetResponse
{ _plaidAuthGetResponseAccounts :: [Account]
, _plaidAuthGetResponseNumbers :: Numbers
, _plaidAuthGetResponseItem :: Item
, _plaidAuthGetResponseRequestId :: RequestId
} deriving (Eq, Show)
makeLenses ''PlaidAuthGetResponse
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 21 }) ''PlaidAuthGetResponse)
data PlaidPublicTokenResponse =
PlaidPublicTokenResponse
{ _plaidPublicTokenResponsePublicToken :: PublicToken
, _plaidPublicTokenResponseRequestId :: Text
} deriving (Eq, Show)
makeLenses ''PlaidPublicTokenResponse
instance ToJSON PlaidPublicTokenResponse where
toJSON PlaidPublicTokenResponse {..} =
object [ "public_token" .= _plaidPublicTokenResponsePublicToken
, "request_id" .= _plaidPublicTokenResponseRequestId
]
instance FromJSON PlaidPublicTokenResponse where
parseJSON = withObject "PlaidPublicTokenResponse" $ \o ->
PlaidPublicTokenResponse
<$> o .: "public_token"
<*> o .: "request_id"
data PlaidAccessTokenResponse =
PlaidAccessTokenResponse
{ _plaidAccessTokenResponseAccessToken :: AccessToken
, _plaidAccessTokenResponseRequestId :: Text
, _plaidAccessTokenResponseItemId :: Text
} deriving (Eq, Show)
makeLenses ''PlaidAccessTokenResponse
instance ToJSON PlaidAccessTokenResponse where
toJSON PlaidAccessTokenResponse {..} =
object [ "access_token" .= _plaidAccessTokenResponseAccessToken
, "request_id" .= _plaidAccessTokenResponseRequestId
, "item_id" .= _plaidAccessTokenResponseItemId
]
instance FromJSON PlaidAccessTokenResponse where
parseJSON = withObject "PlaidAccessTokenResponse" $ \o ->
PlaidAccessTokenResponse
<$> o .: "access_token"
<*> o .: "request_id"
<*> o .: "item_id"
data PhoneNumber =
PhoneNumber
{ _phoneNumberData :: Text
, _phoneNumberPrimary :: Bool
, _phoneNumberType :: Text
} deriving (Eq, Show)
makeLenses ''PhoneNumber
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 12 }) ''PhoneNumber)
data Email =
Email
{ _emailData :: Text
, _emailPrimary :: Bool
, _emailType :: Text
} deriving (Eq, Show)
makeLenses ''Email
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 6 }) ''Email)
data Address =
Address
{ _addressCity :: Text
, _addressRegion :: Text
, _addressStreet :: Text
, _addressPostalCode :: Text
, _addressCountry :: Maybe Text
} deriving (Eq, Show)
makeLenses ''Address
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 8 }) ''Address)
data Addresses =
Addresses
{ _addressesData :: Address
, _addressesPrimary :: Bool
} deriving (Eq, Show)
makeLenses ''Addresses
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 10 }) ''Addresses)
data Owners =
Owners
{ _ownersAddresses :: [Addresses]
, _ownersEmails :: [Email]
, _ownersNames :: [Text]
, _ownersPhoneNumbers :: [PhoneNumber]
} deriving (Eq, Show)
makeLenses ''Owners
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 7 }) ''Owners)
data Accounts =
Accounts
{ _accountsAccountId :: Text
, _accountsBalances :: Balance
, _accountsMask :: Text
, _accountsName :: Text
, _accountsOfficialName :: Maybe Text
, _accountsOwners :: [Owners]
, _accountsSubtype :: Text
, _accountsType :: Text
} deriving (Eq, Show)
$(deriveJSON (defaultOptions { fieldLabelModifier = quietSnake . drop 9 }) ''Accounts)
data PlaidIdentityGetResponse =
PlaidIdentityGetResponse
{ _plaidIdentityGetResponseAccounts :: [Accounts]
, _plaidIdentityGetItem :: Item
, _plaidIdentityGetRequestId :: Text
} deriving (Eq, Show)
makeLenses ''PlaidIdentityGetResponse
$(deriveToJSON (defaultOptions { fieldLabelModifier = camel . drop 25 }) ''PlaidIdentityGetResponse)
instance FromJSON PlaidIdentityGetResponse where
parseJSON = withObject "PlaidIdentityGetResponse" $ \o ->
PlaidIdentityGetResponse
<$> o .: "accounts"
<*> o .: "item"
<*> o .: "request_id"
data IncomeStream =
IncomeStream
{ _incomeStreamMonthlyIncome :: Fixed E2
, _incomeStreamConfidence :: Fixed E2
, _incomeStreamDays :: Fixed E2
, _incomeStreamName :: Text
} deriving (Eq, Show)
makeLenses ''IncomeStream
$(deriveJSON (defaultOptions { fieldLabelModifier = camel . drop 13 }) ''IncomeStream)
data Income =
Income
{ _incomeLastYearIncome :: Fixed E2
, _incomeLastYearIncomeBeforeTax :: Fixed E2
, _incomeProjectedYearlyIncome :: Fixed E2
, _incomeProjectedYearlyIncomeBeforeTax :: Fixed E2
, _incomeIncomeStreams :: [IncomeStream]
, _incomeMaxNumberOfOverlappingIncomeStreams :: Fixed E2
, _incomeNumberOfIncomeStreams :: Fixed E2
} deriving (Eq, Show)
makeLenses ''Income
$(deriveJSON (defaultOptions { fieldLabelModifier = camel . drop 7 }) ''Income)
baseUrl :: Environment -> Url Environment
baseUrl e = Url $ T.pack $ "https://" <> show e <> ".plaid.com"
envUrl :: Environment -> Text
envUrl = unUrl . baseUrl
validInstitutionIds :: [InstitutionId]
validInstitutionIds =
InstitutionId <$>
[ "ins_1"
, "ins_2"
, "ins_3"
, "ins_4"
, "ins_5"
, "ins_6"
, "ins_7"
, "ins_9"
, "ins_10"
, "ins_11"
, "ins_13"
, "ins_14"
, "ins_15"
, "ins_16"
, "ins_19"
, "ins_20"
, "ins_21"
, "ins_23"
, "ins_24"
, "ins_27"
, "ins_29"
]
requestMap :: M.Map Text ByteString
requestMap =
M.fromList
[ ("/auth/get", responseAuthGet)
, ("/transactions/get", responseTransactionsGet)
, ("/public_token/create", responsePublicTokenCreate)
, ("/item/public_token/exchange", responsePublicTokenExchange)
, ("/identity/get", identityJson)
]
instance PlaidHttp Plaid where
executeGet :: Text -> Plaid ByteString
executeGet url = do
request <- parseUrlThrow (T.unpack url)
m <- liftIO $ newManager tlsManagerSettings
response <- httpLbs request m
return $ responseBody response
executePost :: ToJSON a => Text -> a -> Plaid ByteString
executePost url postBody = do
initialRequest <- parseUrlThrow (T.unpack url)
m <- liftIO (newManager tlsManagerSettings)
let request =
initialRequest
{ method = "POST"
, requestBody = RequestBodyLBS (encode postBody)
, requestHeaders = [ ("Content-Type", "application/json") ]
}
response <- httpLbs request m
return $ responseBody response
instance PlaidHttp PlaidTest where
executeGet :: Text -> PlaidTest ByteString
executeGet url =
case M.lookup url requestMap of
Nothing -> return ""
Just bs -> return bs
executePost :: ToJSON a => Text -> a -> PlaidTest ByteString
executePost url _postBody =
case M.lookup url requestMap of
Nothing -> return ""
Just bs -> return bs
instance Semigroup PlaidOptions where
(<>) _ b = b
instance Monoid PlaidOptions where
mempty =
PlaidOptions
{ _plaidOptionsWebHook = ""
, _plaidOptionsOverrideUsername = ""
, _plaidOptionsOverridePassword = ""
}
mappend _ b = b
instance Semigroup (PlaidBody a) where
(<>) _ b = b
instance Monoid (PlaidBody a) where
mempty =
let env =
PlaidEnv
{ _plaidEnvPublicKey = ""
, _plaidEnvClientId = ClientId ""
, _plaidEnvSecret = Secret ""
, _plaidEnvEnvironment = Sandbox
}
in
PlaidBody
{ _plaidBodyEnv = env
, _plaidBodyPublicToken = Nothing
, _plaidBodyAccessToken = Nothing
, _plaidBodyOptions = Nothing
, _plaidBodyPaginationOptions = Nothing
, _plaidBodyInstitutionId = Nothing
, _plaidBodyInitialProducts = Nothing
, _plaidBodyStartDate = Nothing
, _plaidBodyEndDate = Nothing
}
mappend _ b = b