@zio.dev/zio-blocks 0.0.27 → 0.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +1 -1
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +1 -1
- package/guides/zio-schema-migration.md +6 -6
- package/index.md +69 -17
- package/package.json +1 -1
- package/path-interpolator.md +70 -9
- package/reference/allows.md +1155 -34
- package/reference/binding-resolver.md +469 -0
- package/reference/binding.md +1 -1
- package/reference/codec.md +8 -8
- package/reference/combinators.md +345 -0
- package/reference/context.md +639 -67
- package/reference/docs.md +1 -1
- package/reference/dynamic-optic.md +5 -0
- package/reference/dynamic-schema.md +602 -0
- package/reference/dynamic-value.md +5 -0
- package/reference/http-model.md +1716 -0
- package/reference/json-differ.md +320 -0
- package/reference/json-patch.md +803 -0
- package/reference/json.md +1 -1
- package/reference/media-type.md +2 -2
- package/reference/patch.md +4 -0
- package/reference/resource-management-di/index.md +49 -0
- package/reference/resource-management-di/resource.md +1125 -0
- package/{scope.md → reference/resource-management-di/scope.md} +92 -10
- package/reference/resource-management-di/wire.md +832 -0
- package/reference/schema-evolution/as.md +587 -0
- package/reference/schema-evolution/index.md +50 -0
- package/reference/schema-evolution/into.md +1027 -0
- package/reference/schema-expr.md +2 -2
- package/reference/structural-types.md +369 -0
- package/reference/type-class-derivation.md +31 -31
- package/reference/xml.md +606 -45
- package/ringbuffer.md +249 -0
- package/sidebars.js +31 -1
- package/reference/schema-evolution.md +0 -540
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: wire
|
|
3
|
+
title: "Wire"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`Wire[-In, +Out]` is a **compile-time safe recipe for constructing a service and its dependencies**. Wires describe how to construct an `Out` value given access to its dependencies via a `Context[In]` and a `Scope` for finalization.
|
|
7
|
+
|
|
8
|
+
```scala
|
|
9
|
+
sealed trait Wire[-In, +Out] {
|
|
10
|
+
def isShared: Boolean
|
|
11
|
+
def isUnique: Boolean = !isShared
|
|
12
|
+
|
|
13
|
+
def shared: Wire.Shared[In, Out]
|
|
14
|
+
def unique: Wire.Unique[In, Out]
|
|
15
|
+
|
|
16
|
+
def toResource(deps: Context[In]): Resource[Out]
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The type parameters are:
|
|
21
|
+
- **`In` (contravariant)**: the dependencies required to construct the service
|
|
22
|
+
- **`Out` (covariant)**: the service produced
|
|
23
|
+
|
|
24
|
+
Wires are the building blocks of dependency injection in Scope. They form the foundation for constructor-based dependency injection via `Resource.from[T](wires*)`.
|
|
25
|
+
|
|
26
|
+
## Overview
|
|
27
|
+
|
|
28
|
+
A `Wire` is a **lazy recipe**, not an execution. It holds a construction function `(Scope, Context[In]) => Out` and a **sharing strategy** — either `Shared` (reference-counted via `Resource.shared`) or `Unique` (fresh instance per allocation). The Wire itself does nothing until you convert it to a `Resource` and allocate it within a scope.
|
|
29
|
+
|
|
30
|
+
Here's the typical flow:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
Wire (recipe)
|
|
34
|
+
↓
|
|
35
|
+
Resource (lazy, composable)
|
|
36
|
+
↓
|
|
37
|
+
scope.allocate(...)
|
|
38
|
+
↓
|
|
39
|
+
$[T] (scoped value in scope)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Motivation
|
|
43
|
+
|
|
44
|
+
Without Wires, building a multi-layer application requires manual dependency passing:
|
|
45
|
+
|
|
46
|
+
```scala
|
|
47
|
+
final case class Config(dbUrl: String)
|
|
48
|
+
|
|
49
|
+
final class Database(config: Config) extends AutoCloseable {
|
|
50
|
+
def close(): Unit = println(s"closing connection to ${config.dbUrl}")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
final class UserService(db: Database) {
|
|
54
|
+
def getUser(id: Int): String = s"user $id"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
final class App(service: UserService) {
|
|
58
|
+
def run(): Unit = println(service.getUser(1))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Manual wiring:
|
|
62
|
+
Scope.global.scoped { scope =>
|
|
63
|
+
import scope.*
|
|
64
|
+
val config = Config("jdbc:postgres://localhost/db")
|
|
65
|
+
val db = Resource.fromAutoCloseable(new Database(config)).allocate
|
|
66
|
+
val service = new UserService($(db)(identity))
|
|
67
|
+
val app = new App(service)
|
|
68
|
+
app.run()
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
With `Wire` + `Resource.from`, the macro handles the dependency graph:
|
|
73
|
+
|
|
74
|
+
```scala
|
|
75
|
+
Scope.global.scoped { scope =>
|
|
76
|
+
import scope.*
|
|
77
|
+
val app = Resource.from[App](
|
|
78
|
+
Wire(Config("jdbc:postgres://localhost/db"))
|
|
79
|
+
).allocate
|
|
80
|
+
$(app)(_.run())
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Benefits:**
|
|
85
|
+
- **Compile-time graph validation** — cycle detection, duplicate providers, missing dependencies
|
|
86
|
+
- **Automatic finalization** — `AutoCloseable` resources are finalized in LIFO order
|
|
87
|
+
- **Sharing control** — choose which services are singletons (shared) vs fresh per allocation (unique)
|
|
88
|
+
- **Type-safe construction** — no stringly-typed dependency resolution
|
|
89
|
+
|
|
90
|
+
## Installation
|
|
91
|
+
|
|
92
|
+
```scala
|
|
93
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-scope" % "<version>"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
For cross-platform (Scala.js):
|
|
97
|
+
|
|
98
|
+
```scala
|
|
99
|
+
libraryDependencies += "dev.zio" %%% "zio-blocks-scope" % "<version>"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Supported Scala versions: 2.13.x and 3.x.
|
|
103
|
+
|
|
104
|
+
## Construction
|
|
105
|
+
|
|
106
|
+
### Wire.shared[T] — derive a shared wire
|
|
107
|
+
|
|
108
|
+
The `Wire.shared[T]` macro inspects `T`'s primary constructor and generates a shared wire that reuses the same instance across dependents.
|
|
109
|
+
|
|
110
|
+
```scala
|
|
111
|
+
import zio.blocks.scope._
|
|
112
|
+
import zio.blocks.context.Context
|
|
113
|
+
|
|
114
|
+
final case class Config(debug: Boolean)
|
|
115
|
+
|
|
116
|
+
final class Database(config: Config) extends AutoCloseable {
|
|
117
|
+
def query(sql: String): String = s"[db] $sql"
|
|
118
|
+
def close(): Unit = println("database closed")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
val wire: Wire.Shared[Config, Database] = Wire.shared[Database]
|
|
122
|
+
|
|
123
|
+
Scope.global.scoped { scope =>
|
|
124
|
+
import scope.*
|
|
125
|
+
val config = Config(debug = true)
|
|
126
|
+
val deps = Context[Config](config)
|
|
127
|
+
val db = allocate(wire.toResource(deps))
|
|
128
|
+
$(db)(_.query("SELECT 1"))
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Wire.unique[T] — derive a unique wire
|
|
133
|
+
|
|
134
|
+
Like `shared[T]`, but creates a fresh instance each time the wire is used. Use for request-scoped or per-call services.
|
|
135
|
+
|
|
136
|
+
```scala
|
|
137
|
+
import zio.blocks.scope._
|
|
138
|
+
import zio.blocks.context.Context
|
|
139
|
+
|
|
140
|
+
final class RequestHandler {
|
|
141
|
+
val id = scala.util.Random.nextInt()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
val wire: Wire.Unique[Any, RequestHandler] = Wire.unique[RequestHandler]
|
|
145
|
+
|
|
146
|
+
Scope.global.scoped { scope =>
|
|
147
|
+
import scope.*
|
|
148
|
+
val deps = Context.empty[Any]
|
|
149
|
+
val resource = wire.toResource(deps)
|
|
150
|
+
|
|
151
|
+
val h1 = allocate(resource)
|
|
152
|
+
val h2 = allocate(resource)
|
|
153
|
+
|
|
154
|
+
val ids: (Int, Int) = (
|
|
155
|
+
$(h1)(_.id),
|
|
156
|
+
$(h2)(_.id)
|
|
157
|
+
)
|
|
158
|
+
// ids._1 != ids._2 (different instances)
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Wire.apply[T] — lift a pre-existing value
|
|
163
|
+
|
|
164
|
+
Creates a shared wire that injects a value you already have. If the value is `AutoCloseable`, its `close()` method is automatically registered as a finalizer.
|
|
165
|
+
|
|
166
|
+
```scala
|
|
167
|
+
import zio.blocks.scope._
|
|
168
|
+
|
|
169
|
+
final case class Config(dbUrl: String)
|
|
170
|
+
|
|
171
|
+
val config = Config("jdbc:postgres://localhost/db")
|
|
172
|
+
val wire: Wire.Shared[Any, Config] = Wire(config)
|
|
173
|
+
|
|
174
|
+
Scope.global.scoped { scope =>
|
|
175
|
+
import scope.*
|
|
176
|
+
val cfg = allocate(wire.toResource(Context.empty[Any]))
|
|
177
|
+
$(cfg)(_.dbUrl)
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Wire.Shared.fromFunction — manual shared wire construction
|
|
182
|
+
|
|
183
|
+
Use this for custom construction logic when macro derivation doesn't fit.
|
|
184
|
+
|
|
185
|
+
```scala
|
|
186
|
+
import zio.blocks.scope._
|
|
187
|
+
import zio.blocks.context.Context
|
|
188
|
+
|
|
189
|
+
final case class Config(timeout: Int)
|
|
190
|
+
|
|
191
|
+
final class Client(config: Config) {
|
|
192
|
+
def call(): String = s"calling with timeout=${config.timeout}"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
val wire: Wire.Shared[Config, Client] =
|
|
196
|
+
Wire.Shared.fromFunction { (scope, ctx) =>
|
|
197
|
+
val config = ctx.get[Config]
|
|
198
|
+
new Client(config)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
Scope.global.scoped { scope =>
|
|
202
|
+
import scope.*
|
|
203
|
+
val config = Config(30)
|
|
204
|
+
val deps = Context[Config](config)
|
|
205
|
+
val client = allocate(wire.toResource(deps))
|
|
206
|
+
$(client)(_.call())
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Wire.Unique.fromFunction — manual unique wire construction
|
|
211
|
+
|
|
212
|
+
Like `fromFunction`, but for unique wires.
|
|
213
|
+
|
|
214
|
+
```scala
|
|
215
|
+
import zio.blocks.scope._
|
|
216
|
+
import zio.blocks.context.Context
|
|
217
|
+
|
|
218
|
+
final class RequestContext {
|
|
219
|
+
val id = scala.util.Random.nextInt()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
val wire: Wire.Unique[Any, RequestContext] =
|
|
223
|
+
Wire.Unique.fromFunction { (_, _) =>
|
|
224
|
+
new RequestContext
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
Scope.global.scoped { scope =>
|
|
228
|
+
import scope.*
|
|
229
|
+
val deps = Context.empty[Any]
|
|
230
|
+
val resource = wire.toResource(deps)
|
|
231
|
+
|
|
232
|
+
val r1 = allocate(resource)
|
|
233
|
+
val r2 = allocate(resource)
|
|
234
|
+
|
|
235
|
+
val different: Boolean = $(r1)(_.id) != $(r2)(_.id)
|
|
236
|
+
// different == true
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Shared vs Unique
|
|
241
|
+
|
|
242
|
+
The fundamental difference is **reuse semantics**:
|
|
243
|
+
|
|
244
|
+
| Aspect | Shared | Unique |
|
|
245
|
+
|--------|--------|--------|
|
|
246
|
+
| **Construction** | `Wire.shared[T]` macro or `Wire.Shared.fromFunction` | `Wire.unique[T]` macro or `Wire.Unique.fromFunction` |
|
|
247
|
+
| **Resource type** | `Resource.shared[T]` (reference-counted) | `Resource.unique[T]` (fresh per call) |
|
|
248
|
+
| **When to use** | Singletons, expensive resources (connections, thread pools) | Request scoped, stateful per-call (request handlers) |
|
|
249
|
+
| **Instance reuse** | Same instance across entire dependency graph | New instance per allocation |
|
|
250
|
+
| **Finalization** | Runs when last referencing scope closes | Runs when each scope closes |
|
|
251
|
+
|
|
252
|
+
In the diamond pattern (where `App` depends on both `UserService` and `OrderService`, both of which depend on `Database`), a shared wire ensures `Database` is constructed once and both services receive the same instance.
|
|
253
|
+
|
|
254
|
+
```scala
|
|
255
|
+
import zio.blocks.scope._
|
|
256
|
+
|
|
257
|
+
final class Database {
|
|
258
|
+
val id = scala.util.Random.nextInt()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
final class UserService(db: Database) {
|
|
262
|
+
def getDbId(): Int = db.id
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
final class OrderService(db: Database) {
|
|
266
|
+
def getDbId(): Int = db.id
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
final class App(userService: UserService, orderService: OrderService) {
|
|
270
|
+
def check(): Boolean = {
|
|
271
|
+
// With shared Database, these should be equal
|
|
272
|
+
userService.getDbId() == orderService.getDbId()
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Using shared wires for all dependencies
|
|
277
|
+
val resource = Resource.from[App](
|
|
278
|
+
Wire.shared[Database],
|
|
279
|
+
Wire.shared[UserService],
|
|
280
|
+
Wire.shared[OrderService]
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
Scope.global.scoped { scope =>
|
|
284
|
+
import scope.*
|
|
285
|
+
val app = resource.allocate
|
|
286
|
+
$(app)(_.check()) // true: Database is shared
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Core Operations
|
|
291
|
+
|
|
292
|
+
### `Wire#isShared` and `Wire#isUnique`
|
|
293
|
+
|
|
294
|
+
Check the sharing strategy of a wire:
|
|
295
|
+
|
|
296
|
+
```scala
|
|
297
|
+
import zio.blocks.scope._
|
|
298
|
+
|
|
299
|
+
val sharedWire = Wire.shared[String]
|
|
300
|
+
val uniqueWire = Wire.unique[String]
|
|
301
|
+
|
|
302
|
+
println(s"sharedWire.isShared: ${sharedWire.isShared}") // true
|
|
303
|
+
println(s"sharedWire.isUnique: ${sharedWire.isUnique}") // false
|
|
304
|
+
println(s"uniqueWire.isShared: ${uniqueWire.isShared}") // false
|
|
305
|
+
println(s"uniqueWire.isUnique: ${uniqueWire.isUnique}") // true
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### `Wire#shared` and `Wire#unique` — convert between strategies
|
|
309
|
+
|
|
310
|
+
Convert a wire to the opposite strategy:
|
|
311
|
+
|
|
312
|
+
```scala
|
|
313
|
+
import zio.blocks.scope._
|
|
314
|
+
|
|
315
|
+
val original = Wire.shared[String]
|
|
316
|
+
|
|
317
|
+
val converted: Wire.Unique[Any, String] = original.unique
|
|
318
|
+
println(s"original.isShared: ${original.isShared}") // true
|
|
319
|
+
println(s"converted.isUnique: ${converted.isUnique}") // true
|
|
320
|
+
|
|
321
|
+
// Converting back returns a new shared wire
|
|
322
|
+
val backToShared: Wire.Shared[Any, String] = converted.shared
|
|
323
|
+
println(s"backToShared.isShared: ${backToShared.isShared}") // true
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Calling `shared` on an already-shared wire returns `this` (identity); likewise `unique` on a unique wire returns `this`.
|
|
327
|
+
|
|
328
|
+
### `Wire#toResource` — convert a wire to a Resource
|
|
329
|
+
|
|
330
|
+
Converts the wire to a lazy `Resource` by providing the dependency context:
|
|
331
|
+
|
|
332
|
+
```scala
|
|
333
|
+
import zio.blocks.scope._
|
|
334
|
+
import zio.blocks.context.Context
|
|
335
|
+
|
|
336
|
+
final case class Config(value: String)
|
|
337
|
+
|
|
338
|
+
val wire = Wire.shared[Config]
|
|
339
|
+
val deps = Context[Config](Config("hello"))
|
|
340
|
+
|
|
341
|
+
val resource: Resource[Config] = wire.toResource(deps)
|
|
342
|
+
|
|
343
|
+
Scope.global.scoped { scope =>
|
|
344
|
+
import scope.*
|
|
345
|
+
val cfg = allocate(resource)
|
|
346
|
+
$(cfg)(_.value)
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### `Wire#make` — construct directly from a wire
|
|
351
|
+
|
|
352
|
+
Directly construct a value without going through `Resource.toResource`. This is a low-level operation; prefer `allocate(wire.toResource(...))` for safety.
|
|
353
|
+
|
|
354
|
+
```scala
|
|
355
|
+
import zio.blocks.scope._
|
|
356
|
+
import zio.blocks.context.Context
|
|
357
|
+
|
|
358
|
+
final class Service {
|
|
359
|
+
def getName: String = "service"
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
val wire = Wire.shared[Service]
|
|
363
|
+
|
|
364
|
+
Scope.global.scoped { scope =>
|
|
365
|
+
import scope.*
|
|
366
|
+
val service = wire.asInstanceOf[Wire.Shared[Any, Service]].make(scope, Context.empty[Any])
|
|
367
|
+
println(service.getName)
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## Macro Derivation
|
|
372
|
+
|
|
373
|
+
When you call `Wire.shared[T]` or `Wire.unique[T]`, the macro performs these checks:
|
|
374
|
+
|
|
375
|
+
1. **Is `T` a class?** — traits and abstract classes are rejected; only concrete classes can be auto-wired.
|
|
376
|
+
2. **Does `T` have a primary constructor?** — the macro inspects constructor parameters to determine dependencies.
|
|
377
|
+
3. **Is each parameter either a dependency type or a special injected type?** — parameters of type `Scope` or `Finalizer` are recognized and injected automatically; others are looked up in the context.
|
|
378
|
+
4. **Does `T` extend `AutoCloseable`?** — if yes, `close()` is automatically registered as a finalizer in the scope.
|
|
379
|
+
|
|
380
|
+
Example with all three features:
|
|
381
|
+
|
|
382
|
+
```scala
|
|
383
|
+
import zio.blocks.scope._
|
|
384
|
+
|
|
385
|
+
final case class Config(dbUrl: String)
|
|
386
|
+
|
|
387
|
+
final class Logger(using Finalizer) {
|
|
388
|
+
def log(msg: String): Unit = println(msg)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
final class Database(config: Config)(using scope: Scope) extends AutoCloseable {
|
|
392
|
+
def connect(): Unit = {
|
|
393
|
+
scope.defer(println("database connection closed"))
|
|
394
|
+
println(s"connecting to ${config.dbUrl}")
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
def query(sql: String): String = s"result: $sql"
|
|
398
|
+
|
|
399
|
+
def close(): Unit = println("database closed")
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
final class Service(db: Database, logger: Logger) {
|
|
403
|
+
def run(): Unit = {
|
|
404
|
+
logger.log(db.query("SELECT 1"))
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Macro handles Finalizer injection, Scope injection, and AutoCloseable registration
|
|
409
|
+
val wire = Wire.shared[Service]
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### What happens with subtype conflicts
|
|
413
|
+
|
|
414
|
+
If a constructor has dependencies of related types (e.g., both `FileInputStream` and `InputStream`), the macro rejects the wire because `Context` is type-indexed and cannot reliably disambiguate.
|
|
415
|
+
|
|
416
|
+
```scala
|
|
417
|
+
// This will NOT compile
|
|
418
|
+
final class App(input: InputStream, fileInput: FileInputStream)
|
|
419
|
+
val wire = Wire.shared[App] // error: subtype conflict
|
|
420
|
+
|
|
421
|
+
// Fix: wrap one type to make it distinct
|
|
422
|
+
final case class FileInputWrapper(value: FileInputStream)
|
|
423
|
+
final class App(input: InputStream, fileInput: FileInputWrapper)
|
|
424
|
+
val wire = Wire.shared[App] // ok
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Integration with Resource.from
|
|
428
|
+
|
|
429
|
+
`Wire` is designed for use with `Resource.from[T](wires*)`, which performs whole-graph dependency injection:
|
|
430
|
+
|
|
431
|
+
```scala
|
|
432
|
+
import zio.blocks.scope._
|
|
433
|
+
|
|
434
|
+
final case class AppConfig(dbUrl: String)
|
|
435
|
+
|
|
436
|
+
final class Database(config: AppConfig) extends AutoCloseable {
|
|
437
|
+
def query(sql: String): String = s"result: $sql"
|
|
438
|
+
def close(): Unit = ()
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
final class Repository(db: Database) {
|
|
442
|
+
def query(): String = db.query("SELECT *")
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
final class Service(repo: Repository) extends AutoCloseable {
|
|
446
|
+
def run(): String = repo.query()
|
|
447
|
+
def close(): Unit = ()
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
final class App(service: Service) {
|
|
451
|
+
def run(): String = service.run()
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Provide only the leaf dependency; Resource.from derives the rest
|
|
455
|
+
val appResource: Resource[App] = Resource.from[App](
|
|
456
|
+
Wire(AppConfig("jdbc:postgres://localhost/db"))
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
Scope.global.scoped { scope =>
|
|
460
|
+
import scope._
|
|
461
|
+
val app = allocate(appResource)
|
|
462
|
+
$(app)(_.run())
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
When `Resource.from` composes wires, it respects the sharing strategy:
|
|
467
|
+
- **Shared wires** → reference-counted (single instance in the graph)
|
|
468
|
+
- **Unique wires** → fresh per allocation
|
|
469
|
+
|
|
470
|
+
The macro detects cycles, duplicate providers, and missing dependencies at compile time.
|
|
471
|
+
|
|
472
|
+
## Comparison with Alternatives
|
|
473
|
+
|
|
474
|
+
| Feature | Wire | Manual Passing | Service Locator |
|
|
475
|
+
|---------|------|----------------|-----------------|
|
|
476
|
+
| **Type safety** | ✓ (compile-time validation) | ✓ (implicit) | ✗ (string keys) |
|
|
477
|
+
| **Cycle detection** | ✓ (compile time) | ✗ | ✗ (runtime) |
|
|
478
|
+
| **Sharing semantics** | ✓ (configurable) | Manual | ✓ (singleton pattern) |
|
|
479
|
+
| **Finalization** | ✓ (LIFO, automatic) | Manual | Manual |
|
|
480
|
+
| **Performance** | ~0 overhead (macro-generated) | ~0 overhead | ~1 allocation overhead |
|
|
481
|
+
|
|
482
|
+
## Running the Examples
|
|
483
|
+
|
|
484
|
+
All code from this guide is available as runnable examples in the `scope-examples` module.
|
|
485
|
+
|
|
486
|
+
**1. Clone the repository and navigate to the project:**
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
git clone https://github.com/zio/zio-blocks.git
|
|
490
|
+
cd zio-blocks
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
**2. Run individual examples with sbt:**
|
|
494
|
+
|
|
495
|
+
**Basic wire construction: deriving shared wires, lifting values, and converting to resources**
|
|
496
|
+
|
|
497
|
+
```scala title="scope-examples/src/main/scala/wire/WireBasicExample.scala"
|
|
498
|
+
package wire
|
|
499
|
+
|
|
500
|
+
import zio.blocks.scope._
|
|
501
|
+
import zio.blocks.context.Context
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Demonstrates basic Wire construction patterns:
|
|
505
|
+
* - Wire.shared[T] macro derivation
|
|
506
|
+
* - Wire.apply(value) to lift a value
|
|
507
|
+
* - Converting wires to Resources
|
|
508
|
+
* - Using wires in a dependency graph
|
|
509
|
+
*/
|
|
510
|
+
|
|
511
|
+
final case class DbConfig(host: String, port: Int) {
|
|
512
|
+
def url: String = s"jdbc:postgresql://$host:$port/db"
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
final class Database(config: DbConfig) extends AutoCloseable {
|
|
516
|
+
println(s"[Database] Connecting to ${config.url}")
|
|
517
|
+
|
|
518
|
+
def query(sql: String): String = {
|
|
519
|
+
println(s"[Database] Executing: $sql")
|
|
520
|
+
"result"
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
def close(): Unit =
|
|
524
|
+
println("[Database] Connection closed")
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
final class UserService(db: Database) {
|
|
528
|
+
println("[UserService] Initialized")
|
|
529
|
+
|
|
530
|
+
def getUser(id: Int): String = {
|
|
531
|
+
db.query(s"SELECT * FROM users WHERE id = $id")
|
|
532
|
+
s"User(id=$id, name=Alice)"
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
final class BasicApp(service: UserService) {
|
|
537
|
+
def run(): Unit = {
|
|
538
|
+
val user = service.getUser(1)
|
|
539
|
+
println(s"[App] Got: $user")
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
@main def wireBasicExample(): Unit = {
|
|
544
|
+
println("=== Wire Basic Construction Example ===\n")
|
|
545
|
+
|
|
546
|
+
// Create the dependency leaf (config) using Wire.apply
|
|
547
|
+
val configWire: Wire.Shared[Any, DbConfig] =
|
|
548
|
+
Wire(DbConfig("localhost", 5432))
|
|
549
|
+
|
|
550
|
+
// Derive wires for Database and UserService using the macro
|
|
551
|
+
val dbWire: Wire.Shared[DbConfig, Database] =
|
|
552
|
+
Wire.shared[Database]
|
|
553
|
+
|
|
554
|
+
val serviceWire: Wire.Shared[Database, UserService] =
|
|
555
|
+
Wire.shared[UserService]
|
|
556
|
+
|
|
557
|
+
val appWire: Wire.Shared[UserService, BasicApp] =
|
|
558
|
+
Wire.shared[BasicApp]
|
|
559
|
+
|
|
560
|
+
println("[Setup] Created all wires\n")
|
|
561
|
+
|
|
562
|
+
// Use Resource.from to automatically compose the dependency graph
|
|
563
|
+
val appResource: Resource[BasicApp] = Resource.from[BasicApp](
|
|
564
|
+
configWire,
|
|
565
|
+
dbWire,
|
|
566
|
+
serviceWire,
|
|
567
|
+
appWire
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
println("[Setup] Composed resource graph\n")
|
|
571
|
+
|
|
572
|
+
// Allocate within a scope
|
|
573
|
+
Scope.global.scoped { scope =>
|
|
574
|
+
import scope._
|
|
575
|
+
|
|
576
|
+
println("[Scope] Entering scoped region\n")
|
|
577
|
+
|
|
578
|
+
val app: $[BasicApp] = allocate(appResource)
|
|
579
|
+
|
|
580
|
+
println("\n[App] Running application")
|
|
581
|
+
$(app)(_.run())
|
|
582
|
+
|
|
583
|
+
println("\n[Scope] Exiting scoped region - finalizers will run")
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
println("\n=== Example Complete ===")
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/wire/WireBasicExample.scala))
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
sbt "scope-examples/runMain wire.WireBasicExample"
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
**Comparing shared vs unique semantics: shared wires reuse the same instance across dependents, while unique wires create fresh instances**
|
|
597
|
+
|
|
598
|
+
```scala title="scope-examples/src/main/scala/wire/WireSharedUniqueExample.scala"
|
|
599
|
+
package wire
|
|
600
|
+
|
|
601
|
+
import zio.blocks.scope._
|
|
602
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Demonstrates the semantic difference between shared and unique wires:
|
|
606
|
+
* - Shared wires: same instance across dependents (reference-counted)
|
|
607
|
+
* - Unique wires: fresh instance per allocation
|
|
608
|
+
*
|
|
609
|
+
* Uses a counter to track how many times each service is instantiated.
|
|
610
|
+
*/
|
|
611
|
+
|
|
612
|
+
final class Counter {
|
|
613
|
+
private val count = new AtomicInteger(0)
|
|
614
|
+
|
|
615
|
+
def next(): Int = count.incrementAndGet()
|
|
616
|
+
|
|
617
|
+
def value: Int = count.get()
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
final class ServiceA(counter: Counter) {
|
|
621
|
+
val id = counter.next()
|
|
622
|
+
println(s"[ServiceA] Initialized with counter id=$id")
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
final class ServiceB(counter: Counter) {
|
|
626
|
+
val id = counter.next()
|
|
627
|
+
println(s"[ServiceB] Initialized with counter id=$id")
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
final class SharedDependencyApp(a: ServiceA, b: ServiceB) {
|
|
631
|
+
def checkSharing(): Boolean = {
|
|
632
|
+
// If Counter is shared, both services got the same instance
|
|
633
|
+
val aCountId = a.id
|
|
634
|
+
val bCountId = b.id
|
|
635
|
+
println(s"[SharedDependencyApp] ServiceA counter id=$aCountId, ServiceB counter id=$bCountId")
|
|
636
|
+
aCountId != bCountId && a.id < b.id // Sequential IDs from same Counter
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
final class UniqueDependencyApp(a: ServiceA, b: ServiceB) {
|
|
641
|
+
def checkUniqueness(): Boolean = {
|
|
642
|
+
// If Counter is unique, services got different instances (different starting IDs)
|
|
643
|
+
val aCountId = a.id
|
|
644
|
+
val bCountId = b.id
|
|
645
|
+
println(s"[UniqueDependencyApp] ServiceA counter id=$aCountId, ServiceB counter id=$bCountId")
|
|
646
|
+
aCountId != bCountId // Different Counter instances
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
@main def wireSharedUniqueExample(): Unit = {
|
|
651
|
+
println("=== Wire Shared vs Unique Example ===\n")
|
|
652
|
+
|
|
653
|
+
println("--- Test 1: Shared Counter (diamond pattern) ---\n")
|
|
654
|
+
|
|
655
|
+
// Using a SHARED Counter: both ServiceA and ServiceB share the same Counter instance
|
|
656
|
+
val sharedResource: Resource[SharedDependencyApp] = Resource.from[SharedDependencyApp](
|
|
657
|
+
Wire.shared[Counter] // Shared: one instance across the graph
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
Scope.global.scoped { scope =>
|
|
661
|
+
import scope._
|
|
662
|
+
|
|
663
|
+
println("[Scope] Entering scoped region\n")
|
|
664
|
+
|
|
665
|
+
val app = allocate(sharedResource)
|
|
666
|
+
val isShared = $(app)(_.checkSharing())
|
|
667
|
+
|
|
668
|
+
println(s"\n[Result] Counter was shared: $isShared\n")
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
println("\n--- Test 2: Unique Counter ---\n")
|
|
672
|
+
|
|
673
|
+
// Using a UNIQUE Counter: each dependency gets a fresh Counter instance
|
|
674
|
+
val uniqueResource: Resource[UniqueDependencyApp] = Resource.from[UniqueDependencyApp](
|
|
675
|
+
Wire.unique[Counter] // Unique: fresh instance per dependency
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
Scope.global.scoped { scope =>
|
|
679
|
+
import scope._
|
|
680
|
+
|
|
681
|
+
println("[Scope] Entering scoped region\n")
|
|
682
|
+
|
|
683
|
+
val app = allocate(uniqueResource)
|
|
684
|
+
val isUnique = $(app)(_.checkUniqueness())
|
|
685
|
+
|
|
686
|
+
println(s"\n[Result] Counters were unique: $isUnique\n")
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
println("\n=== Example Complete ===")
|
|
690
|
+
}
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/wire/WireSharedUniqueExample.scala))
|
|
694
|
+
|
|
695
|
+
```bash
|
|
696
|
+
sbt "scope-examples/runMain wire.WireSharedUniqueExample"
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
**Manual wire construction: using fromFunction for custom construction logic**
|
|
700
|
+
|
|
701
|
+
```scala title="scope-examples/src/main/scala/wire/WireFromFunctionExample.scala"
|
|
702
|
+
package wire
|
|
703
|
+
|
|
704
|
+
import zio.blocks.scope._
|
|
705
|
+
import zio.blocks.context.Context
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Demonstrates manual wire construction using fromFunction:
|
|
709
|
+
* - Wire.Shared.fromFunction for custom shared wire logic
|
|
710
|
+
* - Wire.Unique.fromFunction for custom unique wire logic
|
|
711
|
+
* - Using manual wires when macro derivation doesn't fit
|
|
712
|
+
*
|
|
713
|
+
* This is useful for complex initialization, conditional logic, or when you
|
|
714
|
+
* need control over which dependencies to use.
|
|
715
|
+
*/
|
|
716
|
+
|
|
717
|
+
final case class ApiKey(value: String)
|
|
718
|
+
|
|
719
|
+
final case class HttpConfig(baseUrl: String, timeout: Int)
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* A custom HTTP client that uses an API key and timeout from config.
|
|
723
|
+
* Demonstrates a scenario where we want custom construction logic rather than
|
|
724
|
+
* simple parameter passing.
|
|
725
|
+
*/
|
|
726
|
+
final class HttpClient(config: HttpConfig, apiKey: ApiKey) extends AutoCloseable {
|
|
727
|
+
println(
|
|
728
|
+
s"[HttpClient] Created with baseUrl=${config.baseUrl}, timeout=${config.timeout}ms, apiKey=${apiKey.value}"
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
def get(path: String): String = {
|
|
732
|
+
println(s"[HttpClient] GET $path with api key")
|
|
733
|
+
"response"
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
def close(): Unit =
|
|
737
|
+
println("[HttpClient] Connection pool closed")
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* A custom authenticator that needs the API key. Demonstrates context
|
|
742
|
+
* extraction in a manual wire.
|
|
743
|
+
*/
|
|
744
|
+
final class Authenticator(apiKey: ApiKey) {
|
|
745
|
+
println(s"[Authenticator] Using API key: ${apiKey.value}")
|
|
746
|
+
|
|
747
|
+
def authenticate(): Boolean = {
|
|
748
|
+
println("[Authenticator] Validating API key...")
|
|
749
|
+
true
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
final class ManualWireApp(client: HttpClient, auth: Authenticator) {
|
|
754
|
+
def run(): Unit =
|
|
755
|
+
if (auth.authenticate()) {
|
|
756
|
+
val response = client.get("/api/users")
|
|
757
|
+
println(s"[App] Got response: $response")
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
@main def wireFromFunctionExample(): Unit = {
|
|
762
|
+
println("=== Wire fromFunction (Manual Construction) Example ===\n")
|
|
763
|
+
|
|
764
|
+
// Manually create wires using fromFunction for custom logic
|
|
765
|
+
// This gives full control when macro derivation isn't suitable
|
|
766
|
+
|
|
767
|
+
val httpClientWire: Wire.Shared[HttpConfig & ApiKey, HttpClient] =
|
|
768
|
+
Wire.Shared.fromFunction { (scope, ctx) =>
|
|
769
|
+
// Extract both dependencies from context
|
|
770
|
+
val config = ctx.get[HttpConfig]
|
|
771
|
+
val apiKey = ctx.get[ApiKey]
|
|
772
|
+
|
|
773
|
+
// Custom initialization logic
|
|
774
|
+
println("[Manual] Custom HttpClient construction")
|
|
775
|
+
val client = new HttpClient(config, apiKey)
|
|
776
|
+
|
|
777
|
+
// Register custom cleanup if needed (in addition to AutoCloseable)
|
|
778
|
+
scope.defer(println("[Manual] HttpClient cleanup deferred"))
|
|
779
|
+
|
|
780
|
+
client
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
val authenticatorWire: Wire.Shared[ApiKey, Authenticator] =
|
|
784
|
+
Wire.Shared.fromFunction { (scope, ctx) =>
|
|
785
|
+
val apiKey = ctx.get[ApiKey]
|
|
786
|
+
|
|
787
|
+
// Custom initialization logic
|
|
788
|
+
println("[Manual] Custom Authenticator construction")
|
|
789
|
+
val auth = new Authenticator(apiKey)
|
|
790
|
+
|
|
791
|
+
scope.defer(println("[Manual] Authenticator cleanup deferred"))
|
|
792
|
+
|
|
793
|
+
auth
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Provide leaf dependencies
|
|
797
|
+
val configWire = Wire(HttpConfig("https://api.example.com", 30000))
|
|
798
|
+
val apiKeyWire = Wire(ApiKey("secret-key-12345"))
|
|
799
|
+
|
|
800
|
+
// Compose into the app resource
|
|
801
|
+
val appResource: Resource[ManualWireApp] = Resource.from[ManualWireApp](
|
|
802
|
+
configWire,
|
|
803
|
+
apiKeyWire,
|
|
804
|
+
httpClientWire,
|
|
805
|
+
authenticatorWire
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
println("[Setup] Created manual wires\n")
|
|
809
|
+
|
|
810
|
+
// Allocate and run
|
|
811
|
+
Scope.global.scoped { scope =>
|
|
812
|
+
import scope._
|
|
813
|
+
|
|
814
|
+
println("[Scope] Entering scoped region\n")
|
|
815
|
+
|
|
816
|
+
val app = allocate(appResource)
|
|
817
|
+
|
|
818
|
+
println("\n[App] Running application")
|
|
819
|
+
$(app)(_.run())
|
|
820
|
+
|
|
821
|
+
println("\n[Scope] Exiting scoped region - finalizers will run")
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
println("\n=== Example Complete ===")
|
|
825
|
+
}
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/wire/WireFromFunctionExample.scala))
|
|
829
|
+
|
|
830
|
+
```bash
|
|
831
|
+
sbt "scope-examples/runMain wire.WireFromFunctionExample"
|
|
832
|
+
```
|