Skip to content

Add closure variants to OperatorFunction for capturing external state#402

Closed
Copilot wants to merge 3 commits intomasterfrom
copilot/fix-db-connection-in-operator-function
Closed

Add closure variants to OperatorFunction for capturing external state#402
Copilot wants to merge 3 commits intomasterfrom
copilot/fix-db-connection-in-operator-function

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Dec 2, 2025

OperatorFunction uses raw fn pointers which cannot capture environment variables, making it impossible to access database connections or app state from custom Casbin functions.

Changes

  • New closure variants: Added Arg0Closure through Arg6Closure using Arc<dyn Fn(...) -> Dynamic + Send + Sync>
  • Registration support: Updated register_function to handle closure variants
  • Clone semantics: Changed from #[derive(Clone, Copy)] to #[derive(Clone)] since Arc doesn't implement Copy
  • Documentation: Updated CUSTOM_FUNCTIONS.md with closure usage examples

Example

#[derive(Clone)]
struct AppState {
    db_connection: Arc<Connection>,
    casbin_enforcer: Arc<Mutex<Enforcer>>,
}

impl AppState {
    fn setup_enforcer(&mut self) {
        let db_conn = self.db_connection.clone();
        
        if let Ok(mut e) = self.casbin_enforcer.lock() {
            e.add_function(
                "matchProductHasStorages",
                OperatorFunction::Arg1Closure(Arc::new(move |product_id: Dynamic| {
                    let id = product_id.as_int().unwrap_or(0);
                    db_conn.check_product_has_storages(id).into()
                })),
            );
        }
    }
}
Original prompt

This section details on the original issue you should resolve

<issue_title>how to access DB connection in OperatorFunction?</issue_title>
<issue_description>Hello,
I have a DB connection in a Axum AppState:

#[derive(Clone)]
struct AppState {
    db_connection: Connection,
    casbin_enforcer: Arc<Mutex<Enforcer>>,
}

To be able to access this connection from a Casbin function I have implemented:

impl AppState {
    fn matchProductHasStoragesWrapper(&self) -> OperatorFunction {
        OperatorFunction::Arg1(|product_id: Dynamic| {
            let product_id_int = product_id.as_int().unwrap_or(0);

            // this does not work when uncommented
            //let db_connection = self.db_connection;

            true.into()
        })
    }
}

I have the compile error: closures can only be coerced to fn types if they do not capture any variables. I have searched a long time for a solution without success.

May be defining the matchProductHasStoragesWrapper as an implementation for AppState is not a good idea?

Thanks for your help.
</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI self-assigned this Dec 2, 2025
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@hsluoyz hsluoyz marked this pull request as ready for review December 2, 2025 04:30
@github-actions
Copy link
Copy Markdown

github-actions bot commented Dec 2, 2025

Benchmark for 3c53722

Click to view benchmark
Test PR Benchmark Master Benchmark %
b_benchmark_rbac_model_large 10.0±0.04ms 9.9±0.03ms +1.01%
benchmark priority model 1461.6±20.06ns 1441.2±16.81ns +1.42%
benchmark_abac_model 1091.5±13.82ns 1081.1±12.13ns +0.96%
benchmark_basic_model 1162.9±7.66ns 1143.8±15.44ns +1.67%
benchmark_key_match 3.9±0.06µs 4.0±0.05µs -2.50%
benchmark_raw 0.0±0.00ns 0.0±0.00ns NaN%
benchmark_rbac_model 3.3±0.09µs 3.2±0.04µs +3.12%
benchmark_rbac_model_medium 825.0±3.81µs 938.0±8.31µs -12.05%
benchmark_rbac_model_with_domains 2.0±0.03µs 2.0±0.01µs 0.00%
benchmark_rbac_with_deny 5.5±0.05µs 5.5±0.04µs 0.00%
benchmark_rbac_with_resource_roles 1427.1±25.50ns 1416.1±16.71ns +0.78%
benchmark_role_manager_large 4.3±0.01ms 4.3±0.01ms 0.00%
benchmark_role_manager_medium 265.4±2.65µs 267.7±3.54µs -0.86%
benchmark_role_manager_small 85.5±1.43µs 85.0±0.88µs +0.59%

