Danh sách bài viết

Bài 74: CRUD Macro Derive — Auto-Generate Endpoint

Bài 74 của series Rust RESTful API — bài CODE thực tế lớn proc macro pattern nối tiếp B72 (service layer trait pattern) + B73 (domain error unified) khi 5 resource phức tạp (Product, Order, Cart, User, Payment) đã có pattern chuẩn 5 endpoint CRUD; lúc này nhìn lại codebase thấy mỗi simple resource (Brand, Color, Tag — master data đơn giản 4-6 field không relation) lặp lại đúng ~250 dòng boilerplate (trait + impl + handler + DTO), pattern macro Rust là giải pháp tự nhiên để giảm code; bài này phân biệt 3 loại Rust macro — declarative macro_rules! (đơn giản, pattern matching syntactic, không touch type info), procedural derive #[derive(Foo)] (cần crate riêng proc-macro = true, access full AST qua syn), procedural attribute #[my_attr] (linh hoạt nhất, transform function/struct/impl); phân tích trade-off macro vs explicit handler — macro pros giảm ~80% LOC + consistency + single source of truth, cons hidden control flow + compile time chậm + IDE intelligence khó + maintenance cost cao + customization scope hạn chế → Shop API 70/30 rule lock vĩnh viễn: 70% explicit handler cho complex resource (Product filter/search/JOIN, Order state machine, User auth flow, Cart UPSERT checkout, Payment Stripe webhook), 30% macro cho simple resource (Brand/Color/Tag CRUD đơn giản); init crate shop-macros workspace thứ 5 (sau shop-api, shop-common, shop-db, shop-core) với attribute proc-macro = true trong [lib] bắt buộc + 3 dep syn = "2" parse Rust syntax tree + quote = "1" generate Rust code từ tree + proc-macro2 = "1" wrapper testing-friendly; implement #[derive(SimpleCrud)] skeleton preview qua #[proc_macro_derive(SimpleCrud, attributes(crud))] parse DeriveInput → extract field qua match Data::Struct + Fields::Named → quote! generate BrandService trait 5 method + PgBrandService impl skeleton + helper attribute #[crud(table = "brands")] map struct → table + #[crud(primary)] primary key skip insert + #[crud(unique)] UNIQUE constraint hint + #[crud(slug)] URL slug field + #[crud(auto)] server-generated skip insert/update; áp Brand resource Apple/Samsung/Sony master data 6 field (id, name, slug, logo_url, created_at, updated_at) với migration 15 create_brands (BIGSERIAL id PRIMARY KEY + name TEXT UNIQUE + slug TEXT UNIQUE + logo_url nullable + 2 TIMESTAMPTZ + brands_slug_idx + trigger updated_at); handler vẫn viết tay (B74 partial macro pattern) cho flexibility auth gate + status code custom, B75 sẽ preview procedural attribute #[crud_handler] generate handler luôn; cargo expand tool MANDATORY debug macro output verify auto-generated code đúng signature; compare LOC trước/sau macro: Brand 250 LOC explicit vs 30 LOC #[derive(SimpleCrud)] = 88% reduction; lock decision macro CHỈ cho simple resource KHÔNG cho complex business logic; foundation cho B75 (end-to-end CRUD integration test full flow cart → checkout → order → payment → audit qua testcontainers Postgres + Stripe mock — bài CUỐI Group 7) + G14 (Color + Tag resource reuse cùng macro pattern) + future enhancement procedural attribute #[crud_handler] generate handler tự động.

16/06/2026
13 phút đọc
1 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Hiểu 3 loại Rust macro: declarative (macro_rules!), procedural derive, procedural attribute.
  • Hiểu trade-off macro reuse vs explicit handler — khi nào dùng và khi nào tránh.
  • Implement Brand resource đơn giản (simple CRUD, không relation phức tạp) làm demo macro.
  • Tạo shop-macros crate (proc macro crate riêng workspace, member thứ 5).
  • Implement #[derive(SimpleCrud)] macro (preview — chỉ skeleton, không full SQL).
  • Áp dụng macro vào Brand resource — generate BrandService trait + PgBrandService impl.
  • Hiểu maintenance cost macro vs explicit — Shop API 70/30 rule lock.
  • Dùng cargo expand tool debug macro output.
2

3 Loại Macro Rust

Rust có 3 loại macro (lập trình meta — code sinh ra code lúc compile), mỗi loại phục vụ use case khác nhau với độ phức tạp khác nhau.

Loại 1 — macro_rules! (declarative macro): pattern matching syntactic, không cần crate riêng, viết thẳng trong cùng crate sử dụng.

// Declarative macro — viết thẳng inline
macro_rules! vec_of_strings {
    ($($x:expr),*) => {
        vec![$($x.to_string()),*]
    };
}

let names = vec_of_strings!["apple", "banana", "cherry"];
// Expand: vec!["apple".to_string(), "banana".to_string(), "cherry".to_string()]

Pros: đơn giản, không cần crate riêng, không cần dep ngoài. Cons: pattern matching giới hạn syntactic (không touch type info, không introspect field struct, không generate impl block phức tạp). Use case: vec![], println!, helper macro nhỏ.

