Skip to content

Commit 4d9d88b

Browse files
authored
Localstack annd rds testing support (#14)
* New runReaderAwsEnvDiscover function * Export runFileSystem * Testcontainers for loccalstack support * New logEntryToJson and logMessageToJson functions * New putJsonStdout function * Export hedgehog Failure type * Modify Log effect to coonvey the severity and message together in the LogMessage * New runReaderM function * New Effectful.Zoo.Environment exporting lookupEnv that throws on error and lookupEnvMaybe that doesn't, both using Text as the type * New lookupParseMaybeEnv and lookupParseEitherEnv functions * New lookupMapEnv function * New functions: failMessage, failWithCustom, byDurationM, and byDeadlineM * New rds-data-test and testcontainers-localstack components
1 parent 7bfef2e commit 4d9d88b

File tree

25 files changed

+892
-33
lines changed

25 files changed

+892
-33
lines changed

components/amazonka/Effectful/Zoo/Amazonka/Api/Run.hs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ module Effectful.Zoo.Amazonka.Api.Run
44
( runDataLogAwsLogEntryToLog,
55
runDataLogAwsLogEntryToLogWith,
66
runDataLogAwsLogEntryLocalToLogWith,
7+
runReaderAwsEnvDiscover,
78
) where
89

10+
import Amazonka qualified as AWS
911
import Control.Monad.IO.Class
1012
import Data.ByteString.Builder qualified as B
13+
import Data.Generics.Product.Any
1114
import Data.Text.Lazy qualified as LT
1215
import Data.Text.Lazy.Encoding qualified as LT
1316
import Data.Time.Clock qualified as IO
1417
import Effectful
18+
import Effectful.Environment
19+
import Effectful.Reader.Static
1520
import Effectful.Zoo.Amazonka.Api
1621
import Effectful.Zoo.Amazonka.Data
1722
import Effectful.Zoo.Core
@@ -21,6 +26,8 @@ import Effectful.Zoo.DataLog.Dynamic
2126
import Effectful.Zoo.Log.Data.LogMessage
2227
import Effectful.Zoo.Log.Data.Severity
2328
import HaskellWorks.Prelude
29+
import Lens.Micro
30+
import System.IO qualified as IO
2431

2532
runDataLogAwsLogEntryToLog :: forall a r. ()
2633
=> r <: DataLog (LogEntry (LogMessage Text))
@@ -61,3 +68,26 @@ runDataLogAwsLogEntryLocalToLogWith mapSeverity context =
6168
, time = now
6269
, source = entry.callStack
6370
}
71+
72+
runReaderAwsEnvDiscover :: forall a r. ()
73+
=> r <: Environment
74+
=> r <: IOE
75+
=> Eff (Reader AwsEnv : r) a
76+
-> Eff r a
77+
runReaderAwsEnvDiscover f = do
78+
logger' <- liftIO $ AWS.newLogger AWS.Debug IO.stdout
79+
80+
mLocalStackHost <- lookupEnv "AWS_LOCALSTACK_HOST"
81+
mLocalStackPort <- lookupEnv "AWS_LOCALSTACK_PORT"
82+
83+
mLocalStackEndpoint <- pure $ (,)
84+
<$> mLocalStackHost
85+
<*> mLocalStackPort
86+
87+
discoveredAwsEnv <- liftIO $ AWS.newEnv AWS.discover
88+
89+
awsEnv <- pure $ discoveredAwsEnv
90+
& the @"logger" .~ logger'
91+
& the @"overrides" %~ maybeSetEndpoint mLocalStackEndpoint
92+
93+
runReader awsEnv f

components/core/Effectful/Zoo/DataLog/Api.hs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
module Effectful.Zoo.DataLog.Api
22
( dataLog,
3+
logEntryToJson,
4+
logMessageToJson,
5+
putJsonStdout,
36
) where
47

8+
import Data.Aeson (Value, object, (.=))
9+
import Data.Aeson qualified as J
10+
import Data.ByteString.Lazy qualified as LBS
511
import Effectful
612
import Effectful.Dispatch.Dynamic
13+
import Effectful.Dispatch.Static
714
import Effectful.Zoo.Core
15+
import Effectful.Zoo.DataLog.Data.LogEntry
816
import Effectful.Zoo.DataLog.Dynamic
17+
import Effectful.Zoo.Log.Data.LogMessage
18+
import GHC.Stack qualified as GHC
919
import HaskellWorks.Prelude
20+
import System.IO qualified as IO
1021

