mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Support module-inferred leaf hosts
This commit is contained in:
@@ -19,11 +19,8 @@ struct EchoLeaf {
|
|||||||
sessions: BTreeMap<HookKey, EchoOpen>,
|
sessions: BTreeMap<HookKey, EchoOpen>,
|
||||||
}
|
}
|
||||||
|
|
||||||
leaf! {
|
#[leaf(id = "org.example.v1.echo", procedures = [EchoOpen], endpoint_struct = EchoLeaf)]
|
||||||
id = "org.example.v1.echo",
|
struct Echo;
|
||||||
procedures = [EchoOpen],
|
|
||||||
endpoint_struct = EchoLeaf,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
struct EchoRequest {
|
struct EchoRequest {
|
||||||
|
|||||||
@@ -19,15 +19,12 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
agent_path(),
|
agent_path(),
|
||||||
Some(Vec::new()),
|
Some(Vec::new()),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![remote_shell::endpoint::RemoteShellEndpoint::protocol_leaf_spec()],
|
vec![remote_shell::endpoint::RemoteShell::protocol_leaf_spec()],
|
||||||
);
|
);
|
||||||
let mut runtime = ProcedureRuntime::<
|
let mut runtime = ProcedureRuntime::<
|
||||||
remote_shell::endpoint::RemoteShellEndpoint,
|
remote_shell::endpoint::RemoteShell,
|
||||||
remote_shell::endpoint::Open,
|
remote_shell::endpoint::Open,
|
||||||
>::new(
|
>::new(endpoint, remote_shell::endpoint::RemoteShell::default());
|
||||||
endpoint,
|
|
||||||
remote_shell::endpoint::RemoteShellEndpoint::default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"connected to controller at {}",
|
"connected to controller at {}",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
);
|
);
|
||||||
let hook_id = endpoint.allocate_hook_id();
|
let hook_id = endpoint.allocate_hook_id();
|
||||||
let shell_leaf_name = remote_shell::endpoint::RemoteShellEndpoint::protocol_leaf_name();
|
let shell_leaf_name = remote_shell::endpoint::RemoteShell::protocol_leaf_name();
|
||||||
let open_procedure = remote_shell::endpoint::Open::protocol_procedure_id();
|
let open_procedure = remote_shell::endpoint::Open::protocol_procedure_id();
|
||||||
|
|
||||||
remote_shell::endpoint::send_forward(
|
remote_shell::endpoint::send_forward(
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
//! Smallest in-process `RemoteShellLeaf` endpoint example.
|
//! Smallest in-process `remote_shell` declaration example.
|
||||||
//!
|
//!
|
||||||
//! This example hosts exactly one protocol endpoint with exactly one leaf, `RemoteShellLeaf`, and
|
//! This example hosts exactly one protocol endpoint with exactly one leaf and performs a local
|
||||||
//! performs a local introspection request against that leaf. It does not open any sockets or spawn
|
//! introspection request against that leaf. The important detail is that the endpoint metadata is
|
||||||
//! a shell process, so it is the easiest place to see how the endpoint and leaf metadata fit
|
//! taken from `remote_shell::endpoint::RemoteShellEndpoint::protocol_leaf_spec()`, which is
|
||||||
//! together.
|
//! generated by the `leaf!` declaration in `unshell-leaves/src/remote_shell/mod.rs`.
|
||||||
|
//!
|
||||||
|
//! It does not open any sockets or spawn a shell process, so it is the easiest place to verify
|
||||||
|
//! that the shared compile-time leaf declaration and the generated endpoint host metadata line up.
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
@@ -12,17 +15,18 @@ use unshell::protocol::tree::{EndpointOutcome, LocalEvent, ProtocolEndpoint};
|
|||||||
use unshell::protocol::{INTROSPECTION_PROCEDURE_ID, LeafIntrospection};
|
use unshell::protocol::{INTROSPECTION_PROCEDURE_ID, LeafIntrospection};
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let leaf_spec = remote_shell::endpoint::RemoteShell::protocol_leaf_spec();
|
||||||
let mut endpoint = ProtocolEndpoint::new(
|
let mut endpoint = ProtocolEndpoint::new(
|
||||||
agent_path(),
|
agent_path(),
|
||||||
Some(Vec::new()),
|
Some(Vec::new()),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![remote_shell::endpoint::RemoteShellEndpoint::protocol_leaf_spec()],
|
vec![leaf_spec.clone()],
|
||||||
);
|
);
|
||||||
|
|
||||||
let hook_id = endpoint.allocate_hook_id();
|
let hook_id = endpoint.allocate_hook_id();
|
||||||
let outcome = endpoint.send_call(
|
let outcome = endpoint.send_call(
|
||||||
agent_path(),
|
agent_path(),
|
||||||
Some(remote_shell::endpoint::RemoteShellEndpoint::protocol_leaf_name()),
|
Some(remote_shell::endpoint::RemoteShell::protocol_leaf_name()),
|
||||||
INTROSPECTION_PROCEDURE_ID,
|
INTROSPECTION_PROCEDURE_ID,
|
||||||
Some(hook_id),
|
Some(hook_id),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
@@ -38,6 +42,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
remote_shell::endpoint::LISTEN_ADDR
|
remote_shell::endpoint::LISTEN_ADDR
|
||||||
);
|
);
|
||||||
println!("endpoint path: {:?}", agent_path());
|
println!("endpoint path: {:?}", agent_path());
|
||||||
|
println!("declared leaf: {}", leaf_spec.name);
|
||||||
println!("leaf: {}", payload.leaf_name);
|
println!("leaf: {}", payload.leaf_name);
|
||||||
println!("procedures: {:?}", payload.procedures);
|
println!("procedures: {:?}", payload.procedures);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ description = "Application-layer UnShell leaves and client surfaces"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
leaf_endpoint = ["dep:portable-pty"]
|
leaf_endpoint = []
|
||||||
leaf_tui = []
|
leaf_tui = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rkyv = { workspace = true }
|
rkyv = { workspace = true }
|
||||||
portable-pty = { workspace = true, optional = true }
|
portable-pty = { workspace = true }
|
||||||
unshell-macros = { workspace = true }
|
unshell-macros = { workspace = true }
|
||||||
unshell-protocol = { workspace = true }
|
unshell-protocol = { workspace = true }
|
||||||
|
|
||||||
|
|||||||
@@ -20,27 +20,27 @@ use super::OpenRequest;
|
|||||||
/// caller-owned hook identity. That makes ownership and cleanup of hook-backed
|
/// caller-owned hook identity. That makes ownership and cleanup of hook-backed
|
||||||
/// shell processes easy to inspect during debugging.
|
/// shell processes easy to inspect during debugging.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct RemoteShellEndpoint {
|
pub struct RemoteShell {
|
||||||
sessions: BTreeMap<HookKey, Open>,
|
sessions: BTreeMap<HookKey, Open>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProcedureStore<Open> for RemoteShellEndpoint {
|
impl ProcedureStore<Open> for RemoteShell {
|
||||||
fn procedure_sessions(&mut self) -> &mut BTreeMap<HookKey, Open> {
|
fn procedure_sessions(&mut self) -> &mut BTreeMap<HookKey, Open> {
|
||||||
&mut self.sessions
|
&mut self.sessions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Procedure<RemoteShellEndpoint> for Open {
|
impl Procedure<RemoteShell> for Open {
|
||||||
type Error = ShellLeafError;
|
type Error = ShellLeafError;
|
||||||
type Input = OpenRequest;
|
type Input = OpenRequest;
|
||||||
|
|
||||||
fn open(_leaf: &mut RemoteShellEndpoint, call: Call<Self::Input>) -> Result<Self, Self::Error> {
|
fn open(_leaf: &mut RemoteShell, call: Call<Self::Input>) -> Result<Self, Self::Error> {
|
||||||
let hook_key = call.response_hook.ok_or(ShellLeafError::MissingHook)?;
|
let hook_key = call.response_hook.ok_or(ShellLeafError::MissingHook)?;
|
||||||
Open::spawn(hook_key.return_path, hook_key.hook_id, call.procedure_id)
|
Open::spawn(hook_key.return_path, hook_key.hook_id, call.procedure_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_data(
|
fn on_data(
|
||||||
_leaf: &mut RemoteShellEndpoint,
|
_leaf: &mut RemoteShell,
|
||||||
session: &mut Self,
|
session: &mut Self,
|
||||||
data: unshell::protocol::tree::IncomingData,
|
data: unshell::protocol::tree::IncomingData,
|
||||||
) -> Result<ProcedureEffect, Self::Error> {
|
) -> Result<ProcedureEffect, Self::Error> {
|
||||||
@@ -48,21 +48,18 @@ impl Procedure<RemoteShellEndpoint> for Open {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_fault(
|
fn on_fault(
|
||||||
_leaf: &mut RemoteShellEndpoint,
|
_leaf: &mut RemoteShell,
|
||||||
_session: &mut Self,
|
_session: &mut Self,
|
||||||
_fault: unshell::protocol::tree::IncomingFault,
|
_fault: unshell::protocol::tree::IncomingFault,
|
||||||
) -> Result<(), Self::Error> {
|
) -> Result<(), Self::Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poll(
|
fn poll(_leaf: &mut RemoteShell, session: &mut Self) -> Result<ProcedureEffect, Self::Error> {
|
||||||
_leaf: &mut RemoteShellEndpoint,
|
|
||||||
session: &mut Self,
|
|
||||||
) -> Result<ProcedureEffect, Self::Error> {
|
|
||||||
session.poll()
|
session.poll()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close(_leaf: &mut RemoteShellEndpoint, mut session: Self) -> Result<(), Self::Error> {
|
fn close(_leaf: &mut RemoteShell, mut session: Self) -> Result<(), Self::Error> {
|
||||||
session.terminate()
|
session.terminate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use portable_pty::{CommandBuilder, ExitStatus, PtySize, native_pty_system};
|
|||||||
use unshell::Procedure;
|
use unshell::Procedure;
|
||||||
use unshell::protocol::tree::{IncomingData, OutgoingData, ProcedureEffect};
|
use unshell::protocol::tree::{IncomingData, OutgoingData, ProcedureEffect};
|
||||||
|
|
||||||
use super::RemoteShellEndpoint;
|
use super::RemoteShell;
|
||||||
use super::errors::ShellLeafError;
|
use super::errors::ShellLeafError;
|
||||||
|
|
||||||
/// Per-hook shell session created by the `open` procedure.
|
/// Per-hook shell session created by the `open` procedure.
|
||||||
@@ -22,7 +22,7 @@ use super::errors::ShellLeafError;
|
|||||||
/// The procedure type is also the stored session type so the mapping between
|
/// The procedure type is also the stored session type so the mapping between
|
||||||
/// one opening procedure and one live hook remains direct and visible.
|
/// one opening procedure and one live hook remains direct and visible.
|
||||||
#[derive(Procedure)]
|
#[derive(Procedure)]
|
||||||
#[procedure(leaf = RemoteShellEndpoint, name = "open")]
|
#[procedure(leaf = RemoteShell, name = "open")]
|
||||||
pub struct Open {
|
pub struct Open {
|
||||||
/// Spawned PTY child process.
|
/// Spawned PTY child process.
|
||||||
pub(super) child: Box<dyn portable_pty::Child + Send>,
|
pub(super) child: Box<dyn portable_pty::Child + Send>,
|
||||||
|
|||||||
@@ -1,48 +1,14 @@
|
|||||||
//! Remote shell leaf and its user-facing surfaces.
|
//! Remote shell leaf and its user-facing surfaces.
|
||||||
//!
|
//!
|
||||||
//! The module always exports the protocol contract for the leaf. Role-specific
|
//! The module always exports the protocol contract for the leaf together with the
|
||||||
//! implementations live behind crate-wide features:
|
//! endpoint and TUI host implementations.
|
||||||
//! - `leaf_endpoint` builds the PTY-backed runtime leaf
|
|
||||||
//! - `leaf_tui` builds a placeholder client-side TUI surface
|
|
||||||
|
|
||||||
use rkyv::{Archive, Deserialize, Serialize};
|
use rkyv::{Archive, Deserialize, Serialize};
|
||||||
#[cfg(not(feature = "leaf_endpoint"))]
|
use unshell_macros::leaf;
|
||||||
use std::string::String;
|
|
||||||
|
|
||||||
#[cfg(feature = "leaf_endpoint")]
|
|
||||||
pub mod endpoint;
|
pub mod endpoint;
|
||||||
#[cfg(feature = "leaf_tui")]
|
|
||||||
pub mod tui;
|
pub mod tui;
|
||||||
|
|
||||||
#[cfg(feature = "leaf_endpoint")]
|
|
||||||
pub use endpoint::Open;
|
|
||||||
#[cfg(feature = "leaf_endpoint")]
|
|
||||||
pub use endpoint::RemoteShellEndpoint;
|
|
||||||
#[cfg(feature = "leaf_tui")]
|
|
||||||
pub use tui::RemoteShellTui;
|
|
||||||
|
|
||||||
#[cfg(not(feature = "leaf_endpoint"))]
|
|
||||||
/// Compile-time procedure symbol kept available even when the endpoint runtime is
|
|
||||||
/// not built, so the leaf declaration still validates its declared inventory.
|
|
||||||
pub struct Open;
|
|
||||||
|
|
||||||
#[cfg(not(feature = "leaf_endpoint"))]
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub struct RemoteShellDeclarationPlaceholder;
|
|
||||||
|
|
||||||
#[cfg(not(feature = "leaf_endpoint"))]
|
|
||||||
impl crate::protocol::tree::ProtocolLeaf for RemoteShellDeclarationPlaceholder {
|
|
||||||
fn leaf_name() -> String {
|
|
||||||
String::from("remote_shell")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "leaf_endpoint"))]
|
|
||||||
impl crate::protocol::tree::ProcedureMetadata for Open {
|
|
||||||
type Leaf = RemoteShellDeclarationPlaceholder;
|
|
||||||
const PROCEDURE_SUFFIX: &'static str = "open";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open-request payload for the remote shell leaf.
|
/// Open-request payload for the remote shell leaf.
|
||||||
///
|
///
|
||||||
/// The shell currently needs no structured arguments, but a named payload type is
|
/// The shell currently needs no structured arguments, but a named payload type is
|
||||||
@@ -50,33 +16,11 @@ impl crate::protocol::tree::ProcedureMetadata for Open {
|
|||||||
#[derive(Archive, Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
|
#[derive(Archive, Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
|
||||||
pub struct OpenRequest;
|
pub struct OpenRequest;
|
||||||
|
|
||||||
#[cfg(any(feature = "leaf_endpoint", feature = "leaf_tui"))]
|
#[leaf(
|
||||||
macro_rules! declare_remote_shell_leaf {
|
|
||||||
($($role_args:tt)*) => {
|
|
||||||
crate::leaf! {
|
|
||||||
name = "remote_shell",
|
name = "remote_shell",
|
||||||
procedures = [Open],
|
procedures = [Open],
|
||||||
$($role_args)*
|
endpoint = endpoint,
|
||||||
}
|
tui = tui,
|
||||||
};
|
)]
|
||||||
}
|
/// Shared compile-time declaration for the `remote_shell` leaf surface.
|
||||||
|
pub struct RemoteShell;
|
||||||
#[cfg(all(feature = "leaf_endpoint", not(feature = "leaf_tui")))]
|
|
||||||
declare_remote_shell_leaf!(endpoint_struct = RemoteShellEndpoint,);
|
|
||||||
|
|
||||||
#[cfg(all(not(feature = "leaf_endpoint"), feature = "leaf_tui"))]
|
|
||||||
declare_remote_shell_leaf!(tui_struct = RemoteShellTui,);
|
|
||||||
|
|
||||||
#[cfg(all(feature = "leaf_endpoint", feature = "leaf_tui"))]
|
|
||||||
declare_remote_shell_leaf!(
|
|
||||||
endpoint_struct = RemoteShellEndpoint,
|
|
||||||
tui_struct = RemoteShellTui,
|
|
||||||
);
|
|
||||||
|
|
||||||
crate::role_leaf! {
|
|
||||||
/// Feature-selected remote shell surface.
|
|
||||||
pub type RemoteShell {
|
|
||||||
endpoint => endpoint::RemoteShellEndpoint,
|
|
||||||
tui => tui::RemoteShellTui,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,23 +8,24 @@ use std::string::String;
|
|||||||
use std::vec::Vec;
|
use std::vec::Vec;
|
||||||
|
|
||||||
use unshell::protocol::DataMessage;
|
use unshell::protocol::DataMessage;
|
||||||
|
use unshell_macros::Procedure;
|
||||||
|
|
||||||
use crate::{LeafTui, TuiError};
|
use crate::{LeafTui, TuiError};
|
||||||
|
|
||||||
/// Stub TUI surface for the remote shell leaf.
|
/// Stub TUI surface for the remote shell leaf.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct RemoteShellTui {
|
pub struct RemoteShell {
|
||||||
transcript: Vec<u8>,
|
transcript: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RemoteShellTui {
|
impl RemoteShell {
|
||||||
/// Returns a short explanation of the current stub status.
|
/// Returns a short explanation of the current stub status.
|
||||||
pub fn status_line(&self) -> &'static str {
|
pub fn status_line(&self) -> &'static str {
|
||||||
"remote shell TUI stub: rendering is placeholder-only for now"
|
"remote shell TUI stub: rendering is placeholder-only for now"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LeafTui for RemoteShellTui {
|
impl LeafTui for RemoteShell {
|
||||||
fn leaf_name(&self) -> String {
|
fn leaf_name(&self) -> String {
|
||||||
Self::protocol_leaf_name()
|
Self::protocol_leaf_name()
|
||||||
}
|
}
|
||||||
@@ -39,3 +40,9 @@ impl LeafTui for RemoteShellTui {
|
|||||||
format!("{}\n\n{}", self.status_line(), body)
|
format!("{}\n\n{}", self.status_line(), body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TUI-side placeholder procedure symbol for the shared `remote_shell` leaf
|
||||||
|
/// declaration.
|
||||||
|
#[derive(Procedure)]
|
||||||
|
#[procedure(leaf = RemoteShell, name = "open")]
|
||||||
|
pub struct Open {}
|
||||||
|
|||||||
+28
-4
@@ -16,7 +16,7 @@ In practical terms, the macro system is responsible for:
|
|||||||
|
|
||||||
- deriving canonical procedure identifiers
|
- deriving canonical procedure identifiers
|
||||||
- generating compile-time procedure inventories for leaves
|
- generating compile-time procedure inventories for leaves
|
||||||
- binding one leaf declaration to separate endpoint and TUI host structs without
|
- binding one leaf declaration to separate endpoint and TUI host modules without
|
||||||
repeating the metadata on each host
|
repeating the metadata on each host
|
||||||
- generating dispatch glue for simple call-driven leaves
|
- generating dispatch glue for simple call-driven leaves
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ The declaration answers:
|
|||||||
|
|
||||||
- what is this leaf called on the wire?
|
- what is this leaf called on the wire?
|
||||||
- which procedure suffixes belong to it?
|
- which procedure suffixes belong to it?
|
||||||
- which host structs implement its endpoint and TUI roles?
|
- which host modules implement its endpoint and TUI roles?
|
||||||
|
|
||||||
The goal is that this information is written once and reused everywhere.
|
The goal is that this information is written once and reused everywhere.
|
||||||
|
|
||||||
@@ -46,6 +46,28 @@ One leaf can have multiple host structs with different responsibilities.
|
|||||||
Those hosts should not each have to repeat the leaf name or procedure inventory.
|
Those hosts should not each have to repeat the leaf name or procedure inventory.
|
||||||
They bind to the declaration instead.
|
They bind to the declaration instead.
|
||||||
|
|
||||||
|
The current convention is module-based. A declaration such as:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[leaf(
|
||||||
|
name = "remote_shell",
|
||||||
|
procedures = [Open],
|
||||||
|
endpoint = endpoint,
|
||||||
|
tui = tui,
|
||||||
|
)]
|
||||||
|
pub struct RemoteShell;
|
||||||
|
```
|
||||||
|
|
||||||
|
means:
|
||||||
|
|
||||||
|
- the endpoint host type is inferred as `endpoint::RemoteShell`
|
||||||
|
- the TUI host type is inferred as `tui::RemoteShell`
|
||||||
|
- type-based procedure metadata is resolved from the endpoint module as
|
||||||
|
`endpoint::Open`
|
||||||
|
|
||||||
|
This convention removes repeated host type paths from the declaration while still
|
||||||
|
keeping the generated code deterministic and inspectable.
|
||||||
|
|
||||||
### 3. Procedure and method metadata
|
### 3. Procedure and method metadata
|
||||||
|
|
||||||
Procedures and future typed remote methods need stable canonical identifiers.
|
Procedures and future typed remote methods need stable canonical identifiers.
|
||||||
@@ -56,9 +78,9 @@ compile-time inventory instead of handwritten lists.
|
|||||||
|
|
||||||
## Current direction
|
## Current direction
|
||||||
|
|
||||||
The public declaration model is now centered on `leaf!`.
|
The public declaration model is now centered on `#[leaf(...)]`.
|
||||||
|
|
||||||
- `leaf!` declares the canonical protocol surface once
|
- `#[leaf(...)]` declares the canonical protocol surface once
|
||||||
- `#[derive(Procedure)]` derives stateful procedure metadata
|
- `#[derive(Procedure)]` derives stateful procedure metadata
|
||||||
- `#[procedures]` derives one-shot call dispatch for simple leaves
|
- `#[procedures]` derives one-shot call dispatch for simple leaves
|
||||||
|
|
||||||
@@ -75,6 +97,8 @@ The system is optimized for a few constraints that matter to this repository.
|
|||||||
- generated code should stay explicit enough to debug
|
- generated code should stay explicit enough to debug
|
||||||
- endpoint and TUI roles should share metadata but not be forced into the same
|
- endpoint and TUI roles should share metadata but not be forced into the same
|
||||||
runtime trait when their behavior differs
|
runtime trait when their behavior differs
|
||||||
|
- host inference should stay convention-based instead of discovery-based so a
|
||||||
|
declaration can be understood from its source without macro expansion tools
|
||||||
- migration should be low-breakage for the existing examples and tests
|
- migration should be low-breakage for the existing examples and tests
|
||||||
|
|
||||||
## Non-goals
|
## Non-goals
|
||||||
|
|||||||
+158
-110
@@ -1,89 +1,46 @@
|
|||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::{format_ident, quote};
|
use quote::{format_ident, quote};
|
||||||
use syn::{
|
use syn::{
|
||||||
Error, Ident, LitStr, Result, Token, Visibility,
|
Error, ItemStruct, LitStr, Path, Result, Token,
|
||||||
parse::{Parse, ParseStream},
|
parse::{Parse, ParseStream},
|
||||||
punctuated::Punctuated,
|
punctuated::Punctuated,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::utils::option_litstr_tokens;
|
use crate::utils::option_litstr_tokens;
|
||||||
|
|
||||||
pub(crate) struct LeafDeclarationInput {
|
pub(crate) struct LeafDeclarationAttributes {
|
||||||
visibility: Visibility,
|
|
||||||
name: Option<LitStr>,
|
name: Option<LitStr>,
|
||||||
id: Option<LitStr>,
|
id: Option<LitStr>,
|
||||||
org: Option<LitStr>,
|
org: Option<LitStr>,
|
||||||
product: Option<LitStr>,
|
product: Option<LitStr>,
|
||||||
version: Option<LitStr>,
|
version: Option<LitStr>,
|
||||||
endpoint_struct: Option<Ident>,
|
|
||||||
tui_struct: Option<Ident>,
|
|
||||||
procedures: Vec<ProcedureRef>,
|
procedures: Vec<ProcedureRef>,
|
||||||
|
host_bindings: Vec<HostBinding>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parse for LeafDeclarationInput {
|
impl Parse for LeafDeclarationAttributes {
|
||||||
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||||
let visibility = if input.peek(Token![pub]) {
|
|
||||||
input.parse()?
|
|
||||||
} else {
|
|
||||||
Visibility::Inherited
|
|
||||||
};
|
|
||||||
|
|
||||||
let assignments = Punctuated::<LeafAssignment, Token![,]>::parse_terminated(input)?;
|
let assignments = Punctuated::<LeafAssignment, Token![,]>::parse_terminated(input)?;
|
||||||
let mut parsed = Self {
|
let mut parsed = Self {
|
||||||
visibility,
|
|
||||||
name: None,
|
name: None,
|
||||||
id: None,
|
id: None,
|
||||||
org: None,
|
org: None,
|
||||||
product: None,
|
product: None,
|
||||||
version: None,
|
version: None,
|
||||||
endpoint_struct: None,
|
|
||||||
tui_struct: None,
|
|
||||||
procedures: Vec::new(),
|
procedures: Vec::new(),
|
||||||
|
host_bindings: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
for assignment in assignments {
|
for assignment in assignments {
|
||||||
match assignment {
|
match assignment {
|
||||||
LeafAssignment::Name(value) => {
|
LeafAssignment::Name(value) => set_once(&mut parsed.name, value, "leaf name")?,
|
||||||
if parsed.name.is_some() {
|
LeafAssignment::Id(value) => set_once(&mut parsed.id, value, "leaf id")?,
|
||||||
return Err(Error::new_spanned(value, "duplicate leaf name"));
|
LeafAssignment::Org(value) => set_once(&mut parsed.org, value, "leaf org")?,
|
||||||
}
|
|
||||||
parsed.name = Some(value);
|
|
||||||
}
|
|
||||||
LeafAssignment::Id(value) => {
|
|
||||||
if parsed.id.is_some() {
|
|
||||||
return Err(Error::new_spanned(value, "duplicate leaf id"));
|
|
||||||
}
|
|
||||||
parsed.id = Some(value);
|
|
||||||
}
|
|
||||||
LeafAssignment::Org(value) => {
|
|
||||||
if parsed.org.is_some() {
|
|
||||||
return Err(Error::new_spanned(value, "duplicate leaf org"));
|
|
||||||
}
|
|
||||||
parsed.org = Some(value);
|
|
||||||
}
|
|
||||||
LeafAssignment::Product(value) => {
|
LeafAssignment::Product(value) => {
|
||||||
if parsed.product.is_some() {
|
set_once(&mut parsed.product, value, "leaf product")?
|
||||||
return Err(Error::new_spanned(value, "duplicate leaf product"));
|
|
||||||
}
|
|
||||||
parsed.product = Some(value);
|
|
||||||
}
|
}
|
||||||
LeafAssignment::Version(value) => {
|
LeafAssignment::Version(value) => {
|
||||||
if parsed.version.is_some() {
|
set_once(&mut parsed.version, value, "leaf version")?
|
||||||
return Err(Error::new_spanned(value, "duplicate leaf version"));
|
|
||||||
}
|
|
||||||
parsed.version = Some(value);
|
|
||||||
}
|
|
||||||
LeafAssignment::EndpointStruct(value) => {
|
|
||||||
if parsed.endpoint_struct.is_some() {
|
|
||||||
return Err(Error::new_spanned(value, "duplicate endpoint_struct"));
|
|
||||||
}
|
|
||||||
parsed.endpoint_struct = Some(value);
|
|
||||||
}
|
|
||||||
LeafAssignment::TuiStruct(value) => {
|
|
||||||
if parsed.tui_struct.is_some() {
|
|
||||||
return Err(Error::new_spanned(value, "duplicate tui_struct"));
|
|
||||||
}
|
|
||||||
parsed.tui_struct = Some(value);
|
|
||||||
}
|
}
|
||||||
LeafAssignment::Procedures(values) => {
|
LeafAssignment::Procedures(values) => {
|
||||||
if !parsed.procedures.is_empty() {
|
if !parsed.procedures.is_empty() {
|
||||||
@@ -91,19 +48,20 @@ impl Parse for LeafDeclarationInput {
|
|||||||
}
|
}
|
||||||
parsed.procedures = values;
|
parsed.procedures = values;
|
||||||
}
|
}
|
||||||
|
LeafAssignment::HostBinding(binding) => parsed.host_bindings.push(binding),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsed.name.is_none() && parsed.id.is_none() {
|
if parsed.name.is_none() && parsed.id.is_none() {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
input.span(),
|
input.span(),
|
||||||
"leaf! requires either `name = \"...\"` or `id = \"...\"`",
|
"#[leaf(...)] requires either `name = \"...\"` or `id = \"...\"`",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if parsed.endpoint_struct.is_none() && parsed.tui_struct.is_none() {
|
if parsed.host_bindings.is_empty() {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
input.span(),
|
input.span(),
|
||||||
"leaf! requires at least one of `endpoint_struct = ...` or `tui_struct = ...`",
|
"#[leaf(...)] requires at least one host binding",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,13 +75,17 @@ enum LeafAssignment {
|
|||||||
Org(LitStr),
|
Org(LitStr),
|
||||||
Product(LitStr),
|
Product(LitStr),
|
||||||
Version(LitStr),
|
Version(LitStr),
|
||||||
EndpointStruct(Ident),
|
|
||||||
TuiStruct(Ident),
|
|
||||||
Procedures(Vec<ProcedureRef>),
|
Procedures(Vec<ProcedureRef>),
|
||||||
|
HostBinding(HostBinding),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HostBinding {
|
||||||
|
module_path: Option<Path>,
|
||||||
|
host_path: Option<Path>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ProcedureRef {
|
enum ProcedureRef {
|
||||||
Symbol(Ident),
|
Symbol(Path),
|
||||||
Suffix(LitStr),
|
Suffix(LitStr),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,16 +100,26 @@ impl Parse for ProcedureRef {
|
|||||||
|
|
||||||
impl Parse for LeafAssignment {
|
impl Parse for LeafAssignment {
|
||||||
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||||
let name: Ident = input.parse()?;
|
let name: Path = input.parse()?;
|
||||||
input.parse::<Token![=]>()?;
|
input.parse::<Token![=]>()?;
|
||||||
match name.to_string().as_str() {
|
let key = name
|
||||||
|
.get_ident()
|
||||||
|
.ok_or_else(|| Error::new_spanned(&name, "leaf keys must be identifiers"))?
|
||||||
|
.to_string();
|
||||||
|
match key.as_str() {
|
||||||
"name" => Ok(Self::Name(input.parse()?)),
|
"name" => Ok(Self::Name(input.parse()?)),
|
||||||
"id" => Ok(Self::Id(input.parse()?)),
|
"id" => Ok(Self::Id(input.parse()?)),
|
||||||
"org" => Ok(Self::Org(input.parse()?)),
|
"org" => Ok(Self::Org(input.parse()?)),
|
||||||
"product" => Ok(Self::Product(input.parse()?)),
|
"product" => Ok(Self::Product(input.parse()?)),
|
||||||
"version" => Ok(Self::Version(input.parse()?)),
|
"version" => Ok(Self::Version(input.parse()?)),
|
||||||
"endpoint_struct" => Ok(Self::EndpointStruct(input.parse()?)),
|
"endpoint_struct" | "tui_struct" => Ok(Self::HostBinding(HostBinding {
|
||||||
"tui_struct" => Ok(Self::TuiStruct(input.parse()?)),
|
module_path: None,
|
||||||
|
host_path: Some(input.parse()?),
|
||||||
|
})),
|
||||||
|
"endpoint" | "tui" => Ok(Self::HostBinding(HostBinding {
|
||||||
|
module_path: Some(input.parse()?),
|
||||||
|
host_path: None,
|
||||||
|
})),
|
||||||
"procedures" => {
|
"procedures" => {
|
||||||
let content;
|
let content;
|
||||||
syn::bracketed!(content in input);
|
syn::bracketed!(content in input);
|
||||||
@@ -156,57 +128,47 @@ impl Parse for LeafAssignment {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
Ok(Self::Procedures(values))
|
Ok(Self::Procedures(values))
|
||||||
}
|
}
|
||||||
_ => Err(Error::new_spanned(name, "unsupported leaf! assignment")),
|
_ => Err(Error::new_spanned(
|
||||||
|
name,
|
||||||
|
"unsupported #[leaf(...)] key; expected one of name, id, org, product, version, procedures, endpoint, tui, endpoint_struct, or tui_struct",
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn expand_leaf_declaration(input: LeafDeclarationInput) -> Result<TokenStream> {
|
pub(crate) fn expand_leaf_declaration(
|
||||||
let _visibility = input.visibility;
|
attr: LeafDeclarationAttributes,
|
||||||
let declaration_ident = format_ident!(
|
item: ItemStruct,
|
||||||
"__UnshellLeafDecl_{}",
|
) -> Result<TokenStream> {
|
||||||
input
|
let declaration_ident = item.ident.clone();
|
||||||
.endpoint_struct
|
let id = option_litstr_tokens(attr.id.as_ref());
|
||||||
.as_ref()
|
let org = option_litstr_tokens(attr.org.as_ref());
|
||||||
.or(input.tui_struct.as_ref())
|
let product = option_litstr_tokens(attr.product.as_ref());
|
||||||
.expect("leaf declaration requires at least one host")
|
let version = option_litstr_tokens(attr.version.as_ref());
|
||||||
);
|
let leaf_name = option_litstr_tokens(attr.name.as_ref());
|
||||||
let id = option_litstr_tokens(input.id.as_ref());
|
let canonical_procedure_module = attr
|
||||||
let org = option_litstr_tokens(input.org.as_ref());
|
.host_bindings
|
||||||
let product = option_litstr_tokens(input.product.as_ref());
|
.iter()
|
||||||
let version = option_litstr_tokens(input.version.as_ref());
|
.find_map(|binding| binding.module_path.as_ref())
|
||||||
let leaf_name = option_litstr_tokens(input.name.as_ref());
|
.cloned();
|
||||||
let procedure_suffixes = input
|
let procedure_suffixes = attr
|
||||||
.procedures
|
.procedures
|
||||||
.iter()
|
.iter()
|
||||||
.map(|procedure| match procedure {
|
.map(|procedure| procedure_suffix_tokens(procedure, canonical_procedure_module.as_ref()))
|
||||||
ProcedureRef::Symbol(procedure) => {
|
.collect::<Result<Vec<_>>>()?;
|
||||||
quote! { <#procedure as ::unshell::protocol::tree::ProcedureMetadata>::PROCEDURE_SUFFIX }
|
let procedure_type_checks = attr
|
||||||
}
|
.host_bindings
|
||||||
ProcedureRef::Suffix(suffix) => quote! { #suffix },
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let procedure_type_checks = input
|
|
||||||
.procedures
|
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|procedure| match procedure {
|
.map(|binding| procedure_type_check_tokens(binding, &attr.procedures, &declaration_ident))
|
||||||
ProcedureRef::Symbol(procedure) => Some(procedure),
|
.collect::<Result<Vec<_>>>()?;
|
||||||
ProcedureRef::Suffix(_) => None,
|
let host_impls = attr
|
||||||
});
|
.host_bindings
|
||||||
|
.iter()
|
||||||
let endpoint_impl = input
|
.map(|binding| expand_binding_impl(binding, &declaration_ident))
|
||||||
.endpoint_struct
|
.collect::<Result<Vec<_>>>()?;
|
||||||
.as_ref()
|
|
||||||
.map(|endpoint_struct| expand_binding_impl(endpoint_struct, &declaration_ident));
|
|
||||||
let tui_impl = input
|
|
||||||
.tui_struct
|
|
||||||
.as_ref()
|
|
||||||
.map(|tui_struct| expand_binding_impl(tui_struct, &declaration_ident));
|
|
||||||
|
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
#[allow(non_camel_case_types)]
|
#item
|
||||||
#[doc(hidden)]
|
|
||||||
pub struct #declaration_ident;
|
|
||||||
|
|
||||||
impl ::unshell::protocol::tree::ProtocolLeaf for #declaration_ident {
|
impl ::unshell::protocol::tree::ProtocolLeaf for #declaration_ident {
|
||||||
fn leaf_name() -> ::unshell::alloc::string::String {
|
fn leaf_name() -> ::unshell::alloc::string::String {
|
||||||
@@ -252,16 +214,16 @@ pub(crate) fn expand_leaf_declaration(input: LeafDeclarationInput) -> Result<Tok
|
|||||||
}
|
}
|
||||||
|
|
||||||
const _: fn() = || {
|
const _: fn() = || {
|
||||||
#(let _ = ::core::marker::PhantomData::<#procedure_type_checks>;)*
|
#(#procedure_type_checks)*
|
||||||
};
|
};
|
||||||
|
|
||||||
#endpoint_impl
|
#(#host_impls)*
|
||||||
#tui_impl
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expand_binding_impl(host: &Ident, declaration: &Ident) -> TokenStream {
|
fn expand_binding_impl(binding: &HostBinding, declaration: &syn::Ident) -> Result<TokenStream> {
|
||||||
quote! {
|
let host = host_path_for_binding(binding, declaration)?;
|
||||||
|
Ok(quote! {
|
||||||
impl ::unshell::protocol::tree::ProtocolLeaf for #host {
|
impl ::unshell::protocol::tree::ProtocolLeaf for #host {
|
||||||
fn leaf_name() -> ::unshell::alloc::string::String {
|
fn leaf_name() -> ::unshell::alloc::string::String {
|
||||||
<#declaration as ::unshell::protocol::tree::ProtocolLeaf>::leaf_name()
|
<#declaration as ::unshell::protocol::tree::ProtocolLeaf>::leaf_name()
|
||||||
@@ -296,5 +258,91 @@ fn expand_binding_impl(host: &Ident, declaration: &Ident) -> TokenStream {
|
|||||||
<Self as ::unshell::protocol::tree::LeafDeclaration>::procedure_id(suffix)
|
<Self as ::unshell::protocol::tree::LeafDeclaration>::procedure_id(suffix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_path_for_binding(binding: &HostBinding, declaration: &syn::Ident) -> Result<Path> {
|
||||||
|
if let Some(path) = &binding.host_path {
|
||||||
|
return Ok(path.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(module_path) = &binding.module_path else {
|
||||||
|
return Err(Error::new(
|
||||||
|
declaration.span(),
|
||||||
|
"leaf binding is missing a host path",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut path = module_path.clone();
|
||||||
|
path.segments.push(format_ident!("{declaration}").into());
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn procedure_suffix_tokens(
|
||||||
|
procedure: &ProcedureRef,
|
||||||
|
canonical_module: Option<&Path>,
|
||||||
|
) -> Result<TokenStream> {
|
||||||
|
match procedure {
|
||||||
|
ProcedureRef::Symbol(procedure) => {
|
||||||
|
let procedure_path = if let Some(module_path) = canonical_module {
|
||||||
|
let mut path = module_path.clone();
|
||||||
|
let ident = procedure.get_ident().ok_or_else(|| {
|
||||||
|
Error::new_spanned(
|
||||||
|
procedure,
|
||||||
|
"procedure names must be bare identifiers when inferred from a module",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
path.segments.push(ident.clone().into());
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
procedure.clone()
|
||||||
|
};
|
||||||
|
Ok(
|
||||||
|
quote! { <#procedure_path as ::unshell::protocol::tree::ProcedureMetadata>::PROCEDURE_SUFFIX },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ProcedureRef::Suffix(suffix) => Ok(quote! { #suffix }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn procedure_type_check_tokens(
|
||||||
|
binding: &HostBinding,
|
||||||
|
procedures: &[ProcedureRef],
|
||||||
|
declaration: &syn::Ident,
|
||||||
|
) -> Result<TokenStream> {
|
||||||
|
let Some(module_path) = &binding.module_path else {
|
||||||
|
return Ok(quote! {});
|
||||||
|
};
|
||||||
|
|
||||||
|
let checks = procedures
|
||||||
|
.iter()
|
||||||
|
.filter_map(|procedure| match procedure {
|
||||||
|
ProcedureRef::Symbol(procedure) => Some(procedure),
|
||||||
|
ProcedureRef::Suffix(_) => None,
|
||||||
|
})
|
||||||
|
.map(|procedure| {
|
||||||
|
let mut path = module_path.clone();
|
||||||
|
let ident = procedure.get_ident().ok_or_else(|| {
|
||||||
|
Error::new_spanned(
|
||||||
|
procedure,
|
||||||
|
"procedure names must be bare identifiers when inferred from a module",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
path.segments.push(ident.clone().into());
|
||||||
|
Ok::<TokenStream, Error>(quote! {
|
||||||
|
let _ = ::core::marker::PhantomData::<#path>;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
|
||||||
|
let _ = declaration;
|
||||||
|
Ok(quote! { #(#checks)* })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_once(target: &mut Option<LitStr>, value: LitStr, label: &str) -> Result<()> {
|
||||||
|
if target.is_some() {
|
||||||
|
return Err(Error::new_spanned(value, format!("duplicate {label}")));
|
||||||
|
}
|
||||||
|
*target = Some(value);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
+25
-22
@@ -6,13 +6,14 @@ mod procedures;
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use syn::{DeriveInput, ItemImpl, parse_macro_input};
|
use syn::{DeriveInput, ItemImpl, ItemStruct, parse_macro_input};
|
||||||
|
|
||||||
/// Declares one compile-time leaf surface and binds it to endpoint and/or TUI
|
/// Declares one compile-time leaf surface and binds it to endpoint and/or TUI
|
||||||
/// host structs.
|
/// host structs.
|
||||||
///
|
///
|
||||||
/// What it is: a function-like macro that generates the shared protocol-visible
|
/// What it is: an attribute macro placed on a marker struct that generates the
|
||||||
/// metadata for one leaf and applies that metadata to the listed host structs.
|
/// shared protocol-visible metadata for one leaf and applies that metadata to the
|
||||||
|
/// listed host structs.
|
||||||
///
|
///
|
||||||
/// Why it exists: endpoint and TUI hosts should not each have to repeat the leaf
|
/// Why it exists: endpoint and TUI hosts should not each have to repeat the leaf
|
||||||
/// name and procedure inventory, and endpoint construction should not need a
|
/// name and procedure inventory, and endpoint construction should not need a
|
||||||
@@ -20,18 +21,20 @@ use syn::{DeriveInput, ItemImpl, parse_macro_input};
|
|||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// unshell::leaf! {
|
/// #[unshell::leaf(
|
||||||
/// name = "remote_shell",
|
/// name = "remote_shell",
|
||||||
/// procedures = [Open, Reset, whoami],
|
/// procedures = [Open],
|
||||||
/// endpoint_struct = RemoteShellEndpoint,
|
/// leaf_endpoint = endpoint::RemoteShellEndpoint,
|
||||||
/// tui_struct = RemoteShellTui,
|
/// leaf_tui = tui::RemoteShellTui,
|
||||||
/// }
|
/// )]
|
||||||
|
/// pub struct RemoteShell;
|
||||||
/// ```
|
/// ```
|
||||||
#[proc_macro]
|
#[proc_macro_attribute]
|
||||||
pub fn leaf(input: TokenStream) -> TokenStream {
|
pub fn leaf(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
match leaf_decl::expand_leaf_declaration(parse_macro_input!(
|
match leaf_decl::expand_leaf_declaration(
|
||||||
input as leaf_decl::LeafDeclarationInput
|
parse_macro_input!(attr as leaf_decl::LeafDeclarationAttributes),
|
||||||
)) {
|
parse_macro_input!(item as ItemStruct),
|
||||||
|
) {
|
||||||
Ok(tokens) => tokens.into(),
|
Ok(tokens) => tokens.into(),
|
||||||
Err(error) => error.to_compile_error().into(),
|
Err(error) => error.to_compile_error().into(),
|
||||||
}
|
}
|
||||||
@@ -49,13 +52,12 @@ pub fn leaf(input: TokenStream) -> TokenStream {
|
|||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// use unshell::{Procedure, leaf};
|
/// use unshell::{Procedure, leaf};
|
||||||
///
|
///
|
||||||
/// struct ShellLeaf;
|
/// #[leaf(
|
||||||
///
|
|
||||||
/// leaf! {
|
|
||||||
/// name = "shell",
|
/// name = "shell",
|
||||||
/// procedures = [OpenSession],
|
/// procedures = [OpenSession],
|
||||||
/// endpoint_struct = ShellLeaf,
|
/// endpoint_struct = ShellLeaf,
|
||||||
/// }
|
/// )]
|
||||||
|
/// struct Shell;
|
||||||
///
|
///
|
||||||
/// struct ShellLeaf;
|
/// struct ShellLeaf;
|
||||||
///
|
///
|
||||||
@@ -86,13 +88,14 @@ pub fn derive_procedure(input: TokenStream) -> TokenStream {
|
|||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// use unshell::{leaf, procedures};
|
/// use unshell::{leaf, procedures};
|
||||||
///
|
///
|
||||||
/// struct EchoLeaf;
|
/// #[leaf(
|
||||||
///
|
|
||||||
/// leaf! {
|
|
||||||
/// id = "org.example.v1.echo",
|
/// id = "org.example.v1.echo",
|
||||||
/// procedures = [echo],
|
/// procedures = ["echo"],
|
||||||
/// endpoint_struct = EchoLeaf,
|
/// endpoint_struct = EchoLeaf,
|
||||||
/// }
|
/// )]
|
||||||
|
/// struct Echo;
|
||||||
|
///
|
||||||
|
/// struct EchoLeaf;
|
||||||
///
|
///
|
||||||
/// #[procedures(error = core::convert::Infallible)]
|
/// #[procedures(error = core::convert::Infallible)]
|
||||||
/// impl EchoLeaf {
|
/// impl EchoLeaf {
|
||||||
|
|||||||
@@ -18,11 +18,8 @@ struct EchoLeaf {
|
|||||||
prefix: String,
|
prefix: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
leaf! {
|
#[leaf(id = "org.example.v1.echo", endpoint_struct = EchoLeaf, procedures = ["echo"])]
|
||||||
id = "org.example.v1.echo",
|
struct Echo;
|
||||||
endpoint_struct = EchoLeaf,
|
|
||||||
procedures = ["echo"],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
struct EchoRequest {
|
struct EchoRequest {
|
||||||
|
|||||||
@@ -17,11 +17,8 @@ impl ProcedureMetadata for Reset {
|
|||||||
const PROCEDURE_SUFFIX: &'static str = "reset";
|
const PROCEDURE_SUFFIX: &'static str = "reset";
|
||||||
}
|
}
|
||||||
|
|
||||||
leaf! {
|
#[leaf(id = "org.example.v1.demo", procedures = [Open, Reset], endpoint_struct = EndpointHost)]
|
||||||
id = "org.example.v1.demo",
|
struct Demo;
|
||||||
procedures = [Open, Reset],
|
|
||||||
endpoint_struct = EndpointHost,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct EndpointHalf;
|
struct EndpointHalf;
|
||||||
struct TuiHalf;
|
struct TuiHalf;
|
||||||
@@ -32,7 +29,7 @@ impl ProcedureMetadata for Connect {
|
|||||||
const PROCEDURE_SUFFIX: &'static str = "connect";
|
const PROCEDURE_SUFFIX: &'static str = "connect";
|
||||||
}
|
}
|
||||||
|
|
||||||
leaf! {
|
#[leaf(
|
||||||
name = "chat",
|
name = "chat",
|
||||||
org = "org",
|
org = "org",
|
||||||
product = "example",
|
product = "example",
|
||||||
@@ -40,7 +37,8 @@ leaf! {
|
|||||||
procedures = [Connect],
|
procedures = [Connect],
|
||||||
endpoint_struct = EndpointHalf,
|
endpoint_struct = EndpointHalf,
|
||||||
tui_struct = TuiHalf,
|
tui_struct = TuiHalf,
|
||||||
}
|
)]
|
||||||
|
struct Chat;
|
||||||
|
|
||||||
struct TuiOnly;
|
struct TuiOnly;
|
||||||
struct Tail;
|
struct Tail;
|
||||||
@@ -50,11 +48,8 @@ impl ProcedureMetadata for Tail {
|
|||||||
const PROCEDURE_SUFFIX: &'static str = "tail";
|
const PROCEDURE_SUFFIX: &'static str = "tail";
|
||||||
}
|
}
|
||||||
|
|
||||||
leaf! {
|
#[leaf(id = "org.example.v1.transcript", procedures = [Tail], tui_struct = TuiOnly)]
|
||||||
id = "org.example.v1.transcript",
|
struct Transcript;
|
||||||
procedures = [Tail],
|
|
||||||
tui_struct = TuiOnly,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn leaf_declaration_generates_endpoint_host_metadata() {
|
fn leaf_declaration_generates_endpoint_host_metadata() {
|
||||||
|
|||||||
@@ -17,11 +17,8 @@ struct StreamLeaf {
|
|||||||
sessions: BTreeMap<HookKey, ProcedureOpen>,
|
sessions: BTreeMap<HookKey, ProcedureOpen>,
|
||||||
}
|
}
|
||||||
|
|
||||||
leaf! {
|
#[leaf(id = "org.example.v1.stream", procedures = [ProcedureOpen], endpoint_struct = StreamLeaf)]
|
||||||
id = "org.example.v1.stream",
|
struct Stream;
|
||||||
procedures = [ProcedureOpen],
|
|
||||||
endpoint_struct = StreamLeaf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProcedureStore<ProcedureOpen> for StreamLeaf {
|
impl ProcedureStore<ProcedureOpen> for StreamLeaf {
|
||||||
fn procedure_sessions(&mut self) -> &mut BTreeMap<HookKey, ProcedureOpen> {
|
fn procedure_sessions(&mut self) -> &mut BTreeMap<HookKey, ProcedureOpen> {
|
||||||
@@ -146,11 +143,8 @@ struct DuplexLeaf {
|
|||||||
sessions: BTreeMap<HookKey, DuplexProcedure>,
|
sessions: BTreeMap<HookKey, DuplexProcedure>,
|
||||||
}
|
}
|
||||||
|
|
||||||
leaf! {
|
#[leaf(id = "org.example.v1.duplex", procedures = [DuplexProcedure], endpoint_struct = DuplexLeaf)]
|
||||||
id = "org.example.v1.duplex",
|
struct Duplex;
|
||||||
procedures = [DuplexProcedure],
|
|
||||||
endpoint_struct = DuplexLeaf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProcedureStore<DuplexProcedure> for DuplexLeaf {
|
impl ProcedureStore<DuplexProcedure> for DuplexLeaf {
|
||||||
fn procedure_sessions(&mut self) -> &mut BTreeMap<HookKey, DuplexProcedure> {
|
fn procedure_sessions(&mut self) -> &mut BTreeMap<HookKey, DuplexProcedure> {
|
||||||
|
|||||||
Reference in New Issue
Block a user