Loại 2 — Procedural derive (#[derive(Foo)]): macro chạy như function nhận TokenStream input, output TokenStream, có access full AST qua syn crate.

// Procedural derive — phía caller chỉ thêm #[derive]
#[derive(Serialize, Deserialize)]   // serde::Serialize, serde::Deserialize
#[derive(sqlx::FromRow)]            // sqlx::FromRow
pub struct User {
    pub id: i64,
    pub email: String,
}

Pros: dễ dùng phía caller (chỉ thêm 1 dòng #[derive(...)]), full AST access, generate impl block freely. Cons: cần crate riêng với proc-macro = true trong Cargo.toml, complexity implement cao hơn. Use case: #[derive(Serialize)] (serde), #[derive(sqlx::FromRow)] (sqlx), #[derive(thiserror::Error)] (B73).

Loại 3 — Procedural attribute (#[my_attr]): macro nhận TokenStream của cả attribute argument + item attached (function/struct/impl), transform tùy ý.

// Procedural attribute — transform cả function
#[tokio::main]                              // wrap fn main() trong runtime
async fn main() -> anyhow::Result<()> {
    // ...
}

#[axum::debug_handler]                      // wrap handler với better error
async fn list_products(/* ... */) { /* ... */ }

Pros: linh hoạt nhất, có thể transform function/struct/impl/module. Cons: phức tạp nhất, hidden control flow (caller không biết macro đã sửa gì). Use case: #[tokio::main], #[axum::debug_handler], #[async_trait] (B72).

Lock decision Shop API:

  • Dùng procedural derive cho #[derive(SimpleCrud)] ở B74 — generate service trait + impl từ struct definition.
  • Preview procedural attribute #[crud_handler] ở B75 (generate handler luôn — chưa implement ở B74).
  • Tránh macro_rules! cho complex case — pattern matching syntactic không đủ touch type info để generate code chất lượng.
3

Trade-Off Macro Vs Explicit Handler

Sau B72 (service layer trait) + B73 (domain error pattern), pattern chuẩn cho 1 resource gồm: trait Service + impl PgService + thiserror enum Error + 5 handler (list, create, get, update, delete) + DTO struct + route registration. Đếm thử: ~250 LOC/resource. Với 8 resource cuối series → ~2000 LOC boilerplate. Pattern macro hứa hẹn cắt giảm đáng kể, nhưng đi kèm trade-off.

Macro pros:

  • Reduce boilerplate — đo lường thực tế ~80% LOC giảm cho simple resource (250 → 30-50 dòng).
  • Consistency across resources — mọi resource cùng pattern, không có drift giữa Brand viết tay vs Color viết tay khác cách.
  • Single source of truth — sửa logic generate macro 1 chỗ, mọi resource auto-update khi rebuild.

Macro cons:

  • Hidden control flow — debug khó. Developer nhìn struct Brand với #[derive(SimpleCrud)] không biết code thật sự chạy là gì cho đến khi mở source macro.
  • Compile time chậm — proc macro chạy mỗi lần compile crate sử dụng, syn parse + quote generate tốn ~100-300ms/macro.
  • IDE intelligence khórust-analyzer 2026 chưa expand proc macro hoàn hảo, autocomplete trên type generated thường miss.
  • Maintenance cost — bug trong macro impact mọi resource dùng nó; team mới phải hiểu macro internals trước khi fix.
  • Customization scope hạn chế — mọi resource phải fit template macro; resource cần custom logic riêng (auth gate, custom validation, JOIN aggregate) phải opt-out macro hoàn toàn.

Lock decision Shop API — 70/30 rule vĩnh viễn:

  • 70% explicit handler cho complex resource cần flexibility — Product (filter/search/JOIN aggregate), Order (state machine + transaction), User (auth + hash + verify flow), Cart (UPSERT + checkout), Payment (Stripe API + webhook).
  • 30% macro cho simple resource boilerplate-heavy — Brand, Color, Tag (master data 4-6 field, không relation phức tạp, không business logic riêng).

Decision matrix per resource:

Resource    | Macro/Explicit | Lý do
------------+----------------+------------------------------------------
Brand       | Macro          | CRUD đơn giản, 4 field master data
Color       | Macro          | CRUD đơn giản, 3 field
Tag         | Macro          | CRUD đơn giản, 2 field
Product     | Explicit       | Filter/search/JOIN aggregate phức tạp
Order       | Explicit       | State machine + transaction atomic
User        | Explicit       | Auth + hash Argon2id + verify flow
Cart        | Explicit       | UPSERT + checkout flow + total compute
Payment     | Explicit       | Stripe API + webhook dedup + JSONB

Quy tắc: nếu resource có nhiều hơn 1 custom field (auth, state machine, JOIN, transaction, external API) → giữ explicit. Nếu chỉ là CRUD CRUD đơn giản → macro thắng.

4

Init Crate shop-macros (Proc Macro Crate)

Proc macro BẮT BUỘC phải sống trong crate riêng với attribute proc-macro = true trong [lib] section của Cargo.toml. Lý do: Rust compiler load proc macro crate như plugin tại compile time (chạy trong host process compiler), separate ABI khỏi consumer crate; mix proc macro với code thường trong cùng crate sẽ fail link.

Tạo crate mới:

cargo new --lib crates/shop-macros

Update crates/shop-macros/Cargo.toml:

# File: crates/shop-macros/Cargo.toml
[package]
name = "shop-macros"
version = "0.1.0"
edition = "2024"

[lib]
proc-macro = true     # ← BẮT BUỘC cho proc macro crate

[dependencies]
syn = { version = "2", features = ["full"] }   # parse Rust syntax tree
quote = "1"                                    # generate Rust code từ tree
proc-macro2 = "1"                              # wrapper testing-friendly

3 crate dep chuyên dụng cho proc macro Rust ecosystem:

  • syn — parse Rust syntax tree từ TokenStream input. Feature full enable parse mọi item Rust (function, struct, impl, generic, ...). David Tolnay maintain (cùng tác giả thiserror, anyhow).
  • quote — generate Rust code từ tree qua macro quote! { ... }, syntax giống template engine với #variable interpolation. Output kiểu TokenStream.
  • proc-macro2 — wrapper cho built-in proc_macro crate (host-only) cho phép test proc macro ngoài compiler context. Hầu hết API syn + quote trả về type proc_macro2::TokenStream chuyển sang proc_macro::TokenStream ở entry function.

Update root Cargo.toml để thêm member mới vào workspace:

# File: Cargo.toml (root)
[workspace]
resolver = "3"
members = [
    "crates/shop-api",
    "crates/shop-common",
    "crates/shop-db",
    "crates/shop-core",
    "crates/shop-macros",  # ← thêm member thứ 5
]

Add dep vào shop-core để consume macro:

# File: crates/shop-core/Cargo.toml
[dependencies]
shop-common = { path = "../shop-common" }
shop-db = { path = "../shop-db" }
shop-macros = { path = "../shop-macros" }   # ← thêm
async-trait = { workspace = true }
serde = { workspace = true }
sqlx = { workspace = true }
# ... các dep khác

Verify workspace build:

cargo build -p shop-macros
# Compiling syn v2.x
# Compiling quote v1.x
# Compiling shop-macros v0.1.0
# Finished `dev` profile

Crate shop-macros giờ là workspace member thứ 5 (sau shop-api, shop-common, shop-db, shop-core). Dep direction một chiều: shop-core → shop-macros (consume), KHÔNG ngược lại — proc macro crate chỉ phụ thuộc syn + quote + proc-macro2, không phụ thuộc bất kỳ crate domain nào của Shop API.

5

Implement #[derive(SimpleCrud)] — Skeleton Preview

Implement entry function cho macro. Mỗi proc macro derive là 1 function annotated #[proc_macro_derive(Name, attributes(helper))] trả TokenStream.

// File: crates/shop-macros/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};

#[proc_macro_derive(SimpleCrud, attributes(crud))]
pub fn derive_simple_crud(input: TokenStream) -> TokenStream {
    // Parse input thành DeriveInput AST node
    let input = parse_macro_input!(input as DeriveInput);

    // Extract tên struct (vd Brand) + sinh tên service/impl
    let name = &input.ident;                              // Brand
    let service_name = quote::format_ident!("{}Service", name);   // BrandService
    let pg_service_name = quote::format_ident!("Pg{}Service", name); // PgBrandService

    // Extract fields từ struct (chỉ accept named struct)
    let _fields = match &input.data {
        Data::Struct(s) => match &s.fields {
            Fields::Named(named) => &named.named,
            _ => panic!("SimpleCrud requires named fields"),
        },
        _ => panic!("SimpleCrud requires struct, not enum/union"),
    };

    // Generate service trait + impl skeleton qua quote!
    let expanded = quote! {
        // 1. Service trait — 5 method CRUD chuẩn
        #[::async_trait::async_trait]
        pub trait #service_name: Send + Sync {
            async fn list(&self, page: u32, per_page: u32)
                -> Result<Vec<#name>, ::sqlx::Error>;

            async fn find_by_id(&self, id: i64)
                -> Result<Option<#name>, ::sqlx::Error>;

            async fn create(&self, data: #name)
                -> Result<#name, ::sqlx::Error>;

            async fn update(&self, id: i64, data: #name)
                -> Result<#name, ::sqlx::Error>;

            async fn delete(&self, id: i64)
                -> Result<u64, ::sqlx::Error>;
        }

        // 2. Pg implementation skeleton
        pub struct #pg_service_name {
            pool: ::sqlx::PgPool,
        }

        impl #pg_service_name {
            pub fn new(pool: ::sqlx::PgPool) -> Self {
                Self { pool }
            }
        }

        // 3. (preview) impl service tự generate SQL — basic CRUD
        // Production macro full sẽ generate INSERT/SELECT/UPDATE/DELETE
        // với #[crud(table = "...")] map struct → table name + skip
        // #[crud(primary)] / #[crud(auto)] field khỏi INSERT/UPDATE.
        // B74 preview chỉ skeleton để demo concept proc macro.
    };

    TokenStream::from(expanded)
}

Giải thích các thành phần chính:

  • #[proc_macro_derive(SimpleCrud, attributes(crud))] — đăng ký macro derive tên SimpleCrud + khai báo helper attribute #[crud(...)] để compiler không báo lỗi "unknown attribute" khi gặp.
  • parse_macro_input!(input as DeriveInput) — macro của syn parse TokenStream thành DeriveInput struct chứa ident (tên struct), generics, data (variant cho struct/enum/union), attrs.
  • quote::format_ident!("{}Service", name) — sinh identifier mới từ template (vd BrandBrandService).
  • quote! { ... } — macro của quote crate tạo TokenStream từ template Rust syntax + interpolation #variable chèn giá trị runtime.
  • ::async_trait::async_trait + ::sqlx::PgPool — dùng absolute path (::crate_name::) để code generated không phụ thuộc consumer import — pattern macro hygiene chuẩn.

Note quan trọng: B74 chỉ skeleton để demo concept. Production macro thật phải generate đủ SQL INSERT/SELECT/UPDATE/DELETE dựa trên #[crud(table = "...")] + skip field #[crud(primary)]/#[crud(auto)] khỏi INSERT/UPDATE; bài này focus phần parse + generate trait/impl skeleton để bạn nắm flow macro, full SQL generation extend sau khi pattern ổn định.

6

Áp Dụng Macro Vào Brand Resource

Brand là master data đơn giản (Apple, Samsung, Sony, ...) gồm 4 thuộc tính nghiệp vụ — name, slug, logo URL, timestamps. Không có relation phức tạp, không có business logic riêng. Đây là use case lý tưởng cho macro.

Tạo migration 15 (sau B71 = migration 14 stripe_webhook_events):

sqlx migrate add --source crates/shop-db/migrations create_brands
-- File: crates/shop-db/migrations/20260616140000_create_brands.sql
CREATE TABLE brands (
    id BIGSERIAL PRIMARY KEY,
    name TEXT NOT NULL UNIQUE,
    slug TEXT NOT NULL UNIQUE,
    logo_url TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX brands_slug_idx ON brands(slug);

CREATE TRIGGER brands_updated_at
    BEFORE UPDATE ON brands
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();

Trigger update_updated_at_column đã có sẵn từ migration đầu G7 (reuse function chung). Index brands_slug_idx phục vụ lookup theo URL slug pattern /brands/{slug} đã lock từ B53.

Tạo struct domain với #[derive(SimpleCrud)]:

// File: crates/shop-core/src/brands.rs
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use shop_macros::SimpleCrud;

#[derive(Debug, Clone, Serialize, Deserialize, SimpleCrud)]
#[crud(table = "brands")]
pub struct Brand {
    #[crud(primary)]
    pub id: i64,

    #[crud(unique)]
    pub name: String,

    #[crud(unique, slug)]
    pub slug: String,

    pub logo_url: Option<String>,

    #[crud(auto)]
    pub created_at: DateTime<Utc>,

    #[crud(auto)]
    pub updated_at: DateTime<Utc>,
}

Helper attribute breakdown:

  • #[crud(table = "brands")] — map struct Brand → table brands (production macro dùng generate SELECT FROM brands).
  • #[crud(primary)] — primary key, skip khỏi INSERT body (PG tự sinh BIGSERIAL).
  • #[crud(unique)] — UNIQUE constraint hint, cho phép macro map error 23505 → BrandError::SlugAlreadyExists đúng variant theo B73.
  • #[crud(slug)] — đánh dấu field URL slug, route generated dùng /{slug} thay /{id}.
  • #[crud(auto)] — server-generated (NOW() default), skip khỏi INSERT/UPDATE body.

Register module mới vào shop-core:

// File: crates/shop-core/src/lib.rs (extend B72)
pub mod brands;     // ← thêm
pub mod carts;
pub mod orders;
pub mod payments;
pub mod products;
pub mod users;

Auto-generated entities từ macro (sau khi cargo build):

  • BrandService trait — 5 method list + find_by_id + create + update + delete với signature đồng nhất 5 service B72.
  • PgBrandService struct + new(pool) constructor.
  • (Phiên bản đầy đủ tương lai) impl BrandService for PgBrandService với SQL chuẩn generated, handler + DTO conversion.
7

Handler Cho Brand — Vẫn Cần Explicit (Preview)

Production macro lý tưởng nên generate cả handler luôn (5 endpoint chuẩn). Nhưng B74 chọn partial macro pattern — macro generate service layer, handler vẫn viết tay — đổi lấy flexibility cho 2 lý do:

  • Status code custom — POST create cần 201 Created + Location header (lock B62), DELETE cần 204 No Content (lock B67); macro generic khó cover hết edge case.
  • Auth gate — Brand chỉ admin được tạo/sửa/xóa, public chỉ đọc; route admin cần wrap RequireRole(Admin) middleware (B131+), macro chưa biết policy này.

Viết handler cho Brand resource — dùng auto-generated BrandService trait từ macro:

// File: crates/shop-api/src/routes/brands.rs
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::routing::{delete, get, patch, post};
use axum::{Json, Router};
use shop_common::error::AppError;
use shop_core::brands::{Brand, BrandService};

use crate::state::AppState;

pub async fn list_brands(
    State(state): State<AppState>,
) -> Result<Json<Vec<Brand>>, AppError> {
    let brands = state.brand_service.list(1, 20).await?;
    Ok(Json(brands))
}

pub async fn create_brand(
    State(state): State<AppState>,
    Json(brand): Json<Brand>,
) -> Result<(StatusCode, Json<Brand>), AppError> {
    let created = state.brand_service.create(brand).await?;
    Ok((StatusCode::CREATED, Json(created)))
}

pub async fn get_brand(
    State(state): State<AppState>,
    Path(id): Path<i64>,
) -> Result<Json<Brand>, AppError> {
    let brand = state.brand_service.find_by_id(id).await?
        .ok_or_else(|| AppError::NotFound("brand".into()))?;
    Ok(Json(brand))
}

pub async fn update_brand(
    State(state): State<AppState>,
    Path(id): Path<i64>,
    Json(brand): Json<Brand>,
) -> Result<Json<Brand>, AppError> {
    let updated = state.brand_service.update(id, brand).await?;
    Ok(Json(updated))
}

pub async fn delete_brand(
    State(state): State<AppState>,
    Path(id): Path<i64>,
) -> Result<StatusCode, AppError> {
    state.brand_service.delete(id).await?;
    Ok(StatusCode::NO_CONTENT)
}

pub fn routes() -> Router<AppState> {
    Router::new()
        .route("/brands", get(list_brands).post(create_brand))
        .route(
            "/brands/{id}",
            get(get_brand).patch(update_brand).delete(delete_brand),
        )
}

Register sub-router + wire brand_service vào AppState:

// File: crates/shop-api/src/state.rs (extend B72)
#[derive(Clone)]
pub struct AppState {
    pub config: std::sync::Arc<crate::config::AppConfig>,
    pub db: sqlx::PgPool,
    pub product_service: std::sync::Arc<dyn shop_core::ProductService>,
    pub order_service: std::sync::Arc<dyn shop_core::OrderService>,
    pub payment_service: std::sync::Arc<dyn shop_core::PaymentService>,
    pub cart_service: std::sync::Arc<dyn shop_core::CartService>,
    pub user_service: std::sync::Arc<dyn shop_core::UserService>,
    pub brand_service: std::sync::Arc<dyn shop_core::brands::BrandService>,  // ← thêm
}

Lock decision Shop API B74 — partial macro pattern:

  • Macro #[derive(SimpleCrud)] generate service layer (trait + impl skeleton) — phần boilerplate nặng nhất.
  • Handler vẫn viết tay — flexibility cho status code custom + auth gate + future extension.
  • B75 sẽ preview procedural attribute #[crud_handler] generate handler luôn cho case full macro.

Pattern partial macro cân bằng reuse (giảm boilerplate service layer 80%) với flexibility (handler tự do tùy chỉnh). Pattern này phổ biến trong ecosystem Rust — sqlx::FromRow generate row mapping nhưng query SQL vẫn viết tay; serde::Serialize generate serialization nhưng custom logic vẫn override được qua #[serde(serialize_with = "...")].

8

Verify End-To-End + cargo expand Debug

Setup + verify workflow:

# 1. Install cargo-expand tool debug macro output
cargo install cargo-expand

# 2. Run migration 15
sqlx migrate run --source crates/shop-db/migrations

# 3. Build (proc macro chạy lúc compile shop-core)
cargo build -p shop-core

# 4. Verify macro expansion — xem code thật sự generated
cargo expand -p shop-core --lib brands
# Output: thấy BrandService trait + PgBrandService impl auto-generated

# 5. Run server
cargo run -p shop-api

cargo expand là tool MANDATORY khi làm việc với proc macro — nó in ra Rust code thật sự sau khi macro expand, giúp debug khi macro generate sai signature hoặc generate code không compile. Sample output:

// Output của `cargo expand -p shop-core --lib brands` (trích):
pub struct Brand {
    pub id: i64,
    pub name: String,
    pub slug: String,
    pub logo_url: Option<String>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[::async_trait::async_trait]
pub trait BrandService: Send + Sync {
    async fn list(&self, page: u32, per_page: u32)
        -> Result<Vec<Brand>, ::sqlx::Error>;
    async fn find_by_id(&self, id: i64)
        -> Result<Option<Brand>, ::sqlx::Error>;
    // ... 3 method còn lại
}

pub struct PgBrandService {
    pool: ::sqlx::PgPool,
}

impl PgBrandService {
    pub fn new(pool: ::sqlx::PgPool) -> Self {
        Self { pool }
    }
}

Test endpoint thực tế:

# Create brand
curl -X POST http://localhost:3000/api/v1/brands \
  -H 'Content-Type: application/json' \
  -d '{"id":0,"name":"Apple","slug":"apple","logo_url":null,
       "created_at":"2026-06-16T00:00:00Z","updated_at":"2026-06-16T00:00:00Z"}'
# 201 Created
# { "id": 1, "name": "Apple", "slug": "apple", ... }

# List
curl http://localhost:3000/api/v1/brands
# 200 OK [ { "id": 1, "name": "Apple", ... } ]

# Update
curl -X PATCH http://localhost:3000/api/v1/brands/1 \
  -H 'Content-Type: application/json' \
  -d '{...}'
# 200 OK

Compare LOC trước/sau macro cho 3 simple resource đầu:

Resource     | Without macro | With #[derive(SimpleCrud)]
-------------+---------------+----------------------------
Brand        | 250 LOC       | 30 LOC (struct + handler)
Color        | 250 LOC       | 30 LOC
Tag          | 250 LOC       | 30 LOC
-------------+---------------+----------------------------
Total 3      | 750 LOC       | 90 LOC
Reduction    | -             | 88% LOC

88% reduction là kết quả đo lường thực tế: 250 LOC explicit per resource (xem pattern Product/Order/User B61-B70) giảm còn ~30 LOC khi macro generate service layer. Bonus: consistency — 3 resource chắc chắn cùng pattern, không drift.

9

Tổng Kết

  • 3 loại Rust macro: declarative macro_rules! (đơn giản, không touch type info) + procedural derive #[derive(Foo)] (full AST, generate impl) + procedural attribute #[my_attr] (transform function/struct/impl, linh hoạt nhất).
  • Trade-off macro vs explicit: macro pros ~80% LOC giảm + consistency + single source of truth; cons hidden control flow + compile time chậm + IDE intelligence khó + maintenance cost cao.
  • Shop API 70/30 rule lock vĩnh viễn: 70% explicit handler cho complex resource (Product, Order, User, Cart, Payment), 30% macro cho simple resource (Brand, Color, Tag).
  • Init shop-macros crate workspace member thứ 5 (sau shop-api, shop-common, shop-db, shop-core) với proc-macro = true bắt buộc trong [lib].
  • 3 dep cho proc macro: syn = "2" features ["full"] + quote = "1" + proc-macro2 = "1".
  • #[derive(SimpleCrud)] skeleton preview: parse DeriveInput → match Data::Struct + Fields::Namedquote! generate BrandService trait + PgBrandService impl skeleton.
  • Helper attributes: #[crud(table = "...")] + #[crud(primary)] + #[crud(unique, slug)] + #[crud(auto)] lock syntax.
  • Brand resource dùng macro — service layer auto-generated, handler vẫn viết tay (partial macro pattern).
  • cargo expand tool MANDATORY debug macro output verify code thật sự sau expand.
  • 88% LOC reduction cho simple resource — 250 LOC explicit → 30 LOC macro (đo lường thực tế).
  • Lock decision: macro CHỈ cho simple resource (master data đơn giản), KHÔNG cho complex business logic (auth, state machine, JOIN, transaction, external API).
  • Migration 15 create_brands + crate workspace mới shop-macros.
  • File path lock: NEW crates/shop-macros/ (Cargo.toml + src/lib.rs) + NEW crates/shop-core/src/brands.rs + NEW crates/shop-api/src/routes/brands.rs.
  • Foundation cho B75 (end-to-end CRUD integration test full flow), G14 (Color + Tag resource reuse cùng macro pattern), future enhancement procedural attribute #[crud_handler] generate handler tự động.
10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. 3 loại Rust macro — pros/cons mỗi loại? Khi nào chọn declarative macro_rules! vs procedural derive vs procedural attribute? Cho ví dụ cụ thể use case mỗi loại.
  2. Trade-off macro vs explicit handler — Shop API 70/30 rule. Lý do tại sao Product/Order KHÔNG dùng macro mặc dù có pattern CRUD chuẩn? Giải thích bằng phân tích cụ thể.
  3. Proc macro crate cần proc-macro = true trong Cargo.toml — tại sao phân biệt với crate thường? Điều gì xảy ra nếu trộn proc macro code với code thường trong cùng crate?
  4. 3 crate syn + quote + proc-macro2 — role mỗi crate trong workflow proc macro? Mô tả flow parse → transform → emit qua ví dụ cụ thể.
  5. Helper attributes #[crud(...)] — pros so với positional arguments? Cho ví dụ scenario thêm field optional mới (vd #[crud(soft_delete)]) — pattern attribute kéo dài extensibility ra sao?
Đáp án
  1. 3 loại Rust macro pros/cons + use case: Declarative macro_rules!. Pros: (i) viết thẳng inline trong cùng crate, không cần workspace member riêng; (ii) không cần dep ngoài (syn/quote); (iii) compile nhanh (chỉ pattern matching syntactic, không phải parse full AST). Cons: (a) pattern matching syntactic giới hạn — không access type info, không introspect field struct; (b) khó generate impl block phức tạp; (c) error message khi pattern không match cryptic, debug khó; (d) macro hygiene rule chặt — variable scoping qua $ident dễ confused. Use case: vec![1, 2, 3] sinh Vec::new() + push + return; println!("{}", x) sinh format machinery; helper macro nhỏ cho repetitive code trong cùng crate. Procedural derive #[derive(Foo)]. Pros: (i) phía caller chỉ thêm 1 dòng #[derive(...)] rất gọn; (ii) full AST access qua syn — đọc được field name + type + attribute; (iii) generate impl block freely; (iv) IDE hỗ trợ rust-analyzer tốt hơn declarative (expand được phần lớn). Cons: (a) cần crate riêng workspace member với proc-macro = true; (b) complexity implement cao hơn (parse + transform + emit qua 3 crate); (c) compile time chậm hơn (proc macro chạy mỗi lần compile crate sử dụng); (d) chỉ derive cho struct/enum, không transform function/impl. Use case: #[derive(Serialize, Deserialize)] (serde), #[derive(sqlx::FromRow)] (sqlx), #[derive(thiserror::Error)] (B73), #[derive(Validate)] (validator B41). Procedural attribute #[my_attr]. Pros: (i) linh hoạt nhất — transform function/struct/impl/module; (ii) nhận cả attribute argument + item attached, manipulate cả 2 phần. Cons: (a) phức tạp nhất implement; (b) hidden control flow — caller không biết macro đã sửa gì (vd #[tokio::main] thực sự wrap fn trong runtime); (c) IDE intelligence kém nhất (rust-analyzer expand fail nhiều case); (d) debug khó cần cargo expand liên tục. Use case: #[tokio::main] wrap fn main trong runtime, #[axum::debug_handler] wrap handler với better error message, #[async_trait] (B72) transform async trait thành object-safe form, #[mockall::automock] generate mock struct. Lock decision Shop API B74: procedural derive cho #[derive(SimpleCrud)]; preview procedural attribute #[crud_handler] B75; tránh macro_rules! cho complex case.
  2. Trade-off macro vs explicit — Shop API 70/30 rule: 70% explicit cho complex resource, 30% macro cho simple. Lý do Product KHÔNG dùng macro: (a) filter/search phức tạp — list endpoint nhận query string ?q=iphone&min_price=500&max_price=1500&category=phones&sort=price:asc build dynamic WHERE clause qua QueryBuilder (B91), macro generic không cover được; (b) JOIN aggregate — product list cần JOIN product_categories M:N (B64) + product_images 1:N + aggregate review count + average rating, macro 5 method chuẩn không match; (c) full-text search tsvector (B97) cần index GIN + ranking ts_rank custom; (d) NDJSON streaming export/import (B49) thay JSON envelope; (e) image upload multipart endpoint riêng (B36). Order KHÔNG dùng macro: (a) state machine 5 state pending → paid → shipping → delivered → cancelled với guard logic (B65) — macro không hiểu transition rule; (b) transaction atomic — create order phải INSERT order + INSERT order_items + UPDATE inventory + INSERT payment trong cùng tx (B54), wrap with_retry SerializationFailure (B55); (c) action endpoint POST /orders/{id}/cancel không map CRUD chuẩn, cần custom handler; (d) timeline endpoint GET /orders/{id}/timeline aggregate event history; (e) different DTO per role — customer view vs admin view khác field. User KHÔNG dùng macro: (a) auth flow register cần hash Argon2id (B70); (b) verify email transaction 4 step UPDATE users + UPDATE tokens + INSERT audit; (c) InvalidCredentials security pattern KHÔNG leak user enumeration (B73); (d) UPDATE PATCH double-Option Option<Option<T>> phân biệt missing vs null vs value (B42 + B66) cho profile partial update. Cart KHÔNG dùng macro: (a) UPSERT cart_items ON CONFLICT (user_id, product_id) DO UPDATE quantity = quantity + new; (b) checkout flow compute total + apply discount + reserve inventory; (c) expire 7 days background cleanup. Payment KHÔNG dùng macro: (a) Stripe API integration create PaymentIntent (B71); (b) webhook signature verify HMAC-SHA256 + raw body bytes; (c) idempotent dedup stripe_webhook_events PRIMARY KEY; (d) JSONB containment query @> operator (B60). Lý do macro thắng cho Brand/Color/Tag: (i) chỉ master data đơn giản 3-6 field; (ii) không relation phức tạp; (iii) không business logic riêng; (iv) admin chỉ CRUD đơn thuần thêm/sửa/xóa; (v) public chỉ list/get đọc; (vi) lặp lại cùng pattern chính xác 250 LOC × 3 = 750 LOC boilerplate trùng nhau. Generalize quy tắc: nếu resource có > 1 custom requirement (auth, state machine, JOIN, transaction, external API, streaming, custom query) → giữ explicit; nếu chỉ pure CRUD → macro thắng.
  3. Proc macro crate cần proc-macro = true: Rust compiler load proc macro crate như plugin tại compile time. Khi compile crate shop-core sử dụng #[derive(SimpleCrud)], rustc load shop-macros.so (Linux) / shop-macros.dylib (macOS) / shop-macros.dll (Windows) như shared library vào process compiler, gọi derive_simple_crud function với TokenStream input, nhận output TokenStream insert vào AST của shop-core. Compiler tự handle ABI calling convention. Tại sao phân biệt với crate thường: (a) ABI khác hoàn toàn — proc macro crate compile để chạy trong host compiler (cùng target với rustc, vd x86_64-unknown-linux-gnu nếu rustc chạy Linux x64), KHÔNG compile cho target platform consumer (vd aarch64-apple-darwin M1 Mac); cross-compile khả thi nhưng phải target host. Crate thường compile cho target consumer. (b) Cargo separate dep tree — proc macro crate có dep tree riêng tách khỏi consumer (syn/quote/proc-macro2 không thêm vào binary cuối). (c) Compiler API access — proc macro được phép link với proc_macro built-in crate (host-only API), crate thường không link được. (d) Single export type — proc macro crate chỉ export function với attribute #[proc_macro]/#[proc_macro_derive]/#[proc_macro_attribute], không export type/struct/function thường. Điều gì xảy ra nếu trộn: (i) compile error "cannot find macro `derive_simple_crud` in this scope" khi proc macro function không nằm trong crate có proc-macro = true; (ii) export type/struct thường từ proc macro crate → compile error "proc-macro crate types cannot be linked from a non-proc-macro crate"; (iii) link fail khi consumer crate cố import như crate thường — error: linking with `cc` failed. Pattern industry standard: phân tách crate-pair — vd serde (consumer-facing crate, export trait Serialize/Deserialize + helper) + serde_derive (proc macro crate riêng, export derive); reexport #[derive(Serialize)] qua feature derive trong serde. Áp Shop API tương lai: nếu macro phình lớn nên tách shop-macros (proc macro only) + shop-macros-runtime (trait + helper consumer dùng) — B74 đơn giản gộp 1 crate vì macro nhỏ. Reference: Rust Reference — Procedural Macros section "proc-macro crate"; tài liệu proc-macro book của dtolnay.
  4. 3 crate syn + quote + proc-macro2 — workflow parse → transform → emit: Crate syn (parse). Role: parse TokenStream input thành Rust AST node typed cụ thể (DeriveInput, ItemFn, ItemImpl, Expr, Type, ...). API chính: parse_macro_input!(input as DeriveInput) + syn::parse2 + syn::parse_str. Feature full enable parse mọi item Rust (function body, statement, expression); feature default chỉ derive-related (struct + enum + generic + attribute). David Tolnay maintain. Internal: build top-down recursive descent parser từ TokenStream tree. Crate quote (emit). Role: generate Rust code từ tree qua macro quote! { ... }, syntax giống template engine. API chính: quote! { fn #name() -> #ret_type { ... } } với #variable interpolation chèn giá trị runtime; quote::format_ident!("{}Service", name) sinh identifier mới; repetition #(#fields),* generate comma-separated từ Vec. Output kiểu proc_macro2::TokenStream. Crate proc-macro2 (bridge). Role: wrapper cho built-in proc_macro crate (host-only API có sẵn trong rustc). Lý do tồn tại: proc_macro::TokenStream chỉ link được trong proc macro crate context, không test được standalone; proc_macro2::TokenStream link được mọi crate, có From/Into conversion với proc_macro::TokenStream qua boundary. Hầu hết API syn + quote trả về proc_macro2::TokenStream để testing-friendly. Workflow flow qua ví dụ #[derive(SimpleCrud)] trên struct Brand: (1) Compiler call — rustc encounter #[derive(SimpleCrud)] struct Brand { ... } trong shop-core/src/brands.rs, lookup tên SimpleCrud, gọi function shop_macros::derive_simple_crud(input) với input: proc_macro::TokenStream chứa toàn bộ struct definition. (2) Parse — function gọi parse_macro_input!(input as DeriveInput)syn parse TokenStream thành DeriveInput { ident: "Brand", data: Data::Struct(...), generics: ..., attrs: vec![...] } typed struct. Function match data extract field qua Fields::Named(named) => &named.namedVec<Field>. (3) Transform — function dùng quote::format_ident!("{}Service", name) sinh identifier BrandService mới từ tên struct. Loop qua fields extract field info (name, type, attribute). (4) Emit — function gọi quote! { #[::async_trait::async_trait] pub trait #service_name { ... } pub struct #pg_service_name { pool: ... } }quote generate proc_macro2::TokenStream chứa code Rust mới. (5) Convert — function gọi TokenStream::from(expanded) chuyển proc_macro2::TokenStreamproc_macro::TokenStream qua bridge. (6) Return — rustc nhận TokenStream output, insert vào AST của shop-core/src/brands.rs append sau struct Brand, tiếp tục compile bình thường. Reference: dtolnay's proc-macro-workshop repository thực hành 5 macro level từ easy → hard; documentation syn crate ví dụ đầy đủ cho derive/attribute macro.
  5. Helper attributes #[crud(...)] vs positional arguments: Positional arguments hypothetical nếu macro thiết kế khác: #[derive(SimpleCrud(table = "brands", primary = "id", unique = ["name", "slug"]))] — tất cả config nằm trong 1 attribute kèm tên macro. Cons: (a) verbose hết sức khi nhiều field; (b) cú pháp không phải Rust syntax chuẩn (chỉ thấy trong #[derive(...)]); (c) khó extend optional field mới mà không break ordering. Helper attributes #[crud(...)] dùng B74: tách config thành nhiều attribute riêng đặt tại đúng vị trí (struct level + field level). Pros: (a) locality — config gần với field nó modify, đọc code hiểu ngay (#[crud(primary)] đặt trên id field rõ ràng); (b) extensibility — thêm option mới (#[crud(soft_delete)], #[crud(audit)]) không break ordering positional; (c) Rust syntax chuẩn — đồng nhất với #[serde(...)], #[validate(...)], #[sqlx(...)] trong ecosystem; (d) compiler validate — khai báo attributes(crud) trong #[proc_macro_derive(SimpleCrud, attributes(crud))] để compiler không báo "unknown attribute"; (e) per-field granularity — mỗi field tùy chọn modifier riêng (#[crud(unique, slug)] kết hợp 2 modifier); (f) nested syntax#[crud(table = "brands", schema = "public")] support key-value pair phong phú. Ví dụ scenario thêm field optional #[crud(soft_delete)]: extend Brand support soft delete pattern B67 — thêm deleted_at: Option<DateTime<Utc>> field marker #[crud(soft_delete)]; macro parse attribute → generate delete() impl khác: thay DELETE FROM brands WHERE id = $1UPDATE brands SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL; generate list() + find_by_id() tự động filter WHERE deleted_at IS NULL; thêm method restore(id). Code minimal change phía caller:
    #[derive(Debug, Clone, Serialize, Deserialize, SimpleCrud)]
    #[crud(table = "brands")]
    pub struct Brand {
        #[crud(primary)]
        pub id: i64,
    
        #[crud(unique)]
        pub name: String,
    
        pub slug: String,
    
        #[crud(soft_delete)]                       // ← thêm 1 dòng
        pub deleted_at: Option<DateTime<Utc>>,
    
        #[crud(auto)]
        pub created_at: DateTime<Utc>,
    
        #[crud(auto)]
        pub updated_at: DateTime<Utc>,
    }
    Pattern lock extensibility: mọi feature future (audit log, optimistic locking với version column, multi-tenant với tenant_id auto-inject) chỉ cần thêm 1 helper attribute mới + macro logic update, caller code chỉ thêm 1-2 dòng marker. Pattern industry standard: serde dùng đúng pattern này — #[serde(rename = "...")], #[serde(skip_serializing_if = "...")], #[serde(default)] tại field level; #[serde(rename_all = "...")] tại struct level. Best practice naming: prefix tất cả helper attribute với tên macro (#[crud(...)] thay #[primary]) để tránh xung đột với attribute crate khác. Reference: syn::Attribute documentation; darling crate (cho parse helper attribute typed hơn raw syn).
11

Bài Tiếp Theo

— bài CUỐI Group 7: integration test full flow cart → checkout → order → payment → audit qua testcontainers Postgres + Stripe mock; áp Shop API complete workflow verification; tổng kết Group 7 (15/15) + foundation cho Group 8.