paginator-rs 0.2.0

Pagination trait for Rust collections
Documentation

paginator-rs

A comprehensive, modular Rust pagination library with support for multiple databases and web frameworks. Built for production use with a focus on ergonomics, performance, and maintainability.

โœจ Features

Core Features

  • ๐ŸŽฏ Flexible Pagination: Page-based and offset/limit pagination
  • ๐Ÿ”ง Builder Pattern: Fluent API for constructing pagination parameters
  • ๐Ÿ“Š Rich Metadata: Automatic calculation of total pages, has_next, has_prev
  • ๐ŸŽจ Sorting Support: Multi-field sorting with ascending/descending order
  • โš ๏ธ Error Handling: Comprehensive error types with helpful messages
  • ๐Ÿ”„ JSON Serialization: Built-in serde support

Database Integrations

  • SQLx (paginator-sqlx): PostgreSQL, MySQL, SQLite support
  • SeaORM (paginator-sea-orm): Type-safe ORM pagination with entity support
  • SurrealDB (paginator-surrealdb): Multi-model database with SQL-like queries

Web Framework Integrations

  • Axum (paginator-axum): Query extractors and JSON responses with headers
  • Rocket (paginator-rocket): Request guards and responders
  • Actix-web (paginator-actix): Extractors, responders, and middleware

๐Ÿงฑ Workspace Structure

paginator-rs/
โ”œโ”€โ”€ paginator-rs/         # Core trait and types
โ”œโ”€โ”€ paginator-utils/      # Shared types (params, response, metadata)
โ”œโ”€โ”€ paginator-sqlx/       # SQLx database integration
โ”œโ”€โ”€ paginator-sea-orm/    # SeaORM integration
โ”œโ”€โ”€ paginator-surrealdb/  # SurrealDB integration
โ”œโ”€โ”€ paginator-axum/       # Axum web framework integration
โ”œโ”€โ”€ paginator-rocket/     # Rocket web framework integration
โ”œโ”€โ”€ paginator-actix/      # Actix-web integration
โ””โ”€โ”€ paginator-examples/   # Usage examples

๐Ÿ“ฆ Installation

Core Library

[dependencies]
paginator-rs = "0.1.2"
paginator-utils = "0.1.2"
serde = { version = "1", features = ["derive"] }

With SQLx (PostgreSQL)

[dependencies]
paginator-sqlx = { version = "0.1", features = ["postgres", "runtime-tokio"] }
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio"] }

With SeaORM

[dependencies]
paginator-sea-orm = { version = "0.1", features = ["sqlx-postgres", "runtime-tokio"] }
sea-orm = { version = "1.1", features = ["sqlx-postgres", "runtime-tokio"] }

With SurrealDB

[dependencies]
paginator-surrealdb = { version = "0.1", features = ["protocol-ws", "kv-mem"] }
surrealdb = { version = "2.1", features = ["protocol-ws", "kv-mem"] }

With Axum

[dependencies]
paginator-axum = "0.1"
axum = "0.7"

With Rocket

[dependencies]
paginator-rocket = "0.1"
rocket = { version = "0.5", features = ["json"] }

With Actix-web

[dependencies]
paginator-actix = "0.1"
actix-web = "4"

๐Ÿš€ Usage Examples

Basic Pagination

use paginator_rs::{PaginationParams, PaginatorBuilder, PaginatorTrait};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, Clone)]
struct User {
    id: u32,
    name: String,
    email: String,
}

// Using builder pattern
let params = PaginatorBuilder::new()
    .page(1)
    .per_page(20)
    .sort_by("name")
    .sort_asc()
    .build();

// Or create directly
let params = PaginationParams::new(1, 20);

Filtering & Search

use paginator_rs::{FilterValue, PaginatorBuilder};

// Example 1: Simple filtering
let params = PaginatorBuilder::new()
    .page(1)
    .per_page(20)
    .filter_eq("status", FilterValue::String("active".to_string()))
    .filter_gt("age", FilterValue::Int(18))
    .build();

// Example 2: Advanced filtering with multiple operators
let params = PaginatorBuilder::new()
    .filter_in("role", vec![
        FilterValue::String("admin".to_string()),
        FilterValue::String("moderator".to_string()),
    ])
    .filter_between("created_at",
        FilterValue::String("2024-01-01".to_string()),
        FilterValue::String("2024-12-31".to_string())
    )
    .build();

