mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Remove the old leaf declaration path
Delete the deprecated Leaf derive path, migrate the remaining tests and example to leaf!, and add direct coverage for endpoint-only, TUI-only, and shared-host leaf declarations.
This commit is contained in:
@@ -1,22 +1,28 @@
|
|||||||
//! Small end-to-end example for the `Leaf` and `procedures` derive macros.
|
//! Small end-to-end example for the `leaf!` and `Procedure` macros.
|
||||||
//!
|
//!
|
||||||
//! This stays entirely local. A controller endpoint opens one call against a single in-process
|
//! This stays entirely local. A controller endpoint opens one hook-backed procedure against a
|
||||||
//! leaf runtime, and the example decodes the returned reply payload.
|
//! single in-process leaf runtime, and the example decodes the returned reply payload.
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::{convert::Infallible, string::String};
|
use std::{collections::BTreeMap, convert::Infallible, string::String};
|
||||||
|
|
||||||
use rkyv::{Archive, Deserialize, Serialize};
|
use rkyv::{Archive, Deserialize, Serialize};
|
||||||
use unshell::protocol::tree::{
|
use unshell::protocol::tree::{
|
||||||
Call, CallLeaf, ChildRoute, EndpointOutcome, Ingress, LeafRuntime, ProtocolEndpoint,
|
Call, ChildRoute, EndpointOutcome, HookKey, Ingress, OutgoingData, Procedure, ProcedureEffect,
|
||||||
|
ProcedureRuntime, ProcedureStore, ProtocolEndpoint,
|
||||||
};
|
};
|
||||||
use unshell::protocol::{PacketType, decode_frame};
|
use unshell::protocol::{PacketType, decode_frame};
|
||||||
use unshell::{Leaf, procedures};
|
use unshell::{Procedure, leaf};
|
||||||
|
|
||||||
#[derive(Leaf)]
|
#[derive(Default)]
|
||||||
#[leaf(org = "org", product = "example", version = "v1", leaf_name = "echo")]
|
|
||||||
struct EchoLeaf {
|
struct EchoLeaf {
|
||||||
prefix: String,
|
sessions: BTreeMap<HookKey, EchoOpen>,
|
||||||
|
}
|
||||||
|
|
||||||
|
leaf! {
|
||||||
|
id = "org.example.v1.echo",
|
||||||
|
procedures = [EchoOpen],
|
||||||
|
endpoint_struct = EchoLeaf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -29,18 +35,53 @@ struct EchoResponse {
|
|||||||
text: String,
|
text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[procedures(error = Infallible)]
|
#[derive(Debug, Clone, PartialEq, Eq, Procedure)]
|
||||||
impl EchoLeaf {
|
#[procedure(leaf = EchoLeaf, name = "echo")]
|
||||||
#[call]
|
struct EchoOpen {
|
||||||
fn echo(&mut self, request: Call<EchoRequest>) -> EchoResponse {
|
prefix: String,
|
||||||
EchoResponse {
|
return_path: Vec<String>,
|
||||||
text: format!("{}{}", self.prefix, request.input.text),
|
hook_id: u64,
|
||||||
}
|
sent_reply: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcedureStore<EchoOpen> for EchoLeaf {
|
||||||
|
fn procedure_sessions(&mut self) -> &mut BTreeMap<HookKey, EchoOpen> {
|
||||||
|
&mut self.sessions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CallLeaf for EchoLeaf {
|
impl Procedure<EchoLeaf> for EchoOpen {
|
||||||
type Error = Infallible;
|
type Error = Infallible;
|
||||||
|
type Input = EchoRequest;
|
||||||
|
|
||||||
|
fn open(_leaf: &mut EchoLeaf, call: Call<Self::Input>) -> Result<Self, Self::Error> {
|
||||||
|
let response_hook = call
|
||||||
|
.response_hook
|
||||||
|
.expect("example call declares a response hook");
|
||||||
|
Ok(Self {
|
||||||
|
prefix: call.input.text,
|
||||||
|
return_path: response_hook.return_path,
|
||||||
|
hook_id: response_hook.hook_id,
|
||||||
|
sent_reply: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll(_leaf: &mut EchoLeaf, session: &mut Self) -> Result<ProcedureEffect, Self::Error> {
|
||||||
|
if session.sent_reply {
|
||||||
|
return Ok(ProcedureEffect::default());
|
||||||
|
}
|
||||||
|
session.sent_reply = true;
|
||||||
|
Ok(ProcedureEffect::close(vec![OutgoingData {
|
||||||
|
dst_path: session.return_path.clone(),
|
||||||
|
hook_id: session.hook_id,
|
||||||
|
procedure_id: EchoOpen::protocol_procedure_id(),
|
||||||
|
data: unshell::protocol::tree::encode_call_reply(&EchoResponse {
|
||||||
|
text: format!("echo: {}", session.prefix),
|
||||||
|
})
|
||||||
|
.expect("response should encode"),
|
||||||
|
end_hook: true,
|
||||||
|
}]))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path(parts: &[&str]) -> Vec<String> {
|
fn path(parts: &[&str]) -> Vec<String> {
|
||||||
@@ -54,12 +95,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![EchoLeaf::protocol_leaf_spec()],
|
vec![EchoLeaf::protocol_leaf_spec()],
|
||||||
);
|
);
|
||||||
let mut runtime = LeafRuntime::new(
|
let mut runtime = ProcedureRuntime::<EchoLeaf, EchoOpen>::new(endpoint, EchoLeaf::default());
|
||||||
endpoint,
|
|
||||||
EchoLeaf {
|
|
||||||
prefix: String::from("echo: "),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut controller = ProtocolEndpoint::new(
|
let mut controller = ProtocolEndpoint::new(
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
@@ -74,7 +110,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let controller_outcome = controller.send_call(
|
let controller_outcome = controller.send_call(
|
||||||
path(&["agent"]),
|
path(&["agent"]),
|
||||||
Some(EchoLeaf::protocol_leaf_name()),
|
Some(EchoLeaf::protocol_leaf_name()),
|
||||||
EchoLeaf::protocol_procedure_id("echo").expect("known procedure suffix"),
|
EchoOpen::protocol_procedure_id(),
|
||||||
Some(hook_id),
|
Some(hook_id),
|
||||||
unshell::protocol::tree::encode_call_reply(&EchoRequest {
|
unshell::protocol::tree::encode_call_reply(&EchoRequest {
|
||||||
text: String::from("hello leaf"),
|
text: String::from("hello leaf"),
|
||||||
@@ -84,7 +120,9 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
return Err("expected controller to forward call".into());
|
return Err("expected controller to forward call".into());
|
||||||
};
|
};
|
||||||
|
|
||||||
let outcome = runtime.receive(&Ingress::Parent, frame)?;
|
let receive_outcome = runtime.receive(&Ingress::Parent, frame)?;
|
||||||
|
assert!(receive_outcome.frames.is_empty());
|
||||||
|
let outcome = runtime.poll()?;
|
||||||
let [response_frame] = outcome.frames.as_slice() else {
|
let [response_frame] = outcome.frames.as_slice() else {
|
||||||
return Err("expected one response frame".into());
|
return Err("expected one response frame".into());
|
||||||
};
|
};
|
||||||
@@ -100,7 +138,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
println!(
|
println!(
|
||||||
"leaf={} procedure={} response={}",
|
"leaf={} procedure={} response={}",
|
||||||
EchoLeaf::protocol_leaf_name(),
|
EchoLeaf::protocol_leaf_name(),
|
||||||
EchoLeaf::protocol_procedure_id("echo").expect("known procedure suffix"),
|
EchoOpen::protocol_procedure_id(),
|
||||||
response.text,
|
response.text,
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+1
-1
@@ -26,6 +26,6 @@ pub use unshell_protocol as protocol;
|
|||||||
/// Re-export the leaf library crate behind the historical `unshell::leaves` path
|
/// Re-export the leaf library crate behind the historical `unshell::leaves` path
|
||||||
pub use unshell_leaves as leaves;
|
pub use unshell_leaves as leaves;
|
||||||
|
|
||||||
pub use unshell_macros::{Leaf, Procedure, leaf, procedures};
|
pub use unshell_macros::{Procedure, leaf, procedures};
|
||||||
|
|
||||||
// pub use ush_obfuscate as obfuscate;
|
// pub use ush_obfuscate as obfuscate;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ pub extern crate alloc;
|
|||||||
|
|
||||||
use unshell_protocol::DataMessage;
|
use unshell_protocol::DataMessage;
|
||||||
|
|
||||||
pub use unshell_macros::{Leaf, Procedure, leaf, procedures};
|
pub use unshell_macros::{Procedure, leaf, procedures};
|
||||||
pub use unshell_protocol as protocol;
|
pub use unshell_protocol as protocol;
|
||||||
|
|
||||||
/// Re-exports one role-specific type behind a stable public alias.
|
/// Re-exports one role-specific type behind a stable public alias.
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
//! - `leaf_tui` builds a placeholder client-side TUI surface
|
//! - `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 std::string::String;
|
||||||
|
|
||||||
#[cfg(feature = "leaf_endpoint")]
|
#[cfg(feature = "leaf_endpoint")]
|
||||||
pub mod endpoint;
|
pub mod endpoint;
|
||||||
@@ -24,6 +26,23 @@ pub use tui::RemoteShellTui;
|
|||||||
/// not built, so the leaf declaration still validates its declared inventory.
|
/// not built, so the leaf declaration still validates its declared inventory.
|
||||||
pub struct Open;
|
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
|
||||||
|
|||||||
+6
-11
@@ -14,7 +14,6 @@ compile time.
|
|||||||
|
|
||||||
In practical terms, the macro system is responsible for:
|
In practical terms, the macro system is responsible for:
|
||||||
|
|
||||||
- deriving canonical leaf identities
|
|
||||||
- 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 structs without
|
||||||
@@ -57,18 +56,14 @@ compile-time inventory instead of handwritten lists.
|
|||||||
|
|
||||||
## Current direction
|
## Current direction
|
||||||
|
|
||||||
The current migration keeps the older derive-based APIs working while adding a
|
The public declaration model is now centered on `leaf!`.
|
||||||
new declaration-first API.
|
|
||||||
|
|
||||||
That migration is intentionally incremental:
|
- `leaf!` declares the canonical protocol surface once
|
||||||
|
- `#[derive(Procedure)]` derives stateful procedure metadata
|
||||||
|
- `#[procedures]` derives one-shot call dispatch for simple leaves
|
||||||
|
|
||||||
1. keep `#[derive(Leaf)]`, `#[derive(Procedure)]`, and `#[procedures]` working
|
The next evolution from here is typed remote-method metadata on top of the same
|
||||||
2. introduce one declaration macro for compile-time leaf metadata
|
declaration model.
|
||||||
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
|
## Design constraints
|
||||||
|
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
use quote::quote;
|
|
||||||
use syn::{Attribute, Data, DeriveInput, Error, Ident, LitStr, Result};
|
|
||||||
|
|
||||||
use crate::utils::{looks_like_canonical_leaf_name, option_litstr_tokens};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct LeafAttributes {
|
|
||||||
name: Option<LitStr>,
|
|
||||||
id: Option<LitStr>,
|
|
||||||
org: Option<LitStr>,
|
|
||||||
product: Option<LitStr>,
|
|
||||||
version: Option<LitStr>,
|
|
||||||
leaf_name: Option<LitStr>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LeafAttributes {
|
|
||||||
fn parse_from(attrs: &[Attribute]) -> Result<Self> {
|
|
||||||
let mut parsed = Self::default();
|
|
||||||
|
|
||||||
for attr in attrs {
|
|
||||||
if !attr.path().is_ident("leaf") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
attr.parse_nested_meta(|meta| {
|
|
||||||
if meta.path.is_ident("name") {
|
|
||||||
if parsed.name.is_some() {
|
|
||||||
return Err(meta.error("duplicate leaf name attribute"));
|
|
||||||
}
|
|
||||||
parsed.name = Some(meta.value()?.parse()?);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if meta.path.is_ident("id") {
|
|
||||||
if parsed.id.is_some() {
|
|
||||||
return Err(meta.error("duplicate leaf id attribute"));
|
|
||||||
}
|
|
||||||
parsed.id = Some(meta.value()?.parse()?);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if meta.path.is_ident("org") {
|
|
||||||
if parsed.org.is_some() {
|
|
||||||
return Err(meta.error("duplicate leaf org attribute"));
|
|
||||||
}
|
|
||||||
parsed.org = Some(meta.value()?.parse()?);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if meta.path.is_ident("product") {
|
|
||||||
if parsed.product.is_some() {
|
|
||||||
return Err(meta.error("duplicate leaf product attribute"));
|
|
||||||
}
|
|
||||||
parsed.product = Some(meta.value()?.parse()?);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if meta.path.is_ident("version") {
|
|
||||||
if parsed.version.is_some() {
|
|
||||||
return Err(meta.error("duplicate leaf version attribute"));
|
|
||||||
}
|
|
||||||
parsed.version = Some(meta.value()?.parse()?);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if meta.path.is_ident("leaf_name") {
|
|
||||||
if parsed.leaf_name.is_some() {
|
|
||||||
return Err(meta.error("duplicate leaf_name attribute"));
|
|
||||||
}
|
|
||||||
parsed.leaf_name = Some(meta.value()?.parse()?);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(meta.error("unsupported #[leaf(...)] attribute"))
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(parsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn explicit_id_value(&self) -> Option<&LitStr> {
|
|
||||||
self.id.as_ref().or(self.name.as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn leaf_name_expression(&self, struct_name: &Ident) -> proc_macro2::TokenStream {
|
|
||||||
let id = option_litstr_tokens(self.id.as_ref().or(self.name.as_ref()));
|
|
||||||
let org = option_litstr_tokens(self.org.as_ref());
|
|
||||||
let product = option_litstr_tokens(self.product.as_ref());
|
|
||||||
let version = option_litstr_tokens(self.version.as_ref());
|
|
||||||
let leaf_name = option_litstr_tokens(self.leaf_name.as_ref());
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
::unshell::protocol::tree::derive_leaf_name(
|
|
||||||
::core::env!("CARGO_PKG_NAME"),
|
|
||||||
::core::env!("CARGO_PKG_VERSION_MAJOR"),
|
|
||||||
::core::env!("CARGO_PKG_VERSION_MINOR"),
|
|
||||||
::core::env!("CARGO_PKG_VERSION_PATCH"),
|
|
||||||
::core::module_path!(),
|
|
||||||
::core::stringify!(#struct_name),
|
|
||||||
#org,
|
|
||||||
#product,
|
|
||||||
#version,
|
|
||||||
#leaf_name,
|
|
||||||
#id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn expand_leaf(input: DeriveInput) -> Result<proc_macro2::TokenStream> {
|
|
||||||
let struct_name = input.ident;
|
|
||||||
match input.data {
|
|
||||||
Data::Struct(_) => {}
|
|
||||||
_ => {
|
|
||||||
return Err(Error::new_spanned(
|
|
||||||
struct_name,
|
|
||||||
"Leaf can only be derived for structs",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let parsed = LeafAttributes::parse_from(&input.attrs)?;
|
|
||||||
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
|
|
||||||
let leaf_name_expr = parsed.leaf_name_expression(&struct_name);
|
|
||||||
let warning_note = parsed
|
|
||||||
.explicit_id_value()
|
|
||||||
.as_ref()
|
|
||||||
.filter(|name| !name.value().is_empty())
|
|
||||||
.filter(|name| !looks_like_canonical_leaf_name(&name.value()))
|
|
||||||
.map(|name| {
|
|
||||||
LitStr::new(
|
|
||||||
&format!(
|
|
||||||
"leaf id `{}` does not follow the recommended dotted format `org.product.vN.leaf_name[.part]`",
|
|
||||||
name.value()
|
|
||||||
),
|
|
||||||
proc_macro2::Span::call_site(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.map(|note| quote! { #[deprecated(note = #note)] });
|
|
||||||
let leaf_name_warning_attr = warning_note.unwrap_or_else(|| quote! {});
|
|
||||||
|
|
||||||
Ok(quote! {
|
|
||||||
impl #impl_generics ::unshell::protocol::tree::ProtocolLeaf for #struct_name #ty_generics #where_clause {
|
|
||||||
fn leaf_name() -> ::unshell::alloc::string::String {
|
|
||||||
#leaf_name_expr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl #impl_generics ::unshell::protocol::tree::LeafBinding for #struct_name #ty_generics #where_clause {
|
|
||||||
type Declaration = Self;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl #impl_generics #struct_name #ty_generics #where_clause {
|
|
||||||
/// Returns the canonical dotted leaf name declared for this type.
|
|
||||||
#leaf_name_warning_attr
|
|
||||||
pub fn protocol_leaf_name() -> ::unshell::alloc::string::String {
|
|
||||||
<Self as ::unshell::protocol::tree::ProtocolLeaf>::leaf_name()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ pub(crate) struct LeafDeclarationInput {
|
|||||||
version: Option<LitStr>,
|
version: Option<LitStr>,
|
||||||
endpoint_struct: Option<Ident>,
|
endpoint_struct: Option<Ident>,
|
||||||
tui_struct: Option<Ident>,
|
tui_struct: Option<Ident>,
|
||||||
procedures: Vec<Ident>,
|
procedures: Vec<ProcedureRef>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parse for LeafDeclarationInput {
|
impl Parse for LeafDeclarationInput {
|
||||||
@@ -119,7 +119,21 @@ enum LeafAssignment {
|
|||||||
Version(LitStr),
|
Version(LitStr),
|
||||||
EndpointStruct(Ident),
|
EndpointStruct(Ident),
|
||||||
TuiStruct(Ident),
|
TuiStruct(Ident),
|
||||||
Procedures(Vec<Ident>),
|
Procedures(Vec<ProcedureRef>),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ProcedureRef {
|
||||||
|
Symbol(Ident),
|
||||||
|
Suffix(LitStr),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for ProcedureRef {
|
||||||
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||||
|
if input.peek(LitStr) {
|
||||||
|
return Ok(Self::Suffix(input.parse()?));
|
||||||
|
}
|
||||||
|
Ok(Self::Symbol(input.parse()?))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parse for LeafAssignment {
|
impl Parse for LeafAssignment {
|
||||||
@@ -137,7 +151,7 @@ impl Parse for LeafAssignment {
|
|||||||
"procedures" => {
|
"procedures" => {
|
||||||
let content;
|
let content;
|
||||||
syn::bracketed!(content in input);
|
syn::bracketed!(content in input);
|
||||||
let values = Punctuated::<Ident, Token![,]>::parse_terminated(&content)?
|
let values = Punctuated::<ProcedureRef, Token![,]>::parse_terminated(&content)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
Ok(Self::Procedures(values))
|
Ok(Self::Procedures(values))
|
||||||
@@ -165,9 +179,20 @@ pub(crate) fn expand_leaf_declaration(input: LeafDeclarationInput) -> Result<Tok
|
|||||||
let procedure_suffixes = input
|
let procedure_suffixes = input
|
||||||
.procedures
|
.procedures
|
||||||
.iter()
|
.iter()
|
||||||
.map(|procedure| LitStr::new(&normalize_suffix(&procedure.to_string()), procedure.span()))
|
.map(|procedure| match procedure {
|
||||||
|
ProcedureRef::Symbol(procedure) => {
|
||||||
|
quote! { <#procedure as ::unshell::protocol::tree::ProcedureMetadata>::PROCEDURE_SUFFIX }
|
||||||
|
}
|
||||||
|
ProcedureRef::Suffix(suffix) => quote! { #suffix },
|
||||||
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let procedure_type_checks = input.procedures.iter();
|
let procedure_type_checks = input
|
||||||
|
.procedures
|
||||||
|
.iter()
|
||||||
|
.filter_map(|procedure| match procedure {
|
||||||
|
ProcedureRef::Symbol(procedure) => Some(procedure),
|
||||||
|
ProcedureRef::Suffix(_) => None,
|
||||||
|
});
|
||||||
|
|
||||||
let endpoint_impl = input
|
let endpoint_impl = input
|
||||||
.endpoint_struct
|
.endpoint_struct
|
||||||
@@ -273,40 +298,3 @@ fn expand_binding_impl(host: &Ident, declaration: &Ident) -> TokenStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_suffix(value: &str) -> String {
|
|
||||||
let mut normalized = String::with_capacity(value.len());
|
|
||||||
let mut previous_was_separator = false;
|
|
||||||
|
|
||||||
for character in value.chars() {
|
|
||||||
if character.is_ascii_uppercase() {
|
|
||||||
if !normalized.is_empty() && !previous_was_separator {
|
|
||||||
normalized.push('_');
|
|
||||||
}
|
|
||||||
normalized.push(character.to_ascii_lowercase());
|
|
||||||
previous_was_separator = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if character.is_ascii_lowercase() || character.is_ascii_digit() {
|
|
||||||
normalized.push(character);
|
|
||||||
previous_was_separator = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !normalized.is_empty() && !previous_was_separator {
|
|
||||||
normalized.push('_');
|
|
||||||
previous_was_separator = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while normalized.ends_with('_') {
|
|
||||||
normalized.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
if normalized.is_empty() {
|
|
||||||
String::from("procedure")
|
|
||||||
} else {
|
|
||||||
normalized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+16
-34
@@ -1,6 +1,5 @@
|
|||||||
//! Proc macros for `unshell` application-layer leaf declarations.
|
//! Proc macros for `unshell` application-layer leaf declarations.
|
||||||
|
|
||||||
mod leaf;
|
|
||||||
mod leaf_decl;
|
mod leaf_decl;
|
||||||
mod procedure;
|
mod procedure;
|
||||||
mod procedures;
|
mod procedures;
|
||||||
@@ -38,33 +37,6 @@ pub fn leaf(input: TokenStream) -> TokenStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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)) {
|
|
||||||
Ok(tokens) => tokens.into(),
|
|
||||||
Err(error) => error.to_compile_error().into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Derives canonical stateful-procedure metadata for one procedure type.
|
/// Derives canonical stateful-procedure metadata for one procedure type.
|
||||||
///
|
///
|
||||||
/// What it is: a derive macro that records one procedure suffix and generates
|
/// What it is: a derive macro that records one procedure suffix and generates
|
||||||
@@ -75,10 +47,16 @@ pub fn derive_leaf(input: TokenStream) -> TokenStream {
|
|||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// use unshell::{Leaf, Procedure};
|
/// use unshell::{Procedure, leaf};
|
||||||
|
///
|
||||||
|
/// struct ShellLeaf;
|
||||||
|
///
|
||||||
|
/// leaf! {
|
||||||
|
/// name = "shell",
|
||||||
|
/// procedures = [OpenSession],
|
||||||
|
/// endpoint_struct = ShellLeaf,
|
||||||
|
/// }
|
||||||
///
|
///
|
||||||
/// #[derive(Leaf)]
|
|
||||||
/// #[leaf(leaf_name = "shell")]
|
|
||||||
/// struct ShellLeaf;
|
/// struct ShellLeaf;
|
||||||
///
|
///
|
||||||
/// #[derive(Procedure)]
|
/// #[derive(Procedure)]
|
||||||
@@ -106,12 +84,16 @@ pub fn derive_procedure(input: TokenStream) -> TokenStream {
|
|||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// use unshell::{Leaf, procedures};
|
/// use unshell::{leaf, procedures};
|
||||||
///
|
///
|
||||||
/// #[derive(Leaf)]
|
|
||||||
/// #[leaf(id = "org.example.v1.echo")]
|
|
||||||
/// struct EchoLeaf;
|
/// struct EchoLeaf;
|
||||||
///
|
///
|
||||||
|
/// leaf! {
|
||||||
|
/// id = "org.example.v1.echo",
|
||||||
|
/// procedures = [echo],
|
||||||
|
/// endpoint_struct = EchoLeaf,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
/// #[procedures(error = core::convert::Infallible)]
|
/// #[procedures(error = core::convert::Infallible)]
|
||||||
/// impl EchoLeaf {
|
/// impl EchoLeaf {
|
||||||
/// #[call]
|
/// #[call]
|
||||||
|
|||||||
@@ -95,9 +95,7 @@ pub(crate) fn expand_procedure(input: DeriveInput) -> Result<proc_macro2::TokenS
|
|||||||
{
|
{
|
||||||
type Leaf = #leaf_ty;
|
type Leaf = #leaf_ty;
|
||||||
|
|
||||||
fn procedure_suffix() -> &'static str {
|
const PROCEDURE_SUFFIX: &'static str = #suffix;
|
||||||
#suffix
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl #impl_generics #procedure_name #ty_generics #where_clause {
|
impl #impl_generics #procedure_name #ty_generics #where_clause {
|
||||||
|
|||||||
@@ -108,25 +108,11 @@ pub(crate) fn expand_procedures(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let suffix_literals = dispatch_arms
|
|
||||||
.iter()
|
|
||||||
.map(|arm| arm.suffix_literal.clone())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let procedure_matches = dispatch_arms.iter().map(|arm| {
|
|
||||||
let suffix = &arm.suffix_literal;
|
|
||||||
quote! { #suffix => <Self as ::unshell::protocol::tree::LeafDeclaration>::procedure_id(#suffix), }
|
|
||||||
});
|
|
||||||
let dispatch_checks = dispatch_arms.iter().map(|arm| arm.dispatch_tokens.clone());
|
let dispatch_checks = dispatch_arms.iter().map(|arm| arm.dispatch_tokens.clone());
|
||||||
|
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
#item
|
#item
|
||||||
|
|
||||||
impl #impl_generics_tokens ::unshell::protocol::tree::LeafDeclaration for #self_ty #where_clause {
|
|
||||||
fn procedure_suffixes() -> &'static [&'static str] {
|
|
||||||
&[#(#suffix_literals),*]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl #impl_generics_tokens ::unshell::protocol::tree::CallProcedures for #self_ty #where_clause {
|
impl #impl_generics_tokens ::unshell::protocol::tree::CallProcedures for #self_ty #where_clause {
|
||||||
type Error = #error_ty;
|
type Error = #error_ty;
|
||||||
|
|
||||||
@@ -142,22 +128,6 @@ pub(crate) fn expand_procedures(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl #impl_generics_tokens #self_ty #where_clause {
|
|
||||||
/// Returns the canonical protocol leaf metadata for this type.
|
|
||||||
pub fn protocol_leaf_spec() -> ::unshell::protocol::tree::LeafSpec {
|
|
||||||
<Self as ::unshell::protocol::tree::LeafDeclaration>::leaf_spec()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolves one local procedure suffix to its full canonical `procedure_id`.
|
|
||||||
pub fn protocol_procedure_id(
|
|
||||||
suffix: &str,
|
|
||||||
) -> ::core::option::Option<::unshell::alloc::string::String> {
|
|
||||||
match suffix {
|
|
||||||
#(#procedure_matches)*
|
|
||||||
_ => ::core::option::Option::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,8 +135,12 @@ fn expand_call_arm(method: &ImplItemFn) -> Result<CallArm> {
|
|||||||
let method_name = &method.sig.ident;
|
let method_name = &method.sig.ident;
|
||||||
let suffix_literal = call_suffix_literal(method)?;
|
let suffix_literal = call_suffix_literal(method)?;
|
||||||
let call_id_expr = quote! {
|
let call_id_expr = quote! {
|
||||||
<Self as ::unshell::protocol::tree::LeafDeclaration>::procedure_id(#suffix_literal)
|
{
|
||||||
.expect("generated procedure id must exist")
|
let mut __unshell_id = <Self as ::unshell::protocol::tree::ProtocolLeaf>::leaf_name();
|
||||||
|
__unshell_id.push('.');
|
||||||
|
__unshell_id.push_str(#suffix_literal);
|
||||||
|
__unshell_id
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let inputs = method
|
let inputs = method
|
||||||
|
|||||||
@@ -9,33 +9,6 @@ pub(crate) fn option_litstr_tokens(value: Option<&LitStr>) -> TokenStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn looks_like_canonical_leaf_name(name: &str) -> bool {
|
|
||||||
let segments = name.split('.').collect::<Vec<_>>();
|
|
||||||
if segments.len() < 4 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for segment in &segments {
|
|
||||||
if segment.is_empty() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !segment.chars().all(|character| {
|
|
||||||
character.is_ascii_lowercase() || character.is_ascii_digit() || character == '_'
|
|
||||||
}) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !segments[2].starts_with('v') || segments[2].len() <= 1 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
segments[2][1..]
|
|
||||||
.chars()
|
|
||||||
.all(|character| character.is_ascii_digit() || character == '_')
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn extract_outer_type_argument<'a>(ty: &'a Type, expected: &str) -> Option<&'a Type> {
|
pub(crate) fn extract_outer_type_argument<'a>(ty: &'a Type, expected: &str) -> Option<&'a Type> {
|
||||||
let Type::Path(TypePath { path, .. }) = ty else {
|
let Type::Path(TypePath { path, .. }) = ty else {
|
||||||
return None;
|
return None;
|
||||||
@@ -85,21 +58,3 @@ pub(crate) fn take_call_attr(attrs: &mut Vec<Attribute>) -> bool {
|
|||||||
attrs.retain(|attr| !attr.path().is_ident("call"));
|
attrs.retain(|attr| !attr.path().is_ident("call"));
|
||||||
original_len != attrs.len()
|
original_len != attrs.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::looks_like_canonical_leaf_name;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn canonical_leaf_name_accepts_minimal_valid_shape() {
|
|
||||||
assert!(looks_like_canonical_leaf_name("org.example.v1.echo"));
|
|
||||||
assert!(looks_like_canonical_leaf_name("org.example.v1.echo.abc123"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn canonical_leaf_name_rejects_wrong_shapes() {
|
|
||||||
assert!(!looks_like_canonical_leaf_name("org.example.echo"));
|
|
||||||
assert!(!looks_like_canonical_leaf_name("org.example.1.echo"));
|
|
||||||
assert!(!looks_like_canonical_leaf_name("Org.example.v1.echo"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,4 +17,4 @@ pub mod protocol;
|
|||||||
pub use protocol::*;
|
pub use protocol::*;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub use unshell_macros::{Leaf, Procedure, procedures};
|
pub use unshell_macros::{Procedure, leaf, procedures};
|
||||||
|
|||||||
@@ -8,18 +8,22 @@ use crate::protocol::tree::{
|
|||||||
decode_call_input, encode_call_reply,
|
decode_call_input, encode_call_reply,
|
||||||
};
|
};
|
||||||
use crate::protocol::{PacketType, decode_frame};
|
use crate::protocol::{PacketType, decode_frame};
|
||||||
use crate::{Leaf, procedures};
|
use crate::{leaf, procedures};
|
||||||
|
|
||||||
fn path(parts: &[&str]) -> Vec<String> {
|
fn path(parts: &[&str]) -> Vec<String> {
|
||||||
parts.iter().map(|part| (*part).to_owned()).collect()
|
parts.iter().map(|part| (*part).to_owned()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Leaf)]
|
|
||||||
#[leaf(id = "org.example.v1.echo")]
|
|
||||||
struct EchoLeaf {
|
struct EchoLeaf {
|
||||||
prefix: String,
|
prefix: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leaf! {
|
||||||
|
id = "org.example.v1.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 {
|
||||||
text: String,
|
text: String,
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
use alloc::{string::String, vec};
|
||||||
|
|
||||||
|
use crate::leaf;
|
||||||
|
use crate::protocol::tree::{LeafBinding, LeafDeclaration, ProcedureMetadata, ProtocolLeaf};
|
||||||
|
|
||||||
|
struct EndpointHost;
|
||||||
|
struct Open;
|
||||||
|
struct Reset;
|
||||||
|
|
||||||
|
impl ProcedureMetadata for Open {
|
||||||
|
type Leaf = EndpointHost;
|
||||||
|
const PROCEDURE_SUFFIX: &'static str = "open";
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcedureMetadata for Reset {
|
||||||
|
type Leaf = EndpointHost;
|
||||||
|
const PROCEDURE_SUFFIX: &'static str = "reset";
|
||||||
|
}
|
||||||
|
|
||||||
|
leaf! {
|
||||||
|
id = "org.example.v1.demo",
|
||||||
|
procedures = [Open, Reset],
|
||||||
|
endpoint_struct = EndpointHost,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EndpointHalf;
|
||||||
|
struct TuiHalf;
|
||||||
|
struct Connect;
|
||||||
|
|
||||||
|
impl ProcedureMetadata for Connect {
|
||||||
|
type Leaf = EndpointHalf;
|
||||||
|
const PROCEDURE_SUFFIX: &'static str = "connect";
|
||||||
|
}
|
||||||
|
|
||||||
|
leaf! {
|
||||||
|
name = "chat",
|
||||||
|
org = "org",
|
||||||
|
product = "example",
|
||||||
|
version = "v2",
|
||||||
|
procedures = [Connect],
|
||||||
|
endpoint_struct = EndpointHalf,
|
||||||
|
tui_struct = TuiHalf,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TuiOnly;
|
||||||
|
struct Tail;
|
||||||
|
|
||||||
|
impl ProcedureMetadata for Tail {
|
||||||
|
type Leaf = TuiOnly;
|
||||||
|
const PROCEDURE_SUFFIX: &'static str = "tail";
|
||||||
|
}
|
||||||
|
|
||||||
|
leaf! {
|
||||||
|
id = "org.example.v1.transcript",
|
||||||
|
procedures = [Tail],
|
||||||
|
tui_struct = TuiOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leaf_declaration_generates_endpoint_host_metadata() {
|
||||||
|
assert_eq!(EndpointHost::protocol_leaf_name(), "org.example.v1.demo");
|
||||||
|
assert_eq!(
|
||||||
|
EndpointHost::protocol_leaf_spec().procedures,
|
||||||
|
vec![
|
||||||
|
String::from("org.example.v1.demo.open"),
|
||||||
|
String::from("org.example.v1.demo.reset"),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
<EndpointHost as LeafBinding>::Declaration::leaf_name(),
|
||||||
|
"org.example.v1.demo"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leaf_declaration_shares_metadata_between_endpoint_and_tui_hosts() {
|
||||||
|
assert_eq!(
|
||||||
|
EndpointHalf::protocol_leaf_name(),
|
||||||
|
TuiHalf::protocol_leaf_name()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
EndpointHalf::protocol_leaf_spec().procedures,
|
||||||
|
TuiHalf::protocol_leaf_spec().procedures
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
<EndpointHalf as LeafBinding>::Declaration::procedure_id("connect"),
|
||||||
|
Some(String::from("org.example.v2.chat.connect"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leaf_declaration_supports_tui_only_hosts() {
|
||||||
|
assert_eq!(TuiOnly::protocol_leaf_name(), "org.example.v1.transcript");
|
||||||
|
assert_eq!(
|
||||||
|
<TuiOnly as LeafDeclaration>::procedure_id("tail"),
|
||||||
|
Some(String::from("org.example.v1.transcript.tail"))
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
mod call;
|
mod call;
|
||||||
|
mod leaf_decl;
|
||||||
mod procedure;
|
mod procedure;
|
||||||
mod protocol;
|
mod protocol;
|
||||||
mod tree;
|
mod tree;
|
||||||
|
|||||||
@@ -6,18 +6,23 @@ use crate::protocol::tree::{
|
|||||||
ProcedureEffect, ProcedureRuntime, ProcedureStore, ProtocolEndpoint, encode_call_reply,
|
ProcedureEffect, ProcedureRuntime, ProcedureStore, ProtocolEndpoint, encode_call_reply,
|
||||||
};
|
};
|
||||||
use crate::protocol::{PacketType, decode_frame};
|
use crate::protocol::{PacketType, decode_frame};
|
||||||
use crate::{Leaf, Procedure};
|
use crate::{Procedure, leaf};
|
||||||
|
|
||||||
fn path(parts: &[&str]) -> Vec<String> {
|
fn path(parts: &[&str]) -> Vec<String> {
|
||||||
parts.iter().map(|part| (*part).to_owned()).collect()
|
parts.iter().map(|part| (*part).to_owned()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Leaf)]
|
#[derive(Default)]
|
||||||
#[leaf(id = "org.example.v1.stream")]
|
|
||||||
struct StreamLeaf {
|
struct StreamLeaf {
|
||||||
sessions: BTreeMap<HookKey, ProcedureOpen>,
|
sessions: BTreeMap<HookKey, ProcedureOpen>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leaf! {
|
||||||
|
id = "org.example.v1.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> {
|
||||||
&mut self.sessions
|
&mut self.sessions
|
||||||
@@ -67,10 +72,7 @@ fn procedure_runtime_routes_data_to_stored_session() {
|
|||||||
path(&["agent"]),
|
path(&["agent"]),
|
||||||
Some(Vec::new()),
|
Some(Vec::new()),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![crate::protocol::tree::LeafSpec {
|
vec![StreamLeaf::protocol_leaf_spec()],
|
||||||
name: StreamLeaf::protocol_leaf_name(),
|
|
||||||
procedures: vec![ProcedureOpen::protocol_procedure_id()],
|
|
||||||
}],
|
|
||||||
);
|
);
|
||||||
let mut runtime =
|
let mut runtime =
|
||||||
ProcedureRuntime::<StreamLeaf, ProcedureOpen>::new(endpoint, StreamLeaf::default());
|
ProcedureRuntime::<StreamLeaf, ProcedureOpen>::new(endpoint, StreamLeaf::default());
|
||||||
@@ -139,12 +141,17 @@ fn procedure_runtime_routes_data_to_stored_session() {
|
|||||||
assert!(runtime.leaf_mut().procedure_sessions().is_empty());
|
assert!(runtime.leaf_mut().procedure_sessions().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Leaf)]
|
#[derive(Default)]
|
||||||
#[leaf(id = "org.example.v1.duplex")]
|
|
||||||
struct DuplexLeaf {
|
struct DuplexLeaf {
|
||||||
sessions: BTreeMap<HookKey, DuplexProcedure>,
|
sessions: BTreeMap<HookKey, DuplexProcedure>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leaf! {
|
||||||
|
id = "org.example.v1.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> {
|
||||||
&mut self.sessions
|
&mut self.sessions
|
||||||
@@ -197,10 +204,7 @@ fn procedure_runtime_keeps_session_after_local_end_until_explicit_close() {
|
|||||||
path(&["agent"]),
|
path(&["agent"]),
|
||||||
Some(Vec::new()),
|
Some(Vec::new()),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![crate::protocol::tree::LeafSpec {
|
vec![DuplexLeaf::protocol_leaf_spec()],
|
||||||
name: DuplexLeaf::protocol_leaf_name(),
|
|
||||||
procedures: vec![DuplexProcedure::protocol_procedure_id()],
|
|
||||||
}],
|
|
||||||
);
|
);
|
||||||
let mut runtime =
|
let mut runtime =
|
||||||
ProcedureRuntime::<DuplexLeaf, DuplexProcedure>::new(endpoint, DuplexLeaf::default());
|
ProcedureRuntime::<DuplexLeaf, DuplexProcedure>::new(endpoint, DuplexLeaf::default());
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ use super::{
|
|||||||
/// struct Open;
|
/// struct Open;
|
||||||
/// impl ProcedureMetadata for Open {
|
/// impl ProcedureMetadata for Open {
|
||||||
/// type Leaf = ExampleLeaf;
|
/// type Leaf = ExampleLeaf;
|
||||||
/// fn procedure_suffix() -> &'static str { "open" }
|
/// const PROCEDURE_SUFFIX: &'static str = "open";
|
||||||
/// }
|
/// }
|
||||||
/// assert_eq!(Open::procedure_id(), "org.example.v1.shell.open");
|
/// assert_eq!(Open::procedure_id(), "org.example.v1.shell.open");
|
||||||
/// ```
|
/// ```
|
||||||
@@ -55,7 +55,12 @@ pub trait ProcedureMetadata: Sized {
|
|||||||
type Leaf: ProtocolLeaf;
|
type Leaf: ProtocolLeaf;
|
||||||
|
|
||||||
/// Returns the local suffix used to derive the full canonical `procedure_id`.
|
/// Returns the local suffix used to derive the full canonical `procedure_id`.
|
||||||
fn procedure_suffix() -> &'static str;
|
const PROCEDURE_SUFFIX: &'static str;
|
||||||
|
|
||||||
|
/// Returns the local suffix used to derive the full canonical `procedure_id`.
|
||||||
|
fn procedure_suffix() -> &'static str {
|
||||||
|
Self::PROCEDURE_SUFFIX
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the canonical `procedure_id` for this procedure.
|
/// Returns the canonical `procedure_id` for this procedure.
|
||||||
fn procedure_id() -> String {
|
fn procedure_id() -> String {
|
||||||
@@ -81,8 +86,7 @@ pub trait ProcedureMetadata: Sized {
|
|||||||
/// struct Open;
|
/// struct Open;
|
||||||
/// impl ProcedureMetadata for Open {
|
/// impl ProcedureMetadata for Open {
|
||||||
/// type Leaf = ExampleLeaf;
|
/// type Leaf = ExampleLeaf;
|
||||||
///
|
/// const PROCEDURE_SUFFIX: &'static str = "open";
|
||||||
/// fn procedure_suffix() -> &'static str { "open" }
|
|
||||||
/// }
|
/// }
|
||||||
/// fn _compat<T: StatefulProcedureMetadata<ExampleLeaf>>() {}
|
/// fn _compat<T: StatefulProcedureMetadata<ExampleLeaf>>() {}
|
||||||
/// _compat::<Open>();
|
/// _compat::<Open>();
|
||||||
@@ -133,15 +137,20 @@ pub trait ProcedureStore<P> {
|
|||||||
/// ```rust
|
/// ```rust
|
||||||
/// use std::collections::BTreeMap;
|
/// use std::collections::BTreeMap;
|
||||||
/// use std::string::String;
|
/// use std::string::String;
|
||||||
/// use unshell::{Leaf, Procedure};
|
/// use unshell::{Procedure, leaf};
|
||||||
/// use unshell::protocol::tree::{Call, HookKey, Procedure, ProcedureEffect, ProcedureStore};
|
/// use unshell::protocol::tree::{Call, HookKey, Procedure, ProcedureEffect, ProcedureStore};
|
||||||
///
|
///
|
||||||
/// #[derive(Default, Leaf)]
|
/// #[derive(Default)]
|
||||||
/// #[leaf(id = "org.example.v1.stream")]
|
|
||||||
/// struct StreamLeaf {
|
/// struct StreamLeaf {
|
||||||
/// sessions: BTreeMap<HookKey, OpenProcedure>,
|
/// sessions: BTreeMap<HookKey, OpenProcedure>,
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
|
/// leaf! {
|
||||||
|
/// id = "org.example.v1.stream",
|
||||||
|
/// procedures = [OpenProcedure],
|
||||||
|
/// endpoint_struct = StreamLeaf,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
/// impl ProcedureStore<OpenProcedure> for StreamLeaf {
|
/// impl ProcedureStore<OpenProcedure> for StreamLeaf {
|
||||||
/// fn procedure_sessions(&mut self) -> &mut BTreeMap<HookKey, OpenProcedure> {
|
/// fn procedure_sessions(&mut self) -> &mut BTreeMap<HookKey, OpenProcedure> {
|
||||||
/// &mut self.sessions
|
/// &mut self.sessions
|
||||||
|
|||||||
Reference in New Issue
Block a user