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.
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.
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