// Example 3: Full-text search
let params = PaginatorBuilder::new()
    .search("john", vec!["name".to_string(), "email".to_string()])
    .build();

// Example 4: Combined filters and search
let params = PaginatorBuilder::new()
    .page(1)
    .per_page(10)
    .filter_eq("status", FilterValue::String("active".to_string()))
    .filter_gt("age", FilterValue::Int(18))
    .search("developer", vec!["title".to_string(), "bio".to_string()])
    .sort_by("created_at")
    .sort_desc()
    .build();

// Get generated SQL WHERE clause
if let Some(where_clause) = params.to_sql_where() {
    println!("WHERE {}", where_clause);
    // Output: WHERE status = 'active' AND age > 18 AND (title ILIKE '%developer%' OR bio ILIKE '%developer%')
}

Available Filter Operators:

  • filter_eq(field, value) - Equal (=)
  • filter_ne(field, value) - Not equal (!=)
  • filter_gt(field, value) - Greater than (>)
  • filter_lt(field, value) - Less than (<)
  • filter_gte(field, value) - Greater than or equal (>=)
  • filter_lte(field, value) - Less than or equal (<=)
  • filter_like(field, pattern) - SQL LIKE pattern matching
  • filter_ilike(field, pattern) - Case-insensitive LIKE
  • filter_in(field, values) - IN array
  • filter_between(field, min, max) - BETWEEN min AND max
  • filter_is_null(field) - IS NULL
  • filter_is_not_null(field) - IS NOT NULL

Search Options:

  • search(query, fields) - Case-insensitive fuzzy search
  • search_exact(query, fields) - Exact match search
  • search_case_sensitive(query, fields) - Case-sensitive search

With Axum

use axum::{Router, routing::get};
use paginator_axum::{PaginationQuery, PaginatedJson};
use serde::Serialize;

#[derive(Serialize)]
struct User {
    id: u32,
    name: String,
}

async fn get_users(
    PaginationQuery(params): PaginationQuery,
) -> PaginatedJson<User> {
    let users = vec![/* fetch from database */];

    // Automatically adds pagination headers
    PaginatedJson::new(users, &params, 100)
}

let app = Router::new().route("/users", get(get_users));

With SQLx (PostgreSQL)

use paginator_sqlx::postgres::paginate_query;
use paginator_rs::PaginatorBuilder;
use sqlx::PgPool;

#[derive(sqlx::FromRow, serde::Serialize)]
struct User {
    id: i32,
    name: String,
}

async fn get_users(pool: &PgPool) -> Result<(), Box<dyn std::error::Error>> {
    let params = PaginatorBuilder::new()
        .page(1)
        .per_page(10)
        .sort_by("created_at")
        .sort_desc()
        .build();

    let result = paginate_query::<_, User>(
        pool,
        "SELECT id, name FROM users WHERE active = true",
        &params,
    ).await?;

    println!("Page {}/{}", result.meta.page, result.meta.total_pages);
    println!("Total users: {}", result.meta.total);

    Ok(())
}

With SeaORM

use paginator_sea_orm::PaginateSeaOrm;
use paginator_rs::PaginationParams;
use sea_orm::{EntityTrait, Database};

async fn get_users(db: &DatabaseConnection) -> Result<(), sea_orm::DbErr> {
    let params = PaginationParams::new(1, 20);

    let result = User::find()
        .filter(user::Column::Active.eq(true))
        .paginate_with(db, &params)
        .await?;

    println!("Found {} users", result.data.len());
    Ok(())
}

With SurrealDB

use surrealdb::Surreal;
use surrealdb::engine::remote::ws::Ws;
use paginator_surrealdb::{paginate_query, paginate_table, QueryBuilder};
use paginator_rs::PaginatorBuilder;

#[derive(serde::Deserialize, serde::Serialize)]
struct User {
    id: String,
    name: String,
    email: String,
    active: bool,
}

