mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Document the macro system architecture
This commit is contained in:
@@ -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.
|
||||||
@@ -8,6 +8,25 @@ mod utils;
|
|||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use syn::{DeriveInput, ItemImpl, parse_macro_input};
|
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))]
|
#[proc_macro_derive(Leaf, attributes(leaf))]
|
||||||
pub fn derive_leaf(input: TokenStream) -> TokenStream {
|
pub fn derive_leaf(input: TokenStream) -> TokenStream {
|
||||||
match leaf::expand_leaf(parse_macro_input!(input as DeriveInput)) {
|
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))]
|
#[proc_macro_derive(Procedure, attributes(procedure))]
|
||||||
pub fn derive_procedure(input: TokenStream) -> TokenStream {
|
pub fn derive_procedure(input: TokenStream) -> TokenStream {
|
||||||
match procedure::expand_procedure(parse_macro_input!(input as DeriveInput)) {
|
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]
|
#[proc_macro_attribute]
|
||||||
pub fn procedures(attr: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn procedures(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
match procedures::expand_procedures(
|
match procedures::expand_procedures(
|
||||||
|
|||||||
Reference in New Issue
Block a user