|
| 1 | +package com.github.rssh.appcontext |
| 2 | + |
| 3 | +import com.github.rssh.appcontext.util.AppContextPure |
| 4 | +import com.github.rssh.toymonad.ToyMonad |
| 5 | +import cps.* |
| 6 | +import cps.syntax.* |
| 7 | + |
| 8 | +import scala.concurrent.ExecutionContext.Implicits.global |
| 9 | + |
| 10 | +object AsyncImplicitsSearchOrderTestClasses { |
| 11 | + |
| 12 | + case class Dependency1(name: String) |
| 13 | + |
| 14 | + object Dependency1 { |
| 15 | + // Async provider in companion object - should have lower priority than InAppContext providers |
| 16 | + given [F[_]: CpsEffectMonad]: AppContextAsyncProvider[F, Dependency1] with { |
| 17 | + def get: F[Dependency1] = summon[CpsEffectMonad[F]].pure(Dependency1("Dependency1:From companion")) |
| 18 | + } |
| 19 | + } |
| 20 | + |
| 21 | + case class Dependency2(name: String) |
| 22 | + |
| 23 | + object Dependency2 { |
| 24 | + // No provider in companion - must be provided externally |
| 25 | + } |
| 26 | + |
| 27 | +} |
| 28 | + |
| 29 | +class AsyncImplicitsSearchOrderTest extends munit.FunSuite { |
| 30 | + |
| 31 | + import AsyncImplicitsSearchOrderTestClasses.* |
| 32 | + |
| 33 | + test("AISO001: AppContextAsyncProviders takes priority over companion-defined AppContextAsyncProvider") { |
| 34 | + // Dependency1 has AppContextAsyncProvider in companion returning "Dependency1:From companion" |
| 35 | + // But when we use InAppContext (AppContextAsyncProviders), the value from providers should take priority |
| 36 | + |
| 37 | + class Component1[F[_]](using CpsEffectMonad[F], InAppContext[(Dependency1, Dependency2)][F]) { |
| 38 | + def getDependency1Name(): F[String] = { |
| 39 | + summon[CpsEffectMonad[F]].map(InAppContext.get[F, Dependency1])(_.name) |
| 40 | + } |
| 41 | + |
| 42 | + def getDependency2Name(): F[String] = { |
| 43 | + summon[CpsEffectMonad[F]].map(InAppContext.get[F, Dependency2])(_.name) |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + val localDep1 = Dependency1("Dependency1:From InAppContext") |
| 48 | + val localDep2 = Dependency2("Dependency2:From InAppContext") |
| 49 | + |
| 50 | + // Explicitly create providers with local values |
| 51 | + given providers: AppContextAsyncProviders[ToyMonad, (Dependency1, Dependency2)] = |
| 52 | + AppContextAsyncProviders.of[ToyMonad, (Dependency1, Dependency2)]((localDep1, localDep2)) |
| 53 | + val c1 = new Component1[ToyMonad] |
| 54 | + |
| 55 | + val resultFuture = ToyMonad.run { |
| 56 | + ToyMonad.CpsEffectToyMonad.flatMap(c1.getDependency1Name()) { name1 => |
| 57 | + ToyMonad.CpsEffectToyMonad.map(c1.getDependency2Name()) { name2 => |
| 58 | + (name1, name2) |
| 59 | + } |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + resultFuture.map { case (name1, name2) => |
| 64 | + // Key assertion: InAppContext value should win over companion |
| 65 | + assert(name1 == "Dependency1:From InAppContext", |
| 66 | + s"Expected 'Dependency1:From InAppContext' but got '$name1'. " + |
| 67 | + "AppContextAsyncProviders (InAppContext) should take priority over companion-defined AppContextAsyncProvider.") |
| 68 | + assert(name2 == "Dependency2:From InAppContext") |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + test("AISO002: fallback to companion AppContextAsyncProvider when no InAppContext in scope") { |
| 73 | + // When there's no InAppContext (AppContextAsyncProviders) in scope, |
| 74 | + // we should fall back to the companion-defined provider |
| 75 | + |
| 76 | + val resultFuture = ToyMonad.run { |
| 77 | + AppContext.asyncGet[ToyMonad, Dependency1] |
| 78 | + } |
| 79 | + |
| 80 | + resultFuture.map { d1 => |
| 81 | + assert(d1.name == "Dependency1:From companion", |
| 82 | + s"Expected 'Dependency1:From companion' but got '${d1.name}'. " + |
| 83 | + "Should fall back to companion-defined AppContextAsyncProvider when no InAppContext in scope.") |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + test("AISO003: InAppContext.get uses lookup with correct priority") { |
| 88 | + // Direct test of InAppContext.get ensuring it uses the lookup mechanism |
| 89 | + |
| 90 | + class Service[F[_]](using CpsEffectMonad[F], InAppContext[(Dependency1 *: EmptyTuple)][F]) { |
| 91 | + def getDep1(): F[Dependency1] = InAppContext.get[F, Dependency1] |
| 92 | + } |
| 93 | + |
| 94 | + val localDep1 = Dependency1("Dependency1:Local override") |
| 95 | + |
| 96 | + // Explicitly create providers with local values |
| 97 | + given providers: AppContextAsyncProviders[ToyMonad, Dependency1 *: EmptyTuple] = |
| 98 | + AppContextAsyncProviders.of[ToyMonad, Dependency1 *: EmptyTuple](localDep1 *: EmptyTuple) |
| 99 | + val service = new Service[ToyMonad] |
| 100 | + |
| 101 | + val resultFuture = ToyMonad.run(service.getDep1()) |
| 102 | + |
| 103 | + resultFuture.map { d1 => |
| 104 | + assert(d1.name == "Dependency1:Local override", |
| 105 | + s"Expected 'Dependency1:Local override' but got '${d1.name}'. " + |
| 106 | + "InAppContext.get should use lookup that prioritizes InAppContext over companion.") |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + test("AISO004: Local sync AppContextProvider preferred over companion async provider") { |
| 111 | + // Test that local AppContextProvider (sync) takes priority over |
| 112 | + // AppContextAsyncProvider defined in companion object. |
| 113 | + // The lookup mechanism converts sync providers via fromSyncProvider at mid priority, |
| 114 | + // which wins over companion async providers at low priority. |
| 115 | + |
| 116 | + val localDep1 = Dependency1("Dependency1:From local sync provider") |
| 117 | + |
| 118 | + // Provide sync AppContextProvider - should win over companion async provider |
| 119 | + given AppContextProvider[Dependency1] = AppContextProvider.of(localDep1) |
| 120 | + |
| 121 | + val resultFuture = ToyMonad.run { |
| 122 | + InAppContext.get[ToyMonad, Dependency1] |
| 123 | + } |
| 124 | + |
| 125 | + resultFuture.map { d1 => |
| 126 | + // Local sync provider should win over companion async provider |
| 127 | + assert(d1.name == "Dependency1:From local sync provider", |
| 128 | + s"Expected 'Dependency1:From local sync provider' but got '${d1.name}'. " + |
| 129 | + "Local AppContextProvider should take priority over companion-defined AppContextAsyncProvider.") |
| 130 | + } |
| 131 | + } |
| 132 | + |
| 133 | + test("AISO005: Local async AppContextAsyncProvider preferred over global sync AppContextProvider") { |
| 134 | + // Test that local AppContextAsyncProvider (in InAppContext) takes priority over |
| 135 | + // global/outer scope AppContextProvider (sync). |
| 136 | + // The fromProviders (from InAppContext) has highest priority. |
| 137 | + |
| 138 | + class Component[F[_]](using CpsEffectMonad[F], InAppContext[(Dependency2 *: EmptyTuple)][F]) { |
| 139 | + def getDependency2(): F[Dependency2] = InAppContext.get[F, Dependency2] |
| 140 | + } |
| 141 | + |
| 142 | + // Global sync provider (lower priority) |
| 143 | + given globalSync: AppContextProvider[Dependency2] = AppContextProvider.of(Dependency2("Dependency2:From global sync")) |
| 144 | + |
| 145 | + // Local async providers via InAppContext (higher priority) |
| 146 | + val localDep2 = Dependency2("Dependency2:From local async InAppContext") |
| 147 | + given providers: AppContextAsyncProviders[ToyMonad, Dependency2 *: EmptyTuple] = |
| 148 | + AppContextAsyncProviders.of[ToyMonad, Dependency2 *: EmptyTuple](localDep2 *: EmptyTuple) |
| 149 | + |
| 150 | + val component = new Component[ToyMonad] |
| 151 | + |
| 152 | + val resultFuture = ToyMonad.run(component.getDependency2()) |
| 153 | + |
| 154 | + resultFuture.map { d2 => |
| 155 | + // Local async provider (via InAppContext) should win over global sync |
| 156 | + assert(d2.name == "Dependency2:From local async InAppContext", |
| 157 | + s"Expected 'Dependency2:From local async InAppContext' but got '${d2.name}'. " + |
| 158 | + "Local InAppContext (async) should take priority over global AppContextProvider (sync).") |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | +} |
0 commit comments