@zio.dev/zio-blocks 0.0.28 → 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.
@@ -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
+ ```