Unify the manual path behind one Handle trait and rewrite the macros onto the same rails - #274
Merged
Conversation
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.
…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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 traitHandle<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.HandlerOutcomeis 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 issubscriber(source, body)plus the settings chain,includeis the single mount surface (the raw subscribe family is gone), and documentation is on by default underasyncapiwith 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 theHandleimpl andsubscriber(..)chain a manual service writes, so both paths ride one mechanism. Thebatch(..)andrawclauses are gone - the form is read from the payload type. TheDeserialized/Serializedpair selects the codec-bypassing lanes by the type, uniformly for inputs, replies, and the typed publish entries (out.message(&wire)ships aSerializeddictionary 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
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
just checkpasses: rustfmt, clippy, andcargo checkwith all features and with--no-default-features)-D warnings)just test)# Examplesdoctest, and user-facing changes are reflected inexamples/where applicable