async fn get_users() -> Result<(), Box<dyn std::error::Error>> {
    // Connect to SurrealDB
    let db = Surreal::new::<Ws>("127.0.0.1:8000").await?;
    db.use_ns("test").use_db("test").await?;

    let params = PaginatorBuilder::new()
        .page(1)
        .per_page(10)
        .sort_by("name")
        .sort_asc()
        .build();

    // Option 1: Using raw query
    let result = paginate_query::<User, _>(
        &db,
        "SELECT * FROM users WHERE active = true",
        &params,
    ).await?;

    // Option 2: Using table helper
    let result = paginate_table::<User, _>(
        &db,
        "users",
        Some("active = true"),
        &params,
    ).await?;

    // Option 3: Using query builder
    let result = QueryBuilder::new()
        .select("*")
        .from("users")
        .where_clause("active = true")
        .and("age > 18")
        .paginate::<User, _>(&db, &params)
        .await?;

    println!("Page {}/{}", result.meta.page, result.meta.total_pages);
    println!("Total users: {}", result.meta.total);

    Ok(())
}

With Rocket

use rocket::{get, routes};
use paginator_rocket::{Pagination, PaginatedJson};

#[derive(Serialize)]
struct User {
    id: u32,
    name: String,
}

#[get("/users")]
async fn get_users(pagination: Pagination) -> PaginatedJson<User> {
    let users = vec![/* ... */];
    PaginatedJson::new(users, &pagination.params, 100)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/api", routes![get_users])
}

With Actix-web

use actix_web::{get, web, App};
use paginator_actix::{PaginationQuery, PaginatedJson};

#[derive(Serialize)]
struct User {
    id: u32,
    name: String,
}

#[get("/users")]
async fn get_users(
    query: web::Query<PaginationQuery>,
) -> PaginatedJson<User> {
    let params = query.as_params();
    let users = vec![/* ... */];

    PaginatedJson::new(users, &params, 100)
}

๐Ÿงช Response Format

{
  "data": [
    { "id": 1, "name": "Alice", "email": "alice@example.com" },
    { "id": 2, "name": "Bob", "email": "bob@example.com" }
  ],
  "meta": {
    "page": 1,
    "per_page": 20,
    "total": 100,
    "total_pages": 5,
    "has_next": true,
    "has_prev": false
  }
}

HTTP Headers (Web Framework Integrations)

X-Total-Count: 100
X-Total-Pages: 5
X-Current-Page: 1
X-Per-Page: 20

๐ŸŽฏ Query Parameters

Basic Pagination & Sorting

GET /api/users?page=2&per_page=20&sort_by=name&sort_direction=asc
  • page: Page number (1-indexed, default: 1)
  • per_page: Items per page (default: 20, max: 100)
  • sort_by: Field to sort by (optional)
  • sort_direction: asc or desc (optional)

With Filters

GET /api/users?page=1&filter=status:eq:active&filter=age:gt:18&filter=role:in:admin,moderator
  • filter: Filter in format field:operator:value
  • Multiple filters can be combined (AND logic)

Filter Format Examples:

  • status:eq:active - Equal
  • age:gt:18 - Greater than
  • age:between:18,65 - Between
  • role:in:admin,moderator,user - In array
  • name:like:%john% - LIKE pattern
  • deleted_at:is_null - IS NULL

With Search

GET /api/users?search=john&search_fields=name,email,bio
  • search: Search query text
  • search_fields: Comma-separated list of fields to search in

Combined Example

GET /api/users?page=1&per_page=10&filter=status:eq:active&filter=age:gt:18&search=developer&search_fields=title,bio&sort_by=created_at&sort_direction=desc

๐Ÿ”ง Builder Pattern

use paginator_rs::PaginatorBuilder;

let params = PaginatorBuilder::new()
    .page(2)
    .per_page(50)
    .sort_by("created_at")
    .sort_desc()
    .build();

โš ๏ธ Error Handling

use paginator_rs::{PaginatorError, PaginatorResult};

// Errors are comprehensive and helpful
match result {
    Ok(response) => println!("Success!"),
    Err(PaginatorError::InvalidPage(page)) => {
        eprintln!("Invalid page: {}. Page must be >= 1", page);
    }
    Err(PaginatorError::InvalidPerPage(per_page)) => {
        eprintln!("Invalid per_page: {}. Must be between 1 and 100", per_page);
    }
    Err(e) => eprintln!("Error: {}", e),
}

๐Ÿ—๏ธ Architecture

  • Easy to Use: Builder pattern and sensible defaults
  • Easy to Debug: Comprehensive error messages and type safety
  • Easy to Maintain: Modular crate structure with clear separation of concerns

๐Ÿ“ Examples

Run the examples:

cargo run --package paginator-examples --bin example

๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

๐Ÿ“„ License

MIT ยฉ 2025 Maulana Sodiqin

๐Ÿ”— Links