diff --git a/zlink-macros/src/lib.rs b/zlink-macros/src/lib.rs index a98cea08..e1a38f5f 100644 --- a/zlink-macros/src/lib.rs +++ b/zlink-macros/src/lib.rs @@ -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>; +/// } +/// # #[derive(Debug, serde::Serialize, serde::Deserialize)] +/// # enum MyError {} +/// ``` /// /// # Example /// diff --git a/zlink-macros/src/naming.rs b/zlink-macros/src/naming.rs index 276fffee..e98ae781 100644 --- a/zlink-macros/src/naming.rs +++ b/zlink-macros/src/naming.rs @@ -186,7 +186,7 @@ impl RenameAll { grammar.accepts(&self.apply_to_variant("Word")) } - fn parse(lit: &LitStr) -> Result { + pub(crate) fn parse(lit: &LitStr) -> Result { let rule = match lit.value().as_str() { "lowercase" => Self::Lower, "UPPERCASE" => Self::Upper, @@ -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(", "), ), )); diff --git a/zlink-macros/src/proxy.rs b/zlink-macros/src/proxy.rs index 9e82210c..b6906240 100644 --- a/zlink-macros/src/proxy.rs +++ b/zlink-macros/src/proxy.rs @@ -25,8 +25,9 @@ pub(crate) fn proxy(attr: TokenStream, input: TokenStream) -> TokenStream { fn proxy_impl(attr: TokenStream, input: TokenStream) -> Result { let mut trait_def = parse2::(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)?; @@ -65,6 +66,7 @@ fn proxy_impl(attr: TokenStream, input: TokenStream) -> Result Result Result Result Result<(String, TokenStream, Option), Error> { +) -> Result< + ( + String, + TokenStream, + Option, + Option, + ), + Error, +> { if attr.is_empty() { return Err(Error::new_spanned( trait_def, @@ -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::(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") { @@ -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)?); } else { return Err(meta.error("unsupported attribute")); } @@ -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> { diff --git a/zlink-macros/src/proxy/chain_extension.rs b/zlink-macros/src/proxy/chain_extension.rs index 981fe7c8..a568a85c 100644 --- a/zlink-macros/src/proxy/chain_extension.rs +++ b/zlink-macros/src/proxy/chain_extension.rs @@ -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, }, }; @@ -17,6 +17,7 @@ pub(super) fn generate_chain_extension_method( method_attrs: &MethodAttrs, crate_path: &TokenStream, param_attrs_map: &std::collections::HashMap, + rename_all: Option, ) -> 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. @@ -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 @@ -147,6 +149,7 @@ fn parse_method_arguments<'a>( method: &'a mut syn::TraitItemFn, has_explicit_lifetimes: bool, param_attrs_map: &std::collections::HashMap, + rename_all: Option, ) -> Result>, Error> { method .sig @@ -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(¶m_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 diff --git a/zlink-macros/src/proxy/chain_method.rs b/zlink-macros/src/proxy/chain_method.rs index 296d251d..d24a3785 100644 --- a/zlink-macros/src/proxy/chain_method.rs +++ b/zlink-macros/src/proxy/chain_method.rs @@ -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, }, }; @@ -22,6 +22,7 @@ pub(super) fn generate_chain_method( method_attrs: &MethodAttrs, crate_path: &TokenStream, param_attrs_map: &std::collections::HashMap, + rename_all: Option, ) -> 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. @@ -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 @@ -153,6 +155,7 @@ fn parse_method_arguments<'a>( method: &'a mut syn::TraitItemFn, has_explicit_lifetimes: bool, param_attrs_map: &std::collections::HashMap, + rename_all: Option, ) -> Result>, Error> { method .sig @@ -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(¶m_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 diff --git a/zlink-macros/src/proxy/method_impl.rs b/zlink-macros/src/proxy/method_impl.rs index 601cad5e..87e115a0 100644 --- a/zlink-macros/src/proxy/method_impl.rs +++ b/zlink-macros/src/proxy/method_impl.rs @@ -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; @@ -19,6 +19,7 @@ pub(super) fn generate_method_impl( method_attrs: &MethodAttrs, crate_path: &TokenStream, param_attrs_map: &std::collections::HashMap, + rename_all: Option, ) -> Result { 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 @@ -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); @@ -201,6 +203,7 @@ fn parse_method_arguments<'a>( method: &'a mut syn::TraitItemFn, has_explicit_lifetimes: bool, param_attrs_map: &std::collections::HashMap, + rename_all: Option, ) -> Result>, Error> { method .sig @@ -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(¶m_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 diff --git a/zlink-macros/src/proxy/utils.rs b/zlink-macros/src/proxy/utils.rs index 7aad3b49..9787c14b 100644 --- a/zlink-macros/src/proxy/utils.rs +++ b/zlink-macros/src/proxy/utils.rs @@ -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, }; @@ -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, +) -> Result, 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) -> Result { let attrs_result = extract_zlink_attrs(attrs, |meta_items| { @@ -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}"); + } +} diff --git a/zlink-macros/tests/proxy/rename.rs b/zlink-macros/tests/proxy/rename.rs index 24cc1508..0b4c25ba 100644 --- a/zlink-macros/tests/proxy/rename.rs +++ b/zlink-macros/tests/proxy/rename.rs @@ -209,3 +209,286 @@ async fn raw_ident_chain_method() { assert_eq!(written["parameters"]["move"], expected); } } + +#[tokio::test] +async fn rename_all_pascal_case() { + use serde::{Deserialize, Serialize}; + use serde_json::json; + use zlink::{Connection, proxy, test_utils::mock_socket::MockSocket}; + + #[derive(Debug, Serialize, Deserialize)] + struct Error; + + #[proxy( + interface = "org.example.RenameAll", + rename_all_arguments = "PascalCase" + )] + trait RenameAllProxy { + #[allow(unused)] + async fn set_config( + &mut self, + dry_run: bool, + config_value: String, + ) -> zlink::Result>; + } + + // Test that rename_all_arguments = "PascalCase" converts parameter names + let responses = json!({}).to_string(); + let socket = MockSocket::with_responses(&[&responses]); + let mut conn = Connection::new(socket); + + conn.set_config(true, "test_value".to_string()) + .await + .unwrap() + .unwrap(); + + let bytes_written = conn.write().write_half().written_data(); + let written: serde_json::Value = + serde_json::from_slice(&bytes_written[..bytes_written.len() - 1]).unwrap(); + assert_eq!(written["method"], "org.example.RenameAll.SetConfig"); + assert_eq!(written["parameters"]["DryRun"], true); + assert_eq!(written["parameters"]["ConfigValue"], "test_value"); +} + +#[tokio::test] +async fn rename_all_with_explicit_override() { + use serde::{Deserialize, Serialize}; + use serde_json::json; + use zlink::{Connection, proxy, test_utils::mock_socket::MockSocket}; + + #[derive(Debug, Serialize, Deserialize)] + struct Error; + + // Explicit #[zlink(rename)] should override rename_all_arguments + #[proxy( + interface = "org.example.RenameAllOverride", + rename_all_arguments = "PascalCase" + )] + trait RenameAllOverrideProxy { + #[allow(unused)] + async fn update_setting( + &mut self, + #[zlink(rename = "customName")] setting_name: String, + setting_value: i32, + ) -> zlink::Result>; + } + + let responses = json!({}).to_string(); + let socket = MockSocket::with_responses(&[&responses]); + let mut conn = Connection::new(socket); + + conn.update_setting("test".to_string(), 42) + .await + .unwrap() + .unwrap(); + + let bytes_written = conn.write().write_half().written_data(); + let written: serde_json::Value = + serde_json::from_slice(&bytes_written[..bytes_written.len() - 1]).unwrap(); + // Explicit rename takes precedence + assert_eq!(written["parameters"]["customName"], "test"); + // rename_all_arguments applies to the other parameter + assert_eq!(written["parameters"]["SettingValue"], 42); +} + +#[tokio::test] +async fn rename_all_camel_case() { + use serde::{Deserialize, Serialize}; + use serde_json::json; + use zlink::{Connection, proxy, test_utils::mock_socket::MockSocket}; + + #[derive(Debug, Serialize, Deserialize)] + struct Error; + + #[proxy( + interface = "org.example.CamelCase", + rename_all_arguments = "camelCase" + )] + trait CamelCaseProxy { + #[allow(unused)] + async fn get_user_info( + &mut self, + user_name: String, + include_details: bool, + ) -> zlink::Result>; + } + + let responses = json!({}).to_string(); + let socket = MockSocket::with_responses(&[&responses]); + let mut conn = Connection::new(socket); + + conn.get_user_info("alice".to_string(), true) + .await + .unwrap() + .unwrap(); + + let bytes_written = conn.write().write_half().written_data(); + let written: serde_json::Value = + serde_json::from_slice(&bytes_written[..bytes_written.len() - 1]).unwrap(); + assert_eq!(written["parameters"]["userName"], "alice"); + assert_eq!(written["parameters"]["includeDetails"], true); +} + +#[tokio::test] +async fn rename_all_chain_methods() { + use futures_util::{pin_mut, stream::StreamExt}; + use serde::{Deserialize, Serialize}; + use serde_json::json; + use zlink::{Connection, proxy, test_utils::mock_socket::MockSocket}; + + #[derive(Debug, Serialize, Deserialize)] + struct Error; + + #[proxy( + interface = "org.example.RenameAllChain", + rename_all_arguments = "PascalCase" + )] + trait RenameAllChainProxy { + #[allow(unused)] + async fn set_config( + &mut self, + dry_run: bool, + config_value: String, + ) -> zlink::Result>; + } + + let reply1 = json!({}).to_string(); + let reply2 = json!({}).to_string(); + let socket = MockSocket::new(&[&reply1, &reply2], vec![vec![]]); + let mut conn = Connection::new(socket); + + { + let replies = conn + .chain_set_config(true, "val1".to_string()) + .unwrap() + .set_config(false, "val2".to_string()) + .unwrap() + .send::<(), Error>() + .await + .unwrap(); + + pin_mut!(replies); + + let (reply1, _fds) = replies.next().await.unwrap().unwrap(); + reply1.unwrap(); + let (reply2, _fds) = replies.next().await.unwrap().unwrap(); + reply2.unwrap(); + } + + let bytes_written = conn.write().write_half().written_data(); + let messages: Vec<&[u8]> = bytes_written + .split(|&b| b == 0) + .filter(|s| !s.is_empty()) + .collect(); + assert_eq!(messages.len(), 2); + + for message in &messages { + let written: serde_json::Value = serde_json::from_slice(message).unwrap(); + // Verify PascalCase parameter names in both chain and chain extension methods + assert!(written["parameters"]["DryRun"].is_boolean()); + assert!(written["parameters"]["ConfigValue"].is_string()); + } +} + +#[tokio::test] +async fn rename_all_raw_identifiers() { + use serde::{Deserialize, Serialize}; + use serde_json::json; + use zlink::{Connection, proxy, test_utils::mock_socket::MockSocket}; + + #[derive(Debug, Serialize, Deserialize)] + struct Error; + + // `r#` is Rust syntax, not part of the name: rename_all_arguments must apply to + // `type`/`move`, never `r#type`/`r#move`. + #[proxy( + interface = "org.example.RenameAllRaw", + rename_all_arguments = "PascalCase" + )] + trait RenameAllRawProxy { + #[allow(unused)] + async fn set_kind( + &mut self, + r#type: String, + r#move: bool, + ) -> zlink::Result>; + } + + let responses = json!({}).to_string(); + let socket = MockSocket::with_responses(&[&responses]); + let mut conn = Connection::new(socket); + + conn.set_kind("disk".to_string(), true) + .await + .unwrap() + .unwrap(); + + let bytes_written = conn.write().write_half().written_data(); + let written: serde_json::Value = + serde_json::from_slice(&bytes_written[..bytes_written.len() - 1]).unwrap(); + assert_eq!(written["method"], "org.example.RenameAllRaw.SetKind"); + assert_eq!(written["parameters"]["Type"], "disk"); + assert_eq!(written["parameters"]["Move"], true); +} + +#[tokio::test] +async fn rename_all_raw_identifiers_chain_methods() { + use futures_util::{pin_mut, stream::StreamExt}; + use serde::{Deserialize, Serialize}; + use serde_json::json; + use zlink::{Connection, proxy, test_utils::mock_socket::MockSocket}; + + #[derive(Debug, Serialize, Deserialize)] + struct Error; + + #[proxy( + interface = "org.example.RenameAllRawChain", + rename_all_arguments = "PascalCase" + )] + trait RenameAllRawChainProxy { + #[allow(unused)] + async fn set_kind( + &mut self, + r#type: String, + r#move: bool, + ) -> zlink::Result>; + } + + let reply1 = json!({}).to_string(); + let reply2 = json!({}).to_string(); + let socket = MockSocket::new(&[&reply1, &reply2], vec![vec![]]); + let mut conn = Connection::new(socket); + + { + let replies = conn + .chain_set_kind("disk".to_string(), true) + .unwrap() + .set_kind("net".to_string(), false) + .unwrap() + .send::<(), Error>() + .await + .unwrap(); + + pin_mut!(replies); + + let (reply1, _fds) = replies.next().await.unwrap().unwrap(); + reply1.unwrap(); + let (reply2, _fds) = replies.next().await.unwrap().unwrap(); + reply2.unwrap(); + } + + let bytes_written = conn.write().write_half().written_data(); + let messages: Vec<&[u8]> = bytes_written + .split(|&b| b == 0) + .filter(|s| !s.is_empty()) + .collect(); + assert_eq!(messages.len(), 2); + + // Both the chain and chain extension methods must apply rename_all_arguments to the unraw'd + // names. + for message in &messages { + let written: serde_json::Value = serde_json::from_slice(message).unwrap(); + assert!(written["parameters"]["Type"].is_string()); + assert!(written["parameters"]["Move"].is_boolean()); + } +}