exercises

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

products.rs (3538B)


      1 //! Product API handlers
      2 //!
      3 //! This module contains the HTTP request handlers for product-related endpoints.
      4 //! Handlers delegate database operations to the repository layer (db module).
      5 
      6 use axum::{
      7     extract::{Path, Query, State},
      8     http::StatusCode,
      9     response::{IntoResponse, Json},
     10 };
     11 use sqlx::PgPool;
     12 use tracing::instrument;
     13 use uuid::Uuid;
     14 
     15 use crate::db;
     16 use crate::models::{ProductQueryParams, ProductsResponse};
     17 use crate::utils::{calculate_pagination, calculate_total_pages, internal_error, not_found_error};
     18 
     19 /// List products with pagination and filtering
     20 ///
     21 /// # Endpoint
     22 /// `GET /products`
     23 ///
     24 /// # Query Parameters
     25 /// - **Pagination:**
     26 ///   - `page` (default: 1) - Page number (1-indexed)
     27 ///   - `page_size` (default: 20, max: 100) - Items per page
     28 ///
     29 /// - **Filters:**
     30 ///   - `name` - Product name partial match (case-insensitive)
     31 ///   - `category_id` - Filter by category ID
     32 ///   - `brand` - Brand partial match (case-insensitive)
     33 ///   - `start_date` / `end_date` - Filter by update date range
     34 ///   - `rating_gt` / `rating_lt` / `rating_eq` - Rating filters
     35 ///   - `min_price` / `max_price` - Price range filters
     36 ///
     37 /// # Response
     38 /// Returns a `ProductsResponse` with products array and pagination metadata.
     39 pub async fn list_products(
     40     State(pool): State<PgPool>,
     41     Query(params): Query<ProductQueryParams>,
     42 ) -> impl IntoResponse {
     43     // Apply pagination defaults and constraints
     44     let (page, page_size, offset) = calculate_pagination(params.page, params.page_size);
     45 
     46     // Delegate to repository layer for database operations
     47     match db::list_products(&pool, &params, page, page_size, offset).await {
     48         Ok((products, total_count)) => {
     49             let total_pages = calculate_total_pages(total_count, page_size);
     50 
     51             let response = ProductsResponse {
     52                 products,
     53                 total_count,
     54                 page,
     55                 page_size,
     56                 total_pages,
     57             };
     58 
     59             (StatusCode::OK, Json(response)).into_response()
     60         }
     61         Err(e) => internal_error("Failed to fetch products", e.to_string()),
     62     }
     63 }
     64 
     65 /// Get detailed product information by UUID
     66 ///
     67 /// # Endpoint
     68 /// `GET /products/{uuid}`
     69 ///
     70 /// # Errors
     71 /// - `404 NOT FOUND` - Product not found or has been deleted
     72 /// - `500 INTERNAL SERVER ERROR` - Database error
     73 #[instrument(
     74     name = "get_product_by_id",
     75     skip(pool),
     76     fields(
     77         product.uuid = %uuid,
     78         product.found = tracing::field::Empty
     79     )
     80 )]
     81 pub async fn get_product_by_id(
     82     State(pool): State<PgPool>,
     83     Path(uuid): Path<Uuid>,
     84 ) -> impl IntoResponse {
     85     // Delegate to repository layer for database query
     86     match db::get_product_by_uuid(&pool, uuid).await {
     87         Ok(Some(mut product)) => {
     88             tracing::Span::current().record("product.found", true);
     89             tracing::debug!(product.uuid = true, product.name = %product.product_name, "Product found");
     90 
     91             // Set the string representation of the product ID
     92             product.set_product_id();
     93             (StatusCode::OK, Json(product)).into_response()
     94         }
     95         Ok(None) => {
     96             tracing::Span::current().record("product.found", false);
     97             tracing::info!(product.found = false, "Product not found"); // INFO not ERROR
     98 
     99             not_found_error(
    100                 "Product not found",
    101                 serde_json::json!({"uuid": uuid.to_string()}),
    102             )
    103         }
    104         Err(e) => internal_error("Failed to fetch product", e.to_string()),
    105     }
    106 }