aro-core 1.0.0

Core domain layer for the Aro web framework
Documentation
#![expect(
    clippy::unwrap_used,
    reason = "benchmark code — panics on failure are acceptable"
)]

use std::sync::Mutex;

use criterion::{Criterion, black_box, criterion_group, criterion_main};

use aro_core::entity::Entity;
use aro_core::error::RepoError;
use aro_core::repository::{BoxFuture, Repository};

// --- Minimal entity ---

#[derive(Debug, Clone, PartialEq)]
struct Item {
    id: u64,
    name: String,
}

impl Entity for Item {
    type Id = u64;
    fn id(&self) -> Self::Id {
        self.id
    }
}

// --- Repository using BoxFuture (object-safe, dynamic dispatch) ---

struct BoxedRepo {
    items: Mutex<Vec<Item>>,
}

impl Repository<Item> for BoxedRepo {
    fn find_by_id(&self, id: u64) -> BoxFuture<'_, Result<Item, RepoError>> {
        Box::pin(async move {
            self.items
                .lock()
                .unwrap()
                .iter()
                .find(|i| i.id == id)
                .cloned()
                .ok_or(RepoError::NotFound)
        })
    }

    fn find_all(&self) -> BoxFuture<'_, Result<Vec<Item>, RepoError>> {
        Box::pin(async move { Ok(self.items.lock().unwrap().clone()) })
    }

    fn create(&self, entity: Item) -> BoxFuture<'_, Result<Item, RepoError>> {
        Box::pin(async move { Ok(entity) })
    }

    fn update(&self, entity: Item) -> BoxFuture<'_, Result<Item, RepoError>> {
        Box::pin(async move { Ok(entity) })
    }

    fn delete(&self, _id: u64) -> BoxFuture<'_, Result<(), RepoError>> {
        Box::pin(async move { Ok(()) })
    }
}

// --- Direct async (no Box::pin) baseline ---

struct DirectRepo {
    items: Mutex<Vec<Item>>,
}

impl DirectRepo {
    #[expect(
        clippy::unused_async,
        reason = "async is needed for benchmark comparison with BoxFuture"
    )]
    async fn find_by_id(&self, id: u64) -> Result<Item, RepoError> {
        self.items
            .lock()
            .unwrap()
            .iter()
            .find(|i| i.id == id)
            .cloned()
            .ok_or(RepoError::NotFound)
    }
}

fn bench_boxfuture_find_by_id(c: &mut Criterion) {
    let rt = tokio::runtime::Builder::new_current_thread()
        .build()
        .unwrap();

    let repo = BoxedRepo {
        items: Mutex::new(vec![Item {
            id: 1,
            name: "Widget".into(),
        }]),
    };

    c.bench_function("BoxFuture find_by_id", |b| {
        b.iter(|| {
            rt.block_on(async {
                let item = repo.find_by_id(black_box(1)).await.unwrap();
                black_box(item);
            });
        });
    });
}

fn bench_direct_find_by_id(c: &mut Criterion) {
    let rt = tokio::runtime::Builder::new_current_thread()
        .build()
        .unwrap();

    let repo = DirectRepo {
        items: Mutex::new(vec![Item {
            id: 1,
            name: "Widget".into(),
        }]),
    };

    c.bench_function("Direct async find_by_id", |b| {
        b.iter(|| {
            rt.block_on(async {
                let item = repo.find_by_id(black_box(1)).await.unwrap();
                black_box(item);
            });
        });
    });
}

fn bench_dyn_dispatch_find_by_id(c: &mut Criterion) {
    let rt = tokio::runtime::Builder::new_current_thread()
        .build()
        .unwrap();

    let repo = BoxedRepo {
        items: Mutex::new(vec![Item {
            id: 1,
            name: "Widget".into(),
        }]),
    };
    let dyn_repo: &dyn Repository<Item> = &repo;

    c.bench_function("dyn Repository find_by_id", |b| {
        b.iter(|| {
            rt.block_on(async {
                let item = dyn_repo.find_by_id(black_box(1)).await.unwrap();
                black_box(item);
            });
        });
    });
}

criterion_group!(
    benches,
    bench_boxfuture_find_by_id,
    bench_direct_find_by_id,
    bench_dyn_dispatch_find_by_id
);
criterion_main!(benches);