@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.
- 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 +62 -16
- package/package.json +1 -1
- package/path-interpolator.md +70 -9
- package/reference/allows.md +96 -0
- 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/http-model.md +1716 -0
- package/reference/json-differ.md +320 -0
- package/reference/json-patch.md +1 -1
- package/reference/media-type.md +2 -2
- 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} +1 -1
- package/reference/resource-management-di/wire.md +832 -0
- package/reference/schema-evolution/as.md +4 -4
- package/reference/schema-evolution/into.md +2 -2
- package/reference/schema-expr.md +2 -2
- package/reference/type-class-derivation.md +31 -31
- package/ringbuffer.md +249 -0
- package/sidebars.js +14 -2
package/reference/context.md
CHANGED
|
@@ -3,155 +3,727 @@ id: context
|
|
|
3
3
|
title: "Context"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
`Context[+R]` is a type-indexed heterogeneous collection
|
|
6
|
+
`Context[+R]` is a type-indexed heterogeneous collection that stores values of different types, indexed by their types, with compile-time type safety for lookups. It provides an immutable, cache-aware dependency container where the phantom type `R` (using intersection types) tracks which types are present.
|
|
7
|
+
|
|
8
|
+
The core type looks like this:
|
|
9
|
+
|
|
10
|
+
```scala
|
|
11
|
+
// Signature (showing public API structure, not actual implementation)
|
|
12
|
+
final class Context[+R] {
|
|
13
|
+
def size: Int
|
|
14
|
+
def isEmpty: Boolean
|
|
15
|
+
def nonEmpty: Boolean
|
|
16
|
+
def get[A >: R](implicit ev: IsNominalType[A]): A
|
|
17
|
+
def getOption[A](implicit ev: IsNominalType[A]): Option[A]
|
|
18
|
+
def add[A](a: A)(implicit ev: IsNominalType[A]): Context[R & A]
|
|
19
|
+
def update[A >: R](f: A => A)(implicit ev: IsNominalType[A]): Context[R]
|
|
20
|
+
def ++[R1](that: Context[R1]): Context[R & R1]
|
|
21
|
+
def prune[A >: R](implicit ev: IsNominalIntersection[A]): Context[A]
|
|
22
|
+
override def toString: String
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Key properties: covariant (`+R`), immutable, cached for repeated lookups, supports only nominal types.
|
|
7
27
|
|
|
8
28
|
## Overview
|
|
9
29
|
|
|
30
|
+
Context serves as a type-safe registry for heterogeneous dependencies. Here's a quick example:
|
|
31
|
+
|
|
10
32
|
```scala
|
|
11
33
|
import zio.blocks.context._
|
|
12
34
|
|
|
13
35
|
case class Config(debug: Boolean)
|
|
36
|
+
case class Logger(name: String)
|
|
14
37
|
case class Metrics(count: Int)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
We can create and retrieve values by type:
|
|
15
41
|
|
|
16
|
-
|
|
17
|
-
val ctx: Context[Config & Metrics] = Context(
|
|
42
|
+
```scala
|
|
43
|
+
val ctx: Context[Config & Logger & Metrics] = Context(
|
|
18
44
|
Config(debug = true),
|
|
45
|
+
Logger("app"),
|
|
19
46
|
Metrics(count = 42)
|
|
20
47
|
)
|
|
48
|
+
// ctx: Context[Config & Logger & Metrics] = Context(repl.MdocSession.MdocApp.Metrics -> Metrics(42), repl.MdocSession.MdocApp.Logger -> Logger(app), repl.MdocSession.MdocApp.Config -> Config(true))
|
|
21
49
|
|
|
22
|
-
// Retrieve values by type
|
|
23
50
|
val config: Config = ctx.get[Config]
|
|
24
|
-
|
|
51
|
+
// config: Config = Config(true)
|
|
52
|
+
val logger: Logger = ctx.get[Logger]
|
|
53
|
+
// logger: Logger = Logger("app")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This ASCII diagram shows how Context maps types to values:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
┌─────────────────────────────────┐
|
|
60
|
+
│ Context[R] │
|
|
61
|
+
│ (Type-Indexed Store) │
|
|
62
|
+
├─────────────────────────────────┤
|
|
63
|
+
│ Config → Config(true) │
|
|
64
|
+
│ Logger → Logger("app") │
|
|
65
|
+
│ Metrics → Metrics(42) │
|
|
66
|
+
├─────────────────────────────────┤
|
|
67
|
+
│ ✓ Type-safe retrieval │
|
|
68
|
+
│ ✓ No casting │
|
|
69
|
+
│ ✓ Cache-aware (O(1) repeats) │
|
|
70
|
+
└─────────────────────────────────┘
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Motivation
|
|
74
|
+
|
|
75
|
+
When building modular applications, we often need to pass multiple dependencies around—a database connection, a config object, a logger, and so on. Existing approaches each have limitations:
|
|
76
|
+
|
|
77
|
+
**`Map[Class[_], Any]`** — no compile-time safety. You must cast the result and remember which keys you registered:
|
|
78
|
+
|
|
79
|
+
```scala
|
|
80
|
+
// Unsafe approach — easy to make mistakes
|
|
81
|
+
val deps = scala.collection.mutable.Map[Class[_], Any]()
|
|
82
|
+
deps(classOf[Config]) = Config(debug = true)
|
|
83
|
+
deps(classOf[Logger]) = Logger("app")
|
|
84
|
+
|
|
85
|
+
val config = deps(classOf[Config]).asInstanceOf[Config] // Manual cast
|
|
86
|
+
val db = deps(classOf[Database]) // Runtime error if missing
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**ZIO's `ZEnvironment`** — type-safe but requires the full ZIO effect system:
|
|
90
|
+
|
|
91
|
+
```scala
|
|
92
|
+
// Requires ZIO context
|
|
93
|
+
import zio._
|
|
94
|
+
|
|
95
|
+
val makeEnv = for {
|
|
96
|
+
config <- ZIO.service[Config]
|
|
97
|
+
logger <- ZIO.service[Logger]
|
|
98
|
+
} yield (config, logger)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Context** — combines compile-time type safety with synchronous, pure code:
|
|
102
|
+
|
|
103
|
+
```scala
|
|
104
|
+
import zio.blocks.context._
|
|
105
|
+
|
|
106
|
+
case class Config(debug: Boolean)
|
|
107
|
+
case class Logger(name: String)
|
|
108
|
+
|
|
109
|
+
// Type-safe, no effects needed
|
|
110
|
+
val ctx = Context(Config(true), Logger("app"))
|
|
111
|
+
val config = ctx.get[Config] // Compile-time proof it exists
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Installation
|
|
115
|
+
|
|
116
|
+
Add the ZIO Blocks Context module to your `build.sbt`:
|
|
117
|
+
|
|
118
|
+
```scala
|
|
119
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-context" % "0.0.29"
|
|
25
120
|
```
|
|
26
121
|
|
|
27
122
|
## Construction
|
|
28
123
|
|
|
29
|
-
|
|
124
|
+
Context provides several ways to create instances. Choose the approach that best fits your use case: start empty and add values incrementally, or construct a fully-populated context directly with `apply`.
|
|
125
|
+
|
|
126
|
+
### Creating Empty Contexts
|
|
127
|
+
|
|
128
|
+
Use `Context.empty` to create an empty context with no entries:
|
|
129
|
+
|
|
130
|
+
```scala
|
|
131
|
+
import zio.blocks.context._
|
|
132
|
+
|
|
133
|
+
val emptyCtx: Context[Any] = Context.empty
|
|
134
|
+
// emptyCtx: Context[Any] = Context()
|
|
135
|
+
val isEmpty = emptyCtx.isEmpty
|
|
136
|
+
// isEmpty: Boolean = true
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
An empty context has type `Context[Any]` and represents no stored dependencies. This is a useful starting point for incremental construction.
|
|
140
|
+
|
|
141
|
+
### Creating Multi-Value Contexts with `Context.apply`
|
|
142
|
+
|
|
143
|
+
`Context.apply` is overloaded to accept 1–10 values and returns a context with type `Context[A1 & A2 & ...]`, reflecting all stored types.
|
|
144
|
+
|
|
145
|
+
#### Single Value
|
|
146
|
+
|
|
147
|
+
Create a context with one value:
|
|
148
|
+
|
|
149
|
+
```scala
|
|
150
|
+
case class Config(debug: Boolean)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
With `Config` defined, we can create a single-value context:
|
|
30
154
|
|
|
31
155
|
```scala
|
|
32
|
-
val
|
|
33
|
-
|
|
34
|
-
val ctx3 = Context(v1, v2, v3, v4, v5) // Context[T1 & T2 & T3 & T4 & T5]
|
|
156
|
+
val single: Context[Config] = Context(Config(debug = true))
|
|
157
|
+
// single: Context[Config] = Context(repl.MdocSession.MdocApp1.Config -> Config(true))
|
|
35
158
|
```
|
|
36
159
|
|
|
37
|
-
|
|
160
|
+
#### Multiple Values
|
|
161
|
+
|
|
162
|
+
Create a context with multiple values—the type parameter automatically becomes an intersection of all stored types:
|
|
163
|
+
|
|
164
|
+
```scala
|
|
165
|
+
case class Logger(name: String)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
With `Config` and `Logger` in scope, we can create a multi-value context:
|
|
169
|
+
|
|
170
|
+
```scala
|
|
171
|
+
val multi: Context[Config & Logger] = Context(
|
|
172
|
+
Config(debug = true),
|
|
173
|
+
Logger("myapp")
|
|
174
|
+
)
|
|
175
|
+
// multi: Context[Config & Logger] = Context(repl.MdocSession.MdocApp1.Logger -> Logger(myapp), repl.MdocSession.MdocApp1.Config -> Config(true))
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Building Incrementally with `Context#add`
|
|
179
|
+
|
|
180
|
+
For contexts that grow over time, use `Context#add` to build incrementally from an empty context. This is useful when dependencies become available at different points in your initialization:
|
|
38
181
|
|
|
39
182
|
```scala
|
|
40
183
|
val ctx = Context.empty
|
|
41
|
-
.add(Config(debug =
|
|
42
|
-
.add(
|
|
43
|
-
//
|
|
184
|
+
.add(Config(debug = false))
|
|
185
|
+
.add(Logger("init"))
|
|
186
|
+
// ctx: Context[Config & Logger] = Context(repl.MdocSession.MdocApp1.Logger -> Logger(init), repl.MdocSession.MdocApp1.Config -> Config(false))
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The context accumulates all added entries:
|
|
190
|
+
|
|
191
|
+
```scala
|
|
192
|
+
val size1 = ctx.size
|
|
193
|
+
// size1: Int = 2
|
|
44
194
|
```
|
|
45
195
|
|
|
46
|
-
|
|
196
|
+
**When to use `Context#add` vs. `Context.apply`:**
|
|
197
|
+
- Use `Context.apply` when you know all dependencies upfront and can construct them together
|
|
198
|
+
- Use `Context#add` when dependencies are added incrementally or conditionally
|
|
199
|
+
|
|
200
|
+
## Core Operations
|
|
201
|
+
|
|
202
|
+
Context supports inspection, retrieval, and modification operations. All methods are type-safe and leverage the phantom type `R` to track what's stored.
|
|
47
203
|
|
|
48
|
-
###
|
|
204
|
+
### Inspection
|
|
49
205
|
|
|
50
|
-
|
|
206
|
+
The following methods let you check the contents of a context without retrieving specific values:
|
|
207
|
+
|
|
208
|
+
#### `Context#size`
|
|
209
|
+
|
|
210
|
+
Returns the number of entries in the context:
|
|
51
211
|
|
|
52
212
|
```scala
|
|
53
|
-
|
|
213
|
+
case class Config(debug: Boolean)
|
|
214
|
+
case class Logger(name: String)
|
|
215
|
+
|
|
216
|
+
val ctx = Context(Config(true), Logger("app"))
|
|
54
217
|
```
|
|
55
218
|
|
|
56
|
-
|
|
219
|
+
Get the number of entries in the context:
|
|
220
|
+
|
|
221
|
+
```scala
|
|
222
|
+
val sz = ctx.size
|
|
223
|
+
// sz: Int = 2
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### `Context#isEmpty`
|
|
227
|
+
|
|
228
|
+
Returns `true` if the context contains no entries:
|
|
57
229
|
|
|
58
230
|
```scala
|
|
59
|
-
|
|
60
|
-
case class Person(name: String, age: Int) extends Named
|
|
231
|
+
case class Config(debug: Boolean)
|
|
61
232
|
|
|
62
|
-
val
|
|
63
|
-
val
|
|
233
|
+
val empty = Context.empty
|
|
234
|
+
val notEmpty = Context(Config(true))
|
|
64
235
|
```
|
|
65
236
|
|
|
66
|
-
|
|
237
|
+
Now check the `isEmpty` status of both contexts:
|
|
238
|
+
|
|
239
|
+
```scala
|
|
240
|
+
val e1 = empty.isEmpty
|
|
241
|
+
// e1: Boolean = true
|
|
242
|
+
val e2 = notEmpty.isEmpty
|
|
243
|
+
// e2: Boolean = false
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
#### `Context#nonEmpty`
|
|
247
|
+
|
|
248
|
+
Returns `true` if the context contains at least one entry (opposite of `isEmpty`):
|
|
249
|
+
|
|
250
|
+
```scala
|
|
251
|
+
case class Config(debug: Boolean)
|
|
252
|
+
|
|
253
|
+
val empty = Context.empty
|
|
254
|
+
val notEmpty = Context(Config(true))
|
|
255
|
+
```
|
|
67
256
|
|
|
68
|
-
|
|
257
|
+
Check the `nonEmpty` status of both contexts:
|
|
69
258
|
|
|
70
259
|
```scala
|
|
71
|
-
val
|
|
72
|
-
|
|
260
|
+
val e1 = empty.nonEmpty
|
|
261
|
+
// e1: Boolean = false
|
|
262
|
+
val e2 = notEmpty.nonEmpty
|
|
263
|
+
// e2: Boolean = true
|
|
73
264
|
```
|
|
74
265
|
|
|
75
|
-
|
|
266
|
+
### Retrieval
|
|
267
|
+
|
|
268
|
+
The following methods let you retrieve values from a context by type:
|
|
269
|
+
|
|
270
|
+
#### `Context#get`
|
|
271
|
+
|
|
272
|
+
Retrieves a value by type. The type bound `A >: R` ensures that a value of type `A` (or a subtype of `A`) is present at compile time:
|
|
76
273
|
|
|
77
|
-
|
|
274
|
+
```scala
|
|
275
|
+
import zio.blocks.context._
|
|
276
|
+
|
|
277
|
+
case class Config(debug: Boolean)
|
|
278
|
+
case class Logger(name: String)
|
|
279
|
+
case class Metrics(count: Int)
|
|
280
|
+
|
|
281
|
+
val ctx = Context(Config(debug = true), Logger("app"), Metrics(100))
|
|
282
|
+
|
|
283
|
+
// Retrieve by exact type
|
|
284
|
+
val config = ctx.get[Config]
|
|
285
|
+
```
|
|
78
286
|
|
|
79
|
-
|
|
287
|
+
Or by supertype (subtype matching):
|
|
80
288
|
|
|
81
289
|
```scala
|
|
82
|
-
|
|
83
|
-
|
|
290
|
+
import zio.blocks.context._
|
|
291
|
+
|
|
292
|
+
trait Animal { def sound: String }
|
|
293
|
+
case class Dog(name: String) extends Animal {
|
|
294
|
+
def sound = "Woof"
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
val ctxDog = Context(Dog("Buddy"))
|
|
298
|
+
val animal = ctxDog.get[Animal]
|
|
84
299
|
```
|
|
85
300
|
|
|
86
|
-
|
|
301
|
+
If you attempt to retrieve a type that is not in the context, the code will not compile because the type bound `A >: R` requires it to be present:
|
|
87
302
|
|
|
88
303
|
```scala
|
|
89
|
-
|
|
90
|
-
val
|
|
91
|
-
ctx2.get[Config].debug // true
|
|
304
|
+
// This is a compile-time error, not a runtime error:
|
|
305
|
+
// val metrics: String = ctx.get[String] // Error: String is not in context type
|
|
92
306
|
```
|
|
93
307
|
|
|
94
|
-
|
|
308
|
+
#### `Context#getOption`
|
|
95
309
|
|
|
96
|
-
|
|
310
|
+
Retrieves a value if present, returning `Option[A]`. Unlike `get`, this method does not require the type to be in the context's type parameter. Use it for optional lookups:
|
|
97
311
|
|
|
98
312
|
```scala
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
313
|
+
case class Config(debug: Boolean)
|
|
314
|
+
|
|
315
|
+
val ctx = Context(Config(debug = true))
|
|
102
316
|
```
|
|
103
317
|
|
|
104
|
-
|
|
318
|
+
Try to retrieve both an existing type and a missing type:
|
|
105
319
|
|
|
106
|
-
|
|
320
|
+
```scala
|
|
321
|
+
val found: Option[Config] = ctx.getOption[Config]
|
|
322
|
+
// found: Option[Config] = Some(Config(true))
|
|
323
|
+
val missing: Option[String] = ctx.getOption[String]
|
|
324
|
+
// missing: Option[String] = None
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Modification
|
|
328
|
+
|
|
329
|
+
All modification methods return a new `Context`—the original remains immutable:
|
|
330
|
+
|
|
331
|
+
#### `Context#add`
|
|
332
|
+
|
|
333
|
+
Adds a value to the context, expanding the phantom type by `& A`:
|
|
334
|
+
|
|
335
|
+
```scala
|
|
336
|
+
import zio.blocks.context._
|
|
337
|
+
|
|
338
|
+
case class Config(debug: Boolean)
|
|
339
|
+
case class Logger(name: String)
|
|
340
|
+
|
|
341
|
+
val ctx1 = Context(Config(true))
|
|
342
|
+
val ctx2 = ctx1.add(Logger("new"))
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
If a value of the same type already exists, it is replaced:
|
|
346
|
+
|
|
347
|
+
```scala
|
|
348
|
+
import zio.blocks.context._
|
|
349
|
+
|
|
350
|
+
case class Config(debug: Boolean)
|
|
351
|
+
case class Logger(name: String)
|
|
352
|
+
|
|
353
|
+
val ctx1 = Context(Config(true))
|
|
354
|
+
val ctx2 = ctx1.add(Logger("new"))
|
|
355
|
+
val ctx3 = ctx2.add(Config(debug = false))
|
|
356
|
+
val replaced = ctx3.get[Config]
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
#### `Context#update`
|
|
360
|
+
|
|
361
|
+
Transforms an existing value if it is present. If the type is not found, the context is returned unchanged:
|
|
362
|
+
|
|
363
|
+
```scala
|
|
364
|
+
import zio.blocks.context._
|
|
365
|
+
|
|
366
|
+
case class Metrics(count: Int)
|
|
367
|
+
|
|
368
|
+
val ctx = Context(Metrics(count = 10))
|
|
369
|
+
val updated = ctx.update[Metrics](m => m.copy(count = m.count + 5))
|
|
370
|
+
val newCount = updated.get[Metrics].count
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
#### `Context#++ (Union)`
|
|
374
|
+
|
|
375
|
+
Combines two contexts into a new context containing all entries. When both contexts contain the same type, the value from the right side (second argument) wins:
|
|
376
|
+
|
|
377
|
+
```scala
|
|
378
|
+
import zio.blocks.context._
|
|
379
|
+
|
|
380
|
+
case class Config(debug: Boolean)
|
|
381
|
+
case class Logger(name: String)
|
|
382
|
+
case class Metrics(count: Int)
|
|
383
|
+
|
|
384
|
+
val left = Context(Config(debug = false), Logger("left"))
|
|
385
|
+
val right = Context(Config(debug = true), Metrics(99))
|
|
386
|
+
val merged = left ++ right
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
#### `Context#prune`
|
|
390
|
+
|
|
391
|
+
Narrows a context to contain only specified types. All other entries are discarded:
|
|
107
392
|
|
|
108
393
|
```scala
|
|
109
|
-
|
|
110
|
-
|
|
394
|
+
import zio.blocks.context._
|
|
395
|
+
|
|
396
|
+
case class Config(debug: Boolean)
|
|
397
|
+
case class Logger(name: String)
|
|
398
|
+
case class Metrics(count: Int)
|
|
111
399
|
|
|
112
|
-
val
|
|
113
|
-
|
|
400
|
+
val full = Context(Config(true), Logger("app"), Metrics(100))
|
|
401
|
+
val justConfig = full.prune[Config]
|
|
402
|
+
val configSize = justConfig.size
|
|
114
403
|
```
|
|
115
404
|
|
|
116
|
-
|
|
405
|
+
#### `Context#toString`
|
|
406
|
+
|
|
407
|
+
Returns a human-readable representation of the context showing all type-value pairs:
|
|
408
|
+
|
|
409
|
+
```scala
|
|
410
|
+
case class Config(debug: Boolean)
|
|
411
|
+
case class Logger(name: String)
|
|
412
|
+
|
|
413
|
+
val ctx = Context(Config(debug = true), Logger("app"))
|
|
414
|
+
```
|
|
117
415
|
|
|
118
|
-
|
|
416
|
+
Convert the context to a human-readable string representation:
|
|
119
417
|
|
|
120
418
|
```scala
|
|
121
|
-
val
|
|
122
|
-
|
|
419
|
+
val str = ctx.toString
|
|
420
|
+
// str: String = "Context(repl.MdocSession.MdocApp1.Logger -> Logger(app), repl.MdocSession.MdocApp1.Config -> Config(true))"
|
|
123
421
|
```
|
|
124
422
|
|
|
125
423
|
## Covariance
|
|
126
424
|
|
|
127
|
-
`Context` is covariant,
|
|
425
|
+
`Context` is covariant in its type parameter, meaning `Context[Dog] <: Context[Animal]` when `Dog <: Animal`. This allows passing a more-specific context to code expecting a more-general one:
|
|
128
426
|
|
|
129
427
|
```scala
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
428
|
+
import zio.blocks.context._
|
|
429
|
+
|
|
430
|
+
trait Animal { def sound: String }
|
|
431
|
+
case class Dog(name: String) extends Animal {
|
|
432
|
+
def sound = "Woof"
|
|
133
433
|
}
|
|
134
434
|
|
|
135
|
-
|
|
136
|
-
|
|
435
|
+
def processAnimal(ctx: Context[Animal]): String = ctx.get[Animal].sound
|
|
436
|
+
|
|
437
|
+
val dogCtx = Context(Dog("Buddy"))
|
|
438
|
+
val sound = processAnimal(dogCtx)
|
|
137
439
|
```
|
|
138
440
|
|
|
139
|
-
|
|
441
|
+
Covariance also applies during retrieval—if you request a supertype, the stored subtype is returned.
|
|
140
442
|
|
|
141
|
-
|
|
443
|
+
## Type Safety: IsNominalType
|
|
142
444
|
|
|
143
|
-
|
|
144
|
-
- Enums (Scala 3)
|
|
145
|
-
- Applied types (`List[Int]`, `Map[K, V]`)
|
|
445
|
+
Context only accepts **nominal types**—concrete classes, case classes, traits, and objects. The compiler automatically derives `IsNominalType[A]` for allowed types and rejects unsupported kinds:
|
|
146
446
|
|
|
147
|
-
|
|
447
|
+
**Supported:**
|
|
448
|
+
- Classes: `case class Config(...)`
|
|
449
|
+
- Traits: `trait Logger`
|
|
450
|
+
- Objects: `object Registry`
|
|
451
|
+
- Applied types: `List[Int]`, `Map[String, Int]`
|
|
452
|
+
- Enums (Scala 3): `enum Color { case Red, Green, Blue }`
|
|
148
453
|
|
|
149
|
-
|
|
150
|
-
-
|
|
454
|
+
**Not supported (compile error):**
|
|
455
|
+
- Intersection types: `A & B` (use the context type parameter instead)
|
|
456
|
+
- Union types: `A | B`
|
|
151
457
|
- Structural types: `{ def foo: Int }`
|
|
152
458
|
|
|
459
|
+
Attempting to store an unsupported type:
|
|
460
|
+
|
|
461
|
+
```scala
|
|
462
|
+
// This fails at compile time:
|
|
463
|
+
// Context.empty.add(null: (String & Int)) // Error: unsupported type
|
|
464
|
+
```
|
|
465
|
+
|
|
153
466
|
## Performance
|
|
154
467
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
468
|
+
**Caching**: When a value is retrieved via `get` or `getOption`, the result is cached. Repeated lookups for the same type return the cached value in O(1) time without traversing the entries again.
|
|
469
|
+
|
|
470
|
+
**Subtype matching**: Supertype lookups scan the entry list linearly to find a compatible subtype. After the first lookup, the result is cached, so subsequent requests for that supertype are O(1).
|
|
471
|
+
|
|
472
|
+
**Platform optimizations**: The JVM implementation uses `ConcurrentHashMap` for thread-safe caching. The JavaScript platform uses a simpler in-memory mutable hash map for efficient lookups.
|
|
473
|
+
|
|
474
|
+
## Comparing Approaches
|
|
475
|
+
|
|
476
|
+
Here is a comparison of Context with related alternatives:
|
|
477
|
+
|
|
478
|
+
| Feature | `Map[Class[_], Any]` | `ZEnvironment` | `Context` |
|
|
479
|
+
|---------------------|----------------------|------------------|-----------|
|
|
480
|
+
| Type-safe retrieval | ✗ (cast required) | ✓ | ✓ |
|
|
481
|
+
| Compile-time proof | ✗ | ✓ | ✓ |
|
|
482
|
+
| Effect-free | ✓ | ✗ (requires ZIO) | ✓ |
|
|
483
|
+
| Immutable | ✓ | ✓ | ✓ |
|
|
484
|
+
| Cached lookups | ✗ | ✓ | ✓ |
|
|
485
|
+
| Supertype matching | ✗ | ✓ | ✓ |
|
|
486
|
+
|
|
487
|
+
## Integration with Wire and Scope
|
|
488
|
+
|
|
489
|
+
`Context` is the dependency carrier in ZIO Blocks' Wire-based dependency injection system. A `Wire[-In, +Out]` describes how to build an output given input dependencies, and contexts supply those dependencies. Wire and Scope work together to provide type-safe, compile-checked dependency injection:
|
|
490
|
+
|
|
491
|
+
```scala
|
|
492
|
+
// Pseudocode illustrating how Context integrates with Wire and Scope
|
|
493
|
+
import zio.blocks.scope._
|
|
494
|
+
import zio.blocks.context._
|
|
495
|
+
|
|
496
|
+
case class Config(debug: Boolean)
|
|
497
|
+
case class Logger(name: String)
|
|
498
|
+
case class Service(config: Config, logger: Logger)
|
|
499
|
+
|
|
500
|
+
// Define a wire that requires Config and Logger to build Service
|
|
501
|
+
val buildService = Wire.make[Config & Logger, Service]
|
|
502
|
+
|
|
503
|
+
// Create a context with the required dependencies
|
|
504
|
+
val deps = Context(Config(debug = true), Logger("app"))
|
|
505
|
+
|
|
506
|
+
// Create a scope and instantiate the service
|
|
507
|
+
Scope.global.scoped { scope =>
|
|
508
|
+
val service: Service = buildService.make(scope, deps)
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
## Running the Examples
|
|
513
|
+
|
|
514
|
+
All code from this guide is available as runnable examples in the `schema-examples` module.
|
|
515
|
+
|
|
516
|
+
**1. Clone the repository and navigate to the project:**
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
git clone https://github.com/zio/zio-blocks.git
|
|
520
|
+
cd zio-blocks
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
**2. Run individual examples with sbt:**
|
|
524
|
+
|
|
525
|
+
**Context construction: creating contexts with apply, empty.add, and inspecting size/isEmpty/nonEmpty**
|
|
526
|
+
|
|
527
|
+
```scala title="schema-examples/src/main/scala/context/ContextConstructionExample.scala"
|
|
528
|
+
package context
|
|
529
|
+
|
|
530
|
+
import zio.blocks.context._
|
|
531
|
+
import util.ShowExpr.show
|
|
532
|
+
|
|
533
|
+
// Context.empty creates an empty, type-safe dependency container.
|
|
534
|
+
// Use Context.apply(...) to construct contexts with 1–10 values.
|
|
535
|
+
// The phantom type parameter tracks which types are present.
|
|
536
|
+
object ContextConstructionExample extends App {
|
|
537
|
+
|
|
538
|
+
case class Config(debug: Boolean)
|
|
539
|
+
case class Logger(name: String)
|
|
540
|
+
case class Metrics(count: Int)
|
|
541
|
+
|
|
542
|
+
// Start with an empty context.
|
|
543
|
+
show(Context.empty.size)
|
|
544
|
+
show(Context.empty.isEmpty)
|
|
545
|
+
|
|
546
|
+
// Create a context with one value.
|
|
547
|
+
val ctx1 = Context(Config(debug = true))
|
|
548
|
+
show(ctx1.size)
|
|
549
|
+
show(ctx1.nonEmpty)
|
|
550
|
+
|
|
551
|
+
// Create a context with multiple values (up to 10 supported).
|
|
552
|
+
val ctx2 = Context(
|
|
553
|
+
Config(debug = false),
|
|
554
|
+
Logger("myapp")
|
|
555
|
+
)
|
|
556
|
+
show(ctx2.size)
|
|
557
|
+
show(ctx2.isEmpty)
|
|
558
|
+
|
|
559
|
+
// Create a larger context.
|
|
560
|
+
val ctx3 = Context(
|
|
561
|
+
Config(debug = true),
|
|
562
|
+
Logger("prod"),
|
|
563
|
+
Metrics(count = 100)
|
|
564
|
+
)
|
|
565
|
+
show(ctx3.size)
|
|
566
|
+
|
|
567
|
+
// Build incrementally from empty using add.
|
|
568
|
+
val ctxBuilt = Context.empty
|
|
569
|
+
.add(Config(debug = false))
|
|
570
|
+
.add(Logger("init"))
|
|
571
|
+
.add(Metrics(count = 0))
|
|
572
|
+
show(ctxBuilt.size)
|
|
573
|
+
show(ctxBuilt.nonEmpty)
|
|
574
|
+
|
|
575
|
+
// toString shows the context contents in a readable format.
|
|
576
|
+
show(ctx3.toString)
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/context/ContextConstructionExample.scala))
|
|
581
|
+
|
|
582
|
+
```bash
|
|
583
|
+
sbt "schema-examples/runMain context.ContextConstructionExample"
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
**Context retrieval: using get, supertype lookups, and getOption for safe access**
|
|
587
|
+
|
|
588
|
+
```scala title="schema-examples/src/main/scala/context/ContextRetrievalExample.scala"
|
|
589
|
+
package context
|
|
590
|
+
|
|
591
|
+
import zio.blocks.context._
|
|
592
|
+
import util.ShowExpr.show
|
|
593
|
+
|
|
594
|
+
// Context#get[A] retrieves a value by type with compile-time proof of existence.
|
|
595
|
+
// Context#getOption[A] retrieves a value if present, returning None if missing.
|
|
596
|
+
// Supertype matching allows retrieving a subtype using a more general supertype.
|
|
597
|
+
object ContextRetrievalExample extends App {
|
|
598
|
+
|
|
599
|
+
case class Config(debug: Boolean)
|
|
600
|
+
case class Logger(name: String)
|
|
601
|
+
case class Metrics(count: Int)
|
|
602
|
+
|
|
603
|
+
trait Animal { def sound: String }
|
|
604
|
+
case class Dog(name: String) extends Animal {
|
|
605
|
+
def sound = "Woof"
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Create a context with multiple values.
|
|
609
|
+
val ctx = Context(
|
|
610
|
+
Config(debug = true),
|
|
611
|
+
Logger("app"),
|
|
612
|
+
Metrics(count = 42)
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
// Retrieve by exact type.
|
|
616
|
+
val config: Config = ctx.get[Config]
|
|
617
|
+
show(config)
|
|
618
|
+
|
|
619
|
+
val logger: Logger = ctx.get[Logger]
|
|
620
|
+
show(logger)
|
|
621
|
+
|
|
622
|
+
val metrics: Metrics = ctx.get[Metrics]
|
|
623
|
+
show(metrics)
|
|
624
|
+
|
|
625
|
+
// getOption returns Some if the type is present.
|
|
626
|
+
val configOpt: Option[Config] = ctx.getOption[Config]
|
|
627
|
+
show(configOpt)
|
|
628
|
+
|
|
629
|
+
// getOption returns None if the type is missing (safe for optional lookups).
|
|
630
|
+
val missingOpt: Option[String] = ctx.getOption[String]
|
|
631
|
+
show(missingOpt)
|
|
632
|
+
|
|
633
|
+
// Supertype matching: retrieve a subtype using a supertype.
|
|
634
|
+
val dogCtx = Context(Dog("Buddy"))
|
|
635
|
+
val animal: Animal = dogCtx.get[Animal]
|
|
636
|
+
show(animal.sound)
|
|
637
|
+
|
|
638
|
+
// getOption also supports supertype matching.
|
|
639
|
+
val animalOpt: Option[Animal] = dogCtx.getOption[Animal]
|
|
640
|
+
show(animalOpt.map(_.sound))
|
|
641
|
+
|
|
642
|
+
// Cache efficiency: repeated lookups return the cached instance.
|
|
643
|
+
val first = ctx.get[Logger]
|
|
644
|
+
val second = ctx.get[Logger]
|
|
645
|
+
show(first eq second)
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/context/ContextRetrievalExample.scala))
|
|
650
|
+
|
|
651
|
+
```bash
|
|
652
|
+
sbt "schema-examples/runMain context.ContextRetrievalExample"
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
**Context modification: adding values, updating existing ones, merging contexts, and pruning types**
|
|
656
|
+
|
|
657
|
+
```scala title="schema-examples/src/main/scala/context/ContextModificationExample.scala"
|
|
658
|
+
package context
|
|
659
|
+
|
|
660
|
+
import zio.blocks.context._
|
|
661
|
+
import util.ShowExpr.show
|
|
662
|
+
|
|
663
|
+
// Context is immutable; modification methods return new contexts.
|
|
664
|
+
// Context#add expands the context with a new value (or replaces if type exists).
|
|
665
|
+
// Context#update transforms a value if present.
|
|
666
|
+
// Context#++ merges two contexts (right side wins on conflict).
|
|
667
|
+
// Context#prune narrows to specific types.
|
|
668
|
+
object ContextModificationExample extends App {
|
|
669
|
+
|
|
670
|
+
case class Config(debug: Boolean)
|
|
671
|
+
case class Logger(name: String)
|
|
672
|
+
case class Metrics(count: Int)
|
|
673
|
+
|
|
674
|
+
// Start with a simple context.
|
|
675
|
+
val ctx1 = Context(Config(debug = false))
|
|
676
|
+
show(ctx1.size)
|
|
677
|
+
|
|
678
|
+
// add expands the context with a new value.
|
|
679
|
+
val ctx2 = ctx1.add(Logger("app"))
|
|
680
|
+
show(ctx2.size)
|
|
681
|
+
show(ctx2.get[Config])
|
|
682
|
+
show(ctx2.get[Logger])
|
|
683
|
+
|
|
684
|
+
// add replaces if the type already exists.
|
|
685
|
+
val ctx3 = ctx2.add(Config(debug = true))
|
|
686
|
+
show(ctx3.size)
|
|
687
|
+
show(ctx3.get[Config].debug)
|
|
688
|
+
|
|
689
|
+
// update transforms a value if present.
|
|
690
|
+
val ctx4 = ctx3.add(Metrics(count = 100))
|
|
691
|
+
val updated = ctx4.update[Metrics](m => m.copy(count = m.count + 50))
|
|
692
|
+
show(updated.get[Metrics].count)
|
|
693
|
+
|
|
694
|
+
// update also works on existing types.
|
|
695
|
+
val configUpdated = updated.update[Config](c => c.copy(debug = false))
|
|
696
|
+
show(configUpdated.get[Config])
|
|
697
|
+
|
|
698
|
+
// ++ merges two contexts; right side wins on conflict.
|
|
699
|
+
val left = Context(Config(debug = false), Logger("left"))
|
|
700
|
+
val right = Context(Config(debug = true), Metrics(count = 99))
|
|
701
|
+
val merged = left ++ right
|
|
702
|
+
show(merged.size)
|
|
703
|
+
show(merged.get[Config].debug)
|
|
704
|
+
show(merged.get[Logger].name)
|
|
705
|
+
show(merged.get[Metrics].count)
|
|
706
|
+
|
|
707
|
+
// prune narrows a context to specific types.
|
|
708
|
+
val full = Context(Config(true), Logger("app"), Metrics(100))
|
|
709
|
+
show(full.size)
|
|
710
|
+
val justConfig = full.prune[Config]
|
|
711
|
+
show(justConfig.size)
|
|
712
|
+
show(justConfig.getOption[Logger])
|
|
713
|
+
|
|
714
|
+
// Chaining modifications.
|
|
715
|
+
val chain = Context.empty
|
|
716
|
+
.add(Config(debug = true))
|
|
717
|
+
.add(Logger("chain"))
|
|
718
|
+
.add(Metrics(count = 0))
|
|
719
|
+
.update[Metrics](m => m.copy(count = 42))
|
|
720
|
+
show(chain.size)
|
|
721
|
+
show(chain.get[Metrics].count)
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/context/ContextModificationExample.scala))
|
|
726
|
+
|
|
727
|
+
```bash
|
|
728
|
+
sbt "schema-examples/runMain context.ContextModificationExample"
|
|
729
|
+
```
|