1122
dataLog :: ()
1223
=> HasCallStack
@@ -16,3 +27,41 @@ dataLog :: ()
1627
dataLog i =
1728
withFrozenCallStack do
1829
send $ DataLog i
30+
31+
logEntryToJson :: forall a. ()
32+
=> (a -> Value)
33+
-> LogEntry a
34+
-> Value
35+
logEntryToJson aToJson (LogEntry value time callstack) =
36+
object
37+
[ "time" .= time
38+
, "data" .= aToJson value
39+
, "callstack" .= fmap callsiteToJson (GHC.getCallStack callstack)
40+
]
41+
where
42+
callsiteToJson :: ([Char], GHC.SrcLoc) -> Value
43+
callsiteToJson (caller, srcLoc) =
44+
object
45+
[ "caller" .= caller
46+
, "package" .= GHC.srcLocPackage srcLoc
47+
, "module" .= GHC.srcLocModule srcLoc
48+
, "file" .= GHC.srcLocFile srcLoc
49+
, "startLine" .= GHC.srcLocStartLine srcLoc
50+
, "startCol" .= GHC.srcLocStartCol srcLoc
51+
, "endLine" .= GHC.srcLocEndLine srcLoc
52+
, "endCol" .= GHC.srcLocEndCol srcLoc
53+
]
54+
55+
logMessageToJson :: LogMessage Text -> Value
56+
logMessageToJson (LogMessage severity message) =
57+
object
58+
[ "severity" .= show severity
59+
, "message" .= message
60+
]
61+
62+
putJsonStdout :: ()
63+
=> Value
64+
-> Eff r ()
65+
putJsonStdout value = do
66+
unsafeEff_ $ LBS.putStr $ J.encode value <> "\n"
67+
unsafeEff_ $ IO.hFlush IO.stdout
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
module Effectful.Zoo.Environment
2+
( -- * Effect
3+
Environment,
4+
5+
-- ** Handlers
6+
E.runEnvironment,
7+
8+
-- * Querying the environment
9+
E.getArgs,
10+
E.getProgName,
11+
E.getExecutablePath,
12+
E.getEnv,
13+
E.getEnvironment,
14+
lookupEnv,
15+
lookupEnvMaybe,
16+
lookupMapEnv,
17+
lookupParseMaybeEnv,
18+
lookupParseEitherEnv,
19+
20+
-- * Modifying the environment
21+
E.setEnv,
22+
E.unsetEnv,
23+
E.withArgs,
24+
E.withProgName,
25+
)
26+
where
27+
28+
import Data.Text qualified as T
29+
import Effectful
30+
import Effectful.Environment (Environment)
31+
import Effectful.Environment qualified as E
32+
import Effectful.Zoo.Core
33+
import Effectful.Zoo.Error.Static
34+
import Effectful.Zoo.Errors.EnvironmentVariableInvalid
35+
import Effectful.Zoo.Errors.EnvironmentVariableMissing
36+
import HaskellWorks.Prelude
37+
38+
lookupEnv :: ()
39+
=> r <: Environment
40+
=> r <: Error EnvironmentVariableMissing
41+
=> Text
42+
-> Eff r Text
43+
lookupEnv envName =
44+
lookupEnvMaybe envName
45+
& onNothingM (throw $ EnvironmentVariableMissing envName)
46+
47+
lookupMapEnv :: ()
48+
=> r <: Environment
49+
=> r <: Error EnvironmentVariableMissing
50+
=> Text
51+
-> (Text -> a)
52+
-> Eff r a
53+
lookupMapEnv envName f =
54+
f <$> lookupEnv envName
55+
56+
lookupParseMaybeEnv :: ()
57+
=> r <: Environment
58+
=> r <: Error EnvironmentVariableInvalid
59+
=> r <: Error EnvironmentVariableMissing
60+
=> Text
61+
-> (Text -> Maybe a)
62+
-> Eff r a
63+
lookupParseMaybeEnv envName parse = do
64+
text <- lookupEnv envName
65+
66+
parse text
67+
& onNothing (throw $ EnvironmentVariableInvalid envName text Nothing)
68+
69+
lookupParseEitherEnv :: ()
70+
=> r <: Environment
71+
=> r <: Error EnvironmentVariableInvalid
72+
=> r <: Error EnvironmentVariableMissing
73+
=> Text
74+
-> (Text -> Either Text a)
75+
-> Eff r a
76+
lookupParseEitherEnv envName parse = do
77+
text <- lookupEnv envName
78+
79+
parse text
80+
& onLeft (throw . EnvironmentVariableInvalid envName text . Just)
81+
82+
lookupEnvMaybe :: ()
83+
=> r <: Environment
84+
=> Text
85+
-> Eff r (Maybe Text)
86+
lookupEnvMaybe envName = do
87+
value <- E.lookupEnv $ T.unpack envName
88+
pure $ T.pack <$> value
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module Effectful.Zoo.Errors.EnvironmentVariableInvalid
2+
( EnvironmentVariableInvalid (..),
3+
) where
4+
5+
import Data.Aeson (ToJSON (..), (.=))
6+
import Data.Aeson qualified as J
7+
import GHC.Generics
8+
import HaskellWorks.Prelude
9+
10+
data EnvironmentVariableInvalid = EnvironmentVariableInvalid
11+
{ variable :: Text
12+
, text :: Text
13+
, reason :: Maybe Text
14+
}
15+
deriving stock (Eq, Show, Generic)
16+
17+
instance ToJSON EnvironmentVariableInvalid where
18+
toJSON e =
19+
J.object
20+
[ "error" .= id @Text "EnvironmentVariableInvalid"
21+
, "variable" .= e.variable
22+
, "text" .= e.text
23+
, "reason" .= e.reason
24+
]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module Effectful.Zoo.Errors.EnvironmentVariableMissing
2+
( EnvironmentVariableMissing (..),
3+
) where
4+
5+
import Data.Aeson (ToJSON (..), (.=))
6+
import Data.Aeson qualified as J
7+
import GHC.Generics
8+
import HaskellWorks.Prelude
9+
10+
newtype EnvironmentVariableMissing = EnvironmentVariableMissing
11+
{ variable :: Text
12+
}
13+
deriving stock (Eq, Show, Generic)
14+
15+
instance ToJSON EnvironmentVariableMissing where
16+
toJSON e =
17+
J.object
18+
[ "error" .= id @Text "EnvironmentVariableMissing"
19+
, "variable" .= e.variable
20+
]

