Skip to content

Unify the manual path behind one Handle trait and rewrite the macros onto the same rails - #274

Merged
powersemmi merged 18 commits into
mainfrom
docs/cursor-rules-refresh
Sep 3, 2026
Merged

Unify the manual path behind one Handle trait and rewrite the macros onto the same rails#274
powersemmi merged 18 commits into
mainfrom
docs/cursor-rules-refresh

Conversation

@powersemmi

@powersemmi powersemmi commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Description

What began as a docs refresh became the 0.7 manual-path redesign, landed here in three layers.

The docs groundwork (the original branch): every guide shows its macro snippet next to a macro-free counterpart, the snippets are extracted from compiled examples and tests, and the Cursor rules are brought back in line with the real API.

The unified manual path (a44ccbe, designed in #275 with its amendment comments): one body trait Handle<In, R = (), O = (), C = (), S = ()> with no associated types, whose verdict is computed from the axes - Result<(), HandlerOutcome> for a plain body, Result<R, HandlerOutcome> for a reply, and the page forms with per-element vectors. HandlerOutcome is the public outcome type carrying the post-settle continuation; the plain status enum is crate-private. The input axis spells batch, raw, and typed headers (Message<Headers, Payload>, decoded by the core under the decode failure policy); the injections arena is typed, built statically by the include-site .out(..) chain, with a mandatory capability bound per marker and direct naming of broker-defined live values; seek is read through the broker's context keys (ctx.context(SeekHandle)), and batch bodies reach it through the page context while positions travel in element headers. Mounting is subscriber(source, body) plus the settings chain, include is the single mount surface (the raw subscribe family is gone), and documentation is on by default under asyncapi with a per-registration .undocumented() opt-out - the recorded deliberate exception to feature additivity.

The macros on the same rails (ff94dd8): the attribute expands into exactly the Handle impl and subscriber(..) chain a manual service writes, so both paths ride one mechanism. The batch(..) and raw clauses are gone - the form is read from the payload type. The Deserialized / Serialized pair selects the codec-bypassing lanes by the type, uniformly for inputs, replies, and the typed publish entries (out.message(&wire) ships a Serialized dictionary member's bytes as they are); the mnemonic is pinned by UI tests in both typo directions, and a codec named on a serialized wire does not compile. The legacy definition-trait emission is deleted and the surface it kept public is crate-private again.

Breaking throughout, deliberately inside the unreleased 0.7 line; the broker 0.7 branches are also unmerged, so the fleet adopts the final shape in one pass, and this branch is the base the broker-adaptation agents will build against. The parity test mounts every axis combination on both surfaces, so coverage is checked by the compiler.

Fixes #275

Type of change

  • Documentation (typos, code examples, or any documentation updates)
  • New feature (a non-breaking change that adds functionality)
  • Breaking change (removing or renaming public API, changing a signature, tightening bounds, or
    raising the MSRV - a minor version bump pre-1.0)

The removals land inside the unreleased 0.7 line, so no extra version bump is taken.

Checklist

  • My code follows the project's style guidelines (just check passes: rustfmt, clippy, and
    cargo check with all features and with --no-default-features)
  • I have performed a self-review of my own code
  • I have made the necessary changes to the documentation (rustdoc and/or the docs site)
  • My changes generate no new warnings (clippy runs with -D warnings)
  • I have added tests that validate my fix or new feature
  • New and existing tests pass locally (just test)
  • Public items carry a compiling # Examples doctest, and user-facing changes are reflected in
    examples/ where applicable

The rules under .cursor/rules had drifted past several redesigns and were
teaching APIs that no longer exist: include_publishing / include_on for
mounting, a publisher built from broker.publisher() at the include site, and an
interior-mutability broker whose connect took &self.

Rewritten against the examples and the broker-authors guide: include is the one
mounting call with its .publisher / .out / .mount chain, publishing goes through
the message builder and the Out slots, the broker contract is the consuming
lifecycle ladder with PublishPolicy and DefaultPublish, and the header types
carry their current names. The core rules also gained the compile-time
invariants ladder, which the project treats as a standing convention and the
file did not mention at all.
The usage rules spelled out an explicit import list in every snippet, which is
not how a service on this crate is meant to be written: the prelude exists to be
the one glob, and on a broker crate globbing that crate's prelude is the
service's broker statement.

Every snippet now opens on the glob, and only what genuinely sits outside it
stays explicit - the in-core broker, codecs, the optional feature modules, and
the few runtime items the prelude deliberately leaves out. Swapping the broker
reads as swapping the glob, which is the point.
…sh surface

The snippets returned the bare `HandlerResult::Ack` variant, which reads
differently from every nack and does not chain, so they now use `ack()` next to
`retry()`, `drop()` and `retry_after(..)`.

Adds what the rules had no entry for at all: post-settle continuations
(`and_after`, its `Settle` return and its at-most-once contract), the `Out`
slot's typing surface end to end - a marker per publishing path, and the third
position narrowing one path to a single type, a tuple or a named
`#[derive(OutMessages)]` set - the three destination positions a message
declaration can open, including the name template's per-placeholder setters, and
raw payloads with their `publish_raw` reply.
The startup-publish snippet wrapped the policy in a TypedPublisher for no
reason, which reads as if the wrapper were part of the call. It is not: the hook
takes a publish policy and is handed the live publisher, and every Publisher
already has the typed and byte builders through PublishExt, so the broker's own
policy is the ordinary case.

Expanded into its own section that says what the two arguments are, when a
wrapper is actually called for (a non-default codec, a per-publisher transform,
a transactional seed) and how the hook's single error type shapes the body.
The usage rules assume the macros feature throughout, which leaves an agent
working in a service without it with nothing to go on. This file writes the same
service by hand: a closure handler, the typed wrapper for decoding, subscribe /
handle registration with HandlerMetadata, and a hand-written main.

It also draws the line explicitly. Extractors, the reply clauses, the Out slots
and the mount-site settings are carried by the definition the attribute mints,
so they go with it; state, headers and publishing keep working through the same
objects the extractors would have handed over. The declarations that have no
macro-free equivalent are listed as such rather than left to be discovered.
The macro-free examples used closures, which is not what #[subscriber] expands
to and reads as a second, lesser style. The attribute emits a unit struct plus
an `impl Handler` whose `async fn handle` carries the body, so the rules now
show that: a named type, and whatever a closure would capture held as a field.

The two impls the attribute adds on top, `Declared` and `SubscriberDef`, exist
to feed `include`, so the hand-written path stops after `Handler` and registers
with `subscribe` / `handle` instead. Adds the layer shape, which is the same
named-type form one level up, and the `IntoSettle` conversion the attribute
inserts around the body but a hand-written impl has to do itself.

Every form here was compiled against the crate before being written down.
The file claimed the mount-site settings, the Out slots, the reply forms and
the derive-fed schemas had no hand-written form, and told the reader to stop
after `impl Handler`. That is wrong: the attribute is sugar over public API and
says so in its own expansion, where `SubscriberBuilder::new` is documented as
the call a hand-written definition makes from its own `Declared` impl, and
`SubscriberSettings` is blanket-implemented for every `Declared`.

Reframed as a choice of how far to write it out: stop at `Handler` and register
with subscribe / handle, or implement the definition too and mount with
`include`, settings and slots included. The derives get the same treatment, with
a pointer to the expansion as the place the whole mapping is written down.
Two fixes the compiler found, both to claims I had written down without
checking. Implementing IncludeDef mounts a hand-written definition but gives it
no settings: the blanket Declared it routes through sets Settings = Self, and
the steps are implemented for SubscriberBuilder, so the route that actually
carries settings is a hand-written Declared whose Settings is a builder, which
is also verbatim what the expansion emits.

The manual quickstart also tripped clippy's unused_async_trait_impl, since a
hand-written handler that awaits nothing is not async. Fixed by the shape the
workspace already settled on for this lint, never an allow: the future returned
directly with ready(..), keeping async fn where the body does await.
Router carries workers(..) where BrokerScope carries nothing, and
subscribe_batch is the macro-free typed-batch path and does resolve the chain
codec, unlike subscribe. A raw batch has no such path and needs the definition,
which is worth saying where a reader would otherwise go looking.
…p honestly

The rules now say the Declared and IncludeDef routes are mutually exclusive (a
direct impl conflicts with the blanket one) and that the builder route is the
one that scales, since SubscriberBuilder forwards every def trait including the
slot binding.

Also fixes a manifest entry that was never true: metrics_http does not build
under its own declared required-features, because the reply publisher needs a
codec and none was named. It compiled only because json is a default feature and
CI does not build examples with defaults off.
Twenty-eight examples that build the same services without the macros feature,
each mirroring the section markers of its macro original so the guides can show
the two side by side. None of them requires `macros`, which is what proves the
hand-written path stands on its own rather than being described as if it did.

They are the expansion written out, not a lesser imitation: the definitions
carry SubscriberDef and a Declared whose Settings is a builder, the derives
become OutgoingDestination, MessageHeaders, OutSlot and OutMessages impls, the
reply forms become PublishingDef and PublishingCall, and the injected slots and
seekers become HasSlots, BindSlots and InjectDef. The name-template address
builder is written out too, so a forgotten segment stays a compile error.
Each guide now carries the two forms as content tabs, so a reader on either
path sees the code they would actually write. Only snippets that genuinely
differ are tabbed: where a section is byte-identical between the two examples
(a Layer impl, a Codec impl, a plain axum route), one fence still says it all.

Two defects surfaced while doing it and are fixed here. The snippet-title hook
anchored its fence pattern at column zero, so every fence nested in a tab lost
its filename header and its link to the source; it now matches an indented
fence and 486 tabbed snippets keep their header. And the typed-headers pages
had never fenced their snippets at all, so the inserted Rust was parsed as
markdown and `#[derive(..)]` rendered as a heading; they are fenced now.
…om tests

Seven test files mirroring the sections the docs embed from tests/, so those
pages can show both forms like the rest. They are tests, not illustrations:
all seven pass, and each keeps its original feature gate minus `macros`, so the
raw one still builds with no codec feature at all.

Records one consequence of the shape the unused-async lint forces. The
dispatcher calls the handler and only then wraps the returned future in
catch_unwind, so a panic raised before the future is built escapes the guard
and kills the task instead of settling by on_failure(panic = ..). A body that
can panic has to stay inside the async block.
Completes the pass: the testing, tracing, slot-extension and raw-subscriber
snippets now show both forms like every other page. Three of the twelve stay a
single fence because their sections carry no macro either way - they are drive
and assertion code, identical in both files.
@powersemmi powersemmi changed the title docs: bring the Cursor rules back in line with the current API docs: legitimize the macro-free path, and bring the Cursor rules back in line with the API Aug 29, 2026
@powersemmi powersemmi self-assigned this Aug 29, 2026
…r constructor

Squash of feat/value-level-defs (PR #276), designed in issue #275. One
body trait with verdicts computed from the axes (HandlerOutcome carrying
the post-settle hook), the typed injections arena bound at the include
site, seek values read through the broker's context keys, typed header
pairs on the input axis, and include as the only mount surface in place
of the raw subscribe family.
…by type

Squash of feat/macro-on-handle (PR #279). The attribute expands into the
same Handle impl and subscriber chain a manual service writes; the batch
and raw clauses and the legacy definition-trait emission are gone; the
Deserialized and Serialized pair selects the codec-bypassing lanes for
inputs, replies, and the typed publish entries; batch bodies reach the
seek handle through the page context.
@powersemmi powersemmi added documentation Improvements or additions to documentation enhancement New feature or request labels Sep 3, 2026
@powersemmi powersemmi changed the title docs: legitimize the macro-free path, and bring the Cursor rules back in line with the API Unify the manual path behind one Handle trait and rewrite the macros onto the same rails Sep 3, 2026
The unified Handle rails and the macro rewrite landed with their dispatch
adapters, injection arena, reply cells, codec-override steps, and TestApp
edges exercised only by the type-level parity module, leaving the line gate
short. Drive those cells far enough to settle: the capped page chunking,
refused payload constructions under every decode policy, the pair input
cells with and without slots and replies, a typed-headers pair reply
through a TypedPublisher, the per-registration codec override on every
input kind, the placeholder sources and diagnostics of the sealed
definitions, and the harness's addressing mistakes, drain, and teardown.
@powersemmi
powersemmi merged commit 6a99e01 into main Sep 3, 2026
17 checks passed
@powersemmi
powersemmi deleted the docs/cursor-rules-refresh branch September 3, 2026 01:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unify the manual path behind one Handle trait and one subscriber constructor

1 participant