Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions zlink-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,25 @@ pub fn derive_introspect_reply_error(input: proc_macro::TokenStream) -> proc_mac
/// * `crate` - Specifies the crate path to use for zlink types. Defaults to `::zlink`.
/// * `chain_name` - Custom name for the generated chain extension trait. Defaults to
/// `{TraitName}Chain`.
/// * `rename_all_arguments` - Applies a case convention to all method argument names. Valid values
/// are `lowercase`, `UPPERCASE`, `PascalCase`, `camelCase`, `snake_case`, `SCREAMING_SNAKE_CASE`,
/// `kebab-case`, `SCREAMING-KEBAB-CASE`. systemd's Varlink APIs, for instance, use `camelCase`
/// argument names (with `PascalCase` where a name mirrors a documented option name), so a proxy
/// for one typically wants `rename_all_arguments = "camelCase"`. Per-argument `#[zlink(rename =
/// "...")]` takes precedence over `rename_all_arguments`. Every produced name must still fit the
/// Varlink field-name grammar (`[A-Za-z][A-Za-z0-9_]*`), so a convention that steps outside it --
/// `kebab-case` on a multi-word argument, say -- is rejected at compile time:
///
/// ```rust,compile_fail
/// # use zlink::proxy;
/// #[proxy(interface = "org.example.Config", rename_all_arguments = "kebab-case")]
/// trait ConfigProxy {
/// // `dry_run` would become `dry-run`, which Varlink cannot express.
/// async fn set_config(&mut self, dry_run: bool) -> zlink::Result<Result<(), MyError>>;
/// }
/// # #[derive(Debug, serde::Serialize, serde::Deserialize)]
/// # enum MyError {}
/// ```
///
/// # Example
///
Expand Down
4 changes: 2 additions & 2 deletions zlink-macros/src/naming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ impl RenameAll {
grammar.accepts(&self.apply_to_variant("Word"))
}

