-
Notifications
You must be signed in to change notification settings - Fork 127
Description
Motivation
ZIO HTTP's Endpoint type currently mixes pure endpoint description (what method, what path, what query params, what the body looks like, what the response shape is) with runtime en/decoding concerns (EncoderDecoder, HttpContentCodec, StringSchemaCodec, CodecConfig). This makes the endpoint type non-portable — it can't be used without pulling in the full zio-http runtime.
We propose a new zio-blocks-endpoint module containing the pure data types that describe an endpoint's shape. The runtime layer (actual serialization, handler binding, request/response construction) stays in zio-http and interprets these descriptors.
This is the companion to #1113 (zio-blocks-http), which provides the pure HTTP data model (Request, Response, Headers, etc.). This module builds on top of it to describe typed endpoints.
Dependencies
zio-blocks-http (Method, Status, Path, MediaType, ...) — #1113
zio-blocks-schema (Schema[A], Reflect, Doc)
zio-blocks-combinators (Combiner, Alternator — PR #948)
zio-blocks-mediatype (MediaType)
zio-blocks-endpoint (depends on all above)
Design Principles
- Same names as zio-http —
Endpoint,PathCodec,HttpCodec,AuthType, notEndpointDescriptoretc. The blocks types ARE the canonical types.zio.blocks.endpoint.Endpointis clear from the package. - No functions in the data —
TransformOrFailis excluded for now (contains opaqueA => Either[String, B]). May be added back later in a blocks-style implementation. - No
Annotatedwrapper nodes — follow the blocksReflectpattern wheredoc,examples,defaultare direct fields on each node, not wrapping AST nodes. - No encode/decode logic — pure description only. The runtime interpreter lives in zio-http.
- No ZIO dependency
Components
| # | Type | Purpose | Key Design Decisions |
|---|---|---|---|
| 1 | Endpoint |
Top-level endpoint description | route: RoutePattern[PathInput], input: HttpCodec[Input], output: HttpCodec[Output], error: HttpCodec[Err], auth: AuthType, doc: Doc. No implement* methods, no Invocation, no codec errors. |
| 2 | RoutePattern[A] |
Method + path pattern | method: Method, pathCodec: PathCodec[A]. No -> handler binding. |
| 3 | PathCodec[A] |
Path matching pattern AST | Sealed trait: Segment, Concat, Annotated. No mutable _optimize cache — pure AST only, compilation to fast matcher is done by the runtime. No TransformOrFail. No Fallback (rarely used, adds complexity). |
| 4 | SegmentCodec[A] |
Single path segment descriptor | Empty, Literal(value), IntSeg(name), LongSeg(name), StringSeg(name), UUIDSeg(name), BoolSeg(name), Trailing. Pure names + types, no decoding logic. |
| 5 | HttpCodec[A] |
Composable typed descriptor for request/response parts | Sealed trait AST with combinator nodes: Empty, Combine(left, right, combiner), Fallback(left, right, alternator). Atom types listed below. No TransformOrFail. No Annotated wrapper — metadata is on the atoms directly. |
| 6 | QueryCodec[A] |
Query parameter descriptor (atom) | name: String, schema: Schema[A], default: Option[A], doc: Doc, examples: Map[String, A], deprecated: Option[Doc]. |
| 7 | HeaderCodec[A] |
Header descriptor (atom) | name: String, schema: Schema[A], default: Option[A], doc: Doc, examples: Map[String, A], deprecated: Option[Doc]. |
| 8 | BodyCodec[A] |
Body content descriptor (atom) | schema: Schema[A], mediaTypes: Set[MediaType], name: Option[String], doc: Doc, examples: Map[String, A], deprecated: Option[Doc]. Describes what, not how to encode. |
| 9 | StatusCodec[A] |
Status code descriptor (atom) | Specified(status: Status) or Unspecified. With doc, examples fields. |
| 10 | AuthType |
Authentication scheme descriptor | Sealed trait: None, Basic, Bearer, Digest, Custom(headerName: String, schema: Schema[A]), Or(left, right), Scoped(inner, scopes: List[String]). Pure description, no codec wiring. |
| 11 | Combiner[L, R] |
Tuple composition | From PR #948 — not defined here, just used. |
| 12 | Alternator[L, R] |
Sum composition | From PR #948 — EitherAlternator / UnionAlternator. Not defined here, just used. |
| 13 | Doc |
Documentation | From zio-blocks-schema — not defined here, just used. |
Metadata Design: Fields, Not Wrappers
Following the pattern established by zio.blocks.schema.Reflect, where doc, examples, and defaultValue are direct fields on every node (not wrapping Annotated AST nodes):
| zio-http (wrapper pattern) | zio-blocks-endpoint (field pattern) |
|---|---|
HttpCodec.Annotated(codec, Metadata.Documented(doc)) |
QueryCodec("name", schema, doc = Doc.p("the name")) |
HttpCodec.Annotated(codec, Metadata.Examples(map)) |
QueryCodec("name", schema, examples = Map("ex1" -> "John")) |
HttpCodec.Annotated(codec, Metadata.Named(name)) |
Already covered by atom's name field |
HttpCodec.Annotated(codec, Metadata.Optional()) |
default: Option[A] on the atom — absence is OK when default is present. For Schema[Option[A]], absence naturally produces None. No separate flag needed. |
HttpCodec.Annotated(codec, Metadata.Deprecated(doc)) |
deprecated: Option[Doc] field on atom |
Benefits:
- No AST depth just for metadata — cleaner, faster traversal
- Self-describing — default values are inspectable (useful for OpenAPI
default:field) - Consistent with how
Reflect.Record,Reflect.Variant, etc. already work in zio-blocks-schema
Open Design Question: HttpCodec AST Shape
The combinator nodes (Empty, Combine, Fallback) are shared between request and response codecs, but the atom types differ:
Request atoms: QueryCodec, HeaderCodec, BodyCodec, MethodCodec
Response atoms: StatusCodec, HeaderCodec, BodyCodec
Three options under consideration:
| Option | Shape | Trade-off |
|---|---|---|
| A: Two separate ASTs | RequestCodec[A] and ResponseCodec[A], each with own atom types |
Clean type separation. Duplicates combinator nodes (Combine, Fallback). |
| B: One generic AST | HttpCodec[Atoms, A] parameterized by atom kind |
No duplication. More abstract — atoms discriminated by phantom/union type parameter. |
| C: Unified AST with tags | Single HttpCodec[AtomTypes, A] with phantom type tags (like zio-http today) |
Proven pattern. Tags leak complexity into API. |
Input welcome on which approach fits best with the blocks design philosophy.
What Stays in zio-http
| Component | Why It Stays |
|---|---|
EncoderDecoder |
Interprets descriptor AST into actual request/response encode/decode |
HttpContentCodec[A] |
Maps MediaType → BinaryCodec[A] — actual serialization |
StringSchemaCodec |
Runtime query/header string codec |
Endpoint.implement* |
Binds handler to endpoint — produces Route |
EndpointExecutor / EndpointClient |
Runtime execution machinery |
CodecConfig |
Runtime codec configuration |
Invocation |
Runtime: pairs endpoint with actual input values |
| OpenAPI generation | Reads the descriptor AST, produces OpenAPI spec |
PathCodec _optimize / compiled matcher |
Runtime optimization compiled from the pure PathCodec AST |
Non-Goals
- No encode/decode logic — pure description only
- No ZIO dependency — no effects anywhere
- No
TransformOrFail— excluded for now. Functions in data make it non-inspectable/non-serializable. May add back later in a blocks-idiomatic way. - No
PathCodec.Fallback— rarely used, adds AST complexity - No OpenAPI generation — stays in zio-http (or a separate module)
- No server/client runtime — no
Handler,Route,Server,Client