Repository: facet-rs/facet (Rust)

Issue:
skip_serializing_if: generate closure to enable auto-ref/deref
## Problem

When using `skip_serializing_if` with a predicate function, the function signature must exactly match the field type:

```rust
#[derive(Facet)]
struct Config {
    #[facet(skip_serializing_if = is_empty)]
    description: String,
}

// This works:
fn is_empty(s: &String) -> bool { s.is_empty() }

// This does NOT work (even though it should via auto-deref):
fn is_empty(s: &str) -> bool { s.is_empty() }
```

The error is:
```
error[E0308]: mismatched types
    = note: expected fn pointer `for<'a> fn(&'a std::string::String) -> _`
                  found fn item `for<'a> fn(&'a str) -> _ {is_empty}`
```

Using `&String` triggers clippy's `ptr_arg` lint, forcing users to add `#[allow(clippy::ptr_arg)]`.

## Root Cause

The macro generates code that stores the user's function directly as a function pointer. This requires an exact type match - no auto-ref/deref coercion happens.

## Proposed Solution

Instead of storing the user's function directly, generate a closure that forwards the call:

```rust
// Current (approximate):
|ptr| unsafe { user_predicate(ptr.get::<T>()) }

// Proposed:
|ptr| unsafe { 
    let val: &T = ptr.get::<T>();
    user_predicate(val)  // auto-deref kicks in here
}
```

This would allow `fn is_empty(s: &str)` to work with a `String` field, matching Rust's normal coercion rules.

## Workaround

For now, users must match the exact field type and silence clippy:

```rust
#[allow(clippy::ptr_arg)]
fn is_empty(s: &String) -> bool { s.is_empty() }
```

Found while working on #1033.


Discussion:

Patch:
diff --git a/facet-macros-impl/src/attr_grammar/make_parse_attr.rs b/facet-macros-impl/src/attr_grammar/make_parse_attr.rs
index 8b56fd752..819e32bff 100644
--- a/facet-macros-impl/src/attr_grammar/make_parse_attr.rs
+++ b/facet-macros-impl/src/attr_grammar/make_parse_attr.rs
@@ -895,28 +895,41 @@ impl ParsedGrammar {
                         }
                     }
                     VariantKind::Predicate(target_ty) => {
-                        // For predicate variants, we generate the transmute directly in the attribute macro
+                        // For predicate variants, we generate a wrapper function in the attribute macro
                         // because we need access to $ty which __dispatch_attr doesn't have.
                         //
                         // IMPORTANT: We store the function pointer directly, NOT wrapped in an Attr enum.
                         // This is because the retrieval code (e.g., skip_serializing_if_fn()) uses data_ref
                         // to read the raw function pointer.
+                        //
+                        // We generate a wrapper function instead of transmuting directly so that
+                        // auto-deref works at the call site. This allows users to write:
+                        //   fn is_empty(s: &str) -> bool { ... }
+                        // for a String field, instead of requiring the exact type:
+                        //   fn is_empty(s: &String) -> bool { ... }
                         let _crate_path = self.crate_path.as_ref().expect(
                             "crate_path is required for predicate variants; add `crate_path ::your_crate;` to the grammar"
                         );
                         // Qualify the target type - use ::facet:: since these types are re-exported there
                         let qualified_target_ty = quote! { ::facet::#target_ty };
                         quote! {
-                            // Field-level with args: wrap the user's fn(&T) -> bool in a transmute
+                            // Field-level with args: wrap the user's predicate in a function that
+                            // enables auto-deref at the call site.
                             // Store the function pointer directly (not wrapped in Attr enum)
                             (@ns { $ns:path } #key_ident { $field:tt : $ty:ty | $($args:tt)* }) => {{
                                 ::facet::ExtensionAttr {
                                     ns: #ns_expr,
                                     key: #key_str,
                                     data: &const {
-                                        unsafe {
-                                            ::core::mem::transmute::<fn(& $ty) -> bool, #qualified_target_ty>(($($args)*))
+                                        // Define a wrapper function that calls the user's predicate.
+                                        // The call site `predicate(ptr.get::<$ty>())` enables auto-deref,
+                                        // so `fn(&str) -> bool` works for a `String` field.
+                                        unsafe fn __predicate_wrapper(ptr: ::facet::PtrConst<'_>) -> bool {
+                                            let predicate = ($($args)*);
+                                            predicate(ptr.get::<$ty>())
                                         }
+                                        // Coerce function item to function pointer
+                                        __predicate_wrapper as #qualified_target_ty
                                     } as *const #qualified_target_ty as *const (),
                                     shape: <() as ::facet::Facet>::SHAPE,
                                 }


Test patch:
diff --git a/facet-yaml/tests/serialize/struct_.rs b/facet-yaml/tests/serialize/struct_.rs
index 286ad6c0c..9b766e732 100644
--- a/facet-yaml/tests/serialize/struct_.rs
+++ b/facet-yaml/tests/serialize/struct_.rs
@@ -235,8 +235,7 @@ fn test_skip_serializing_if_none() -> Result<()> {
 fn test_skip_serializing_if_custom_predicate() -> Result<()> {
     facet_testhelpers::setup();
 
-    #[allow(clippy::ptr_arg)] // see https://github.com/facet-rs/facet/issues/1036
-    fn is_empty(s: &String) -> bool {
+    fn is_empty(s: &str) -> bool {
         s.is_empty()
     }