Skip to content

كائنات نقل البيانات

كائنات نقل البيانات (DTOs) تُعرّف شكل البيانات التي تدخل وتخرج من الـ API. تفصل نماذج قاعدة البيانات الداخلية عن عقد الـ API العام، مما يسمح لك بتطوير مخططك بدون كسر العملاء. يُولّد FORGE DTOs مع تحقق مدمج ودعم ترقيم الصفحات واشتقاق مخطط OpenAPI.

اصطلاح التسمية

الـ DTOs تتبع نمط تسمية متسق:

النمطالغرضمثال
Create{Resource}Requestجسم POST لإنشاء موردCreateUserRequest
Update{Resource}Requestجسم PUT/PATCH للتحديثUpdateUserRequest
{Resource}Responseاستجابة مورد واحدUserResponse
{Resource}ListResponseعنصر قائمة (قد يكون أخف)UserListResponse
طلب وارد                     استجابة صادرة
────────────                   ────────────
CreateUserRequest  ──▶ Handler ──▶  UserResponse
UpdateUserRequest  ──▶ Handler ──▶  UserResponse
PaginatedRequest   ──▶ Handler ──▶  PaginatedResponse<UserResponse>

DTOs الطلب

CreateUserRequest

rust
use serde::Deserialize;
use utoipa::ToSchema;
use uuid::Uuid;
use validator::Validate;

#[derive(Debug, Deserialize, Validate, ToSchema)]
pub struct CreateUserRequest {
    /// User's full name
    #[schema(example = "Jane Doe")]
    #[validate(length(min = 1, max = 255, message = "Name is required"))]
    pub name: String,

    /// Email address (must be unique)
    #[schema(example = "jane@example.com")]
    #[validate(email(message = "Invalid email format"))]
    pub email: String,

    /// Password (minimum 8 characters)
    #[schema(example = "securepassword123")]
    #[validate(length(min = 8, message = "Password must be at least 8 characters"))]
    pub password: String,

    /// Mobile number (optional)
    #[schema(example = "+966500000000")]
    #[validate(length(min = 8, max = 15))]
    pub mobile: Option<String>,

    /// Whether the user account is active
    #[schema(example = true)]
    pub is_active: Option<bool>,

    /// Role IDs to assign to the user
    pub role_ids: Option<Vec<Uuid>>,
}

UpdateUserRequest

DTOs التحديث تستخدم Option لكل حقل لدعم التحديثات الجزئية:

rust
#[derive(Debug, Deserialize, Validate, ToSchema)]
pub struct UpdateUserRequest {
    #[schema(example = "Jane Updated")]
    #[validate(length(min = 1, max = 255))]
    pub name: Option<String>,

    #[schema(example = "jane.updated@example.com")]
    #[validate(email)]
    pub email: Option<String>,

    #[validate(length(min = 8))]
    pub password: Option<String>,

    #[validate(length(min = 8, max = 15))]
    pub mobile: Option<String>,

    pub is_active: Option<bool>,

    pub role_ids: Option<Vec<Uuid>>,
}

نصيحة

استخدام Option<T> لجميع حقول التحديث يسمح للعملاء بإرسال الحقول التي يريدون تغييرها فقط. حقل مُعيّن لـ null في JSON يُعيّن لـ None في Rust، بمعنى "لا تُحدّث هذا الحقل."

DTOs الاستجابة

UserResponse

rust
use serde::Serialize;
use utoipa::ToSchema;
use uuid::Uuid;
use chrono::{DateTime, Utc};

#[derive(Debug, Serialize, ToSchema)]
pub struct UserResponse {
    pub id: Uuid,
    pub name: String,
    pub email: String,
    pub mobile: Option<String>,
    pub avatar: Option<String>,
    pub is_active: bool,
    pub roles: Vec<RoleResponse>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Serialize, ToSchema)]
pub struct RoleResponse {
    pub id: Uuid,
    pub name: String,
    pub display_name: Option<String>,
}

تحذير

DTOs الاستجابة يجب ألا تتضمن أبداً حقولاً حساسة مثل كلمات المرور أو المعرّفات الداخلية أو طوابع الحذف الناعم. عيّن دائماً من نموذجك لـ DTO استجابة قبل إرجاع البيانات للعميل.

تحويل النماذج للاستجابات

نفّذ traits الـ From لتحويل نظيف من نموذج لـ DTO:

rust
impl From<User> for UserResponse {
    fn from(user: User) -> Self {
        Self {
            id: user.id,
            name: user.name,
            email: user.email,
            mobile: user.mobile,
            avatar: user.avatar,
            is_active: user.is_active,
            roles: vec![], // Loaded separately
            created_at: user.created_at,
            updated_at: user.updated_at,
        }
    }
}

impl From<UserWithRoles> for UserResponse {
    fn from(data: UserWithRoles) -> Self {
        Self {
            id: data.user.id,
            name: data.user.name,
            email: data.user.email,
            mobile: data.user.mobile,
            avatar: data.user.avatar,
            is_active: data.user.is_active,
            roles: data.roles.into_iter().map(RoleResponse::from).collect(),
            created_at: data.user.created_at,
            updated_at: data.user.updated_at,
        }
    }
}

DTOs المشتركة

ApiResponse

الغلاف القياسي لجميع الاستجابات الناجحة:

