mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-09 06:47:59 -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,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>,
|
||||
endpoint_struct: Option<Ident>,
|
||||
tui_struct: Option<Ident>,
|
||||
procedures: Vec<Ident>,
|
||||
procedures: Vec<ProcedureRef>,
|
||||
}
|
||||
|
||||
impl Parse for LeafDeclarationInput {
|
||||
@@ -119,7 +119,21 @@ enum LeafAssignment {
|
||||
Version(LitStr),
|
||||
EndpointStruct(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 {
|
||||
@@ -137,7 +151,7 @@ impl Parse for LeafAssignment {
|
||||
"procedures" => {
|
||||
let content;
|
||||
syn::bracketed!(content in input);
|
||||
let values = Punctuated::<Ident, Token![,]>::parse_terminated(&content)?
|
||||
let values = Punctuated::<ProcedureRef, Token![,]>::parse_terminated(&content)?
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
Ok(Self::Procedures(values))
|
||||
@@ -165,9 +179,20 @@ pub(crate) fn expand_leaf_declaration(input: LeafDeclarationInput) -> Result<Tok
|
||||
let procedure_suffixes = input
|
||||
.procedures
|
||||
.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<_>>();
|
||||
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
|
||||
.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.
|
||||
|
||||
mod leaf;
|
||||
mod leaf_decl;
|
||||
mod procedure;
|
||||
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.
|
||||
///
|
||||
/// 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
|
||||
/// ```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;
|
||||
///
|
||||
/// #[derive(Procedure)]
|
||||
@@ -106,12 +84,16 @@ pub fn derive_procedure(input: TokenStream) -> TokenStream {
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use unshell::{Leaf, procedures};
|
||||
/// use unshell::{leaf, procedures};
|
||||
///
|
||||
/// #[derive(Leaf)]
|
||||
/// #[leaf(id = "org.example.v1.echo")]
|
||||
/// struct EchoLeaf;
|
||||
///
|
||||
/// leaf! {
|
||||
/// id = "org.example.v1.echo",
|
||||
/// procedures = [echo],
|
||||
/// endpoint_struct = EchoLeaf,
|
||||
/// }
|
||||
///
|
||||
/// #[procedures(error = core::convert::Infallible)]
|
||||
/// impl EchoLeaf {
|
||||
/// #[call]
|
||||
|
||||
@@ -95,9 +95,7 @@ pub(crate) fn expand_procedure(input: DeriveInput) -> Result<proc_macro2::TokenS
|
||||
{
|
||||
type Leaf = #leaf_ty;
|
||||
|
||||
fn procedure_suffix() -> &'static str {
|
||||
#suffix
|
||||
}
|
||||
const PROCEDURE_SUFFIX: &'static str = #suffix;
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
Ok(quote! {
|
||||
#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 {
|
||||
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 suffix_literal = call_suffix_literal(method)?;
|
||||
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
|
||||
|
||||
@@ -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> {
|
||||
let Type::Path(TypePath { path, .. }) = ty else {
|
||||
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"));
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user