plexus-macros
Procedural macros for Plexus RPC activations. The macros are the source of truth for the client generation pipeline: your Rust function signature is the schema.
Overview: macros → schema → clients
#[plexus_macros::activation] Rust macros (this crate)
↓ schemars::schema_for!() + MethodRole / DeprecationInfo metadata
PluginSchema (serde JSON, pinned in plexus-core)
↓ Synapse (Haskell) fetches over Plexus RPC
Synapse IR (deduplicated, compiler-ready)
↓ plexus-codegen-{rust,typescript,python}
Client libraries (type-safe, documented, deprecation-aware)
Everything downstream — generated clients, OpenAPI docs, deprecation warnings in TypeScript IDEs — is derived from what #[plexus_macros::activation] emits into PluginSchema. Do not hand-write schemas; the macro owns them.
Documentation is primary: /// everywhere
/// doc comments are the primary source of every description that reaches the wire. The macros extract them at expansion time and emit them into PluginSchema. Generated clients (TypeScript, Rust, Python) render them as TSDoc / rustdoc / docstrings. Synapse shows them in the CLI tree.
Where you write /// |
Where it appears on the wire |
|---|---|
On the impl block annotated with #[plexus_macros::activation] |
PluginSchema.description |
On each #[plexus_macros::method] fn |
MethodSchema.description |
On each #[plexus_macros::child] fn |
MethodSchema.description (role: StaticChild / DynamicChild) |
On a struct / enum that derives JsonSchema |
JSON Schema description for that type |
| On a field of such a struct | JSON Schema description for that field |
| On an enum variant | JSON Schema description for that variant |
Parameters are the only exception — individual parameter descriptions must use the params(name = "...", other = "...") attr on #[plexus_macros::method] (Rust has no per-argument doc-comment syntax).
Precedence — if both an explicit attr arg and a /// doc comment are present, the explicit attr arg wins. In practice, prefer ///; reach for the attr arg only when you need to diverge (e.g., a deliberately terser wire description than the internal Rust docs).
Minimal example — best practices
The smallest realistic activation. Uses /// for every description, has one method with a params(...) attr, and exposes a static #[plexus_macros::child] into a nested namespace. No description = "..." attr args anywhere — the macros pick up doc comments automatically.
use stream;
use Stream;
use JsonSchema;
use ;
/// A message echoed back to the caller.
/// Echo service — returns messages verbatim and exposes a nested `stats` child.
;
/// Echo messages back verbatim; nested `stats` child exposes call introspection.
/// Call-count introspection for the `echo` service.
;
/// Introspection for the Echo service — invocation counts and timing.
What this demonstrates:
///on theimplblock (line with#[plexus_macros::activation]) suppliesPluginSchema.description. Nodescription = "..."attr arg needed.///on each method fn suppliesMethodSchema.description. Again, nodescription = "..."attr arg.params(message = "...")is the only way to document individual params — Rust has no per-argument doc-comment syntax.#[plexus_macros::child]on a zero-arg accessor fn creates a static child namespace.synapse lforge echo stats totalroutes into the nested activation.- Stream item type derives
JsonSchema, so its variant and field///doc comments flow through to the wire.
Synapse invocations:
Upgrade path from this minimal form: add params(...) as more methods gain documented params; add more #[plexus_macros::child] accessors for nested namespaces; introduce #[derive(PlexusRequest)] + request = ... once you need auth / cookies / per-request context; add #[deprecated] + #[plexus_macros::removed_in] when rotating methods. Each of those is covered in the comprehensive example below.
Comprehensive example
A single realistic activation that exercises every macro in the crate. Every attribute shown is currently supported on 0.5.x. Features that are less common are called out inline.
The example below uses several
description = "..."attr args explicitly — partly for readability in a reference doc, partly because the comprehensive activation description is the easier way to show thelong_descriptioncompanion. In your own code prefer///doc comments per the Documentation is primary section above.
use stream;
use Stream;
use ;
use StandardBidirChannel;
use JsonSchema;
use ;
use Arc;
// -------------------------------------------------------------------
// 1. Stream item type — a plain domain enum.
// The caller-wraps streaming architecture means you do NOT need a
// `StreamEvent` derive. Standard serde + JsonSchema is enough.
// -------------------------------------------------------------------
// -------------------------------------------------------------------
// 2. Typed handles — generated by `#[derive(HandleEnum)]`.
// Handles are opaque strings on the wire; this derive makes them
// typed on the Rust side. `plugin_id_type` pins the instantiation
// when the owning activation is generic (IR-21).
// -------------------------------------------------------------------
// -------------------------------------------------------------------
// 3. Typed request — generated by `#[derive(PlexusRequest)]`.
// This struct describes the per-request HTTP context: cookies,
// headers, query params, peer address, auth context. The
// activation-level `request = CounterRequest` attr below wires it
// into the dispatch path; fields become injectable via
// `#[activation_param]` on method parameters.
// -------------------------------------------------------------------
// -------------------------------------------------------------------
// 4. The activation itself.
// -------------------------------------------------------------------
; // stand-in for the child activation
;
The example above is the full feature surface of plexus-macros 0.5.x. Real activations typically use a subset — see plexus-substrate for concrete, minimally-documented usage (echo and bash are the smallest; cone, claudecode, and solar exercise children, handles, and generics).
Request forwarding: PlexusRequest + request = ...
A typed view of an inbound HTTP request. Without it, your method sees only the RPC arguments; with it, you get cookies / headers / query params / peer address / auth context as first-class Rust fields — extracted, validated, and type-checked at dispatch time.
What it is
plexus_core::request::PlexusRequest is a trait implemented by #[derive(PlexusRequest)]. It has two methods:
RawRequestContext is the raw inputs — http::HeaderMap, Uri, optional AuthContext, optional SocketAddr. The derive generates both extract (run per-call by the dispatch code) and request_schema (included in the activation's PluginSchema with x-plexus-source hints).
Why it exists
Auth tokens live in cookies. CSRF checks need the Origin header. Tenancy lives in query strings. Per-peer rate limits need the socket address. Before PlexusRequest, every activation had to hand-parse this out of a RawRequestContext — boilerplate, inconsistent errors, no schema. With the derive:
- Typed extraction: one
#[from_cookie("...")]replaces several lines of header parsing. - Consistent errors: missing required fields return
PlexusError::Unauthenticated. - Schema metadata: generated clients know which inputs come from cookies vs. body.
- Composition: the same struct is reused across every method of the activation.
Field attributes
| Attribute | Source | Required? | Notes |
|---|---|---|---|
#[from_cookie("name")] |
Named Cookie header value |
Non-Option → required (401 if missing) |
Multi-value cookie headers are parsed correctly. |
#[from_header("name")] |
Named HTTP header | Non-Option → required |
Case-insensitive per http crate. |
#[from_query("name")] |
Named URI query parameter | Non-Option → required |
Uses form_urlencoded::parse. |
#[from_peer] |
ctx.peer (socket addr) |
Option<SocketAddr> recommended |
Transports without peer addr → None. |
#[from_auth_context] |
ctx.auth (AuthContext) |
Option<AuthContext> recommended |
Populated by auth middleware. |
| (no attribute) | PlexusRequestField::extract_from_raw |
Defined by the field type | Use for newtypes like ValidOrigin. |
Option<T> fields treat "absent" as None and never fail; non-Option fields return PlexusError::Unauthenticated on missing input. Parse failures (e.g. a cookie value that cannot parse into the declared type) also return Unauthenticated.
Wiring it into an activation
The activation attribute takes a request = <Type> argument:
Two ways to consume the extracted fields in methods:
-
#[activation_param]— the named parameter is pulled from the request struct by field name. The Rust type must match the declared field type (the macro enforces this):async + Send + 'static -
#[from_auth(expr)]— runs an arbitrary resolver expression against the extracted auth context. Useful when you want a validated user type, not raw auth:async + Send + 'static
To opt a method out of activation-level extraction (e.g. a public health probe), use request = ():
async + Send + 'static
Wire-level: x-plexus-source
The derive augments the generated JSON Schema with a non-standard x-plexus-source key on each property, so generated clients know which input channel to use:
End-to-end example
Given this activation:
A request:
POST /rpc HTTP/1.1
Cookie: access_token=eyJhbGci...
Origin: https://app.example.com
Content-Type: application/json
{ "method": "app.me", "params": {} }
is dispatched as:
- Transport builds a
RawRequestContextfrom headers + URI. - Dispatch calls
AppRequest::extract(&raw)→AppRequest { token: "eyJhbGci…", origin: Some("https://app.example.com") }. - The
tokenfield is threaded intomeas itstokenparameter. meruns; streamed output is serialised back over the transport.
When to use PlexusRequest vs. method params
Rule of thumb:
- Method params — per-call inputs that belong to the business logic:
item_id,query,body. PlexusRequest+#[activation_param]— per-request context that crosses every method: auth tokens, session cookies, tenancy, peer addr.
If a value would be the same for every method call in a request, it belongs in the request struct.
Per-macro reference
#[plexus_macros::activation(...)]
Attached to an impl block to generate the activation's Activation trait impl, method enum, RPC trait, and (when #[child] methods are present) ChildRouter impl.
| Arg | Type | Default | Notes |
|---|---|---|---|
namespace = "..." |
string | (required) | Activation namespace. Appears as plugin_schema.namespace. |
version = "..." |
string | CARGO_PKG_VERSION |
Semver; used by handles and deprecation schemas. |
description = "..." |
string | (none) | Max 15 words — macro errors on overflow. Use long_description for more. |
long_description = "..." |
string | (none) | Unbounded documentation; projected into PluginSchema. |
request = MyType |
type | (none) | Typed request struct; see the Request forwarding section. |
resolve_handle |
flag | (off) | Generates Activation::resolve_handle that delegates to self.resolve_handle_impl(handle). You implement the impl. |
crate_path = "..." |
string | auto (via proc-macro-crate) |
Override the plexus-core import path. Rarely needed after CHILD-6. |
plugin_id = "UUID" |
string | deterministic UUIDv5 from namespace | Explicit plugin UUID. Must parse as a UUID. |
namespace_fn = "method" |
string | (none) | Advanced: generate namespace() as a method call instead of a constant. |
hub |
flag | (off, deprecated) | Deprecated — hub mode is inferred from #[child] methods. Removed in 0.6. |
children = [ident, ...] |
ident list | (empty, deprecated) | Deprecated — use #[child] methods. Removed in 0.6. |
#[plexus_macros::method(...)]
Attached to methods inside an #[activation] impl. Marks the method as an RPC endpoint and collects schema metadata.
| Arg | Type | Default | Notes |
|---|---|---|---|
description = "..." |
string | first /// doc-comment block (CHILD-5) |
Explicit arg wins over doc comments. |
name = "..." |
string | method identifier | Override the RPC name without renaming the Rust fn. |
streaming |
flag | (off) | Advertises the method as AsyncGenerator-shaped in generated clients. |
bidirectional |
flag | (off) | Method uses StandardBidirChannel. Use bidirectional(request = "...", response = "...") for custom types. |
http_method = "..." |
string | (none) | One of GET|POST|PUT|DELETE|PATCH. Hint for REST generators. |
params(name = "doc", ...) |
key/value list | (empty) | Per-parameter descriptions. Less common now that field-level /// extraction works; retained for cases where parameters have no natural Rust doc site. |
returns(Variant, ...) |
ident list | (all variants) | Filter the return enum to named variants. Rare. |
request = () |
tuple | (inherits) | Skip activation-level request extraction for this method (public endpoint escape hatch). |
request = OtherType |
type | (inherits) | Reserved — per-method request type override is not yet wired end-to-end. |
Method parameter attributes (applied inside the fn signature):
#[activation_param]— inject a field from the activation-level request struct; name + type must match.#[from_auth(expr)]— callexpr(&auth_context)and bind the result; useful for validated user types.
#[plexus_macros::child] / #[plexus_macros::child(list = "...", search = "...")]
Attached to methods inside an #[activation] impl to register them in the generated ChildRouter. Two accepted shapes:
| Shape | Role | Notes |
|---|---|---|
fn NAME(&self) -> Child |
Static child | Routing name is the method identifier. Sync or async. |
fn NAME(&self, name: &str) -> Option<Child> |
Dynamic fallback | Called for any name not matched by a static child. Sync or async. |
Optional arguments on dynamic #[child]:
| Arg | Type | Advertises capability | Sibling method shape |
|---|---|---|---|
list = "method" |
ident | ChildCapabilities::LIST |
(async) fn METHOD(&self) -> impl Stream<Item = String> (or BoxStream<'_, String>) |
search = "method" |
ident | ChildCapabilities::SEARCH |
(async) fn METHOD(&self, query: &str) -> impl Stream<Item = String> |
Sibling methods are validated at macro-expansion time — typos in list = "..." or a wrong signature become compile errors pointing at the attribute.
#[child] methods do not appear as RPC endpoints in the schema; they contribute to routing only. The presence of any #[child] method flips the activation into hub mode (replacing the deprecated hub flag).
#[plexus_macros::removed_in("X")]
Companion to Rust's built-in #[deprecated]. Carries the removal-version field that rustc's #[deprecated] doesn't accept.
async + Send + 'static
The macro writes this into MethodSchema.deprecation as DeprecationInfo { since, removed_in, message }. Without the companion attribute, removed_in defaults to the string "unspecified".
Applying #[plexus_macros::removed_in] to a method without #[deprecated] is a compile error.
#[derive(PlexusRequest)]
See Request forwarding for the full treatment. Generates:
impl PlexusRequest for YourStruct(extraction + schema).impl schemars::JsonSchema for YourStructusingplexus_core::__schemars(no directschemarsdependency required from the caller).
Supported field attributes:
#[from_cookie("name")] #[from_header("name")] #[from_query("name")]
#[from_peer] #[from_auth_context]
Fields without any source attribute must implement PlexusRequestField — used for newtype wrappers that carry their own validation.
#[derive(HandleEnum)]
Generates to_handle(), impl TryFrom<&Handle>, and impl From<_> for Handle for an enum that represents the set of typed handles an activation issues.
Enum-level #[handle(...)] arguments:
| Arg | Required? | Notes |
|---|---|---|
plugin_id = "CONST_OR_PATH" |
yes | Path to the activation's plugin UUID constant — e.g. "Counter::PLUGIN_ID" or "MY_PLUGIN_ID". |
version = "X.Y.Z" |
yes | Semver for handles emitted by this enum. |
plugin_id_type = "Type" |
no | Concrete instantiation to qualify plugin_id when the owning activation is generic (e.g. "Cone<NoParent>"). Fixes E0283 ambiguity. |
crate_path = "..." |
no ("plexus_core") |
Override import path. |
Variant-level #[handle(...)] arguments:
| Arg | Required? | Notes |
|---|---|---|
method = "..." |
yes | The handle.method value on the wire. |
table = "..." |
no | SQLite table name (consumed by future handle-resolution tooling). |
key = "..." |
no | Primary key column. |
key_field = "..." |
no | Which variant field holds the key. Defaults to the first field. |
strip_prefix = "..." |
no | Strip this prefix before looking up by key. |
#[derive(StreamEvent)] — deprecated
Kept for backward compatibility. The caller-wraps streaming architecture means stream-item enums should just use Serialize + Deserialize + JsonSchema (standard derives). Do not reach for StreamEvent in new code.
#[derive(JsonSchema)] (compat shim)
Only available when plexus-macros is compiled with the schemars-compat feature and aliased as schemars in a crate's dev-dependencies. Produces a no-op impl JsonSchema, preventing a duplicate-impl conflict with the impl JsonSchema generated by #[derive(PlexusRequest)] when test code writes #[derive(PlexusRequest, schemars::JsonSchema)].
Library crates almost never need this directly; the companion crate plexus-schemars-compat is the public entry point.
Migration from deprecated surfaces
All deprecations in plexus-macros 0.5 continue to compile with warnings through 0.5.x and are removed in 0.6.
#[hub_methods] → #[plexus_macros::activation]
Before:
After:
Identical semantics. #[hub_methods] emits a deprecation warning pointing at the replacement.
#[hub_method] → #[plexus_macros::method]
Before:
async
After:
async
hub = true / children = [...] → #[plexus_macros::child] methods
Before:
After:
Hub mode is now inferred from the presence of #[child] methods (CHILD-8). The hub flag and children = [...] list emit deprecation warnings and are scheduled for removal in 0.6.
Output pipeline
The macro output feeds the same pipeline today as it did in 0.4, but with richer metadata:
#[plexus_macros::activation] (this crate)
↓ emits
PluginSchema { methods: [MethodSchema { role, deprecation, params, ... }] }
↓ served over Plexus RPC (plexus-transport)
Synapse (Haskell) fetches, deduplicates, builds Synapse IR
↓ feeds
plexus-codegen-{rust,typescript,python}
↓ produces
Generated clients with typed methods, streaming iterators, and
deprecation warnings that mirror your `#[deprecated]` annotations
See the substrate reference server (plexus-substrate) for end-to-end examples of every feature in production use:
bash,echo— minimal single-method activations.cone,claudecode— generic activations withHandleEnum, dynamic children, andresolve_handle.solar— hub activation with dynamic child routing +listcapability.interactive— bidirectional channels.changelog,mustache— multi-method flat activations.
License
MIT