@zio.dev/zio-blocks 0.0.33 → 0.0.55
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/adr/2026-07-18-data-migration.md +123 -0
- package/guides/async-getting-started.md +687 -0
- package/guides/compile-time-resource-safety-with-scope.md +21 -16
- package/guides/getting-started-with-mux.md +1395 -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 +640 -165
- package/guides/sql-checked-interpolation.md +173 -0
- package/guides/sql-transactions.md +286 -0
- package/guides/telemetry-guide.md +1130 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +248 -389
- 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 +1499 -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/config-decoder.md +460 -0
- package/reference/config/config-source.md +489 -0
- package/reference/config/errors.md +278 -0
- package/reference/config/flags.md +369 -0
- package/reference/config/formats.md +314 -0
- package/reference/config/index.md +304 -0
- package/reference/config/rollout.md +336 -0
- package/reference/context.md +9 -52
- package/reference/data-migration.md +269 -0
- package/reference/datastar/attributes.md +302 -0
- package/reference/datastar/events.md +234 -0
- package/reference/datastar/index.md +256 -0
- package/reference/datastar/signals.md +230 -0
- package/reference/datastar/sse.md +295 -0
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/bulk-creation.md +96 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +745 -0
- package/reference/endpoint/path-codec.md +225 -0
- package/reference/endpoint/route-pattern.md +194 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +199 -0
- package/reference/html.md +1424 -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 +807 -0
- package/reference/htmx/response-headers.md +240 -0
- package/reference/http-model/headers.md +735 -0
- package/reference/http-model/index.md +49 -0
- package/reference/http-model/model.md +1517 -0
- package/reference/http-model/schema-codecs.md +522 -0
- package/reference/http-model/schema.md +750 -0
- package/reference/http-model/server-sent-event.md +341 -0
- package/reference/jwt.md +195 -0
- package/reference/maybe.md +943 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.md +254 -0
- package/reference/mux.mdx +828 -0
- package/reference/openapi.md +1351 -0
- package/reference/projection.md +654 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -98
- package/reference/resource-management/scope.md +28 -220
- package/reference/resource-management/wire.md +5 -55
- 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 +185 -0
- package/reference/ringbuffer/mpsc.mdx +164 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +416 -0
- package/reference/{allows.md → schema/allows.md} +4 -100
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +3 -4
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +510 -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} +11 -11
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +196 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +52 -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} +167 -72
- package/reference/schema/reflect-transformer.md +140 -0
- 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/schema-search.md +263 -0
- package/reference/{schema.md → schema/schema.md} +22 -2
- 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 +1032 -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 +148 -0
- package/reference/sql/db-tx.md +114 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +288 -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 +363 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/core/index.md +32 -0
- package/reference/streams/core/pipeline.md +854 -0
- package/reference/streams/core/sink.md +1404 -0
- package/reference/streams/core/stream.md +3236 -0
- package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
- package/reference/streams/execution-and-compatibility/index.md +35 -0
- package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
- package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
- package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
- package/reference/streams/index.md +726 -0
- package/reference/streams/primitives/index.md +30 -0
- package/reference/streams/primitives/reader.md +1992 -0
- package/reference/streams/primitives/writer.md +1201 -0
- package/reference/telemetry/common/any-value.md +90 -0
- package/reference/telemetry/common/attribute-key.md +87 -0
- package/reference/telemetry/common/attributes.md +118 -0
- package/reference/telemetry/common/index.md +39 -0
- package/reference/telemetry/common/instrumentation-scope.md +24 -0
- package/reference/telemetry/common/resource.md +34 -0
- package/reference/telemetry/index.md +311 -0
- package/reference/telemetry/logging/index.md +197 -0
- package/reference/telemetry/logging/log-enrichment.md +72 -0
- package/reference/telemetry/logging/log-formatter.md +100 -0
- package/reference/telemetry/logging/log-record-processor.md +56 -0
- package/reference/telemetry/logging/log-record.md +44 -0
- package/reference/telemetry/logging/log-writer.md +64 -0
- package/reference/telemetry/logging/logger-provider.md +142 -0
- package/reference/telemetry/logging/logger.md +83 -0
- package/reference/telemetry/logging/severity.md +62 -0
- package/reference/telemetry/metrics/index.md +150 -0
- package/reference/telemetry/metrics/instruments.md +183 -0
- package/reference/telemetry/metrics/labeled-instruments.md +74 -0
- package/reference/telemetry/metrics/meter-provider.md +76 -0
- package/reference/telemetry/metrics/meter.md +98 -0
- package/reference/telemetry/metrics/metric-data.md +57 -0
- package/reference/telemetry/otel/custom-exporter.md +216 -0
- package/reference/telemetry/otel/index.md +212 -0
- package/reference/telemetry/tracing/index.md +155 -0
- package/reference/telemetry/tracing/sampler.md +89 -0
- package/reference/telemetry/tracing/span-builder.md +57 -0
- package/reference/telemetry/tracing/span-context.md +39 -0
- package/reference/telemetry/tracing/span-data.md +32 -0
- package/reference/telemetry/tracing/span-kind.md +55 -0
- package/reference/telemetry/tracing/span-processor.md +53 -0
- package/reference/telemetry/tracing/span-status.md +47 -0
- package/reference/telemetry/tracing/span.md +117 -0
- package/reference/telemetry/tracing/tracer-provider.md +91 -0
- package/reference/telemetry/tracing/tracer.md +52 -0
- package/reference/typeid.md +5 -83
- package/sidebars.js +376 -43
- package/undocumented-report.md +528 -270
- 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,225 @@
|
|
|
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). Its definition begins:
|
|
7
|
+
|
|
8
|
+
```scala
|
|
9
|
+
sealed trait PathCodec[A]
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Motivation
|
|
13
|
+
|
|
14
|
+
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.
|
|
15
|
+
|
|
16
|
+
## Structure
|
|
17
|
+
|
|
18
|
+
`PathCodec` is an ADT with four node types:
|
|
19
|
+
|
|
20
|
+
| Node | Meaning |
|
|
21
|
+
| ----------- | -------------------------------------------------------------- |
|
|
22
|
+
| `Segment` | A single path segment, described by a `SegmentCodec[A]` |
|
|
23
|
+
| `Concat` | Two path codecs composed sequentially with `/` or `++` |
|
|
24
|
+
| `Transform` | Bidirectional type mapping over an existing codec |
|
|
25
|
+
| `Fallback` | Two literal alternatives (applies only with `orElse`) |
|
|
26
|
+
|
|
27
|
+
## Construction
|
|
28
|
+
|
|
29
|
+
We build a `PathCodec` from smart constructors that produce typed segment nodes, from a path string, or by wrapping a `SegmentCodec` directly.
|
|
30
|
+
|
|
31
|
+
### Predefined segment constructors
|
|
32
|
+
|
|
33
|
+
The most common path building blocks are available as smart constructors on the companion:
|
|
34
|
+
|
|
35
|
+
```scala
|
|
36
|
+
import zio.blocks.endpoint._
|
|
37
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
38
|
+
|
|
39
|
+
val literalUsers: PathCodec[Unit] = PathCodec.literal("users")
|
|
40
|
+
val intId: PathCodec[Int] = PathCodec.int("id")
|
|
41
|
+
val longId: PathCodec[Long] = PathCodec.long("id")
|
|
42
|
+
val stringSlug: PathCodec[String] = PathCodec.string("slug")
|
|
43
|
+
val uuidId: PathCodec[java.util.UUID] = PathCodec.uuid("id")
|
|
44
|
+
val boolFlag: PathCodec[Boolean] = PathCodec.bool("enabled")
|
|
45
|
+
val rest: PathCodec[zio.http.Path] = PathCodec.trailing
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`PathCodec.literal` validates the value — it rejects empty strings and strings containing `/` or characters requiring URL encoding.
|
|
49
|
+
|
|
50
|
+
### From a string
|
|
51
|
+
|
|
52
|
+
To build a codec from a slash-separated string of literal segments, use `PathCodec.apply`:
|
|
53
|
+
|
|
54
|
+
```scala
|
|
55
|
+
import zio.blocks.endpoint._
|
|
56
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
57
|
+
|
|
58
|
+
val path: PathCodec[Unit] = PathCodec("/api/v1/users")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This is equivalent to concatenating `PathCodec.literal` for each segment.
|
|
62
|
+
|
|
63
|
+
### From a `SegmentCodec`
|
|
64
|
+
|
|
65
|
+
To wrap a custom `SegmentCodec[A]` into a `PathCodec[A]`, use `PathCodec.apply(segment)`:
|
|
66
|
+
|
|
67
|
+
```scala
|
|
68
|
+
import zio.blocks.endpoint._
|
|
69
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
70
|
+
|
|
71
|
+
val combined = PathCodec(SegmentCodec.literal("v") ~ SegmentCodec.int("version"))
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
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.
|
|
75
|
+
|
|
76
|
+
## Marking a capture as unused with `.unused`
|
|
77
|
+
|
|
78
|
+
Sometimes a route needs to capture a segment for matching or formatting, but a downstream handler intentionally does not consume that variable. `PathCodec` instances expose `.unused`, which converts the value type to `Unit` while keeping the same path shape:
|
|
79
|
+
|
|
80
|
+
```scala
|
|
81
|
+
import zio.blocks.endpoint._
|
|
82
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
83
|
+
|
|
84
|
+
val userId: PathCodec[Int] = PathCodec.int("id")
|
|
85
|
+
val ignoredUserId: PathCodec[Unit] = PathCodec.int("id").unused
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`.unused` converts the path codec's value type to `Unit`: the route still matches the same shape and renders `{name}` placeholders, but decoding yields `Unit` instead of the captured value, so no handler parameter is required. `format` on an unused path fails because there is no value to encode back into the ignored segment.
|
|
89
|
+
|
|
90
|
+
`PathCodec.unused` composes normally with `/` and is unaffected by `transform` / `transformOrFail` lifting.
|
|
91
|
+
|
|
92
|
+
## Composition
|
|
93
|
+
|
|
94
|
+
Path codecs compose in two ways: sequential concatenation with `/` or `++`, and literal alternatives with `orElse`.
|
|
95
|
+
|
|
96
|
+
### Sequential composition with `/` and `++`
|
|
97
|
+
|
|
98
|
+
`/` 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:
|
|
99
|
+
|
|
100
|
+
```scala
|
|
101
|
+
import zio.blocks.endpoint._
|
|
102
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
103
|
+
|
|
104
|
+
val route: PathCodec[Int] = PathCodec.literal("users") / PathCodec.int("id")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
In the context of a `RoutePattern`, the same operator works directly:
|
|
108
|
+
|
|
109
|
+
```scala
|
|
110
|
+
import zio.blocks.endpoint._
|
|
111
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
112
|
+
import zio.http.Method
|
|
113
|
+
|
|
114
|
+
val pattern = Method.GET / "users" / PathCodec.int("id") / "posts"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Literal alternatives with `orElse`
|
|
118
|
+
|
|
119
|
+
To match either of two literal segments, use `orElse`:
|
|
120
|
+
|
|
121
|
+
```scala
|
|
122
|
+
import zio.blocks.endpoint._
|
|
123
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
124
|
+
|
|
125
|
+
val either: PathCodec[Unit] =
|
|
126
|
+
PathCodec.literal("users").orElse(PathCodec.literal("members"))
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`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.
|
|
130
|
+
|
|
131
|
+
## Decoding and Formatting
|
|
132
|
+
|
|
133
|
+
`PathCodec` is bidirectional: `PathCodec#decode` turns a runtime `Path` into a typed value, and `PathCodec#format` turns a typed value back into a `Path`.
|
|
134
|
+
|
|
135
|
+
### `PathCodec#decode`
|
|
136
|
+
|
|
137
|
+
To extract a typed value from a runtime `Path`:
|
|
138
|
+
|
|
139
|
+
```scala
|
|
140
|
+
import zio.blocks.endpoint._
|
|
141
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
142
|
+
import zio.http.Path
|
|
143
|
+
|
|
144
|
+
val codec = PathCodec.int("id")
|
|
145
|
+
|
|
146
|
+
val result: Either[String, Int] = codec.decode(Path("/42"))
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`PathCodec#decode` returns `Left(message)` when no segment matches or a segment cannot be parsed.
|
|
150
|
+
|
|
151
|
+
### `PathCodec#format`
|
|
152
|
+
|
|
153
|
+
To turn a typed value into a `Path`:
|
|
154
|
+
|
|
155
|
+
```scala
|
|
156
|
+
import zio.blocks.endpoint._
|
|
157
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
158
|
+
import zio.http.Path
|
|
159
|
+
|
|
160
|
+
val codec = PathCodec.uuid("id")
|
|
161
|
+
|
|
162
|
+
val path: Either[String, Path] =
|
|
163
|
+
codec.format(java.util.UUID.fromString("550e8400-e29b-41d4-a716-446655440000"))
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### `PathCodec#matches`
|
|
167
|
+
|
|
168
|
+
To test whether a `Path` matches without extracting a value:
|
|
169
|
+
|
|
170
|
+
```scala
|
|
171
|
+
import zio.blocks.endpoint._
|
|
172
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
173
|
+
import zio.http.Path
|
|
174
|
+
|
|
175
|
+
val codec = PathCodec.literal("users")
|
|
176
|
+
val matched = codec.matches(Path("/users"))
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Type Transformations
|
|
180
|
+
|
|
181
|
+
Use these methods to map the typed value that `PathCodec` decodes or encodes without changing the underlying path structure.
|
|
182
|
+
|
|
183
|
+
### `PathCodec#transform`
|
|
184
|
+
|
|
185
|
+
To map the decoded value to a different type without changing the path structure, use `PathCodec#transform`. Both directions must be total:
|
|
186
|
+
|
|
187
|
+
```scala
|
|
188
|
+
import zio.blocks.endpoint._
|
|
189
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
190
|
+
import zio.blocks.endpoint.PathCodec._
|
|
191
|
+
|
|
192
|
+
final case class UserId(value: Int)
|
|
193
|
+
|
|
194
|
+
val userIdCodec: PathCodec[UserId] =
|
|
195
|
+
PathCodec.int("id").transform(UserId(_), _.value)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `PathCodec#transformOrFail`
|
|
199
|
+
|
|
200
|
+
When decoding or encoding can fail, use `PathCodec#transformOrFail`. A `Left` from the decode function causes the path not to match:
|
|
201
|
+
|
|
202
|
+
```scala
|
|
203
|
+
import zio.blocks.endpoint._
|
|
204
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
205
|
+
|
|
206
|
+
val nonNegativeInt: PathCodec[Int] =
|
|
207
|
+
PathCodec.int("count").transformOrFail(
|
|
208
|
+
n => if (n >= 0) Right(n) else Left(s"Expected non-negative, got $n"),
|
|
209
|
+
(n: Int) => Right(n)
|
|
210
|
+
)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Rendering
|
|
214
|
+
|
|
215
|
+
`PathCodec#render` produces a human-readable path string. Dynamic segments appear as `{name}` by default:
|
|
216
|
+
|
|
217
|
+
```scala
|
|
218
|
+
import zio.blocks.endpoint._
|
|
219
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
220
|
+
|
|
221
|
+
val codec = PathCodec.literal("users") / PathCodec.int("id")
|
|
222
|
+
val rendered: String = codec.render
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
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,194 @@
|
|
|
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. 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
|
+
## Motivation
|
|
17
|
+
|
|
18
|
+
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.
|
|
19
|
+
|
|
20
|
+
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.
|
|
21
|
+
|
|
22
|
+
## Construction
|
|
23
|
+
|
|
24
|
+
Several construction forms exist. The method-first syntax is the most common and most readable; the others cover less typical use cases.
|
|
25
|
+
|
|
26
|
+
### Method-first syntax (recommended)
|
|
27
|
+
|
|
28
|
+
The primary syntax uses the `Method` extension method `/` to produce a `RoutePattern` directly:
|
|
29
|
+
|
|
30
|
+
```scala
|
|
31
|
+
import zio.blocks.endpoint._
|
|
32
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
33
|
+
import zio.http.Method
|
|
34
|
+
|
|
35
|
+
val getUsers = Method.GET / "users"
|
|
36
|
+
val postUser = Method.POST / "users"
|
|
37
|
+
val deleteOrder = Method.DELETE / "orders" / PathCodec.uuid("orderId")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This is the recommended construction style: it reads like the route itself and keeps the method close to the path.
|
|
41
|
+
|
|
42
|
+
### Constant constructors
|
|
43
|
+
|
|
44
|
+
Pre-built method-only patterns are available as constants on the `RoutePattern` companion:
|
|
45
|
+
|
|
46
|
+
```scala
|
|
47
|
+
import zio.blocks.endpoint._
|
|
48
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
49
|
+
|
|
50
|
+
val get = RoutePattern.GET
|
|
51
|
+
val post = RoutePattern.POST
|
|
52
|
+
val put = RoutePattern.PUT
|
|
53
|
+
val delete = RoutePattern.DELETE
|
|
54
|
+
val patch = RoutePattern.PATCH
|
|
55
|
+
val head = RoutePattern.HEAD
|
|
56
|
+
val options = RoutePattern.OPTIONS
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
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.
|
|
60
|
+
|
|
61
|
+
### From a `Path` value
|
|
62
|
+
|
|
63
|
+
To construct a pattern from a runtime `Path` (all literal segments), use `RoutePattern.apply(method, path)`:
|
|
64
|
+
|
|
65
|
+
```scala
|
|
66
|
+
import zio.blocks.endpoint._
|
|
67
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
68
|
+
import zio.http.{Method, Path}
|
|
69
|
+
|
|
70
|
+
val route = RoutePattern(Method.GET, Path("/users/active"))
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Catch-all trailing patterns
|
|
74
|
+
|
|
75
|
+
To match any path suffix, use `RoutePattern.any`:
|
|
76
|
+
|
|
77
|
+
```scala
|
|
78
|
+
import zio.blocks.endpoint._
|
|
79
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
80
|
+
import zio.http.{Method, Path}
|
|
81
|
+
|
|
82
|
+
val catchAll: RoutePattern[Path] = RoutePattern.any
|
|
83
|
+
val getAny: RoutePattern[Path] = RoutePattern.any(Method.GET)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
A trailing catch-all captures a `zio.http.Path` runtime value without declaring any named path variables.
|
|
87
|
+
|
|
88
|
+
## Path Composition with `/`
|
|
89
|
+
|
|
90
|
+
To append additional `PathCodec` segments to a `RoutePattern`, use the `/` method:
|
|
91
|
+
|
|
92
|
+
```scala
|
|
93
|
+
import zio.blocks.endpoint._
|
|
94
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
95
|
+
import zio.http.Method
|
|
96
|
+
|
|
97
|
+
val route = Method.GET / "users" / PathCodec.int("id") / "posts"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
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)`.
|
|
101
|
+
|
|
102
|
+
## Decoding and Encoding
|
|
103
|
+
|
|
104
|
+
`RoutePattern` is bidirectional: `RoutePattern#decode` validates a live request against the pattern, and `RoutePattern#encode` rebuilds a request pair from a typed path value.
|
|
105
|
+
|
|
106
|
+
### `RoutePattern#decode`
|
|
107
|
+
|
|
108
|
+
To check whether a method and path match, and extract the typed path value:
|
|
109
|
+
|
|
110
|
+
```scala
|
|
111
|
+
import zio.blocks.endpoint._
|
|
112
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
113
|
+
import zio.http.{Method, Path}
|
|
114
|
+
|
|
115
|
+
val route = Method.GET / "users" / PathCodec.int("id")
|
|
116
|
+
|
|
117
|
+
val result: Either[String, Int] = route.decode(Method.GET, Path("/users/42"))
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`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.
|
|
121
|
+
|
|
122
|
+
### `RoutePattern#encode`
|
|
123
|
+
|
|
124
|
+
To turn a typed value back into a `(Method, Path)` pair:
|
|
125
|
+
|
|
126
|
+
```scala
|
|
127
|
+
import zio.blocks.endpoint._
|
|
128
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
129
|
+
import zio.http.{Method, Path}
|
|
130
|
+
|
|
131
|
+
val route = Method.POST / "orders" / PathCodec.uuid("orderId")
|
|
132
|
+
|
|
133
|
+
val result: Either[String, (Method, Path)] = route.encode(java.util.UUID.fromString("550e8400-e29b-41d4-a716-446655440000"))
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### `RoutePattern#matches`
|
|
137
|
+
|
|
138
|
+
To test membership without extracting the value:
|
|
139
|
+
|
|
140
|
+
```scala
|
|
141
|
+
import zio.blocks.endpoint._
|
|
142
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
143
|
+
import zio.http.{Method, Path}
|
|
144
|
+
|
|
145
|
+
val route = Method.GET / "users"
|
|
146
|
+
val matches = route.matches(Method.GET, Path("/users"))
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Structural Operations
|
|
150
|
+
|
|
151
|
+
Three structural operations transform an existing `RoutePattern` without building a new one from scratch: `RoutePattern#alternatives`, `RoutePattern#nest`, and `RoutePattern#render`.
|
|
152
|
+
|
|
153
|
+
### Alternatives
|
|
154
|
+
|
|
155
|
+
`RoutePattern#alternatives` expands `Method.ANY` and `Method.Methods` into a flat list of single-method patterns. `RouteTree` calls this before inserting into the trie:
|
|
156
|
+
|
|
157
|
+
```scala
|
|
158
|
+
import zio.blocks.endpoint._
|
|
159
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
160
|
+
import zio.http.Method
|
|
161
|
+
|
|
162
|
+
val anyGet: RoutePattern[?] = RoutePattern.any(Method.GET)
|
|
163
|
+
val expanded = anyGet.alternatives
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Nesting
|
|
167
|
+
|
|
168
|
+
`RoutePattern#nest` prepends a literal path prefix without modifying the route's type or dynamic segments:
|
|
169
|
+
|
|
170
|
+
```scala
|
|
171
|
+
import zio.blocks.endpoint._
|
|
172
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
173
|
+
import zio.http.Method
|
|
174
|
+
|
|
175
|
+
val route = Method.GET / "users" / PathCodec.int("id")
|
|
176
|
+
val versioned = route.nest(PathCodec("/api/v1"))
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
This is useful for adding a version prefix to a group of existing routes without rewriting each one.
|
|
180
|
+
|
|
181
|
+
### Rendering
|
|
182
|
+
|
|
183
|
+
`RoutePattern#render` produces a human-readable string representation, useful for logging and OpenAPI path generation:
|
|
184
|
+
|
|
185
|
+
```scala
|
|
186
|
+
import zio.blocks.endpoint._
|
|
187
|
+
import zio.blocks.endpoint.RoutePattern._
|
|
188
|
+
import zio.http.Method
|
|
189
|
+
|
|
190
|
+
val route = Method.GET / "users" / PathCodec.int("id")
|
|
191
|
+
val rendered: String = route.render
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
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.
|