diff --git a/unshell-macros/ABOUT.md b/unshell-macros/ABOUT.md new file mode 100644 index 0000000..6f0a690 --- /dev/null +++ b/unshell-macros/ABOUT.md @@ -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. diff --git a/unshell-macros/src/lib.rs b/unshell-macros/src/lib.rs index 09fefc6..17ae581 100644 --- a/unshell-macros/src/lib.rs +++ b/unshell-macros/src/lib.rs @@ -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(