rust
#[derive(Debug, Serialize, ToSchema)]
pub struct ApiResponse<T: Serialize> {
    pub success: bool,
    pub data: T,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

impl<T: Serialize> ApiResponse<T> {
    pub fn success(data: T) -> Self {
        Self {
            success: true,
            data,
            message: None,
        }
    }

    pub fn with_message(data: T, message: impl Into<String>) -> Self {
        Self {
            success: true,
            data,
            message: Some(message.into()),
        }
    }
}

ErrorResponse

الغلاف القياسي لاستجابات الخطأ:

rust
#[derive(Debug, Serialize, ToSchema)]
pub struct ErrorResponse {
    pub success: bool,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub errors: Option<serde_json::Value>,
}

PaginatedRequest

معاملات ترقيم الصفحات الواردة:

rust
#[derive(Debug, Deserialize, ToSchema)]
pub struct PaginatedRequest {
    /// Page number (starts from 1)
    #[serde(default = "default_page")]
    pub page: i64,

    /// Number of items per page
    #[serde(default = "default_per_page")]
    pub per_page: i64,

    /// Optional search query
    pub search: Option<String>,

    /// Sort field
    pub sort_by: Option<String>,

    /// Sort direction (asc or desc)
    pub sort_order: Option<String>,
}

fn default_page() -> i64 { 1 }
fn default_per_page() -> i64 { 15 }

PaginatedResponse

غلاف الاستجابة المُرقّمة:

rust
#[derive(Debug, Serialize, ToSchema)]
pub struct PaginatedResponse<T: Serialize> {
    pub data: Vec<T>,
    pub meta: PaginationMeta,
}

#[derive(Debug, Serialize, ToSchema)]
pub struct PaginationMeta {
    pub current_page: i64,
    pub per_page: i64,
    pub total: i64,
    pub total_pages: i64,
}

impl<T: Serialize> PaginatedResponse<T> {
    pub fn new(data: Vec<T>, total: i64, page: i64, per_page: i64) -> Self {
        Self {
            data,
            meta: PaginationMeta {
                current_page: page,
                per_page,
                total,
                total_pages: (total as f64 / per_page as f64).ceil() as i64,
            },
        }
    }
}

التحقق

يستخدم FORGE crate الـ validator للتحقق من الطلبات. التحقق يعمل تلقائياً عند تحليل DTO الطلب.

المُحققات المتاحة

rust
#[derive(Deserialize, Validate)]
pub struct ExampleRequest {
    // Text length
    #[validate(length(min = 1, max = 255))]
    pub name: String,

    // Email format
    #[validate(email)]
    pub email: String,

    // URL format
    #[validate(url)]
    pub website: Option<String>,

    // Numeric range
    #[validate(range(min = 0, max = 100))]
    pub score: i32,

    // Regex pattern
    #[validate(regex(path = "SLUG_REGEX"))]
    pub slug: String,

    // Custom validation
    #[validate(custom(function = "validate_password_strength"))]
    pub password: String,

    // Nested validation
    #[validate(nested)]
    pub address: AddressRequest,
}

lazy_static! {
    static ref SLUG_REGEX: regex::Regex =
        regex::Regex::new(r"^[a-z0-9]+(?:-[a-z0-9]+)*$").unwrap();
}

التحقق في الـ Handlers

تحقق من الطلبات الواردة باستخدام طريقة validate():

rust
pub async fn create_user(
    State(pool): State<PgPool>,
    Json(payload): Json<CreateUserRequest>,
) -> Result<impl IntoResponse, AppError> {
    // Validate the request
    payload.validate().map_err(|e| {
        AppError::ValidationError(format_validation_errors(e))
    })?;

    // Continue with business logic
    let user = UserService::create(&pool, payload).await?;
    Ok(Json(ApiResponse::success(UserResponse::from(user))))
}

صيغة خطأ التحقق

أخطاء التحقق تُرجع استجابة JSON مُنظّمة:

json
{
  "success": false,
  "message": "Validation failed",
  "errors": {
    "email": ["Invalid email format"],
    "password": ["Password must be at least 8 characters"],
    "name": ["Name is required"]
  }
}

DTOs المحتوى مع الترجمات

للمحتوى القابل للترجمة، الـ DTOs تتعامل مع بنية ترجمات JSONB:

rust
#[derive(Debug, Deserialize, Validate, ToSchema)]
pub struct CreateContentRequest {
    #[validate(regex(path = "SLUG_REGEX"))]
    pub slug: String,

    pub content_type: String,

    /// Translations indexed by language
    /// Example: { "en": { "title": "Hello", "body": "..." }, "ar": { "title": "مرحبا" } }
    pub translations: serde_json::Value,

    pub metadata: Option<serde_json::Value>,
    pub is_active: Option<bool>,
    pub sort_order: Option<i32>,
}

#[derive(Debug, Serialize, ToSchema)]
pub struct ContentResponse {
    pub id: Uuid,
    pub slug: String,
    pub content_type: String,
    pub translations: serde_json::Value,
    pub metadata: Option<serde_json::Value>,
    pub is_active: bool,
    pub sort_order: i32,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

انظر أيضاً

  • الـ Handlers — حيث تُستخدم DTOs لتحليل الطلبات وبناء الاستجابات
  • النماذج — نماذج قاعدة البيانات التي تتحول DTOs منها وإليها
  • توثيق API — كيف يُولّد ToSchema مخططات OpenAPI
  • معالجة الأخطاء — صيغة استجابة خطأ التحقق

Released under the MIT License.