Skip to content

المعالجات

المعالجات (Handlers) هي دوال غير متزامنة تُعالج طلبات HTTP الواردة. تقع في أعلى طبقة التطبيق، مسؤولة عن تحليل بيانات الطلب، والتفويض للـ services، وبناء استجابات HTTP. يُنظّم FORGE المعالجات في وحدات admin وpublic، كل منها يتبع أنماطاً متسقة لعمليات CRUD ومعالجة الأخطاء.

تنظيم المعالجات

src/handlers/
├── mod.rs              # Module exports
├── auth.rs             # Login, register, refresh, logout
├── profile.rs          # Current user profile
├── lookups.rs          # Public lookup endpoints
├── menus.rs            # Public menu endpoints
└── admin/
    ├── mod.rs          # Admin module exports
    ├── users.rs        # Users CRUD
    ├── roles.rs        # Roles CRUD
    ├── contents.rs     # Content CRUD
    ├── settings.rs     # Settings management
    ├── lookups.rs      # Lookup CRUD
    └── menus.rs        # Menus CRUD

معالجات Admin (src/handlers/admin/) تتطلب مصادقة وصلاحيات. تتعامل مع عمليات CRUD الكاملة لإدارة بيانات التطبيق.

المعالجات العامة (src/handlers/) قد تكون بدون مصادقة. تُقدّم البيانات لعملاء الواجهة الأمامية، مثل صفحات المحتوى وقيم lookup.

تشريح المعالج

كل معالج يتبع نفس النمط:

1. استخراج مستخدم المصادقة (إذا محمي)
2. استخراج والتحقق من المدخلات
3. استدعاء طبقة service
4. إرجاع استجابة مُنظّمة
rust
pub async fn create_user(
    State(pool): State<PgPool>,                     // 1. Database pool
    Extension(current_user): Extension<AuthUser>,    // 2. Authenticated user
    Json(payload): Json<CreateUserRequest>,           // 3. Request body
) -> Result<impl IntoResponse, AppError> {           // 4. Return type
    // Validate input
    payload.validate()
        .map_err(|e| AppError::ValidationError(format_validation_errors(e)))?;

    // Delegate to service
    let user = UserService::create(&pool, payload).await?;

    // Return response
    Ok((
        StatusCode::CREATED,
        Json(ApiResponse::success(UserResponse::from(user))),
    ))
}

نصيحة

يجب أن تكون المعالجات نحيفة. تستخرج البيانات من الطلب، تُمررها للـ service، وتُنسّق الاستجابة. منطق الأعمال وقواعد التحقق واستعلامات قاعدة البيانات تنتمي لـ طبقة service.

أمثلة معالجات CRUD

القائمة (مع ترقيم الصفحات)

rust
/// List all users with pagination and optional search
#[utoipa::path(
    get,
    path = "/api/admin/users",
    tag = "Users",
    params(
        ("page" = Option<i64>, Query, description = "رقم الصفحة"),
        ("per_page" = Option<i64>, Query, description = "عدد العناصر لكل صفحة"),
        ("search" = Option<String>, Query, description = "البحث بالاسم أو البريد"),
    ),
    responses(
        (status = 200, description = "قائمة مستخدمين مُرقّمة", body = PaginatedResponse<UserResponse>),
        (status = 401, description = "غير مُصادق"),
    ),
    security(("bearer_auth" = []))
)]
pub async fn list(
    State(pool): State<PgPool>,
    Extension(_current_user): Extension<AuthUser>,
    Query(params): Query<PaginatedRequest>,
) -> Result<impl IntoResponse, AppError> {
    let (users, total) = UserService::list(&pool, &params).await?;

    let response = PaginatedResponse::new(
        users.into_iter().map(UserResponse::from).collect(),
        total,
        params.page,
        params.per_page,
    );

    Ok(Json(ApiResponse::success(response)))
}

عرض (مورد واحد)

rust
/// Get a single user by ID
#[utoipa::path(
    get,
    path = "/api/admin/users/{id}",
    tag = "Users",
    params(
        ("id" = Uuid, Path, description = "معرّف المستخدم"),
    ),
    responses(
        (status = 200, description = "تفاصيل المستخدم", body = ApiResponse<UserResponse>),
        (status = 404, description = "المستخدم غير موجود"),
    ),
    security(("bearer_auth" = []))
)]
pub async fn show(
    State(pool): State<PgPool>,
    Extension(_current_user): Extension<AuthUser>,
    Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
    let user = UserService::find_by_id(&pool, id)
        .await?
        .ok_or(AppError::NotFound("User not found".into()))?;

    Ok(Json(ApiResponse::success(UserResponse::from(user))))
}

إنشاء

rust
/// Create a new user
#[utoipa::path(
    post,
    path = "/api/admin/users",
    tag = "Users",
    request_body = CreateUserRequest,
    responses(
        (status = 201, description = "تم إنشاء المستخدم", body = ApiResponse<UserResponse>),
        (status = 422, description = "خطأ تحقق", body = ErrorResponse),
    ),
    security(("bearer_auth" = []))
)]
pub async fn create(
    State(pool): State<PgPool>,
    Extension(_current_user): Extension<AuthUser>,
    Json(payload): Json<CreateUserRequest>,
) -> Result<impl IntoResponse, AppError> {
    payload.validate()
        .map_err(|e| AppError::ValidationError(format_validation_errors(e)))?;

    let user = UserService::create(&pool, payload).await?;

    Ok((
        StatusCode::CREATED,
        Json(ApiResponse::success(UserResponse::from(user))),
    ))
}