components/core/Effectful/Zoo/FileSystem.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ module Effectful.Zoo.FileSystem
1313
removePathForcibly,
1414
doesFileExist,
1515
doesDirectoryExist,
16+
17+
runFileSystem,
1618
) where
1719

1820
import Data.Aeson (FromJSON)
@@ -23,7 +25,7 @@ import Data.Text qualified as T
2325
import Data.Yaml qualified as Y
2426
import Effectful
2527
import Effectful.Dispatch.Static
26-
import Effectful.FileSystem (FileSystem)
28+
import Effectful.FileSystem (FileSystem, runFileSystem)
2729
import Effectful.Zoo.Core
2830
import Effectful.Zoo.Error.Static
2931
import Effectful.Zoo.Exception

components/core/Effectful/Zoo/Log/Api/Generic.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module Effectful.Zoo.Log.Api.Generic
1111
import Effectful
1212
import Effectful.Dispatch.Dynamic
1313
import Effectful.Zoo.Core
14+
import Effectful.Zoo.Log.Data.LogMessage
1415
import Effectful.Zoo.Log.Data.Severity
1516
import Effectful.Zoo.Log.Dynamic
1617
import HaskellWorks.Prelude
@@ -23,7 +24,7 @@ log :: ()
2324
-> Eff r ()
2425
log severity message =
2526
withFrozenCallStack $
26-
send (Log severity message)
27+
send (Log (LogMessage severity message))
2728

2829
trace :: ()
2930
=> HasCallStack

components/core/Effectful/Zoo/Log/Api/LazyText.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module Effectful.Zoo.Log.Api.LazyText
1111
import Effectful
1212
import Effectful.Dispatch.Dynamic
1313
import Effectful.Zoo.Core
14+
import Effectful.Zoo.Log.Data.LogMessage
1415
import Effectful.Zoo.Log.Data.Severity
1516
import Effectful.Zoo.Log.Dynamic
1617
import HaskellWorks.Prelude
@@ -23,7 +24,7 @@ log :: ()
2324
-> Eff r ()
2425
log severity message =
2526
withFrozenCallStack $
26-
send (Log severity message)
27+
send (Log (LogMessage severity message))
2728

2829
trace :: ()
2930
=> HasCallStack

components/core/Effectful/Zoo/Log/Api/String.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module Effectful.Zoo.Log.Api.String
1111
import Effectful
1212
import Effectful.Dispatch.Dynamic
1313
import Effectful.Zoo.Core
14+
import Effectful.Zoo.Log.Data.LogMessage
1415
import Effectful.Zoo.Log.Data.Severity
1516
import Effectful.Zoo.Log.Dynamic
1617
import HaskellWorks.Prelude
@@ -23,7 +24,7 @@ log :: ()
2324
-> Eff r ()
2425
log severity message =
2526
withFrozenCallStack $
26-
send (Log severity message)
27+
send (Log (LogMessage severity message))
2728

2829
trace :: ()
2930
=> HasCallStack

components/core/Effectful/Zoo/Log/Api/Text.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Effectful
1212
import Effectful.Dispatch.Dynamic
1313
import Effectful.Zoo.Core
1414
import Effectful.Zoo.Log.Data.Severity
15+
import Effectful.Zoo.Log.Data.LogMessage
1516
import Effectful.Zoo.Log.Dynamic
1617
import HaskellWorks.Prelude
1718

@@ -23,7 +24,7 @@ log :: ()
2324
-> Eff r ()
2425
log severity message =
2526
withFrozenCallStack $
26-
send (Log severity message)
27+
send (Log (LogMessage severity message))
2728

2829
trace :: ()
2930
=> HasCallStack

0 commit comments

Comments
 (0)