fn parse(lit: &LitStr) -> Result<Self, Error> {
pub(crate) fn parse(lit: &LitStr) -> Result<Self, Error> {
let rule = match lit.value().as_str() {
"lowercase" => Self::Lower,
"UPPERCASE" => Self::Upper,
Expand All @@ -200,7 +200,7 @@ impl RenameAll {
return Err(Error::new_spanned(
lit,
format!(
"unknown `rename_all` value `{unknown}`, expected one of: {}",
"`{unknown}` is not a known case convention, expected one of: {}",
VALID_RENAME_ALL.join(", "),
),
));
Expand Down
26 changes: 21 additions & 5 deletions zlink-macros/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ pub(crate) fn proxy(attr: TokenStream, input: TokenStream) -> TokenStream {
fn proxy_impl(attr: TokenStream, input: TokenStream) -> Result<TokenStream, Error> {
let mut trait_def = parse2::<ItemTrait>(input)?;

// Parse the interface name, crate path, and chain name from the attribute
let (interface_name, crate_path, chain_name) = parse_proxy_attributes(&attr, &trait_def)?;
// Parse the interface name, crate path, chain name, and rename_all from the attribute
let (interface_name, crate_path, chain_name, rename_all) =
parse_proxy_attributes(&attr, &trait_def)?;

// Validate trait definition
validate_trait(&trait_def)?;
Expand Down Expand Up @@ -65,6 +66,7 @@ fn proxy_impl(attr: TokenStream, input: TokenStream) -> Result<TokenStream, Erro
&method_attrs,
&crate_path,
&param_attrs_map,
rename_all,
)?;
if !extension_method.is_empty() {
chain_extension_methods.push(extension_method);
Expand All @@ -81,6 +83,7 @@ fn proxy_impl(attr: TokenStream, input: TokenStream) -> Result<TokenStream, Erro
&method_attrs,
&crate_path,
&param_attrs_map,
rename_all,
)?;
methods.push(method_impl);

Expand All @@ -92,6 +95,7 @@ fn proxy_impl(attr: TokenStream, input: TokenStream) -> Result<TokenStream, Erro
&method_attrs,
&crate_path,
&param_attrs_map,
rename_all,
)?;
if !chain_trait.is_empty() {
chain_method_traits.push(chain_trait);
Expand Down Expand Up @@ -130,7 +134,15 @@ fn proxy_impl(attr: TokenStream, input: TokenStream) -> Result<TokenStream, Erro
fn parse_proxy_attributes(
attr: &TokenStream,
trait_def: &ItemTrait,
) -> Result<(String, TokenStream, Option<syn::Ident>), Error> {
) -> Result<
(
String,
TokenStream,
Option<syn::Ident>,
Option<crate::naming::RenameAll>,
),
Error,
> {
if attr.is_empty() {
return Err(Error::new_spanned(
trait_def,
Expand All @@ -141,13 +153,14 @@ fn parse_proxy_attributes(

// Try parsing as a simple string literal first (backward compatibility)
if let Ok(Lit::Str(lit_str)) = parse2::<Lit>(attr.clone()) {
return Ok((lit_str.value(), quote! { ::zlink }, None));
return Ok((lit_str.value(), quote! { ::zlink }, None, None));
}

// Parse as name-value pairs
let mut interface_name = None;
let mut crate_path = None;
let mut chain_name = None;
let mut rename_all = None;

let parser = syn::meta::parser(|meta| {
if meta.path.is_ident("interface") {
Expand All @@ -161,6 +174,9 @@ fn parse_proxy_attributes(
} else if meta.path.is_ident("chain_name") {
let value: syn::LitStr = meta.value()?.parse()?;
chain_name = Some(syn::Ident::new(&value.value(), value.span()));
} else if meta.path.is_ident("rename_all_arguments") {
let value: syn::LitStr = meta.value()?.parse()?;
rename_all = Some(crate::naming::RenameAll::parse(&value)?);
Comment thread
zeenix marked this conversation as resolved.
} else {
return Err(meta.error("unsupported attribute"));
}
Expand All @@ -179,7 +195,7 @@ fn parse_proxy_attributes(

let crate_path = crate_path.unwrap_or_else(|| quote! { ::zlink });

Ok((interface_name, crate_path, chain_name))
Ok((interface_name, crate_path, chain_name, rename_all))
}

fn validate_trait(trait_def: &ItemTrait) -> Result<(), Error> {
Expand Down
17 changes: 11 additions & 6 deletions zlink-macros/src/proxy/chain_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use syn::{Error, FnArg, Pat};
use super::{
types::{ArgInfo, MethodAttrs},
utils::{
ParamAttrs, convert_to_single_lifetime, parse_return_type, snake_case_to_pascal_case,
type_contains_lifetime,
ParamAttrs, convert_to_single_lifetime, parse_return_type, resolve_serialized_name,
snake_case_to_pascal_case, type_contains_lifetime,
},
};

Expand All @@ -17,6 +17,7 @@ pub(super) fn generate_chain_extension_method(
method_attrs: &MethodAttrs,
crate_path: &TokenStream,
param_attrs_map: &std::collections::HashMap<String, ParamAttrs>,
rename_all: Option<crate::naming::RenameAll>,
) -> Result<(TokenStream, TokenStream), Error> {
// Only the wire name is unraw'd; `method_ident` stays raw so the generated fn still matches
// the trait it implements.
Expand Down Expand Up @@ -50,7 +51,8 @@ pub(super) fn generate_chain_extension_method(
let method_where_clause = method.sig.generics.where_clause.clone();

// Parse method arguments (skip &mut self)
let arg_infos = parse_method_arguments(method, has_explicit_lifetimes, param_attrs_map)?;
let arg_infos =
parse_method_arguments(method, has_explicit_lifetimes, param_attrs_map, rename_all)?;
let has_any_lifetime = arg_infos.iter().any(|info| info.has_lifetime);

// Validate FD parameters
Expand Down Expand Up @@ -147,6 +149,7 @@ fn parse_method_arguments<'a>(
method: &'a mut syn::TraitItemFn,
has_explicit_lifetimes: bool,
param_attrs_map: &std::collections::HashMap<String, ParamAttrs>,
rename_all: Option<crate::naming::RenameAll>,
) -> Result<Vec<ArgInfo<'a>>, Error> {
method
.sig
Expand All @@ -165,9 +168,11 @@ fn parse_method_arguments<'a>(
let ty = &pat_type.ty;

// Get pre-extracted parameter attributes
let param_name = name.to_string();
let param_attrs = param_attrs_map.get(&param_name);
let serialized_name = param_attrs.and_then(|attrs| attrs.rename.clone());
let param_attrs = param_attrs_map.get(&name.to_string());
let serialized_name = match resolve_serialized_name(name, param_attrs, rename_all) {
Ok(serialized_name) => serialized_name,
Err(err) => return Some(Err(err)),
};
let is_fds = param_attrs.map(|attrs| attrs.is_fds).unwrap_or(false);

// Only convert to single lifetime if there are no explicit lifetimes
Expand Down
17 changes: 11 additions & 6 deletions zlink-macros/src/proxy/chain_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use syn::{Error, FnArg, Pat};
use super::{
types::{ArgInfo, MethodAttrs},
utils::{
ParamAttrs, convert_to_single_lifetime, parse_return_type, snake_case_to_pascal_case,
type_contains_lifetime,
ParamAttrs, convert_to_single_lifetime, parse_return_type, resolve_serialized_name,
snake_case_to_pascal_case, type_contains_lifetime,
},
};

Expand All @@ -22,6 +22,7 @@ pub(super) fn generate_chain_method(
method_attrs: &MethodAttrs,
crate_path: &TokenStream,
param_attrs_map: &std::collections::HashMap<String, ParamAttrs>,
rename_all: Option<crate::naming::RenameAll>,
) -> Result<(TokenStream, TokenStream), Error> {
// Only the wire name and the `chain_` ident are unraw'd; `method_ident` stays raw so the
// generated fn still matches the trait it implements.
Expand Down Expand Up @@ -60,7 +61,8 @@ pub(super) fn generate_chain_method(
let method_where_clause = method.sig.generics.where_clause.clone();

// Parse method arguments (skip &mut self)
let arg_infos = parse_method_arguments(method, has_explicit_lifetimes, param_attrs_map)?;
let arg_infos =
parse_method_arguments(method, has_explicit_lifetimes, param_attrs_map, rename_all)?;
let has_any_lifetime = arg_infos.iter().any(|info| info.has_lifetime);

// Handle lifetimes for function signature - only add if no explicit lifetimes
Expand Down Expand Up @@ -153,6 +155,7 @@ fn parse_method_arguments<'a>(
method: &'a mut syn::TraitItemFn,
has_explicit_lifetimes: bool,
param_attrs_map: &std::collections::HashMap<String, ParamAttrs>,
rename_all: Option<crate::naming::RenameAll>,
) -> Result<Vec<ArgInfo<'a>>, Error> {
method
.sig
Expand All @@ -171,9 +174,11 @@ fn parse_method_arguments<'a>(
let ty = &pat_type.ty;

// Get pre-extracted parameter attributes
let param_name = name.to_string();
let param_attrs = param_attrs_map.get(&param_name);
let serialized_name = param_attrs.and_then(|attrs| attrs.rename.clone());
let param_attrs = param_attrs_map.get(&name.to_string());
let serialized_name = match resolve_serialized_name(name, param_attrs, rename_all) {
Ok(serialized_name) => serialized_name,
Err(err) => return Some(Err(err)),
};
let is_fds = param_attrs.map(|attrs| attrs.is_fds).unwrap_or(false);

// Only convert to single lifetime if there are no explicit lifetimes
Expand Down
15 changes: 10 additions & 5 deletions zlink-macros/src/proxy/method_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use super::{
types::{ArgInfo, MethodAttrs},
utils::{
ParamAttrs, collect_used_type_params, convert_to_single_lifetime, parse_return_type,
snake_case_to_pascal_case, type_contains_lifetime,
resolve_serialized_name, snake_case_to_pascal_case, type_contains_lifetime,
},
};
use crate::utils::is_option_type;
Expand All @@ -19,6 +19,7 @@ pub(super) fn generate_method_impl(
method_attrs: &MethodAttrs,
crate_path: &TokenStream,
param_attrs_map: &std::collections::HashMap<String, ParamAttrs>,
rename_all: Option<crate::naming::RenameAll>,
) -> Result<TokenStream, Error> {
let method_name = &method.sig.ident;
// Only the wire name is unraw'd; the generated fn keeps the raw ident so it still matches the
Expand All @@ -39,7 +40,8 @@ pub(super) fn generate_method_impl(
let method_output = method.sig.output.clone();

// Process all method arguments in a single pass
let arg_infos = parse_method_arguments(method, has_explicit_lifetimes, param_attrs_map)?;
let arg_infos =
parse_method_arguments(method, has_explicit_lifetimes, param_attrs_map, rename_all)?;

// Extract the data we need from the processed arguments
let has_any_lifetime = arg_infos.iter().any(|info| info.has_lifetime);
Expand Down Expand Up @@ -201,6 +203,7 @@ fn parse_method_arguments<'a>(
method: &'a mut syn::TraitItemFn,
has_explicit_lifetimes: bool,
param_attrs_map: &std::collections::HashMap<String, ParamAttrs>,
rename_all: Option<crate::naming::RenameAll>,
) -> Result<Vec<ArgInfo<'a>>, Error> {
method
.sig
Expand All @@ -219,9 +222,11 @@ fn parse_method_arguments<'a>(
let ty = &pat_type.ty;

// Get pre-extracted parameter attributes
let param_name = name.to_string();
let param_attrs = param_attrs_map.get(&param_name);
let serialized_name = param_attrs.and_then(|attrs| attrs.rename.clone());
let param_attrs = param_attrs_map.get(&name.to_string());
let serialized_name = match resolve_serialized_name(name, param_attrs, rename_all) {
Ok(serialized_name) => serialized_name,
Err(err) => return Some(Err(err)),
};
let is_fds = param_attrs.map(|attrs| attrs.is_fds).unwrap_or(false);

// Check if the type is optional
Expand Down
83 changes: 81 additions & 2 deletions zlink-macros/src/proxy/utils.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use crate::utils::*;
use crate::{
naming::{self, Grammar, NameSource, RenameAll},
utils::*,
};
use std::collections::HashSet;
use syn::{
Attribute, Error, Expr, GenericArgument, Lit, Meta, PathArguments, ReturnType, Type,
Attribute, Error, Expr, GenericArgument, Ident, Lit, Meta, PathArguments, ReturnType, Type,
punctuated::Punctuated,
};

Expand Down Expand Up @@ -164,6 +167,36 @@ pub(super) struct ParamAttrs {
pub is_fds: bool,
}

/// The name a parameter should carry on the wire, when it differs from its Rust ident.
///
/// An explicit `#[zlink(rename = "...")]` wins over the trait-level `rename_all_arguments`. A
/// `rename_all_arguments` convention is applied to the unraw'd ident -- `r#` is Rust syntax,
/// never part of the name -- and the name it produces is checked against the Varlink field
/// grammar, which conventions like `kebab-case` cannot always satisfy.
pub(super) fn resolve_serialized_name(
ident: &Ident,
param_attrs: Option<&ParamAttrs>,
rename_all: Option<RenameAll>,
) -> Result<Option<String>, Error> {
if let Some(rename) = param_attrs.and_then(|attrs| attrs.rename.clone()) {
return Ok(Some(rename));
}

let Some(rule) = rename_all else {
return Ok(None);
};

let name = rule.apply_to_field(&naming::unraw(ident));
naming::validate(
&name,
Grammar::Field,
"parameter name",
NameSource::Ident(ident),
)?;

Ok(Some(name))
}

/// Extract parameter attributes from zlink attributes and remove processed attributes.
pub(super) fn extract_param_attrs(attrs: &mut Vec<Attribute>) -> Result<ParamAttrs, Error> {
let attrs_result = extract_zlink_attrs(attrs, |meta_items| {
Expand Down Expand Up @@ -645,3 +678,49 @@ fn extract_stream_item_fds_types(ty: &Type) -> Result<(Type, Type), Error> {
)),
}
}

#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;

/// `rename_all` must see `type`, not `r#type`: the prefix is Rust syntax, not the name.
#[test]
fn rename_all_applies_to_the_unrawed_ident() {
let ident: Ident = parse_quote!(r#type);
let name = resolve_serialized_name(&ident, None, Some(RenameAll::Pascal)).unwrap();

assert_eq!(name.as_deref(), Some("Type"));
}

#[test]
fn explicit_rename_beats_rename_all() {
let ident: Ident = parse_quote!(dry_run);
let attrs = ParamAttrs {
rename: Some("customName".into()),
is_fds: false,
};
let name = resolve_serialized_name(&ident, Some(&attrs), Some(RenameAll::Pascal)).unwrap();

assert_eq!(name.as_deref(), Some("customName"));
}

#[test]
fn no_attrs_leave_the_ident_alone() {
let ident: Ident = parse_quote!(dry_run);

assert_eq!(resolve_serialized_name(&ident, None, None).unwrap(), None);
}

/// A produced name the Varlink field grammar cannot express is a compile error, not a wire
/// name the peer rejects.
#[test]
fn invalid_produced_name_is_rejected() {
let ident: Ident = parse_quote!(dry_run);
let err = resolve_serialized_name(&ident, None, Some(RenameAll::Kebab))
.unwrap_err()
.to_string();

assert!(err.contains("`dry-run`"), "must name the bad name: {err}");
}
}
Loading
Loading