packages feed

registry-0.2.0.0: test/Test/Data/Registry/Make/SpecializationSpec.hs

{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE IncoherentInstances   #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE RecordWildCards       #-}
{-# OPTIONS_GHC -fno-warn-partial-type-signatures #-}
{-# OPTIONS_GHC -fno-warn-missing-signatures #-}

module Test.Data.Registry.Make.SpecializationSpec where

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 <$> unsafeRun @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
  <: 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