Document the macro system architecture

This commit is contained in:
Michael Mikovsky
2026-04-26 13:43:06 -06:00
parent 990be30232
commit fccd61ea29
2 changed files with 158 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
# UnShell Macros
This crate owns the compile-time declaration layer for UnShell application-facing
leaves.
## Purpose
The protocol crate intentionally stays generic: it knows how to route packets,
validate framing, and deliver local events, but it should not need handwritten
registration code for every leaf.
The macro layer exists to move as much of that registration work as possible to
compile time.
In practical terms, the macro system is responsible for:
- deriving canonical leaf identities
- deriving canonical procedure identifiers
- generating compile-time procedure inventories for leaves
- binding one leaf declaration to separate endpoint and TUI host structs without
repeating the metadata on each host
- generating dispatch glue for simple call-driven leaves
## Model
There are three layers in the intended design.
### 1. Leaf declaration
One declaration is the source of truth for one protocol leaf.
The declaration answers:
- what is this leaf called on the wire?
- which procedure suffixes belong to it?
- which host structs implement its endpoint and TUI roles?
The goal is that this information is written once and reused everywhere.
### 2. Host structs
One leaf can have multiple host structs with different responsibilities.
- the endpoint host owns runtime state and protocol-side behavior
- the TUI host owns user-interface state and interpretation behavior
Those hosts should not each have to repeat the leaf name or procedure inventory.
They bind to the declaration instead.
### 3. Procedure and method metadata
Procedures and future typed remote methods need stable canonical identifiers.
The macro layer generates those identifiers from the leaf declaration and the
local suffix for each procedure or method. That lets the runtime consume a
compile-time inventory instead of handwritten lists.
## Current direction
The current migration keeps the older derive-based APIs working while adding a
new declaration-first API.
That migration is intentionally incremental:
1. keep `#[derive(Leaf)]`, `#[derive(Procedure)]`, and `#[procedures]` working
2. introduce one declaration macro for compile-time leaf metadata
3. let endpoint and TUI structs bind to the declaration instead of duplicating
metadata
4. remove leaf-owned endpoint-construction boilerplate by generating leaf specs
from the declaration
5. add typed remote-method metadata on top of the same declaration model
## Design constraints
The system is optimized for a few constraints that matter to this repository.
- compile-time declaration should replace handwritten runtime registration where
possible
- protocol-visible names should remain deterministic and canonical
- generated code should stay explicit enough to debug
- endpoint and TUI roles should share metadata but not be forced into the same
runtime trait when their behavior differs
- migration should be low-breakage for the existing examples and tests
## Non-goals
This crate does not own transport, connection management, or packet execution.
Those remain in `unshell-protocol` and higher application layers.
The macro crate should generate metadata and glue, not hide the runtime model.
+68
View File
@@ -8,6 +8,25 @@ mod utils;
use proc_macro::TokenStream;
use syn::{DeriveInput, ItemImpl, parse_macro_input};
/// Derives canonical protocol-leaf identity helpers for one host type.
///
/// What it is: a derive macro that implements `ProtocolLeaf` and generates the
/// `protocol_leaf_name()` convenience method.
///
/// Why it exists: simple leaves and compatibility paths still need a lightweight
/// way to say "this host type exposes this canonical wire name" without writing
/// the trait implementation by hand.
///
/// # Example
/// ```ignore
/// use unshell::Leaf;
///
/// #[derive(Leaf)]
/// #[leaf(leaf_name = "echo")]
/// struct EchoLeaf;
///
/// assert!(EchoLeaf::protocol_leaf_name().contains("echo"));
/// ```
#[proc_macro_derive(Leaf, attributes(leaf))]
pub fn derive_leaf(input: TokenStream) -> TokenStream {
match leaf::expand_leaf(parse_macro_input!(input as DeriveInput)) {
@@ -16,6 +35,28 @@ pub fn derive_leaf(input: TokenStream) -> TokenStream {
}
}
/// Derives canonical stateful-procedure metadata for one procedure type.
///
/// What it is: a derive macro that records one procedure suffix and generates
/// the canonical `protocol_procedure_id()` helper for that procedure.
///
/// Why it exists: hook-backed procedures need one stable `procedure_id`, but the
/// runtime should not require each procedure to handwrite the identifier logic.
///
/// # Example
/// ```ignore
/// use unshell::{Leaf, Procedure};
///
/// #[derive(Leaf)]
/// #[leaf(leaf_name = "shell")]
/// struct ShellLeaf;
///
/// #[derive(Procedure)]
/// #[procedure(leaf = ShellLeaf, name = "open")]
/// struct OpenSession;
///
/// assert!(OpenSession::protocol_procedure_id().ends_with(".open"));
/// ```
#[proc_macro_derive(Procedure, attributes(procedure))]
pub fn derive_procedure(input: TokenStream) -> TokenStream {
match procedure::expand_procedure(parse_macro_input!(input as DeriveInput)) {
@@ -24,6 +65,33 @@ pub fn derive_procedure(input: TokenStream) -> TokenStream {
}
}
/// Generates dispatch glue for a simple call-driven leaf impl block.
///
/// What it is: an attribute macro placed on one `impl` block whose `#[call]`
/// methods define the callable surface for that leaf.
///
/// Why it exists: one-shot leaves should be able to declare a small RPC-like API
/// on ordinary Rust methods while still producing the canonical procedure list
/// and dispatch logic expected by the protocol runtime.
///
/// # Example
/// ```ignore
/// use unshell::{Leaf, procedures};
///
/// #[derive(Leaf)]
/// #[leaf(id = "org.example.v1.echo")]
/// struct EchoLeaf;
///
/// #[procedures(error = core::convert::Infallible)]
/// impl EchoLeaf {
/// #[call]
/// fn echo(&mut self, input: String) -> String {
/// input
/// }
/// }
///
/// assert!(EchoLeaf::protocol_procedure_id("echo").is_some());
/// ```
#[proc_macro_attribute]
pub fn procedures(attr: TokenStream, item: TokenStream) -> TokenStream {
match procedures::expand_procedures(