Copilot AI and others added 2 commits December 2, 2025 04:42
Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com>
Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix DB connection access in OperatorFunction Add closure variants to OperatorFunction for capturing external state Dec 2, 2025
Copilot AI requested a review from hsluoyz December 2, 2025 04:49
@github-actions
Copy link
Copy Markdown

github-actions bot commented Dec 2, 2025

Benchmark for 3c53722

Click to view benchmark
Test PR Benchmark Master Benchmark %
b_benchmark_rbac_model_large 9.5±0.02ms 9.6±0.02ms -1.04%
benchmark priority model 1485.1±12.87ns 1506.3±16.95ns -1.41%
benchmark_abac_model 1092.3±8.19ns 1101.7±10.62ns -0.85%
benchmark_basic_model 1207.9±11.93ns 1205.4±13.65ns +0.21%
benchmark_key_match 4.0±0.05µs 4.1±0.05µs -2.44%
benchmark_raw 0.0±0.00ns 0.0±0.00ns NaN%
benchmark_rbac_model 3.2±0.09µs 3.3±0.04µs -3.03%
benchmark_rbac_model_medium 786.9±8.14µs 795.3±12.38µs -1.06%
benchmark_rbac_model_with_domains 1987.1±19.38ns 2.0±0.02µs -0.64%
benchmark_rbac_with_deny 5.3±0.05µs 5.7±0.07µs -7.02%
benchmark_rbac_with_resource_roles 1437.6±18.20ns 1429.4±15.90ns +0.57%
benchmark_role_manager_large 4.2±0.01ms 4.3±0.01ms -2.33%
benchmark_role_manager_medium 264.6±6.66µs 264.3±2.36µs +0.11%
benchmark_role_manager_small 80.9±1.09µs 81.5±1.30µs -0.74%

@github-actions
Copy link
Copy Markdown

github-actions bot commented Dec 2, 2025

Benchmark for 3c53722

Click to view benchmark
Test PR Benchmark Master Benchmark %
b_benchmark_rbac_model_large 9.4±0.05ms 9.9±0.16ms -5.05%
benchmark priority model 1479.7±19.54ns 1437.2±20.38ns +2.96%
benchmark_abac_model 1109.0±8.91ns 1098.9±26.87ns +0.92%
benchmark_basic_model 1238.6±13.89ns 1155.1±7.45ns +7.23%
benchmark_key_match 4.0±0.07µs 4.0±0.03µs 0.00%
benchmark_raw 0.0±0.00ns 0.0±0.00ns NaN%
benchmark_rbac_model 3.1±0.16µs 3.2±0.03µs -3.13%
benchmark_rbac_model_medium 781.6±13.77µs 816.0±7.59µs -4.22%
benchmark_rbac_model_with_domains 1997.9±31.80ns 1955.7±31.16ns +2.16%
benchmark_rbac_with_deny 5.3±0.34µs 5.4±0.07µs -1.85%
benchmark_rbac_with_resource_roles 1465.3±13.17ns 1386.4±35.59ns +5.69%
benchmark_role_manager_large 4.2±0.01ms 4.3±0.04ms -2.33%
benchmark_role_manager_medium 266.4±3.72µs 268.4±2.24µs -0.75%
benchmark_role_manager_small 80.7±1.19µs 86.6±0.65µs -6.81%

@tbellembois
Copy link
Copy Markdown

tbellembois commented Dec 3, 2025

What if the db_connection is a tokio_rusqlite connection? We need to call the async function call https://docs.rs/tokio-rusqlite/latest/tokio_rusqlite/struct.Connection.html#method.call to get the underlying connection. The closure is not async.

May be the best practice is to use a pool like https://docs.rs/r2d2_sqlite/latest/r2d2_sqlite/index.html?

This works well with a r2d2 connection pool.

Here is my code:

use r2d2::{self, Pool};
use r2d2_sqlite::SqliteConnectionManager;
...

