diff --git a/examples/protocol/remote_shell_single_endpoint.rs b/examples/protocol/remote_shell_single_endpoint.rs index 129b1cc..3413360 100644 --- a/examples/protocol/remote_shell_single_endpoint.rs +++ b/examples/protocol/remote_shell_single_endpoint.rs @@ -10,22 +10,19 @@ use std::error::Error; +use unshell::create_endpoint; use unshell::leaves::remote_shell; -use unshell::protocol::tree::{EndpointOutcome, LocalEvent, ProtocolEndpoint}; +use unshell::protocol::tree::{Endpoint, EndpointOutcome, LocalEvent, ProtocolEndpoint}; use unshell::protocol::{INTROSPECTION_PROCEDURE_ID, LeafIntrospection}; fn main() -> Result<(), Box> { + let mut endpoint: ProtocolEndpoint = + create_endpoint!("agent", remote_shell::endpoint::RemoteShell::default()); let leaf_spec = remote_shell::endpoint::RemoteShell::protocol_leaf_spec(); - let mut endpoint = ProtocolEndpoint::new( - agent_path(), - Some(Vec::new()), - Vec::new(), - vec![leaf_spec.clone()], - ); let hook_id = endpoint.allocate_hook_id(); let outcome = endpoint.send_call( - agent_path(), + Vec::new(), Some(remote_shell::endpoint::RemoteShell::protocol_leaf_name()), INTROSPECTION_PROCEDURE_ID, Some(hook_id), @@ -41,13 +38,10 @@ fn main() -> Result<(), Box> { "remote-shell examples normally listen on {}", remote_shell::endpoint::LISTEN_ADDR ); - println!("endpoint path: {:?}", agent_path()); + println!("endpoint id: {:?}", endpoint.local_id()); + println!("endpoint path: {:?}", endpoint.path()); println!("declared leaf: {}", leaf_spec.name); println!("leaf: {}", payload.leaf_name); println!("procedures: {:?}", payload.procedures); Ok(()) } - -fn agent_path() -> Vec { - vec![String::from("agent")] -} diff --git a/src/lib.rs b/src/lib.rs index 4e6260d..f964ab6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,4 +28,40 @@ pub use unshell_leaves as leaves; pub use unshell_macros::{Procedure, leaf, procedures}; +/// Creates a root-assumed endpoint from one local identifier plus any number of leaf hosts. +/// +/// What it is: a convenience macro that builds a `ProtocolEndpoint` whose protocol path starts at +/// root, with no parent or children, and whose leaf inventory is inferred from the supplied host +/// values. +/// +/// Why it exists: the common bootstrap case should not require callers to manually construct an +/// empty path, `Vec`, and a `Vec` when they already have leaf host values. +/// +/// # Example +/// ```rust +/// use unshell::{create_endpoint, leaf}; +/// use unshell::protocol::tree::Endpoint; +/// +/// #[derive(Default)] +/// struct DemoLeaf; +/// +/// #[leaf(id = "org.example.v1.demo", procedures = ["ping"], endpoint_struct = DemoLeaf)] +/// struct Demo; +/// +/// let endpoint = create_endpoint!("demo", DemoLeaf::default()); +/// assert!(endpoint.path().is_empty()); +/// assert_eq!(endpoint.local_id(), Some("demo")); +/// ``` +#[macro_export] +macro_rules! create_endpoint { + ($id:expr $(, $leaf:expr )* $(,)?) => {{ + let mut __unshell_leaf_specs = ::unshell::alloc::vec::Vec::new(); + $( + let __unshell_leaf = $leaf; + __unshell_leaf_specs.push(::unshell::protocol::tree::leaf_spec_of(&__unshell_leaf)); + )* + ::unshell::protocol::tree::ProtocolEndpoint::root($id, __unshell_leaf_specs) + }}; +} + // pub use ush_obfuscate as obfuscate; diff --git a/unshell-protocol/src/protocol/tree/endpoint/builders.rs b/unshell-protocol/src/protocol/tree/endpoint/builders.rs index dc2da34..ef2ca33 100644 --- a/unshell-protocol/src/protocol/tree/endpoint/builders.rs +++ b/unshell-protocol/src/protocol/tree/endpoint/builders.rs @@ -135,6 +135,7 @@ impl ProtocolEndpoint { .collect::>(); Self { + local_id: None, routing: CompiledRoutes::new(&path, ®istered_child_paths, parent_path.is_some()), path, children, @@ -147,6 +148,52 @@ impl ProtocolEndpoint { } } + #[must_use] + /// Creates a root-assumed endpoint with one local identifier and predeclared leaves. + /// + /// What it is: a convenience constructor for the common bootstrap state where an endpoint has + /// one local name but has not yet been assigned a non-root path by a parent connection. + /// + /// Why it exists: endpoint creation should not require every caller to manually pass an empty + /// path, no parent, and no children just to host one or more known leaves. + /// + /// # Example + /// ```rust + /// use unshell::protocol::tree::{LeafSpec, ProtocolEndpoint}; + /// let endpoint = ProtocolEndpoint::root( + /// "worker", + /// vec![LeafSpec { + /// name: "service".into(), + /// procedures: vec!["example.service.v1.invoke".into()], + /// }], + /// ); + /// assert!(endpoint.path().is_empty()); + /// assert_eq!(endpoint.local_id(), Some("worker")); + /// ``` + pub fn root(local_id: impl Into, leaves: Vec) -> Self { + let mut endpoint = Self::new(Vec::new(), None, Vec::new(), leaves); + endpoint.local_id = Some(local_id.into()); + endpoint + } + + #[must_use] + /// Returns the endpoint's local bootstrap identifier, if one was assigned. + /// + /// What it is: a lightweight label separate from the protocol path. + /// + /// Why it exists: a freshly created endpoint may know its own local identity before a parent + /// connection assigns its final tree path. + /// + /// # Example + /// ```rust + /// use unshell::protocol::tree::ProtocolEndpoint; + /// let endpoint = ProtocolEndpoint::root("worker", Vec::new()); + /// assert_eq!(endpoint.local_id(), Some("worker")); + /// ``` + pub fn local_id(&self) -> Option<&str> { + self.local_id.as_deref() + } + /// Registers a procedure that is handled directly by the endpoint. /// /// Endpoint-level procedures exist for protocol services that are not attached to one leaf, diff --git a/unshell-protocol/src/protocol/tree/endpoint/core.rs b/unshell-protocol/src/protocol/tree/endpoint/core.rs index 9db059e..fa28e05 100644 --- a/unshell-protocol/src/protocol/tree/endpoint/core.rs +++ b/unshell-protocol/src/protocol/tree/endpoint/core.rs @@ -286,6 +286,7 @@ pub trait Endpoint { /// ``` #[derive(Debug, Default)] pub struct ProtocolEndpoint { + pub(crate) local_id: Option, pub(crate) path: Vec, pub(crate) children: Vec, pub(crate) routing: CompiledRoutes, diff --git a/unshell-protocol/src/protocol/tree/leaf.rs b/unshell-protocol/src/protocol/tree/leaf.rs index e42d36d..8cf9e08 100644 --- a/unshell-protocol/src/protocol/tree/leaf.rs +++ b/unshell-protocol/src/protocol/tree/leaf.rs @@ -92,6 +92,34 @@ pub trait LeafDeclaration: ProtocolLeaf { } } +/// Returns the canonical `LeafSpec` for one concrete leaf host value. +/// +/// What it is: a tiny typed helper that uses a host value only for type inference. +/// +/// Why it exists: endpoint-construction macros can accept ordinary host expressions like +/// `RemoteShell::default()` and still derive the compile-time `LeafSpec` without the caller +/// spelling the leaf type twice. +/// +/// # Example +/// ```rust +/// use unshell::protocol::tree::{LeafDeclaration, ProtocolLeaf, leaf_spec_of}; +/// struct ExampleLeaf; +/// impl ProtocolLeaf for ExampleLeaf { +/// fn leaf_name() -> String { "org.example.v1.echo".into() } +/// } +/// impl LeafDeclaration for ExampleLeaf { +/// fn procedure_suffixes() -> &'static [&'static str] { &["invoke"] } +/// } +/// let spec = leaf_spec_of(&ExampleLeaf); +/// assert_eq!(spec.name, "org.example.v1.echo"); +/// ``` +pub fn leaf_spec_of(_: &L) -> LeafSpec +where + L: LeafDeclaration, +{ + L::leaf_spec() +} + /// Declares that one host struct is bound to one compile-time leaf declaration. /// /// What it is: a trait that links a concrete host type, such as an endpoint or diff --git a/unshell-protocol/src/protocol/tree/mod.rs b/unshell-protocol/src/protocol/tree/mod.rs index 71e432a..f147f30 100644 --- a/unshell-protocol/src/protocol/tree/mod.rs +++ b/unshell-protocol/src/protocol/tree/mod.rs @@ -24,7 +24,9 @@ pub use endpoint::{ ProtocolEndpoint, }; pub use hook::{ActiveHook, HookConflict, HookKey, HookTable, PendingHook}; -pub use leaf::{CallProcedures, LeafBinding, LeafDeclaration, ProtocolLeaf, derive_leaf_name}; +pub use leaf::{ + CallProcedures, LeafBinding, LeafDeclaration, ProtocolLeaf, derive_leaf_name, leaf_spec_of, +}; pub use procedure::{ Procedure, ProcedureEffect, ProcedureMetadata, ProcedureRuntime, ProcedureRuntimeError, ProcedureRuntimeOutcome, ProcedureStore, StatefulProcedureMetadata,