registry-0.3.0.0: test/Test/Data/Registry/Make/SpecializationSpec.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE IncoherentInstances #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
{-# OPTIONS_GHC -fno-warn-partial-type-signatures #-}
module Test.Data.Registry.Make.SpecializationSpec where
import Control.Monad.Trans.Resource
import Data.Registry
import Protolude hiding (C1)
import Test.Tasty.Extensions
-- | Case 1: contextual setting of different values for a given type
test_specialization_1 = test "values can use other values depending on some context" $ do
(c1, c2) <- liftIO $
do
let r =
fun newUseConfig2
<: fun newUseConfig1
<: val (Config 3)
let r' =
specialize @UseConfig1 (Config 1) $
specialize @UseConfig2 (Config 2) r
pure (printConfig1 (make @UseConfig1 r'), printConfig2 (make @UseConfig2 r'))
c1 === Config 1
c2 === Config 2
-- | Case 2: if there are 2 specialization taking effect for 2 different types
-- the one that is the children of the other in the current context wins
test_specialization_2 = test "more specialized context" $ do
c <- liftIO $
do
let r =
fun newClient1
<: fun newUseConfig
<: val (Config 3)
let r' =
specialize @Client1 (Config 1) $
specialize @UseConfig (Config 2) r
pure $ printClientConfig1 (make @Client1 r')
annotate "this is the more specialized context"
c === Config 2
-- | Case 3: this time the specialization must "propagate" to components
-- using the specialized values, note that the `UseConfig` component needs to be
-- duplicated because it is on the path of the specialization
test_specialization_3 = test "specialized values must be kept up to their start context" $ do
(c1, c2) <- liftIO $
do
let r =
fun newBase
<: fun newClient1
<: fun newClient2
<: fun newUseConfig
<: val (Config 3)
let r' =
specialize @Client1 (Config 1) $
specialize @Client2 (Config 2) r
pure $ printBase (make @Base r')
c1 === Config 1
c2 === Config 2
-- we want the following graph
{-
+---------- Base ------------+
| |
v v
(client1 :: Client1) (client2 :: Client2)
| |
v v
(useConfig1 :: UseConfig) (useConfig2 :: UseConfig)
| |
v v
(config1 :: Config) (config2 :: Config)
-}
newtype Config = Config Int deriving (Eq, Show)
newtype UseConfig1 = UseConfig1 {printConfig1 :: Config}
newUseConfig1 config = UseConfig1 {printConfig1 = config}
newtype UseConfig2 = UseConfig2 {printConfig2 :: Config}
newUseConfig2 config = UseConfig2 {printConfig2 = config}
newtype UseConfig = UseConfig {printConfig :: Config}
newUseConfig config = UseConfig {printConfig = config}
newtype Client1 = Client1 {printClientConfig1 :: Config}
newClient1 useConfig = Client1 {printClientConfig1 = printConfig useConfig}
newtype Client2 = Client2 {printClientConfig2 :: Config}
newClient2 useConfig = Client2 {printClientConfig2 = printConfig useConfig}
newtype Base = Base {printBase :: (Config, Config)}
newBase client1 client2 = Base {printBase = (printClientConfig1 client1, printClientConfig2 client2)}
-- | Case 4: we can specialize values across a given "path" in the graph
test_specialization_4 = test "values can be specialized for a given path" $ do
(c1, c2, c3) <- liftIO $
do
let r =
funTo @RIO newBase2
<: funTo @RIO newClient1
<: funTo @RIO newClient2
<: funTo @RIO newUseConfig
<: valTo @RIO (Config 3)
let r' =
specializePathValTo @RIO @[RIO Base2, RIO Client1, RIO UseConfig] (Config 1)
. specializeValTo @RIO @(RIO UseConfig) (Config 2)
$ r
printBase2 <$> runResourceT (make @(RIO Base2) r')
c1 === Config 1
c2 === Config 2
c3 === Config 3
data Base2 = Base2
{ client1 :: Client1,
useConfig :: UseConfig,
config3 :: Config
}
newBase2 = Base2
printBase2 Base2 {..} = (printClientConfig1 client1, printConfig useConfig, config3)
-- we want the following graph
{-
+---------- Base2 -----------+-----> (config3 :: Config)
| |
v v
(client1 :: Client1) (useConfig2 :: UseConfig)
| |
v v
(useConfig1 :: UseConfig) (config2 :: Config)
|
v
(config1 :: Config)
-}
-- | Case 5 (taken from a real case)
-- In that case a non-specialized value could be taken for a given
-- instead of being re-created because it has specialized dependencies for
-- a given context
-- For this test, we track how a component, the Supervisor, is being configured
-- depending on which path it belongs
test_specialization_5 = test "values can be specialized for a given path - other case" $ do
let app = make @App appRegistry
annotate "the stats store client is well configured"
let (twitterConfig, statsSqlConfig, statsSupervisorConfig) = statsStoreConfig (statsStore app)
twitterConfig === "for the twitter client"
statsSqlConfig === "for sql under the stats store"
statsSupervisorConfig === "default"
annotate "the app is well configured"
(app & supervisor & supervisorConfig) === ("default" :: Text)
(app & sql & sqlConfig) === ("for sql in general" :: Text)
appRegistry :: Registry _ _
appRegistry =
specializeVal @Sql (SupervisorConfig "for sql in general")
. specializePathVal @[StatsStore, Sql] (SupervisorConfig "for sql under the stats store")
. specializeVal @TwitterClient (SupervisorConfig "for the twitter client")
$ fun App
<: fun newStatsStore
<: fun newTwitterClient
<: fun newSql
<: fun newSupervisor
<: val (SupervisorConfig "default")
data App = App
{ sql :: Sql,
twitterClient :: TwitterClient,
supervisor :: Supervisor,
statsStore :: StatsStore
}
newtype Sql = Sql {sqlConfig :: Text}
newtype StatsStore = StatsStore {statsStoreConfig :: (Text, Text, Text)} -- (twitter, sql, supervisor)
newtype TwitterClient = TwitterClient {twitterConfig :: Text}
newtype Supervisor = Supervisor {supervisorConfig :: Text}
newtype SupervisorConfig = SupervisorConfig Text deriving (Eq, Show)
newSupervisor :: SupervisorConfig -> Supervisor
newSupervisor (SupervisorConfig n) = Supervisor {supervisorConfig = n}
newSql :: Supervisor -> Sql
newSql s = Sql {sqlConfig = supervisorConfig s}
newTwitterClient :: Supervisor -> TwitterClient
newTwitterClient s = TwitterClient {twitterConfig = supervisorConfig s}
newStatsStore :: TwitterClient -> Sql -> Supervisor -> StatsStore
newStatsStore client sql supervisor =
StatsStore
{ statsStoreConfig = (twitterConfig client, sqlConfig sql, supervisorConfig supervisor)
}
-- | Case 6 (taken from a real case...)
test_specialization_6 = test "specialized values must not be affected by memoization" $ do
someData <- liftIO $ do
r <- aRegistryIO
make @(IO SomeData) r
(someData & toOverride & toOverrideConfig) === ("specialized config" :: Text)
(someData & toKeepDefault & toKeepDefaultConfig) === ("default config" :: Text)
data SomeData = SomeData
{ toKeepDefault :: ToKeepDefault,
toOverride :: ToOverride,
inCommon :: InCommon
}
newtype ToOverride = ToOverride {toOverrideConfig :: Text}
newtype ToKeepDefault = ToKeepDefault {toKeepDefaultConfig :: Text}
newtype InCommon = InCommon {config :: SomeConfig}
newtype SomeConfig = SomeConfig Text deriving (Eq, Show)
newToKeepDefault :: InCommon -> ToKeepDefault
newToKeepDefault (InCommon (SomeConfig t)) = ToKeepDefault {toKeepDefaultConfig = t}
newToOverride :: InCommon -> ToOverride
newToOverride (InCommon (SomeConfig t)) = ToOverride {toOverrideConfig = t}
aRegistryIO :: IO (Registry _ _)
aRegistryIO =
memoizeAll @IO $
specializePathValTo @IO @[IO ToOverride, IO InCommon] (SomeConfig "specialized config") $
funTo @IO SomeData
<: funTo @IO newToKeepDefault
<: funTo @IO newToOverride
<: funTo @IO InCommon
<: fun (\(c:: IO SomeConfig) -> InCommon <$> c)
<: valTo @IO (SomeConfig "default config")
test_make_specialized_values = test "specialized values can be made" $ do
let r =
fun newBase2
<: fun newClient2
<: fun newClient1
<: fun newUseConfig
<: val (Config 3)
let r' =
specializePathVal @[Base2, Client1, UseConfig] (Config 1)
. specializeVal @UseConfig (Config 2)
$ r
makeSpecialized @UseConfig r' === Config 2
makeSpecializedPath @[Base2, Client1, UseConfig] r' === Config 1