on master help sign in
8 files changed+165−6
since splitsnapshots
unifiedsplit
bucks.cabal+28−3 src/Data/Money.hs+3 src/Data/Money/Currencies.hs+6 src/Data/Money/Internal.hs+15−2 src/Data/Money/OpenApi.hs+22 src/Data/Money/Rel8.hs+3−1 stack.yaml+1 test/Rel8Spec.hs+87
bucks.cabal+28−3
@@ -10,7 +10,7 @@ maintainer: nick@typeable.io
1010category: Data
1111build-type: Simple
1212
13−source-repository: head
13+source-repository head
1414 type: git
1515 location: https://github.com/typeable/bucks.git
1616
@@ -19,6 +19,11 @@ flag aeson
1919 default: False
2020 manual: True
2121
22+flag openapi
23+ description: Enable OpenAPI instances
24+ default: False
25+ manual: True
26+
2227flag rel8
2328 description: Enable Rel8 support
2429 default: False
@@ -37,11 +42,31 @@ library
3742 if flag(aeson)
3843 other-modules: Data.Money.JSON
3944 build-depends: aeson >= 2.0 && < 3.0
40− ghc-options: -DUSE_AESON
45+ cpp-options: -DUSE_AESON
46+ if flag(openapi)
47+ other-modules: Data.Money.OpenApi
48+ build-depends: openapi3
49+ cpp-options: -DUSE_OPENAPI
4150 if flag(rel8)
4251 other-modules: Data.Money.Rel8
4352 build-depends: rel8 >= 1.4,
4453 scientific
45− ghc-options: -DUSE_REL8
54+ cpp-options: -DUSE_REL8
4655 hs-source-dirs: src
4756 default-language: Haskell2010
57+
58+test-suite rel8-test
59+ import: warnings
60+ type: exitcode-stdio-1.0
61+ hs-source-dirs: test
62+ main-is: Rel8Spec.hs
63+ build-depends: base >= 4.14 && < 5.0,
64+ bucks,
65+ hasql,
66+ hspec,
67+ rel8
68+ default-language: Haskell2010
69+ if flag(rel8)
70+ buildable: True
71+ else
72+ buildable: False
src/Data/Money.hs+3
@@ -7,6 +7,9 @@ import Data.Money.Internal as Export
77#ifdef USE_AESON
88import Data.Money.JSON as Export ()
99#endif
10+#ifdef USE_OPENAPI
11+import Data.Money.OpenApi as Export ()
12+#endif
1013#ifdef USE_REL8
1114import Data.Money.Rel8 as Export ()
1215#endif
src/Data/Money/Currencies.hs+6
@@ -11,3 +11,9 @@ type GBP = Currency "GBP" -- Good Boy Points lmao xDDDDD
1111type RUB = Currency "RUB"
1212type CNY = Currency "CNY"
1313type XBT = Currency "XBT"
14+type DKK = Currency "DKK"
15+type NOK = Currency "NOK"
16+type PLN = Currency "PLN"
17+type SEK = Currency "SEK"
18+type AUD = Currency "AUD"
19+type NZD = Currency "NZD"
src/Data/Money/Internal.hs+15−2
@@ -1,5 +1,6 @@
11{-# LANGUAGE AllowAmbiguousTypes #-}
22{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DeriveDataTypeable #-}
34{-# LANGUAGE DeriveGeneric #-}
45{-# LANGUAGE KindSignatures #-}
56{-# LANGUAGE ScopedTypeVariables #-}
@@ -7,23 +8,27 @@
78module Data.Money.Internal ( Money(..)
89 , Currency
910 , KnownCurrency(..)
11+ , (*%)
12+ , (/%)
1013 ) where
1114
15+import Data.Data
1216import Data.Fixed (Centi)
1317import Data.Group
14−import Data.Proxy
1518import GHC.Generics
1619import GHC.TypeLits
20+import GHC.Stack (HasCallStack)
1721
1822-- | Money type. This is actually 'Centi' from 'Data.Fixed' in disguise.
1923-- The type parameter is phantom, you probably want to use 'Currency' there.
2024-- See also 'Data.Money.Currencies' for currency types.
2125newtype Money curr = Money { getAmount :: Centi }
22− deriving (Eq, Ord, Generic)
26+ deriving (Eq, Ord, Generic, Data)
2327
2428-- | Empty type, used only to carry its type parameter with a currency code around. E.g. 'Currency "USD"', 'Currency "GBP"', et cetera.
2529-- See Data.Money.Currencies for pre-defined currency types.
2630data Currency (name :: Symbol)
31+ deriving (Generic, Data)
2732
2833-- | Statically known currency type class.
2934class KnownCurrency c where
@@ -55,3 +60,11 @@ instance KnownCurrency c => Read (Money c) where
5560 | otherwise = []
5661 where expCurr = currencyCode @c <> " "
5762 l = length expCurr
63+
64+(*%) :: Money curr -> Centi -> Money curr
65+(Money m) *% pct = Money (m * pct)
66+
67+(/%) :: HasCallStack => Money curr -> Centi -> Money curr
68+_ /% 0 = error "Called /% with 0 divisor"
69+(Money m) /% pct = Money (fromRational r)
70+ where r = toRational m / toRational pct
src/Data/Money/OpenApi.hsnew file+22
@@ -0,0 +1,22 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE MonoLocalBinds #-}
4+{-# LANGUAGE OverloadedLists #-}
5+{-# LANGUAGE OverloadedStrings #-}
6+module Data.Money.OpenApi where
7+
8+import Data.Fixed
9+import Data.Money.Internal
10+import Data.OpenApi
11+import Data.Typeable
12+
13+instance (KnownCurrency c, Typeable (Money c)) => ToSchema (Money c) where
14+ declareNamedSchema _ = do
15+ centiSchema <- declareSchemaRef (Proxy :: Proxy Centi)
16+ strSchema <- declareSchemaRef (Proxy :: Proxy String)
17+ return $ NamedSchema (Just "Money") $
18+ mempty { _schemaType = Just OpenApiObject
19+ , _schemaProperties = [ ("currency", strSchema)
20+ , ("amount", centiSchema) ]
21+ , _schemaRequired = [ "currency", "amount" ]
22+ }
src/Data/Money/Rel8.hs+3−1
@@ -7,11 +7,13 @@ import Data.Money.Internal
77import Data.Scientific
88import Rel8
99
10+import Debug.Trace
11+
1012instance DBType (Money curr) where
1113 typeInformation =
1214 let sci = typeInformation @Scientific
1315 in TypeInformation { encode = encode sci . realToFrac . getAmount
1416 , decode = fmap (Money . realToFrac) (decode sci)
15− , typeName = "money"
17+ , typeName = "decimal"
1618 }
1719
stack.yaml+1
@@ -6,6 +6,7 @@ packages:
66flags:
77 bucks:
88 aeson: true
9+ openapi: true
910 rel8: true
1011
1112nix:
test/Rel8Spec.hsnew file+87
@@ -0,0 +1,87 @@
1+{-# LANGUAGE DeriveAnyClass #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE DuplicateRecordFields #-}
4+{-# LANGUAGE FlexibleInstances #-}
5+{-# LANGUAGE ImportQualifiedPost #-}
6+{-# LANGUAGE OverloadedStrings #-}
7+{-# OPTIONS_GHC -Wno-orphans #-}
8+module Main where
9+
10+import Control.Exception
11+import Data.Money
12+import GHC.Generics
13+import Hasql.Connection (Connection, ConnectionError)
14+import Hasql.Connection qualified as Hasql
15+import Hasql.Session qualified as Hasql
16+import Rel8
17+import Test.Hspec
18+
19+instance Exception ConnectionError
20+
21+data Exc = Exc String
22+ deriving (Show)
23+
24+instance Exception Exc
25+
26+data Bucks f = Bucks { amount :: f (Money USD)
27+ } deriving (Generic, Rel8able)
28+
29+bucksSchema :: TableSchema (Bucks Name)
30+bucksSchema = TableSchema { schema = Nothing
31+ , name = "bucks"
32+ , columns = Bucks { amount = "amount" }
33+ }
34+
35+dbsets :: Hasql.Settings
36+dbsets = Hasql.settings "10.233.1.2" 5432 "bucks" "password" "bucks"
37+
38+throwLeft :: (Exception e, Applicative f) => Either e a -> f a
39+throwLeft (Left e) = throw e
40+throwLeft (Right a) = pure a
41+
42+onLeft :: Applicative f => Either a b -> (a -> f b) -> f b
43+onLeft (Left a) f = f a
44+onLeft (Right b) _ = pure b
45+
46+createConnection :: IO Connection
47+createConnection = throwLeft =<< Hasql.acquire dbsets
48+
49+createTable :: Connection -> IO ()
50+createTable conn = throwLeft =<< Hasql.run sql conn
51+ where sql = Hasql.sql "CREATE TABLE bucks (amount decimal not null);"
52+
53+dropTable :: Connection -> IO ()
54+dropTable conn = throwLeft =<< Hasql.run sql conn
55+ where sql = Hasql.sql "DROP TABLE IF EXISTS bucks;"
56+
57+truncateTable :: Connection -> IO ()
58+truncateTable conn = throwLeft =<< Hasql.run sql conn
59+ where sql = Hasql.sql "TRUNCATE bucks;"
60+
61+insertMoney :: Connection -> Money USD -> IO ()
62+insertMoney conn m = throwLeft =<< Hasql.run (Hasql.statement () (insert ins)) conn
63+ where ins = Insert { into = bucksSchema
64+ , rows = values [ Bucks (lit m) ]
65+ , onConflict = Abort
66+ , returning = pure ()
67+ }
68+
69+fetchMoney :: Connection -> IO (Money USD)
70+fetchMoney conn = f =<< throwLeft =<< Hasql.run (Hasql.statement () st) conn
71+ where st = select $ do b <- each bucksSchema
72+ return (amount b)
73+ f [] = throw (Exc "Zero entries returned")
74+ f [x] = pure x
75+ f _ = throw (Exc "Multiple entries returned")
76+
77+main :: IO ()
78+main = do conn <- createConnection
79+ dropTable conn
80+ createTable conn
81+ hspec $ before_ (truncateTable conn) $ do
82+
83+ describe "Database tests" $ do
84+ it "insert == select" $ do
85+ let m = Money 12.23
86+ insertMoney conn m
87+ fetchMoney conn `shouldReturn` m