Mục lục
- Mục Tiêu Bài Học
- 3 Loại Macro Rust
- Trade-Off Macro Vs Explicit Handler
- Init Crate shop-macros (Proc Macro Crate)
- Implement #[derive(SimpleCrud)] — Skeleton Preview
- Áp Dụng Macro Vào Brand Resource
- Handler Cho Brand — Vẫn Cần Explicit (Preview)
- Verify End-To-End + cargo expand Debug
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
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-macroscrate (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
BrandServicetrait +PgBrandServiceimpl. - Hiểu maintenance cost macro vs explicit — Shop API 70/30 rule lock.
- Dùng
cargo expandtool debug macro output.
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.
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
Brandvớ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-analyzer2026 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.
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ừTokenStreaminput. Featurefullenable 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 macroquote! { ... }, syntax giống template engine với#variableinterpolation. Output kiểuTokenStream.proc-macro2— wrapper cho built-inproc_macrocrate (host-only) cho phép test proc macro ngoài compiler context. Hầu hết APIsyn+quotetrả về typeproc_macro2::TokenStreamchuyển sangproc_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.
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ênSimpleCrud+ 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ủasynparseTokenStreamthànhDeriveInputstruct chứaident(tên struct),generics,data(variant cho struct/enum/union),attrs.quote::format_ident!("{}Service", name)— sinh identifier mới từ template (vdBrand→BrandService).quote! { ... }— macro củaquotecrate tạoTokenStreamtừ template Rust syntax + interpolation#variablechè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.
Á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 structBrand→ tablebrands(production macro dùng generateSELECT 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):
BrandServicetrait — 5 methodlist+find_by_id+create+update+deletevới signature đồng nhất 5 service B72.PgBrandServicestruct +new(pool)constructor.- (Phiên bản đầy đủ tương lai) impl
BrandService for PgBrandServicevới SQL chuẩn generated, handler + DTO conversion.
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 = "...")].
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.
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-macroscrate workspace member thứ 5 (sau shop-api, shop-common, shop-db, shop-core) vớiproc-macro = truebắt buộc trong[lib]. - 3 dep cho proc macro:
syn = "2"features["full"]+quote = "1"+proc-macro2 = "1". #[derive(SimpleCrud)]skeleton preview: parseDeriveInput→ matchData::Struct+Fields::Named→quote!generateBrandServicetrait +PgBrandServiceimpl 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 expandtool 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ớishop-macros. - File path lock: NEW
crates/shop-macros/(Cargo.toml + src/lib.rs) + NEWcrates/shop-core/src/brands.rs+ NEWcrates/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.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- 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. - 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ể.
- Proc macro crate cần
proc-macro = truetrongCargo.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? - 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ể. - 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
- 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ó generateimplblock phức tạp; (c) error message khi pattern không match cryptic, debug khó; (d) macro hygiene rule chặt — variable scoping qua$identdễ confused. Use case:vec![1, 2, 3]sinhVec::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 quasyn— đọc được field name + type + attribute; (iii) generateimplblock 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ớiproc-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ầncargo expandliê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ánhmacro_rules!cho complex case. - 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:ascbuild dynamic WHERE clause quaQueryBuilder(B91), macro generic không cover được; (b) JOIN aggregate — product list cần JOINproduct_categoriesM:N (B64) +product_images1: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 endpointPOST /orders/{id}/cancelkhông map CRUD chuẩn, cần custom handler; (d) timeline endpointGET /orders/{id}/timelineaggregate 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-OptionOption<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. - Proc macro crate cần
proc-macro = true: Rust compiler load proc macro crate như plugin tại compile time. Khi compile crateshop-coresử dụng#[derive(SimpleCrud)],rustcloadshop-macros.so(Linux) /shop-macros.dylib(macOS) /shop-macros.dll(Windows) như shared library vào process compiler, gọiderive_simple_crudfunction vớiTokenStreaminput, nhận outputTokenStreaminsert vào AST củashop-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, vdx86_64-unknown-linux-gnunếu rustc chạy Linux x64), KHÔNG compile cho target platform consumer (vdaarch64-apple-darwinM1 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-macro2không thêm vào binary cuối). (c) Compiler API access — proc macro được phép link vớiproc_macrobuilt-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 — vdserde(consumer-facing crate, export trait Serialize/Deserialize + helper) +serde_derive(proc macro crate riêng, export derive); reexport#[derive(Serialize)]qua featurederivetrongserde. Áp Shop API tương lai: nếu macro phình lớn nên táchshop-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ệuproc-macrobook của dtolnay. - 3 crate
syn+quote+proc-macro2— workflow parse → transform → emit: Cratesyn(parse). Role: parseTokenStreaminput 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. Featurefullenable 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ừTokenStreamtree. Cratequote(emit). Role: generate Rust code từ tree qua macroquote! { ... }, syntax giống template engine. API chính:quote! { fn #name() -> #ret_type { ... } }với#variableinterpolation chèn giá trị runtime;quote::format_ident!("{}Service", name)sinh identifier mới; repetition#(#fields),*generate comma-separated từVec. Output kiểuproc_macro2::TokenStream. Crateproc-macro2(bridge). Role: wrapper cho built-inproc_macrocrate (host-only API có sẵn trong rustc). Lý do tồn tại:proc_macro::TokenStreamchỉ link được trong proc macro crate context, không test được standalone;proc_macro2::TokenStreamlink được mọi crate, cóFrom/Intoconversion vớiproc_macro::TokenStreamqua boundary. Hầu hết APIsyn+quotetrả vềproc_macro2::TokenStreamđể testing-friendly. Workflow flow qua ví dụ#[derive(SimpleCrud)]trên structBrand: (1) Compiler call — rustc encounter#[derive(SimpleCrud)] struct Brand { ... }trongshop-core/src/brands.rs, lookup tênSimpleCrud, gọi functionshop_macros::derive_simple_crud(input)vớiinput: proc_macro::TokenStreamchứa toàn bộ struct definition. (2) Parse — function gọiparse_macro_input!(input as DeriveInput)→synparseTokenStreamthànhDeriveInput { ident: "Brand", data: Data::Struct(...), generics: ..., attrs: vec![...] }typed struct. Function matchdataextract field quaFields::Named(named) => &named.named→Vec<Field>. (3) Transform — function dùngquote::format_ident!("{}Service", name)sinh identifierBrandServicemới từ tên struct. Loop quafieldsextract field info (name, type, attribute). (4) Emit — function gọiquote! { #[::async_trait::async_trait] pub trait #service_name { ... } pub struct #pg_service_name { pool: ... } }→quotegenerateproc_macro2::TokenStreamchứa code Rust mới. (5) Convert — function gọiTokenStream::from(expanded)chuyểnproc_macro2::TokenStream→proc_macro::TokenStreamqua bridge. (6) Return — rustc nhậnTokenStreamoutput, insert vào AST củashop-core/src/brands.rsappend sau structBrand, tiếp tục compile bình thường. Reference: dtolnay'sproc-macro-workshoprepository thực hành 5 macro level từ easy → hard; documentationsyncrate ví dụ đầy đủ cho derive/attribute macro. - 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ênidfield 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áoattributes(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)]: extendBrandsupport soft delete pattern B67 — thêmdeleted_at: Option<DateTime<Utc>>field marker#[crud(soft_delete)]; macro parse attribute → generatedelete()impl khác: thayDELETE FROM brands WHERE id = $1→UPDATE brands SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL; generatelist()+find_by_id()tự động filterWHERE deleted_at IS NULL; thêm methodrestore(id). Code minimal change phía caller:
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:#[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>, }serdedù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::Attributedocumentation;darlingcrate (cho parse helper attribute typed hơn rawsyn).
Bài Tiếp Theo
Bài 75: End-to-End CRUD Integration Test — 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.
