Skip to content

Commit c40760d

Browse files
committed
Test case: Dependency injection via Providers
1 parent 42578c0 commit c40760d

File tree

2 files changed

+197
-0
lines changed

2 files changed

+197
-0
lines changed

tests/run/Providers.check

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
11
2+
hi
3+
List(1, 2, 3)
4+
hi
5+
6+
Direct:
7+
You've just been subscribed to RockTheJVM. Welcome, Daniel
8+
Acquired connection
9+
Executing query: insert into subscribers(name, email) values Daniel daniel@RocktheJVM.com
10+
You've just been subscribed to RockTheJVM. Welcome, Martin
11+
Acquired connection
12+
Executing query: insert into subscribers(name, email) values Martin odersky@gmail.com
13+
14+
Injected
15+
You've just been subscribed to RockTheJVM. Welcome, Daniel
16+
Acquired connection
17+
Executing query: insert into subscribers(name, email) values Daniel daniel@RocktheJVM.com
18+
You've just been subscribed to RockTheJVM. Welcome, Martin
19+
Acquired connection
20+
Executing query: insert into subscribers(name, email) values Martin odersky@gmail.com

tests/run/Providers.scala

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import language.experimental.modularity
2+
import compiletime.constValue
3+
import compiletime.ops.int.S
4+
5+
// Featherweight dependency injection library, inspired by the use case
6+
// laid out in the ZIO course of RockTheJVM.
7+
8+
/** Some things that are not part of Tuple yet, but that would be nice to have. */
9+
object TupleUtils:
10+
11+
/** The index of the first element type of the tuple `Xs` that is a subtype of `X` */
12+
type IndexOf[Xs <: Tuple, X] <: Int = Xs match
13+
case X *: _ => 0
14+
case _ *: ys => S[IndexOf[ys, X]]
15+
16+
/** A trait describing a selection from a tuple `Xs` returning an element of type `X` */
17+
trait Select[Xs <: Tuple, X]:
18+
def apply(xs: Xs): X
19+
20+
/** A given implementing `Select` to return the first element of tuple `Xs`
21+
* that has a static type matching `X`.
22+
*/
23+
given [Xs <: NonEmptyTuple, X] => (idx: ValueOf[IndexOf[Xs, X]]) => Select[Xs, X]:
24+
def apply(xs: Xs) = xs.apply(idx.value).asInstanceOf[X]
25+
26+
/** A featherweight library for dependency injection */
27+
object Providers:
28+
import TupleUtils.*
29+
30+
/** A provider is a zero-cost wrapper around a type that is intended
31+
* to be passed implicitly
32+
*/
33+
opaque type Provider[T] = T
34+
35+
def provide[X](x: X): Provider[X] = x
36+
37+
def provided[X](using p: Provider[X]): X = p
38+
39+
/** Project a provider to one of its element types */
40+
given [Xs <: Tuple, X] => (ps: Provider[Xs], select: Select[Xs, X]) => Provider[X] =
41+
select(ps)
42+
43+
/** Form a compound provider wrapping a tuple */
44+
given [X, Xs <: Tuple] => (p: Provider[X], ps: Provider[Xs]) => Provider[X *: Xs] =
45+
p *: ps
46+
47+
given Provider[EmptyTuple] = EmptyTuple
48+
49+
end Providers
50+
51+
@main def Test =
52+
import TupleUtils.*
53+
54+
type P = (Int, String, List[Int])
55+
val x: P = (11, "hi", List(1, 2, 3))
56+
val selectInt = summon[Select[P, Int]]
57+
println(selectInt(x))
58+
val selectString = summon[Select[P, String]]
59+
println(selectString(x))
60+
val selectList = summon[Select[P, List[Int]]]
61+
println(selectList(x))
62+
val selectObject = summon[Select[P, Object]]
63+
println(selectObject(x)) // prints "hi"
64+
println(s"\nDirect:")
65+
Explicit().test()
66+
println(s"\nInjected")
67+
Injected().test()
68+
69+
/** Demonstrator for explicit dependency construction */
70+
class Explicit:
71+
72+
case class User(name: String, email: String)
73+
74+
class UserSubscription(emailService: EmailService, db: UserDatabase):
75+
def subscribe(user: User) =
76+
emailService.email(user)
77+
db.insert(user)
78+
79+
class EmailService:
80+
def email(user: User) =
81+
println(s"You've just been subscribed to RockTheJVM. Welcome, ${user.name}")
82+
83+
class UserDatabase(pool: ConnectionPool):
84+
def insert(user: User) =
85+
val conn = pool.get()
86+
conn.runQuery(s"insert into subscribers(name, email) values ${user.name} ${user.email}")
87+
88+
class ConnectionPool(n: Int):
89+
def get(): Connection =
90+
println(s"Acquired connection")
91+
Connection()
92+
93+
class Connection():
94+
def runQuery(query: String): Unit =
95+
println(s"Executing query: $query")
96+
97+
def test() =
98+
val subscriptionService =
99+
UserSubscription(
100+
EmailService(),
101+
UserDatabase(
102+
ConnectionPool(10)
103+
)
104+
)
105+
106+
def subscribe(user: User) =
107+
val sub = subscriptionService
108+
sub.subscribe(user)
109+
110+
subscribe(User("Daniel", "daniel@RocktheJVM.com"))
111+
subscribe(User("Martin", "odersky@gmail.com"))
112+
113+
end Explicit
114+
115+
/** The same application as `Explicit` but using dependency injection */
116+
class Injected:
117+
import Providers.*
118+
119+
case class User(name: String, email: String)
120+
121+
class UserSubscription(using Provider[(EmailService, UserDatabase)]):
122+
def subscribe(user: User) =
123+
provided[EmailService].email(user)
124+
provided[UserDatabase].insert(user)
125+
126+
class EmailService:
127+
def email(user: User) =
128+
println(s"You've just been subscribed to RockTheJVM. Welcome, ${user.name}")
129+
130+
class UserDatabase(using Provider[ConnectionPool]):
131+
def insert(user: User) =
132+
val conn = provided[ConnectionPool].get()
133+
conn.runQuery(s"insert into subscribers(name, email) values ${user.name} ${user.email}")
134+
135+
class ConnectionPool(n: Int):
136+
def get(): Connection =
137+
println(s"Acquired connection")
138+
Connection()
139+
140+
class Connection():
141+
def runQuery(query: String): Unit =
142+
println(s"Executing query: $query")
143+
144+
def test() =
145+
given Provider[EmailService] = provide(EmailService())
146+
given Provider[ConnectionPool] = provide(ConnectionPool(10))
147+
given Provider[UserDatabase] = provide(UserDatabase())
148+
given Provider[UserSubscription] = provide(UserSubscription())
149+
150+
def subscribe(user: User)(using Provider[UserSubscription]) =
151+
val sub = provided[UserSubscription]
152+
sub.subscribe(user)
153+
154+
subscribe(User("Daniel", "daniel@RocktheJVM.com"))
155+
subscribe(User("Martin", "odersky@gmail.com"))
156+
end test
157+
158+
// explicit version, not used here
159+
object explicit:
160+
val subscriptionService =
161+
UserSubscription(
162+
using provide(
163+
EmailService(),
164+
UserDatabase(
165+
using provide(
166+
ConnectionPool(10)
167+
)
168+
)
169+
)
170+
)
171+
172+
given Provider[UserSubscription] = provide(subscriptionService)
173+
end explicit
174+
end Injected
175+
176+
177+

0 commit comments

Comments
 (0)