#[derive(Clone)]
struct AppState {
    db_connection_pool: Arc<Pool<SqliteConnectionManager>>,
    casbin_enforcer: Arc<Mutex<Enforcer>>,
}


impl AppState {
    fn match_product_has_storages_wrapper(&mut self) {
        let db_connection_pool = self.db_connection_pool.clone();

        if let Ok(mut e) = self.casbin_enforcer.lock() {
            e.add_function(
                "matchProductHasStorages",
                OperatorFunction::Arg1Closure(Arc::new(move |product_id: Dynamic| {
                    let product_id: u64 = product_id.as_int().unwrap_or(0) as u64;

                    // TODO: manage errors.
                    let db_connection = db_connection_pool.get().unwrap();

                    // TODO: manage errors.
                    let result = match_product_has_storages(db_connection.deref(), product_id)
                        .unwrap_or_default();

                    result.into()
                })),
            );
        };
    }
}

Thanks.

@apache apache deleted a comment from tbellembois Dec 5, 2025
@hsluoyz
Copy link
Copy Markdown
Member

hsluoyz commented Dec 5, 2025

What if the db_connection is a tokio_rusqlite connection? We need to call the async function call https://docs.rs/tokio-rusqlite/latest/tokio_rusqlite/struct.Connection.html#method.call to get the underlying connection. The closure is not async.

May be the best practice is to use a pool like https://docs.rs/r2d2_sqlite/latest/r2d2_sqlite/index.html?

This works well with a r2d2 connection pool.

Here is my code:

use r2d2::{self, Pool};
use r2d2_sqlite::SqliteConnectionManager;
...

#[derive(Clone)]
struct AppState {
    db_connection_pool: Arc<Pool<SqliteConnectionManager>>,
    casbin_enforcer: Arc<Mutex<Enforcer>>,
}


impl AppState {
    fn match_product_has_storages_wrapper(&mut self) {
        let db_connection_pool = self.db_connection_pool.clone();

        if let Ok(mut e) = self.casbin_enforcer.lock() {
            e.add_function(
                "matchProductHasStorages",
                OperatorFunction::Arg1Closure(Arc::new(move |product_id: Dynamic| {
                    let product_id: u64 = product_id.as_int().unwrap_or(0) as u64;

                    // TODO: manage errors.
                    let db_connection = db_connection_pool.get().unwrap();

                    // TODO: manage errors.
                    let result = match_product_has_storages(db_connection.deref(), product_id)
                        .unwrap_or_default();

                    result.into()
                })),
            );
        };
    }
}

Thanks.

@copilot modify based on above

also fix below failed CI check:

Run actions-rs/cargo@v1
/home/runner/.cargo/bin/cargo clippy -- -D warnings
    Checking casbin ***.17.0 (/home/runner/work/casbin-rs/casbin-rs)
error: very complex type used. Consider factoring parts into `type` definitions
Error:    --> src/model/function_map.rs:91:9
    |
 91 | /         Arc<
 92 | |             dyn Fn(
 93 | |                     Dynamic,
 94 | |                     Dynamic,
...   |
101 | |                 + Sync,
102 | |         >,
    | |_________^
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/beta/index.html#type_complexity
    = note: `-D clippy::type-complexity` implied by `-D warnings`
    = help: to override `-D warnings` add `#[allow(clippy::type_complexity)]`

error: could not compile `casbin` (lib) due to 1 previous error
Error: The process '/home/runner/.cargo/bin/cargo' failed with exit code 101

@hsluoyz hsluoyz closed this Dec 6, 2025
@codecov
Copy link
Copy Markdown

codecov bot commented Dec 8, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0.00%. Comparing base (c5a05d3) to head (a1b1d1a).
⚠️ Report is 28 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff            @@
##           master   #402       +/-   ##
=========================================
- Coverage   65.54%      0   -65.55%     
=========================================
  Files          25      0       -25     
  Lines        1956      0     -1956     
=========================================
- Hits         1282      0     -1282     
+ Misses        674      0      -674     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

how to access DB connection in OperatorFunction?

4 participants