@zio.dev/zio-blocks 0.0.33 → 0.0.51
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/compile-time-resource-safety-with-scope.md +16 -17
- package/guides/getting-started-with-mux.md +1507 -0
- package/guides/query-dsl-extending.md +161 -102
- package/guides/query-dsl-fluent-builder.md +217 -157
- package/guides/query-dsl-reified-optics.md +12 -10
- package/guides/query-dsl-sql.md +246 -165
- package/guides/telemetry-guide.md +1069 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +292 -50
- package/package.json +1 -1
- package/plans/config-follow-up-prs.md +188 -0
- package/plans/config-pr-assessment-roadmap.md +310 -0
- package/reference/MuxDataFlow.jsx +250 -0
- package/reference/async.md +651 -0
- package/reference/chunk.md +3533 -308
- package/reference/codegen/case-class.md +436 -0
- package/reference/codegen/emitter-config.md +383 -0
- package/reference/codegen/examples.md +664 -0
- package/reference/codegen/field.md +316 -0
- package/reference/codegen/index.md +317 -0
- package/reference/codegen/scala-emitter.md +392 -0
- package/reference/codegen/scala-file.md +276 -0
- package/reference/codegen/sealed-trait.md +408 -0
- package/reference/codegen/type-definition.md +340 -0
- package/reference/codegen/type-ref.md +201 -0
- package/reference/combinators.md +347 -117
- package/reference/config.md +158 -0
- package/reference/context.md +4 -4
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +825 -0
- package/reference/endpoint/path-codec.md +237 -0
- package/reference/endpoint/route-pattern.md +196 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +212 -0
- package/reference/html.md +1120 -0
- package/reference/htmx/attribute-values.md +359 -0
- package/reference/htmx/hx-encoding.md +111 -0
- package/reference/htmx/hx-params.md +204 -0
- package/reference/htmx/hx-swap.md +276 -0
- package/reference/htmx/hx-sync.md +251 -0
- package/reference/htmx/hx-target.md +314 -0
- package/reference/htmx/hx-trigger.md +457 -0
- package/reference/htmx/hx-url-update.md +239 -0
- package/reference/htmx/index.md +855 -0
- package/reference/http-model/index.md +47 -0
- package/reference/http-model/model.md +1481 -0
- package/reference/http-model/schema.md +747 -0
- package/reference/maybe.md +826 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.mdx +823 -0
- package/reference/openapi.md +1351 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -2
- package/reference/resource-management/scope.md +28 -12
- package/reference/resource-management/wire.md +3 -7
- package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
- package/reference/ringbuffer/MpscDiagram.jsx +618 -0
- package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
- package/reference/ringbuffer/SpscDiagram.jsx +677 -0
- package/reference/ringbuffer/advanced.mdx +109 -0
- package/reference/ringbuffer/index.mdx +145 -0
- package/reference/ringbuffer/mpmc.mdx +151 -0
- package/reference/ringbuffer/mpsc.mdx +132 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +344 -0
- package/reference/{allows.md → schema/allows.md} +4 -4
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +2 -3
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +480 -0
- package/reference/schema/built-in-codecs/csv.md +564 -0
- package/reference/schema/built-in-codecs/index.md +77 -0
- package/reference/schema/built-in-codecs/json/index.md +295 -0
- package/reference/schema/built-in-codecs/json/json-config.md +217 -0
- package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
- package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
- package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
- package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
- package/reference/schema/built-in-codecs/messagepack.md +508 -0
- package/reference/schema/built-in-codecs/thrift.md +433 -0
- package/reference/schema/built-in-codecs/toon.md +1078 -0
- package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
- package/reference/schema/built-in-codecs/yaml.md +552 -0
- package/reference/{codec.md → schema/codec.md} +10 -10
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +50 -0
- package/reference/schema/migration.md +297 -0
- package/reference/{modifier.md → schema/modifier.md} +58 -7
- package/reference/{optics.md → schema/optics.md} +2 -2
- package/reference/{patch.md → schema/patch.md} +1 -1
- package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
- package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
- package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
- package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
- package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
- package/reference/{schema.md → schema/schema.md} +12 -0
- package/reference/{structural-types.md → schema/structural-types.md} +1 -1
- package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
- package/reference/smithy.md +533 -0
- package/reference/sql/db-codec-deriver.md +71 -0
- package/reference/sql/db-codec.md +687 -0
- package/reference/sql/db-con.md +271 -0
- package/reference/sql/db-connection.md +153 -0
- package/reference/sql/db-param-writer.md +77 -0
- package/reference/sql/db-param.md +66 -0
- package/reference/sql/db-result-reader.md +146 -0
- package/reference/sql/db-tx.md +82 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +254 -0
- package/reference/sql/index.md +341 -0
- package/reference/sql/repo.md +600 -0
- package/reference/sql/sql-dialect.md +73 -0
- package/reference/sql/sql-logger.md +62 -0
- package/reference/sql/sql-name-mapper.md +70 -0
- package/reference/sql/table-metadata.md +134 -0
- package/reference/sql/table.md +448 -0
- package/reference/sql/transactor-zio.md +399 -0
- package/reference/sql/transactor.md +353 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/concurrent-operators.md +106 -0
- package/reference/streams/index.md +653 -0
- package/reference/streams/pipeline.md +718 -0
- package/reference/streams/reader.md +1284 -0
- package/reference/streams/scala-2-compatibility.md +55 -0
- package/reference/streams/sink.md +1426 -0
- package/reference/streams/stream.md +2526 -0
- package/reference/streams/writer.md +1045 -0
- package/reference/streams/zero-boxing.md +275 -0
- package/reference/telemetry.md +693 -0
- package/reference/typeid.md +5 -19
- package/sidebars.js +238 -43
- package/reference/formats.md +0 -694
- package/reference/http-model.md +0 -1716
- package/reference/streams.md +0 -989
- package/ringbuffer.md +0 -249
- /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
- /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
- /package/reference/{lazy.md → schema/lazy.md} +0 -0
- /package/reference/{reflect.md → schema/reflect.md} +0 -0
- /package/reference/{registers.md → schema/registers.md} +0 -0
- /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
- /package/reference/{syntax.md → schema/syntax.md} +0 -0
- /package/reference/{validation.md → schema/validation.md} +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: path-codec
|
|
3
|
+
title: "PathCodec"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`PathCodec[A]` is a composable descriptor for URL path structures. It holds a tree of segment codecs connected by concatenation and fallback nodes, and provides bidirectional path conversion: `PathCodec#decode` extracts a typed value from a `Path`, returning `Either[String, A]` (the typed value or error), and `PathCodec#format` formats a typed value back to a `Path`, returning `Either[String, Path]` (the path or error). In addition to its runtime value type `A`, each codec also carries a phantom `PathVars` track that records the ordered list of declared path-variable markers contributed by its dynamic segments. Its definition begins:
|
|
7
|
+
|
|
8
|
+
```scala
|
|
9
|
+
sealed trait PathCodec[A] {
|
|
10
|
+
type PathVars
|
|
11
|
+
}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`PathVars` is purely type-level: it has zero runtime footprint and does not affect decoding or formatting. It exists so downstream tooling (for example, handler macros or static checks) can recover which named path variables a route declared, in order, and whether any of them were explicitly marked as ignored.
|
|
15
|
+
|
|
16
|
+
## Motivation
|
|
17
|
+
|
|
18
|
+
URL paths need both matching and generation. A routing library that only matches paths requires a separate URL-builder for links and redirects, leading to duplication and drift. `PathCodec` is bidirectional: every codec that can decode `/users/42` into `42: Int` can also format `42` back into `/users/42`. This makes it safe to use the same path definition for routing, link generation, and OpenAPI path parameter documentation.
|
|
19
|
+
|
|
20
|
+
## Structure
|
|
21
|
+
|
|
22
|
+
`PathCodec` is an ADT with four node types:
|
|
23
|
+
|
|
24
|
+
| Node | Meaning |
|
|
25
|
+
| ----------- | -------------------------------------------------------------- |
|
|
26
|
+
| `Segment` | A single path segment, described by a `SegmentCodec[A]` |
|
|
27
|
+
| `Concat` | Two path codecs composed sequentially with `/` or `++` |
|
|
28
|
+
| `Transform` | Bidirectional type mapping over an existing codec |
|
|
29
|
+
| `Fallback` | Two literal alternatives (applies only with `orElse`) |
|
|
30
|
+
|
|
31
|
+
## Construction
|
|
32
|
+
|
|
33
|
+
We build a `PathCodec` from smart constructors that produce typed segment nodes, from a path string, or by wrapping a `SegmentCodec` directly.
|
|
34
|
+
|
|
35
|
+
### Predefined segment constructors
|
|
36
|
+
|
|
37
|
+
The most common path building blocks are available as smart constructors on the companion:
|
|
38
|
+
|
|
39
|
+
```scala
|
|
40
|
+
import zio.blocks.endpoint._
|
|
41
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
42
|
+
|
|
43
|
+
val literalUsers: PathCodec[Unit] = PathCodec.literal("users")
|
|
44
|
+
val intId: PathCodec[Int] = PathCodec.int("id")
|
|
45
|
+
val longId: PathCodec[Long] = PathCodec.long("id")
|
|
46
|
+
val stringSlug: PathCodec[String] = PathCodec.string("slug")
|
|
47
|
+
val uuidId: PathCodec[java.util.UUID] = PathCodec.uuid("id")
|
|
48
|
+
val boolFlag: PathCodec[Boolean] = PathCodec.bool("enabled")
|
|
49
|
+
val rest: PathCodec[zio.http.Path] = PathCodec.trailing
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`PathCodec.literal` is a macro that validates the value at compile time — it rejects empty strings and strings containing `/` or characters requiring URL encoding.
|
|
53
|
+
|
|
54
|
+
For literal names like `PathCodec.int("id")`, the name is preserved as a singleton type inside `PathVars`, so the phantom track remembers not just that the codec captures an `Int`, but that it came from the path variable named `"id"`.
|
|
55
|
+
|
|
56
|
+
### From a string
|
|
57
|
+
|
|
58
|
+
To build a codec from a slash-separated string of literal segments, use `PathCodec.apply`:
|
|
59
|
+
|
|
60
|
+
```scala
|
|
61
|
+
import zio.blocks.endpoint._
|
|
62
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
63
|
+
|
|
64
|
+
val path: PathCodec[Unit] = PathCodec("/api/v1/users")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This is equivalent to concatenating `PathCodec.literal` for each segment.
|
|
68
|
+
|
|
69
|
+
### From a `SegmentCodec`
|
|
70
|
+
|
|
71
|
+
To wrap a custom `SegmentCodec[A]` into a `PathCodec[A]`, use `PathCodec.apply(segment)`:
|
|
72
|
+
|
|
73
|
+
```scala
|
|
74
|
+
import zio.blocks.endpoint._
|
|
75
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
76
|
+
|
|
77
|
+
val combined = PathCodec(SegmentCodec.literal("v") ~ SegmentCodec.int("version"))
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
There is also an implicit conversion from `SegmentCodec[A]` to `PathCodec[A]` and from `String` to `PathCodec[Unit]`, so both can appear directly in `/` expressions.
|
|
81
|
+
|
|
82
|
+
## Phantom `PathVars` Track and `.unused`
|
|
83
|
+
|
|
84
|
+
Every dynamic path segment contributes one phantom marker to `PathCodec#PathVars`:
|
|
85
|
+
|
|
86
|
+
- `PathCodec.int("id")` contributes `PathVar["id", Int]`
|
|
87
|
+
- `PathCodec.uuid("orderId")` contributes `PathVar["orderId", UUID]`
|
|
88
|
+
- literal segments and `PathCodec.trailing` contribute no markers
|
|
89
|
+
|
|
90
|
+
Sequential composition with `/` or `++` concatenates those markers in declaration order, matching the left-to-right route shape.
|
|
91
|
+
|
|
92
|
+
Sometimes a route needs to capture a segment for matching or formatting, but a downstream handler intentionally does not consume that variable. For that case, single-variable codecs expose `.unused`, which keeps the runtime behavior identical while relabeling the phantom marker to `PathVar.Ignored[Name, Type]`:
|
|
93
|
+
|
|
94
|
+
```scala
|
|
95
|
+
import zio.blocks.endpoint._
|
|
96
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
97
|
+
|
|
98
|
+
val userId: PathCodec[Int] = PathCodec.int("id")
|
|
99
|
+
val ignoredUserId: PathCodec[Int] = PathCodec.int("id").unused
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`.unused` has zero runtime cost: decoding, formatting, rendering, and matching all behave exactly the same as the non-`.unused` codec. The only difference is the phantom `PathVars` marker, which tells tooling that this declared path variable was intentionally ignored.
|
|
103
|
+
|
|
104
|
+
## Composition
|
|
105
|
+
|
|
106
|
+
Path codecs compose in two ways: sequential concatenation with `/` or `++`, and literal alternatives with `orElse`.
|
|
107
|
+
|
|
108
|
+
### Sequential composition with `/` and `++`
|
|
109
|
+
|
|
110
|
+
`/` and `++` are equivalent: both concatenate two path codecs. The result type is flattened automatically (so `Unit / Int` gives `Int`, not `(Unit, Int)`), eliminating `Unit` components:
|
|
111
|
+
|
|
112
|
+
```scala
|
|
113
|
+
import zio.blocks.endpoint._
|
|
114
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
115
|
+
|
|
116
|
+
val route: PathCodec[Int] = PathCodec.literal("users") / PathCodec.int("id")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
In the context of a `RoutePattern`, the same operator works directly:
|
|
120
|
+
|
|
121
|
+
```scala
|
|
122
|
+
import zio.blocks.endpoint._
|
|
123
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
124
|
+
import zio.http.Method
|
|
125
|
+
|
|
126
|
+
val pattern = Method.GET / "users" / PathCodec.int("id") / "posts"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Literal alternatives with `orElse`
|
|
130
|
+
|
|
131
|
+
To match either of two literal segments, use `orElse`:
|
|
132
|
+
|
|
133
|
+
```scala
|
|
134
|
+
import zio.blocks.endpoint._
|
|
135
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
136
|
+
|
|
137
|
+
val either: PathCodec[Unit] =
|
|
138
|
+
PathCodec.literal("users").orElse(PathCodec.literal("members"))
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`orElse` is for literal alternatives only. Both branches must be `PathCodec[Unit]` with no captured path variables, and `PathCodec#alternatives` still validates at runtime that the branches are genuinely literal-only. In practice, use `orElse` only with `PathCodec.literal(...)` or string-literal path codecs.
|
|
142
|
+
|
|
143
|
+
## Decoding and Formatting
|
|
144
|
+
|
|
145
|
+
`PathCodec` is bidirectional: `PathCodec#decode` turns a runtime `Path` into a typed value, and `PathCodec#format` turns a typed value back into a `Path`.
|
|
146
|
+
|
|
147
|
+
### `PathCodec#decode`
|
|
148
|
+
|
|
149
|
+
To extract a typed value from a runtime `Path`:
|
|
150
|
+
|
|
151
|
+
```scala
|
|
152
|
+
import zio.blocks.endpoint._
|
|
153
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
154
|
+
import zio.http.Path
|
|
155
|
+
|
|
156
|
+
val codec = PathCodec.int("id")
|
|
157
|
+
|
|
158
|
+
val result: Either[String, Int] = codec.decode(Path("/42"))
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`PathCodec#decode` returns `Left(message)` when no segment matches or a segment cannot be parsed.
|
|
162
|
+
|
|
163
|
+
### `PathCodec#format`
|
|
164
|
+
|
|
165
|
+
To turn a typed value into a `Path`:
|
|
166
|
+
|
|
167
|
+
```scala
|
|
168
|
+
import zio.blocks.endpoint._
|
|
169
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
170
|
+
import zio.http.Path
|
|
171
|
+
|
|
172
|
+
val codec = PathCodec.uuid("id")
|
|
173
|
+
|
|
174
|
+
val path: Either[String, Path] =
|
|
175
|
+
codec.format(java.util.UUID.fromString("550e8400-e29b-41d4-a716-446655440000"))
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### `PathCodec#matches`
|
|
179
|
+
|
|
180
|
+
To test whether a `Path` matches without extracting a value:
|
|
181
|
+
|
|
182
|
+
```scala
|
|
183
|
+
import zio.blocks.endpoint._
|
|
184
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
185
|
+
import zio.http.Path
|
|
186
|
+
|
|
187
|
+
val codec = PathCodec.literal("users")
|
|
188
|
+
val matched = codec.matches(Path("/users"))
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Type Transformations
|
|
192
|
+
|
|
193
|
+
Use these methods to map the typed value that `PathCodec` decodes or encodes without changing the underlying path structure.
|
|
194
|
+
|
|
195
|
+
### `PathCodec#transform`
|
|
196
|
+
|
|
197
|
+
To map the decoded value to a different type without changing the path structure, use `PathCodec#transform`. Both directions must be total:
|
|
198
|
+
|
|
199
|
+
```scala
|
|
200
|
+
import zio.blocks.endpoint._
|
|
201
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
202
|
+
import zio.blocks.endpoint.PathCodec._
|
|
203
|
+
|
|
204
|
+
final case class UserId(value: Int)
|
|
205
|
+
|
|
206
|
+
val userIdCodec: PathCodec[UserId] =
|
|
207
|
+
PathCodec.int("id").transform[UserId](UserId(_), _.value)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### `PathCodec#transformOrFail`
|
|
211
|
+
|
|
212
|
+
When decoding or encoding can fail, use `PathCodec#transformOrFail`. A `Left` from the decode function causes the path not to match:
|
|
213
|
+
|
|
214
|
+
```scala
|
|
215
|
+
import zio.blocks.endpoint._
|
|
216
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
217
|
+
|
|
218
|
+
val nonNegativeInt: PathCodec[Int] =
|
|
219
|
+
PathCodec.int("count").transformOrFail[Int](
|
|
220
|
+
n => if (n >= 0) Right(n) else Left(s"Expected non-negative, got $n"),
|
|
221
|
+
n => Right(n)
|
|
222
|
+
)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Rendering
|
|
226
|
+
|
|
227
|
+
`PathCodec#render` produces a human-readable path string. Dynamic segments appear as `{name}` by default:
|
|
228
|
+
|
|
229
|
+
```scala
|
|
230
|
+
import zio.blocks.endpoint._
|
|
231
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
232
|
+
|
|
233
|
+
val codec = PathCodec.literal("users") / PathCodec.int("id")
|
|
234
|
+
val rendered: String = codec.render
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
To use a different prefix/suffix for dynamic segments (e.g., `:id` for Express-style paths), call the lower-level `PathCodec.render(codec, prefix = ":", suffix = "")`.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: route-pattern
|
|
3
|
+
title: "RoutePattern"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`RoutePattern[A]` pairs an HTTP method with a typed path pattern. It is the primary routing descriptor in `zio-blocks-endpoint`: every `Endpoint` carries a `RoutePattern` that determines which HTTP method and URL path it matches. Like `PathCodec`, it also carries a phantom `PathVars` track that mirrors the ordered path-variable declarations contributed by its path codec. Its shape is:
|
|
7
|
+
|
|
8
|
+
```scala
|
|
9
|
+
final case class RoutePattern[A](
|
|
10
|
+
method: Method,
|
|
11
|
+
pathCodec: PathCodec[A],
|
|
12
|
+
doc: Doc = Doc.empty
|
|
13
|
+
)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The `PathVars` track is not a constructor parameter because it is purely type-level: it has zero runtime footprint and is derived from `pathCodec`. A literal-only route contributes no path-variable markers; a route with dynamic segments preserves them in declaration order.
|
|
17
|
+
|
|
18
|
+
## Motivation
|
|
19
|
+
|
|
20
|
+
HTTP routing requires matching both a method (GET, POST, …) and a path (`/users/42`). `RoutePattern` holds both in a single typed value. The type parameter `A` is the type of values extracted from the dynamic path segments — `Unit` for fully-literal paths, `Int` for a single integer segment, `(String, UUID)` for two dynamic segments, and so on.
|
|
21
|
+
|
|
22
|
+
Using a typed route pattern means route construction and path extraction are verified at compile time: the type of the extracted path value is always consistent with the path codec definition. The phantom `PathVars` track keeps the declared path-variable names and ignored-variable markers available to tooling without changing runtime behavior.
|
|
23
|
+
|
|
24
|
+
## Construction
|
|
25
|
+
|
|
26
|
+
Several construction forms exist. The method-first syntax is the most common and most readable; the others cover less typical use cases.
|
|
27
|
+
|
|
28
|
+
### Method-first syntax (recommended)
|
|
29
|
+
|
|
30
|
+
The primary syntax uses the `Method` extension method `/` to produce a `RoutePattern` directly:
|
|
31
|
+
|
|
32
|
+
```scala
|
|
33
|
+
import zio.blocks.endpoint._
|
|
34
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
35
|
+
import zio.http.Method
|
|
36
|
+
|
|
37
|
+
val getUsers = Method.GET / "users"
|
|
38
|
+
val postUser = Method.POST / "users"
|
|
39
|
+
val deleteOrder = Method.DELETE / "orders" / PathCodec.uuid("orderId")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This is the recommended construction style: it reads like the route itself and keeps the method close to the path.
|
|
43
|
+
|
|
44
|
+
### Constant constructors
|
|
45
|
+
|
|
46
|
+
Pre-built method-only patterns are available as constants on the `RoutePattern` companion:
|
|
47
|
+
|
|
48
|
+
```scala
|
|
49
|
+
import zio.blocks.endpoint._
|
|
50
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
51
|
+
|
|
52
|
+
val get = RoutePattern.GET
|
|
53
|
+
val post = RoutePattern.POST
|
|
54
|
+
val put = RoutePattern.PUT
|
|
55
|
+
val delete = RoutePattern.DELETE
|
|
56
|
+
val patch = RoutePattern.PATCH
|
|
57
|
+
val head = RoutePattern.HEAD
|
|
58
|
+
val options = RoutePattern.OPTIONS
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
These are equivalent to `RoutePattern(Method.GET)` etc., with an empty path codec. Additional constants like `CONNECT` and `TRACE` are also available for the complete set of HTTP methods.
|
|
62
|
+
|
|
63
|
+
### From a `Path` value
|
|
64
|
+
|
|
65
|
+
To construct a pattern from a runtime `Path` (all literal segments), use `RoutePattern.apply(method, path)`:
|
|
66
|
+
|
|
67
|
+
```scala
|
|
68
|
+
import zio.blocks.endpoint._
|
|
69
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
70
|
+
import zio.http.{Method, Path}
|
|
71
|
+
|
|
72
|
+
val route = RoutePattern(Method.GET, Path("/users/active"))
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Catch-all trailing patterns
|
|
76
|
+
|
|
77
|
+
To match any path suffix, use `RoutePattern.any`:
|
|
78
|
+
|
|
79
|
+
```scala
|
|
80
|
+
import zio.blocks.endpoint._
|
|
81
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
82
|
+
import zio.http.{Method, Path}
|
|
83
|
+
|
|
84
|
+
val catchAll: RoutePattern[Path] = RoutePattern.any
|
|
85
|
+
val getAny: RoutePattern[Path] = RoutePattern.any(Method.GET)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
These helpers preserve `PathVars = SegmentCodec.NoPathVars`: a trailing catch-all captures a `zio.http.Path` runtime value, but it does not declare any named path variables.
|
|
89
|
+
|
|
90
|
+
## Path Composition with `/`
|
|
91
|
+
|
|
92
|
+
To append additional `PathCodec` segments to a `RoutePattern`, use the `/` method:
|
|
93
|
+
|
|
94
|
+
```scala
|
|
95
|
+
import zio.blocks.endpoint._
|
|
96
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
97
|
+
import zio.http.Method
|
|
98
|
+
|
|
99
|
+
val route = Method.GET / "users" / PathCodec.int("id") / "posts"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Each `/` call produces a new `RoutePattern` with a widened type. The type is automatically flattened, eliminating `Unit` components: `Unit / Int / Unit` becomes `Int`, not `((Unit, Int), Unit)`. At the same time, the phantom `PathVars` track is concatenated left-to-right, so a route like `Method.GET / PathCodec.int("id") / PathCodec.string("slug")` preserves the declaration order of `"id"` then `"slug"` for downstream tooling.
|
|
103
|
+
|
|
104
|
+
## Decoding and Encoding
|
|
105
|
+
|
|
106
|
+
`RoutePattern` is bidirectional: `RoutePattern#decode` validates a live request against the pattern, and `RoutePattern#encode` rebuilds a request pair from a typed path value.
|
|
107
|
+
|
|
108
|
+
### `RoutePattern#decode`
|
|
109
|
+
|
|
110
|
+
To check whether a method and path match, and extract the typed path value:
|
|
111
|
+
|
|
112
|
+
```scala
|
|
113
|
+
import zio.blocks.endpoint._
|
|
114
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
115
|
+
import zio.http.{Method, Path}
|
|
116
|
+
|
|
117
|
+
val route = Method.GET / "users" / PathCodec.int("id")
|
|
118
|
+
|
|
119
|
+
val result: Either[String, Int] = route.decode(Method.GET, Path("/users/42"))
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`RoutePattern#decode` returns `Left` if the method does not match or any segment fails to parse, and `Right(value)` with the extracted path value otherwise. HEAD requests automatically fall back to GET for compatibility.
|
|
123
|
+
|
|
124
|
+
### `RoutePattern#encode`
|
|
125
|
+
|
|
126
|
+
To turn a typed value back into a `(Method, Path)` pair:
|
|
127
|
+
|
|
128
|
+
```scala
|
|
129
|
+
import zio.blocks.endpoint._
|
|
130
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
131
|
+
import zio.http.{Method, Path}
|
|
132
|
+
|
|
133
|
+
val route = Method.POST / "orders" / PathCodec.uuid("orderId")
|
|
134
|
+
|
|
135
|
+
val result: Either[String, (Method, Path)] = route.encode(java.util.UUID.fromString("550e8400-e29b-41d4-a716-446655440000"))
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### `RoutePattern#matches`
|
|
139
|
+
|
|
140
|
+
To test membership without extracting the value:
|
|
141
|
+
|
|
142
|
+
```scala
|
|
143
|
+
import zio.blocks.endpoint._
|
|
144
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
145
|
+
import zio.http.{Method, Path}
|
|
146
|
+
|
|
147
|
+
val route = Method.GET / "users"
|
|
148
|
+
val matches = route.matches(Method.GET, Path("/users"))
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Structural Operations
|
|
152
|
+
|
|
153
|
+
Three structural operations transform an existing `RoutePattern` without building a new one from scratch: `RoutePattern#alternatives`, `RoutePattern#nest`, and `RoutePattern#render`.
|
|
154
|
+
|
|
155
|
+
### Alternatives
|
|
156
|
+
|
|
157
|
+
`RoutePattern#alternatives` expands `Method.ANY` and `Method.Methods` into a flat list of single-method patterns. `RouteTree` calls this before inserting into the trie:
|
|
158
|
+
|
|
159
|
+
```scala
|
|
160
|
+
import zio.blocks.endpoint._
|
|
161
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
162
|
+
import zio.http.Method
|
|
163
|
+
|
|
164
|
+
val anyGet: RoutePattern[?] = RoutePattern.any(Method.GET)
|
|
165
|
+
val expanded = anyGet.alternatives
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Nesting
|
|
169
|
+
|
|
170
|
+
`RoutePattern#nest` prepends a literal path prefix without modifying the route's type or dynamic segments:
|
|
171
|
+
|
|
172
|
+
```scala
|
|
173
|
+
import zio.blocks.endpoint._
|
|
174
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
175
|
+
import zio.http.Method
|
|
176
|
+
|
|
177
|
+
val route = Method.GET / "users" / PathCodec.int("id")
|
|
178
|
+
val versioned = route.nest(PathCodec("/api/v1"))
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
This is useful for adding a version prefix to a group of existing routes without rewriting each one.
|
|
182
|
+
|
|
183
|
+
### Rendering
|
|
184
|
+
|
|
185
|
+
`RoutePattern#render` produces a human-readable string representation, useful for logging and OpenAPI path generation:
|
|
186
|
+
|
|
187
|
+
```scala
|
|
188
|
+
import zio.blocks.endpoint._
|
|
189
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
190
|
+
import zio.http.Method
|
|
191
|
+
|
|
192
|
+
val route = Method.GET / "users" / PathCodec.int("id")
|
|
193
|
+
val rendered: String = route.render
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Dynamic segments are rendered as `{name}` by default, matching OpenAPI path parameter convention.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: route-tree
|
|
3
|
+
title: "RouteTree"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`RouteTree[A]` is a routing trie keyed by HTTP method and path. It maps `(Method, Path)` pairs to values of type `A`, performing prefix-tree lookup with segment-priority ordering. Server-side interpreters primarily use it to build efficient route dispatch tables from a collection of `RoutePattern` values. Its structure is:
|
|
7
|
+
|
|
8
|
+
```scala
|
|
9
|
+
final case class RouteTree[A](
|
|
10
|
+
roots: Map[Method, SegmentSubtree[A]]
|
|
11
|
+
)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`SegmentSubtree[A]` is a single level of the trie, holding literal-keyed branches and priority-ordered dynamic segment branches:
|
|
15
|
+
|
|
16
|
+
```scala
|
|
17
|
+
final case class SegmentSubtree[A](
|
|
18
|
+
literals: Map[String, SegmentSubtree[A]],
|
|
19
|
+
others: ListMap[SegmentCodec.Key, (SegmentCodec[_], SegmentSubtree[A])],
|
|
20
|
+
value: Option[A]
|
|
21
|
+
)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Motivation
|
|
25
|
+
|
|
26
|
+
Routing a `(Method, Path)` pair to a handler requires checking many patterns efficiently. A flat scan through all routes is O(n) and scales poorly. `RouteTree` uses a prefix trie keyed on path segments, so matching is O(depth) — proportional to the number of segments in the path, not the number of registered routes.
|
|
27
|
+
|
|
28
|
+
Within each trie level, literal segments are stored in a `Map[String, ...]` for O(1) lookup, while dynamic segments are stored in a `ListMap` ordered by match priority. The priority order (literal → int → long → uuid → bool → string → combined → trailing) ensures that more specific segments win ambiguous matches.
|
|
29
|
+
|
|
30
|
+
## Building a `RouteTree`
|
|
31
|
+
|
|
32
|
+
Start from an empty tree and add patterns with `RouteTree#add`:
|
|
33
|
+
|
|
34
|
+
```scala
|
|
35
|
+
import zio.blocks.endpoint._
|
|
36
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
37
|
+
import zio.http.{Method, Path}
|
|
38
|
+
|
|
39
|
+
val tree = RouteTree.empty[String]
|
|
40
|
+
.add(Method.GET / "users", "list-users")
|
|
41
|
+
.add(Method.GET / "users" / PathCodec.int("id"), "get-user")
|
|
42
|
+
.add(Method.POST / "users", "create-user")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`RouteTree#add` calls `RoutePattern#alternatives` internally to expand `Method.ANY` and `Method.Methods` into individual method entries before inserting into the trie.
|
|
46
|
+
|
|
47
|
+
## Lookup
|
|
48
|
+
|
|
49
|
+
`RouteTree#get` looks up a `(Method, Path)` pair and returns the associated value if a match is found:
|
|
50
|
+
|
|
51
|
+
```scala
|
|
52
|
+
import zio.blocks.endpoint._
|
|
53
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
54
|
+
import zio.http.{Method, Path}
|
|
55
|
+
|
|
56
|
+
val tree = RouteTree.empty[String]
|
|
57
|
+
.add(Method.GET / "users" / PathCodec.int("id"), "get-user")
|
|
58
|
+
|
|
59
|
+
val result: Option[String] = tree.get(Method.GET, Path("/users/42"))
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
HEAD requests automatically fall back to the GET subtree if no HEAD handler is registered, matching the HTTP specification.
|
|
63
|
+
|
|
64
|
+
## Merging
|
|
65
|
+
|
|
66
|
+
`RouteTree#merge` combines two trees. On conflicts, the right-hand-side value takes precedence:
|
|
67
|
+
|
|
68
|
+
```scala
|
|
69
|
+
import zio.blocks.endpoint._
|
|
70
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
71
|
+
import zio.http.Method
|
|
72
|
+
|
|
73
|
+
val treeA = RouteTree.empty[String].add(Method.GET / "users", "users-v1")
|
|
74
|
+
val treeB = RouteTree.empty[String].add(Method.GET / "users", "users-v2")
|
|
75
|
+
|
|
76
|
+
val merged = treeA.merge(treeB)
|
|
77
|
+
// GET /users → "users-v2"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Mapping
|
|
81
|
+
|
|
82
|
+
`RouteTree#map` transforms all values in the trie while preserving its structure:
|
|
83
|
+
|
|
84
|
+
```scala
|
|
85
|
+
import zio.blocks.endpoint._
|
|
86
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
87
|
+
import zio.http.Method
|
|
88
|
+
|
|
89
|
+
val tree: RouteTree[String] = RouteTree.empty[String]
|
|
90
|
+
.add(Method.GET / "users", "get-users")
|
|
91
|
+
|
|
92
|
+
val lengths: RouteTree[Int] = tree.map(_.length)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Match Priority
|
|
96
|
+
|
|
97
|
+
Within a single trie level, `SegmentSubtree` tries literal branches first, then dynamic branches in priority order. Given a path like `/users/active`:
|
|
98
|
+
|
|
99
|
+
1. `literals.get("active")` is tried first — if an exact literal route for `"active"` exists, it matches.
|
|
100
|
+
2. Otherwise, dynamic segment codecs are tried in order: `Int` (fails, `"active"` is not numeric), `Long` (fails), `UUID` (fails), `Bool` (fails), `String` (succeeds).
|
|
101
|
+
|
|
102
|
+
This means `/users/42` matches an `Int` route even if a `String` route is also registered, because integers are higher priority than strings.
|
|
103
|
+
|
|
104
|
+
## `SegmentSubtree`
|
|
105
|
+
|
|
106
|
+
`SegmentSubtree` is the internal per-level trie node. It is not typically constructed directly — `RouteTree#add` builds it internally. The two key fields are:
|
|
107
|
+
|
|
108
|
+
- **`literals`**: a `Map[String, SegmentSubtree[A]]` for O(1) exact-match lookups.
|
|
109
|
+
- **`others`**: a `ListMap[SegmentCodec.Key, (SegmentCodec[_], SegmentSubtree[A])]` ordered by priority for dynamic segment matching.
|
|
110
|
+
|
|
111
|
+
The `value: Option[A]` field holds the registered value when a complete path terminates at this node. Trailing segments have special handling: if a `Trailing` codec is registered and the current index is past the end of the path segments, the lookup returns its subtree value.
|