تحديث

rust
/// Update an existing user
#[utoipa::path(
    put,
    path = "/api/admin/users/{id}",
    tag = "Users",
    params(("id" = Uuid, Path, description = "معرّف المستخدم")),
    request_body = UpdateUserRequest,
    responses(
        (status = 200, description = "تم تحديث المستخدم", body = ApiResponse<UserResponse>),
        (status = 404, description = "المستخدم غير موجود"),
        (status = 422, description = "خطأ تحقق", body = ErrorResponse),
    ),
    security(("bearer_auth" = []))
)]
pub async fn update(
    State(pool): State<PgPool>,
    Extension(_current_user): Extension<AuthUser>,
    Path(id): Path<Uuid>,
    Json(payload): Json<UpdateUserRequest>,
) -> Result<impl IntoResponse, AppError> {
    payload.validate()
        .map_err(|e| AppError::ValidationError(format_validation_errors(e)))?;

    let user = UserService::update(&pool, id, payload)
        .await?
        .ok_or(AppError::NotFound("User not found".into()))?;

    Ok(Json(ApiResponse::success(UserResponse::from(user))))
}

حذف (حذف ناعم)

rust
/// Delete a user (soft delete)
#[utoipa::path(
    delete,
    path = "/api/admin/users/{id}",
    tag = "Users",
    params(("id" = Uuid, Path, description = "معرّف المستخدم")),
    responses(
        (status = 200, description = "تم حذف المستخدم"),
        (status = 404, description = "المستخدم غير موجود"),
    ),
    security(("bearer_auth" = []))
)]
pub async fn delete(
    State(pool): State<PgPool>,
    Extension(current_user): Extension<AuthUser>,
    Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
    // Prevent self-deletion
    if current_user.id == id {
        return Err(AppError::Forbidden(
            "You cannot delete your own account".into(),
        ));
    }

    UserService::delete(&pool, id)
        .await?
        .ok_or(AppError::NotFound("User not found".into()))?;

    Ok(Json(ApiResponse::success("User deleted successfully")))
}

تحذير

عمليات الحذف تستخدم الحذف الناعم بتعيين deleted_at بدلاً من إزالة الصف. هذا يحفظ سلامة البيانات ويسمح بالاستعادة. طبقة service تتعامل مع منطق الحذف الناعم.

المُستخرجات

يوفر Axum مُستخرجات (extractors) لتحليل أجزاء مختلفة من طلب HTTP:

المُستخرجالمصدرمثال
State(pool)حالة التطبيقpool قاعدة البيانات
Extension(user)امتدادات الطلبالمستخدم المُصادق (يُعيّنه middleware)
Json(body)جسم الطلبحمولة JSON
Query(params)سلسلة الاستعلام?page=1&per_page=15
Path(id)أجزاء مسار URL/users/{id}
TypedHeader(header)ترويسات الطلبترويسة Authorization

ترتيب المُستخرجات

Axum يُعالج المُستخرجات بالترتيب. ضع المُستخرجات التي تستهلك جسم الطلب (Json) أخيراً:

rust
pub async fn update(
    State(pool): State<PgPool>,         // First: shared state
    Extension(user): Extension<AuthUser>, // Second: extensions
    Path(id): Path<Uuid>,               // Third: URL parameters
    Json(payload): Json<UpdateRequest>,   // Last: request body (consumes it)
) -> Result<impl IntoResponse, AppError> {
    // ...
}

نصيحة

إذا حصلت على خطأ تجميع حول المُستخرجات، تحقق من الترتيب. مُستخرج Json يجب أن يكون دائماً المعامل الأخير لأنه يستهلك جسم الطلب.

المعالجات العامة

المعالجات العامة تُقدّم البيانات بدون طلب مصادقة:

rust
/// Get published content by slug
pub async fn get_content(
    State(pool): State<PgPool>,
    Path(slug): Path<String>,
    Query(params): Query<LocaleQuery>,
) -> Result<impl IntoResponse, AppError> {
    let content = ContentService::find_by_slug(&pool, &slug)
        .await?
        .ok_or(AppError::NotFound("Content not found".into()))?;

    if !content.is_active {
        return Err(AppError::NotFound("Content not found".into()));
    }

    Ok(Json(ApiResponse::success(ContentResponse::from(content))))
}

معالجة الأخطاء في المعالجات

جميع المعالجات تُرجع Result<impl IntoResponse, AppError>. الأخطاء تنتشر باستخدام عامل ?:

rust
pub async fn show(
    State(pool): State<PgPool>,
    Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, AppError> {
    // Database errors propagate via ?
    let user = UserService::find_by_id(&pool, id)
        .await?  // sqlx::Error -> AppError automatically
        .ok_or(AppError::NotFound("User not found".into()))?;

    Ok(Json(ApiResponse::success(UserResponse::from(user))))
}

انظر معالجة الأخطاء لنوع AppError الكامل وكيف يُعيّن لرموز حالة HTTP.

انظر أيضاً

  • الـ Routes — كيف تُسجّل المعالجات لمسارات URL
  • الـ Services — طبقة منطق الأعمال التي تُفوّض لها المعالجات
  • الـ DTOs — أنواع الطلب والاستجابة المستخدمة في المعالجات
  • الـ Middleware — فحوصات المصادقة والصلاحيات قبل تشغيل المعالجات
  • توثيق API — تعليقات Utoipa على المعالجات

Released under the MIT License.