casbin 2.20.0

An authorization library that supports access control models like ACL, RBAC, ABAC.
Documentation
# Custom Functions in Casbin-RS

## Overview

Custom functions in Casbin-RS now support flexible argument types through Rhai's `Dynamic` type. This means you can create custom functions that work with:

- **Strings** (as `ImmutableString`)
- **Integers** (i32 or i64)
- **Booleans**
- **Floats** (f32 or f64)
- **Arrays**
- **Maps**
- And more...

This improvement addresses the limitation where custom functions previously only accepted `ImmutableString` arguments.

## Basic Usage

### Adding a Custom Function

Custom functions are added using the `add_function` method on an `Enforcer` instance:

```rust
use casbin::prelude::*;
use rhai::Dynamic;

// Create your enforcer
let mut e = Enforcer::new("model.conf", "policy.csv").await?;

// Add a custom function
e.add_function(
    "myFunction",
    OperatorFunction::Arg2(|arg1: Dynamic, arg2: Dynamic| {
        // Your custom logic here
        true.into() // Return a Dynamic value
    }),
);
```

## Examples

### 1. String-based Custom Function

For custom functions that work with strings, you can use the helper function `dynamic_to_str`:

```rust
use casbin::model::function_map::dynamic_to_str;

e.add_function(
    "stringContains",
    OperatorFunction::Arg2(|haystack: Dynamic, needle: Dynamic| {
        let haystack_str = dynamic_to_str(&haystack);
        let needle_str = dynamic_to_str(&needle);
        haystack_str.contains(needle_str.as_ref()).into()
    }),
);
```

Or simply convert to String:

```rust
e.add_function(
    "stringMatch",
    OperatorFunction::Arg2(|s1: Dynamic, s2: Dynamic| {
        let str1 = s1.to_string();
        let str2 = s2.to_string();
        (str1 == str2).into()
    }),
);
```

### 2. Integer-based Custom Function

```rust
e.add_function(
    "greaterThan",
    OperatorFunction::Arg2(|a: Dynamic, b: Dynamic| {
        let a_int = a.as_int().unwrap_or(0);
        let b_int = b.as_int().unwrap_or(0);
        (a_int > b_int).into()
    }),
);
```

### 3. Boolean-based Custom Function

```rust
e.add_function(
    "customAnd",
    OperatorFunction::Arg2(|a: Dynamic, b: Dynamic| {
        let a_bool = a.as_bool().unwrap_or(false);
        let b_bool = b.as_bool().unwrap_or(false);
        (a_bool && b_bool).into()
    }),
);
```

### 4. Multi-argument Custom Function

```rust
e.add_function(
    "between",
    OperatorFunction::Arg3(|val: Dynamic, min: Dynamic, max: Dynamic| {
        let val_int = val.as_int().unwrap_or(0);
        let min_int = min.as_int().unwrap_or(0);
        let max_int = max.as_int().unwrap_or(0);
        (val_int >= min_int && val_int <= max_int).into()
    }),
);
```

### 5. Mixed-type Custom Function

```rust
e.add_function(
    "complexCheck",
    OperatorFunction::Arg3(|name: Dynamic, age: Dynamic, is_admin: Dynamic| {
        let name_str = name.to_string();
        let age_int = age.as_int().unwrap_or(0);
        let admin_bool = is_admin.as_bool().unwrap_or(false);
        
        // Custom logic with different types
        let result = name_str.len() > 3 && age_int >= 18 && admin_bool;
        result.into()
    }),
);
```

## Using Custom Functions in Matchers

Once registered, custom functions can be used in your policy matchers:

```conf
[matchers]
m = greaterThan(r.age, 18) && stringContains(r.path, p.path)
```

## OperatorFunction Variants

The `OperatorFunction` enum supports functions with 0 to 6 arguments:

- `Arg0`: `fn() -> Dynamic`
- `Arg1`: `fn(Dynamic) -> Dynamic`
- `Arg2`: `fn(Dynamic, Dynamic) -> Dynamic`
- `Arg3`: `fn(Dynamic, Dynamic, Dynamic) -> Dynamic`
- `Arg4`: `fn(Dynamic, Dynamic, Dynamic, Dynamic) -> Dynamic`
- `Arg5`: `fn(Dynamic, Dynamic, Dynamic, Dynamic, Dynamic) -> Dynamic`
- `Arg6`: `fn(Dynamic, Dynamic, Dynamic, Dynamic, Dynamic, Dynamic) -> Dynamic`

## Working with Dynamic Types

Rhai's `Dynamic` type provides several methods to extract values:

- `as_int()` - Extract as integer (returns `Result<i64, &str>`)
- `as_bool()` - Extract as boolean (returns `Result<bool, &str>`)
- `as_float()` - Extract as float (returns `Result<f64, &str>`)
- `is_string()` - Check if it's a string
- `into_immutable_string()` - Convert to ImmutableString (consumes the Dynamic)
- `to_string()` - Convert to String (works for any type)

## Backward Compatibility

All existing code continues to work. The change from `ImmutableString` to `Dynamic` is backward compatible because:

1. Strings are automatically converted to `Dynamic` by Rhai
2. The `dynamic_to_str` helper function makes string extraction easy
3. All built-in functions have been updated and tested

## Migration Guide

If you have existing custom functions using `ImmutableString`, update them like this:

**Before:**
```rust
e.add_function(
    "myFunc",
    OperatorFunction::Arg2(
        |s1: ImmutableString, s2: ImmutableString| {
            // logic here
            true.into()
        }
    ),
);
```

**After:**
```rust
e.add_function(
    "myFunc",
    OperatorFunction::Arg2(|s1: Dynamic, s2: Dynamic| {
        let str1 = s1.to_string();
        let str2 = s2.to_string();
        // logic here
        true.into()
    }),
);
```

## See Also

- [Casbin Documentation]https://casbin.org/docs/function
- [Rhai Documentation]https://rhai.rs/book/
- Test: `test_custom_function_with_dynamic_types` in `src/enforcer.rs`