mirror of
https://github.com/tyrchen/cursor-rust-rules.git
synced 2025-12-23 01:30:00 +00:00
feature: improve rules
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,8 @@ alwaysApply: false
|
||||
## 🎯 FUNDAMENTAL PRINCIPLES
|
||||
|
||||
### Code Organization
|
||||
- **Functionality-based files**: Use meaningful file names like `node.rs`, `workflow.rs`, `execution.rs` instead of generic `models.rs`, `traits.rs`, `types.rs`
|
||||
- **Meaningful naming**: Avoid names like `WorkflowValidatorImpl` - use descriptive, specific names
|
||||
- **Functionality-based files**: Use meaningful file names like `user.rs`, `product.rs`, `auth.rs` instead of generic `models.rs`, `traits.rs`, `types.rs`
|
||||
- **Meaningful naming**: Avoid names like `UserServiceImpl` - use descriptive, specific names
|
||||
- **File size limits**: Maximum 500 lines per file (excluding tests)
|
||||
- **Function size**: Maximum 150 lines per function
|
||||
- **Single Responsibility**: Each module should have one clear purpose
|
||||
@@ -35,34 +35,135 @@ serde = { workspace = true, features = ["derive"] }
|
||||
# Request permission before modifying Cargo.toml
|
||||
```
|
||||
|
||||
### Standard Crate Recommendations
|
||||
When adding new dependencies, prefer these battle-tested crates:
|
||||
|
||||
```toml
|
||||
# Core utilities
|
||||
anyhow = "1.0" # Error handling
|
||||
thiserror = "2.0" # Error type definitions
|
||||
derive_more = { version = "2", features = ["full"] } # Extended derive macros
|
||||
typed-builder = "0.21" # Builder pattern
|
||||
|
||||
# Async/Concurrency
|
||||
tokio = { version = "1.45", features = [
|
||||
"macros",
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
"sync"
|
||||
] }
|
||||
async-trait = "0.1" # Async traits
|
||||
futures = "0.3" # Async utilities
|
||||
dashmap = { version = "6", features = ["serde"] } # Concurrent HashMap
|
||||
|
||||
# Serialization
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
base64 = "0.22"
|
||||
|
||||
# Web/HTTP
|
||||
axum = { version = "0.8", features = ["macros", "http2"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = [
|
||||
"charset",
|
||||
"rustls-tls-webpki-roots",
|
||||
"http2",
|
||||
"json",
|
||||
"cookies",
|
||||
"gzip",
|
||||
"brotli",
|
||||
"zstd",
|
||||
"deflate"
|
||||
] }
|
||||
tower = { version = "0.5", features = ["util"] }
|
||||
tower-http = { version = "0.6", features = ["cors", "trace"] }
|
||||
http = "1"
|
||||
|
||||
# Database
|
||||
sqlx = { version = "0.8", features = [
|
||||
"chrono",
|
||||
"postgres",
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"time",
|
||||
"uuid"
|
||||
] }
|
||||
|
||||
# Documentation/API
|
||||
utoipa = { version = "5", features = ["axum_extras"] }
|
||||
utoipa-axum = { version = "0.2" }
|
||||
utoipa-swagger-ui = { version = "9", features = [
|
||||
"axum",
|
||||
"vendored"
|
||||
], default-features = false }
|
||||
schemars = { version = "0.8", features = ["chrono", "url"] }
|
||||
|
||||
# Time/Date
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
time = { version = "0.3", features = ["serde"] }
|
||||
|
||||
# Templating/Text Processing
|
||||
minijinja = { version = "2", features = [
|
||||
"json",
|
||||
"loader",
|
||||
"loop_controls",
|
||||
"speedups"
|
||||
] }
|
||||
regex = "1"
|
||||
htmd = "0.2" # HTML to Markdown
|
||||
|
||||
# Authentication/Security
|
||||
jsonwebtoken = "9.0"
|
||||
uuid = { version = "1.17", features = ["v4", "serde"] }
|
||||
|
||||
# Data Processing
|
||||
jsonpath-rust = "1"
|
||||
url = "2.5"
|
||||
|
||||
# CLI (when needed)
|
||||
clap = { version = "4.0", features = ["derive"] }
|
||||
|
||||
# Utilities
|
||||
rand = "0.8"
|
||||
getrandom = "0.3"
|
||||
atomic_enum = "0.3" # Atomic enumerations
|
||||
|
||||
# Logging/Observability
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
```
|
||||
|
||||
### Version Strategy
|
||||
- **Always use latest versions** when adding new dependencies
|
||||
- **Request permission** before modifying `Cargo.toml`
|
||||
- **Check workspace first** - never duplicate dependencies unnecessarily
|
||||
- **Use specific feature flags** to minimize compilation time and binary size
|
||||
- **Prefer rustls over openssl** for TLS (better for cross-compilation)
|
||||
|
||||
## 🏗️ CODE STRUCTURE PATTERNS
|
||||
|
||||
### Data Structure Organization
|
||||
```rust
|
||||
// ✅ Good: Functionality-based organization
|
||||
// src/workflow.rs - All workflow-related types and logic
|
||||
// src/user.rs - All user-related types and logic
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")] // Always use camelCase for JSON serialization
|
||||
pub struct WorkflowDefinition {
|
||||
pub workflow_id: String,
|
||||
pub struct User {
|
||||
pub user_id: String,
|
||||
pub display_name: String,
|
||||
pub email: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// ✅ Good: Meaningful trait names
|
||||
pub trait WorkflowValidator {
|
||||
fn validate(&self, workflow: &WorkflowDefinition) -> Result<(), ValidationError>;
|
||||
pub trait UserValidator {
|
||||
fn validate(&self, user: &User) -> Result<(), ValidationError>;
|
||||
}
|
||||
|
||||
// ❌ Bad: Generic file organization
|
||||
// src/models.rs, src/traits.rs, src/types.rs
|
||||
// ❌ Bad: Poor naming
|
||||
// struct WorkflowValidatorImpl
|
||||
// struct UserValidatorImpl
|
||||
```
|
||||
|
||||
### Serde Configuration
|
||||
@@ -71,13 +172,13 @@ pub trait WorkflowValidator {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApiResponse {
|
||||
pub workflow_id: String,
|
||||
pub user_id: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
// This serializes to:
|
||||
// {"workflowId": "...", "createdAt": "...", "isActive": true}
|
||||
// {"userId": "...", "createdAt": "...", "isActive": true}
|
||||
```
|
||||
|
||||
## 🔧 BUILD AND QUALITY CHECKS
|
||||
@@ -115,30 +216,30 @@ expect_used = "deny"
|
||||
```rust
|
||||
// ✅ Good: Feature-based modules
|
||||
src/
|
||||
├── workflow/
|
||||
├── user/
|
||||
│ ├── mod.rs
|
||||
│ ├── validator.rs // WorkflowValidator trait and implementations
|
||||
│ ├── executor.rs // WorkflowExecutor logic
|
||||
│ └── definition.rs // WorkflowDefinition types
|
||||
├── node/
|
||||
│ ├── service.rs // UserService logic
|
||||
│ ├── repository.rs // User data access
|
||||
│ └── validator.rs // User validation
|
||||
├── product/
|
||||
│ ├── mod.rs
|
||||
│ ├── registry.rs // NodeRegistry (not NodeTypeRegistry)
|
||||
│ └── executor.rs // Node execution logic
|
||||
└── storage/
|
||||
│ ├── catalog.rs // Product catalog logic
|
||||
│ └── pricing.rs // Product pricing logic
|
||||
└── auth/
|
||||
├── mod.rs
|
||||
├── entities.rs // Database entities
|
||||
└── repositories.rs // Data access patterns
|
||||
├── token.rs // Token management
|
||||
└── session.rs // Session handling
|
||||
```
|
||||
|
||||
### Naming Best Practices
|
||||
```rust
|
||||
// ✅ Good naming examples
|
||||
pub struct WorkflowValidator; // Clear, specific
|
||||
pub struct NodeExecutor; // Action-oriented
|
||||
pub struct UserService; // Clear, specific
|
||||
pub struct ProductCatalog; // Action-oriented
|
||||
pub struct DatabaseConnection; // Descriptive
|
||||
|
||||
// ❌ Bad naming examples
|
||||
pub struct WorkflowValidatorImpl; // Unnecessary "Impl" suffix
|
||||
pub struct UserServiceImpl; // Unnecessary "Impl" suffix
|
||||
pub struct Helper; // Too generic
|
||||
pub struct Manager; // Vague responsibility
|
||||
```
|
||||
@@ -153,15 +254,15 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_workflow_validation() {
|
||||
let validator = WorkflowValidator::new();
|
||||
let workflow = WorkflowDefinition::default();
|
||||
assert!(validator.validate(&workflow).is_ok());
|
||||
fn test_user_validation() {
|
||||
let validator = UserValidator::new();
|
||||
let user = User::default();
|
||||
assert!(validator.validate(&user).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Don't create separate test files for unit tests
|
||||
// tests/workflow_test.rs (this is for integration tests only)
|
||||
// tests/user_test.rs (this is for integration tests only)
|
||||
```
|
||||
|
||||
### Test Naming
|
||||
@@ -171,12 +272,12 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_valid_workflow_passes_validation() {
|
||||
fn test_valid_email_passes_validation() {
|
||||
// Test name clearly describes the scenario
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_workflow_id_returns_error() {
|
||||
fn test_empty_email_returns_error() {
|
||||
// Specific about what's being tested
|
||||
}
|
||||
}
|
||||
@@ -186,26 +287,27 @@ mod tests {
|
||||
|
||||
### Code Documentation
|
||||
```rust
|
||||
/// Validates workflow definitions according to business rules.
|
||||
/// Validates user data according to business rules.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// let validator = WorkflowValidator::new();
|
||||
/// let workflow = WorkflowDefinition::builder()
|
||||
/// .workflow_id("test-workflow")
|
||||
/// let validator = UserValidator::new();
|
||||
/// let user = User::builder()
|
||||
/// .email("user@example.com")
|
||||
/// .display_name("John Doe")
|
||||
/// .build();
|
||||
///
|
||||
/// assert!(validator.validate(&workflow).is_ok());
|
||||
/// assert!(validator.validate(&user).is_ok());
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `ValidationError` if:
|
||||
/// - Workflow ID is empty or invalid
|
||||
/// - Email is empty or invalid format
|
||||
/// - Display name is too long
|
||||
/// - Required fields are missing
|
||||
/// - Business rules are violated
|
||||
pub struct WorkflowValidator {
|
||||
pub struct UserValidator {
|
||||
rules: Vec<ValidationRule>,
|
||||
}
|
||||
```
|
||||
@@ -220,7 +322,7 @@ pub struct WorkflowValidator {
|
||||
// src/helpers.rs - unclear responsibility
|
||||
|
||||
// ❌ Don't use implementation suffixes
|
||||
pub struct WorkflowValidatorImpl;
|
||||
pub struct UserValidatorImpl;
|
||||
pub struct DatabaseManagerImpl;
|
||||
|
||||
// ❌ Don't mix concerns in single files
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,15 @@ alwaysApply: false
|
||||
```toml
|
||||
# Cargo.toml - Tokio configuration
|
||||
[dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync", "time", "fs"] }
|
||||
tokio = { version = "1.45", features = [
|
||||
"macros",
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
"sync"
|
||||
] }
|
||||
dashmap = { version = "6", features = ["serde"] }
|
||||
async-trait = "0.1"
|
||||
futures = "0.3"
|
||||
```
|
||||
|
||||
## 🔒 SYNCHRONIZATION PRIMITIVES
|
||||
@@ -30,28 +38,28 @@ use tokio::sync::{RwLock, Mutex, broadcast, mpsc, oneshot};
|
||||
use std::sync::Arc;
|
||||
|
||||
// ✅ Good: Async-friendly RwLock
|
||||
pub struct WorkflowCache {
|
||||
data: Arc<RwLock<HashMap<String, WorkflowDefinition>>>,
|
||||
pub struct UserCache {
|
||||
data: Arc<RwLock<HashMap<String, User>>>,
|
||||
}
|
||||
|
||||
impl WorkflowCache {
|
||||
impl UserCache {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(&self, id: &str) -> Option<WorkflowDefinition> {
|
||||
pub async fn get(&self, id: &str) -> Option<User> {
|
||||
let data = self.data.read().await;
|
||||
data.get(id).cloned()
|
||||
}
|
||||
|
||||
pub async fn insert(&self, id: String, workflow: WorkflowDefinition) {
|
||||
pub async fn insert(&self, id: String, user: User) {
|
||||
let mut data = self.data.write().await;
|
||||
data.insert(id, workflow);
|
||||
data.insert(id, user);
|
||||
}
|
||||
|
||||
pub async fn remove(&self, id: &str) -> Option<WorkflowDefinition> {
|
||||
pub async fn remove(&self, id: &str) -> Option<User> {
|
||||
let mut data = self.data.write().await;
|
||||
data.remove(id)
|
||||
}
|
||||
@@ -68,24 +76,24 @@ use dashmap::DashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
// ✅ Preferred: DashMap for concurrent hash maps
|
||||
pub struct NodeRegistry {
|
||||
nodes: Arc<DashMap<String, Box<dyn NodeType>>>,
|
||||
pub struct ServiceRegistry {
|
||||
services: Arc<DashMap<String, Box<dyn Service>>>,
|
||||
categories: Arc<DashMap<String, Vec<String>>>,
|
||||
}
|
||||
|
||||
impl NodeRegistry {
|
||||
impl ServiceRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
nodes: Arc::new(DashMap::new()),
|
||||
services: Arc::new(DashMap::new()),
|
||||
categories: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_node(&self, id: String, node: Box<dyn NodeType>) {
|
||||
let category = node.category().to_string();
|
||||
pub fn register_service(&self, id: String, service: Box<dyn Service>) {
|
||||
let category = service.category().to_string();
|
||||
|
||||
// Insert the node
|
||||
self.nodes.insert(id.clone(), node);
|
||||
// Insert the service
|
||||
self.services.insert(id.clone(), service);
|
||||
|
||||
// Update category index
|
||||
self.categories
|
||||
@@ -94,8 +102,8 @@ impl NodeRegistry {
|
||||
.push(id);
|
||||
}
|
||||
|
||||
pub fn get_node(&self, id: &str) -> Option<dashmap::mapref::one::Ref<String, Box<dyn NodeType>>> {
|
||||
self.nodes.get(id)
|
||||
pub fn get_service(&self, id: &str) -> Option<dashmap::mapref::one::Ref<String, Box<dyn Service>>> {
|
||||
self.services.get(id)
|
||||
}
|
||||
|
||||
pub fn list_by_category(&self, category: &str) -> Vec<String> {
|
||||
@@ -105,14 +113,14 @@ impl NodeRegistry {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn list_all_nodes(&self) -> Vec<String> {
|
||||
self.nodes.iter().map(|entry| entry.key().clone()).collect()
|
||||
pub fn list_all_services(&self) -> Vec<String> {
|
||||
self.services.iter().map(|entry| entry.key().clone()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Avoid: Mutex<HashMap> for concurrent access
|
||||
// pub struct BadNodeRegistry {
|
||||
// nodes: Arc<Mutex<HashMap<String, Box<dyn NodeType>>>>
|
||||
// pub struct BadServiceRegistry {
|
||||
// services: Arc<Mutex<HashMap<String, Box<dyn Service>>>>
|
||||
// }
|
||||
```
|
||||
|
||||
@@ -124,7 +132,7 @@ use tokio::sync::mpsc;
|
||||
use tracing::{info, error};
|
||||
|
||||
pub struct EventProcessor {
|
||||
sender: mpsc::UnboundedSender<WorkflowEvent>,
|
||||
sender: mpsc::UnboundedSender<SystemEvent>,
|
||||
}
|
||||
|
||||
impl EventProcessor {
|
||||
@@ -137,17 +145,17 @@ impl EventProcessor {
|
||||
(processor, handle)
|
||||
}
|
||||
|
||||
pub fn send_event(&self, event: WorkflowEvent) -> Result<(), mpsc::error::SendError<WorkflowEvent>> {
|
||||
pub fn send_event(&self, event: SystemEvent) -> Result<(), mpsc::error::SendError<SystemEvent>> {
|
||||
self.sender.send(event)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventProcessorHandle {
|
||||
receiver: mpsc::UnboundedReceiver<WorkflowEvent>,
|
||||
receiver: mpsc::UnboundedReceiver<SystemEvent>,
|
||||
}
|
||||
|
||||
impl EventProcessorHandle {
|
||||
fn new(receiver: mpsc::UnboundedReceiver<WorkflowEvent>) -> Self {
|
||||
fn new(receiver: mpsc::UnboundedReceiver<SystemEvent>) -> Self {
|
||||
Self { receiver }
|
||||
}
|
||||
|
||||
@@ -160,19 +168,19 @@ impl EventProcessorHandle {
|
||||
info!("Event processor stopped");
|
||||
}
|
||||
|
||||
async fn process_event(&self, event: WorkflowEvent) -> Result<(), ProcessingError> {
|
||||
async fn process_event(&self, event: SystemEvent) -> Result<(), ProcessingError> {
|
||||
match event {
|
||||
WorkflowEvent::Started { workflow_id, .. } => {
|
||||
info!("Workflow {} started", workflow_id);
|
||||
// Process workflow start
|
||||
SystemEvent::UserRegistered { user_id, .. } => {
|
||||
info!("User {} registered", user_id);
|
||||
// Process user registration
|
||||
}
|
||||
WorkflowEvent::Completed { workflow_id, .. } => {
|
||||
info!("Workflow {} completed", workflow_id);
|
||||
// Process workflow completion
|
||||
SystemEvent::OrderCompleted { order_id, .. } => {
|
||||
info!("Order {} completed", order_id);
|
||||
// Process order completion
|
||||
}
|
||||
WorkflowEvent::Failed { workflow_id, error, .. } => {
|
||||
error!("Workflow {} failed: {}", workflow_id, error);
|
||||
// Process workflow failure
|
||||
SystemEvent::PaymentFailed { payment_id, error, .. } => {
|
||||
error!("Payment {} failed: {}", payment_id, error);
|
||||
// Process payment failure
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -210,11 +218,11 @@ pub async fn start_event_monitoring(event_bus: Arc<EventBus>) {
|
||||
tokio::spawn(async move {
|
||||
while let Ok(event) = receiver.recv().await {
|
||||
match event {
|
||||
SystemEvent::NodeExecutionStarted { node_id, .. } => {
|
||||
info!("Node {} started execution", node_id);
|
||||
SystemEvent::UserRegistered { user_id, .. } => {
|
||||
info!("User {} registered", user_id);
|
||||
}
|
||||
SystemEvent::NodeExecutionCompleted { node_id, .. } => {
|
||||
info!("Node {} completed execution", node_id);
|
||||
SystemEvent::OrderCompleted { order_id, .. } => {
|
||||
info!("Order {} completed", order_id);
|
||||
}
|
||||
SystemEvent::SystemShutdown => {
|
||||
info!("System shutdown requested");
|
||||
@@ -235,13 +243,13 @@ pub struct AsyncValidator {
|
||||
}
|
||||
|
||||
impl AsyncValidator {
|
||||
pub async fn validate_workflow(&self, workflow: WorkflowDefinition) -> Result<ValidationResult, ValidationError> {
|
||||
pub async fn validate_user(&self, user: User) -> Result<ValidationResult, ValidationError> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
// Spawn validation task
|
||||
let workflow_clone = workflow.clone();
|
||||
let user_clone = user.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = perform_validation(workflow_clone).await;
|
||||
let result = perform_validation(user_clone).await;
|
||||
let _ = tx.send(result);
|
||||
});
|
||||
|
||||
@@ -251,12 +259,12 @@ impl AsyncValidator {
|
||||
}
|
||||
}
|
||||
|
||||
async fn perform_validation(workflow: WorkflowDefinition) -> Result<ValidationResult, ValidationError> {
|
||||
async fn perform_validation(user: User) -> Result<ValidationResult, ValidationError> {
|
||||
// Expensive validation logic
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
|
||||
if workflow.nodes.is_empty() {
|
||||
return Err(ValidationError::EmptyWorkflow);
|
||||
if user.email.is_empty() {
|
||||
return Err(ValidationError::EmptyEmail);
|
||||
}
|
||||
|
||||
Ok(ValidationResult::Valid)
|
||||
@@ -270,24 +278,24 @@ async fn perform_validation(workflow: WorkflowDefinition) -> Result<ValidationRe
|
||||
use tokio::task::JoinSet;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct WorkflowExecutor {
|
||||
pub struct BatchProcessor {
|
||||
// Internal state
|
||||
}
|
||||
|
||||
impl WorkflowExecutor {
|
||||
pub async fn execute_workflow_parallel(&self, workflow: &WorkflowDefinition) -> Result<ExecutionResult, ExecutionError> {
|
||||
impl BatchProcessor {
|
||||
pub async fn process_batch_parallel(&self, items: &[ProcessingItem]) -> Result<BatchResult, ProcessingError> {
|
||||
let mut join_set = JoinSet::new();
|
||||
let mut results = HashMap::new();
|
||||
|
||||
// Execute nodes in parallel where possible
|
||||
for node in &workflow.nodes {
|
||||
if self.can_execute_parallel(node, &results) {
|
||||
let node_clone = node.clone();
|
||||
let executor = self.clone();
|
||||
// Process items in parallel where possible
|
||||
for item in items {
|
||||
if self.can_process_parallel(item, &results) {
|
||||
let item_clone = item.clone();
|
||||
let processor = self.clone();
|
||||
|
||||
join_set.spawn(async move {
|
||||
let result = executor.execute_node(&node_clone).await;
|
||||
(node_clone.id.clone(), result)
|
||||
let result = processor.process_item(&item_clone).await;
|
||||
(item_clone.id.clone(), result)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -295,21 +303,21 @@ impl WorkflowExecutor {
|
||||
// Collect results
|
||||
while let Some(result) = join_set.join_next().await {
|
||||
match result {
|
||||
Ok((node_id, execution_result)) => {
|
||||
results.insert(node_id, execution_result?);
|
||||
Ok((item_id, processing_result)) => {
|
||||
results.insert(item_id, processing_result?);
|
||||
}
|
||||
Err(join_error) => {
|
||||
return Err(ExecutionError::TaskFailed(join_error.to_string()));
|
||||
return Err(ProcessingError::TaskFailed(join_error.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExecutionResult { node_results: results })
|
||||
Ok(BatchResult { item_results: results })
|
||||
}
|
||||
|
||||
fn can_execute_parallel(&self, node: &NodeDefinition, completed_results: &HashMap<String, NodeResult>) -> bool {
|
||||
fn can_process_parallel(&self, item: &ProcessingItem, completed_results: &HashMap<String, ItemResult>) -> bool {
|
||||
// Check if all dependencies are satisfied
|
||||
node.dependencies.iter().all(|dep| completed_results.contains_key(dep))
|
||||
item.dependencies.iter().all(|dep| completed_results.contains_key(dep))
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -334,7 +342,7 @@ impl Application {
|
||||
|
||||
pub async fn start(&mut self) -> Result<(), ApplicationError> {
|
||||
// Start background services
|
||||
self.start_workflow_executor().await?;
|
||||
self.start_user_service().await?;
|
||||
self.start_event_processor().await?;
|
||||
self.start_health_monitor().await?;
|
||||
|
||||
@@ -345,18 +353,18 @@ impl Application {
|
||||
self.shutdown_gracefully().await
|
||||
}
|
||||
|
||||
async fn start_workflow_executor(&mut self) -> Result<(), ApplicationError> {
|
||||
async fn start_user_service(&mut self) -> Result<(), ApplicationError> {
|
||||
let token = self.shutdown_token.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = token.cancelled() => {
|
||||
info!("Workflow executor shutdown requested");
|
||||
info!("User service shutdown requested");
|
||||
break;
|
||||
}
|
||||
_ = tokio::time::sleep(tokio::time::Duration::from_secs(1)) => {
|
||||
// Process workflows
|
||||
// Process user operations
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -409,19 +417,19 @@ mod tests {
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_workflow_cache_concurrent_access() {
|
||||
let cache = WorkflowCache::new();
|
||||
let workflow = WorkflowDefinition::default();
|
||||
async fn test_user_cache_concurrent_access() {
|
||||
let cache = UserCache::new();
|
||||
let user = User::default();
|
||||
|
||||
// Test concurrent insertions
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for i in 0..10 {
|
||||
let cache_clone = cache.clone();
|
||||
let workflow_clone = workflow.clone();
|
||||
let user_clone = user.clone();
|
||||
|
||||
handles.push(tokio::spawn(async move {
|
||||
cache_clone.insert(format!("workflow_{}", i), workflow_clone).await;
|
||||
cache_clone.insert(format!("user_{}", i), user_clone).await;
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -430,9 +438,9 @@ mod tests {
|
||||
handle.await.unwrap();
|
||||
}
|
||||
|
||||
// Verify all workflows were inserted
|
||||
// Verify all users were inserted
|
||||
for i in 0..10 {
|
||||
let result = cache.get(&format!("workflow_{}", i)).await;
|
||||
let result = cache.get(&format!("user_{}", i)).await;
|
||||
assert!(result.is_some());
|
||||
}
|
||||
}
|
||||
@@ -445,8 +453,8 @@ mod tests {
|
||||
let processor_task = tokio::spawn(handle.run());
|
||||
|
||||
// Send test events
|
||||
let event = WorkflowEvent::Started {
|
||||
workflow_id: "test-workflow".to_string(),
|
||||
let event = SystemEvent::UserRegistered {
|
||||
user_id: "test-user".to_string(),
|
||||
timestamp: Utc::now(),
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,13 @@ alwaysApply: false
|
||||
```toml
|
||||
# Cargo.toml - SQLx configuration
|
||||
[dependencies]
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres", "sqlite", "uuid", "chrono", "json"] }
|
||||
sqlx = { version = "0.8", features = [
|
||||
"chrono",
|
||||
"postgres",
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"uuid"
|
||||
] }
|
||||
```
|
||||
|
||||
## 🔧 QUERY PATTERNS
|
||||
@@ -28,23 +34,23 @@ sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres", "sqli
|
||||
// ✅ Preferred: Use sqlx::query_as with custom types
|
||||
#[derive(Debug, Clone, sqlx::FromRow, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WorkflowExecution {
|
||||
pub struct User {
|
||||
pub id: Uuid,
|
||||
pub workflow_id: String,
|
||||
pub status: ExecutionStatus,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
impl WorkflowExecution {
|
||||
impl User {
|
||||
pub async fn find_by_id(
|
||||
pool: &PgPool,
|
||||
id: Uuid
|
||||
) -> Result<Option<Self>, sqlx::Error> {
|
||||
sqlx::query_as::<_, WorkflowExecution>(
|
||||
"SELECT id, workflow_id, status, created_at, updated_at, metadata
|
||||
FROM workflow_executions
|
||||
sqlx::query_as::<_, User>(
|
||||
"SELECT id, username, email, created_at, updated_at, is_active
|
||||
FROM users
|
||||
WHERE id = $1"
|
||||
)
|
||||
.bind(id)
|
||||
@@ -52,20 +58,18 @@ impl WorkflowExecution {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_by_workflow(
|
||||
pub async fn list_active_users(
|
||||
pool: &PgPool,
|
||||
workflow_id: &str,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> Result<Vec<Self>, sqlx::Error> {
|
||||
sqlx::query_as::<_, WorkflowExecution>(
|
||||
"SELECT id, workflow_id, status, created_at, updated_at, metadata
|
||||
FROM workflow_executions
|
||||
WHERE workflow_id = $1
|
||||
sqlx::query_as::<_, User>(
|
||||
"SELECT id, username, email, created_at, updated_at, is_active
|
||||
FROM users
|
||||
WHERE is_active = true
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3"
|
||||
LIMIT $1 OFFSET $2"
|
||||
)
|
||||
.bind(workflow_id)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(pool)
|
||||
@@ -88,28 +92,33 @@ use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct User {
|
||||
pub struct Product {
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub price: rust_decimal::Decimal,
|
||||
pub category_id: Uuid,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub is_active: bool,
|
||||
pub is_available: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateUserRequest {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub struct CreateProductRequest {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub price: rust_decimal::Decimal,
|
||||
pub category_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateUserRequest {
|
||||
pub username: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
pub struct UpdateProductRequest {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub price: Option<rust_decimal::Decimal>,
|
||||
pub is_available: Option<bool>,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -344,6 +353,37 @@ CREATE TRIGGER update_users_updated_at
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
```
|
||||
|
||||
### Product Table Example
|
||||
```sql
|
||||
-- migrations/20240501000002_create_products_table.sql
|
||||
CREATE TABLE categories (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE products (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
category_id UUID NOT NULL REFERENCES categories(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
is_available BOOLEAN NOT NULL DEFAULT true
|
||||
);
|
||||
|
||||
CREATE INDEX idx_products_category ON products(category_id);
|
||||
CREATE INDEX idx_products_price ON products(price);
|
||||
CREATE INDEX idx_products_name ON products(name);
|
||||
|
||||
CREATE TRIGGER update_products_updated_at
|
||||
BEFORE UPDATE ON products
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
```
|
||||
|
||||
## 🔧 CONNECTION MANAGEMENT
|
||||
|
||||
### Database Pool Configuration
|
||||
|
||||
225
.cursor/rules/rust/features/http-client.mdc
Normal file
225
.cursor/rules/rust/features/http-client.mdc
Normal file
@@ -0,0 +1,225 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# 🌐 HTTP CLIENT BEST PRACTICES
|
||||
|
||||
> **TL;DR:** Modern HTTP client patterns using reqwest with proper error handling, timeouts, and security configurations.
|
||||
|
||||
## 🔧 REQWEST CONFIGURATION
|
||||
|
||||
### Standard Dependencies
|
||||
```toml
|
||||
# Cargo.toml - HTTP client configuration
|
||||
[dependencies]
|
||||
reqwest = { version = "0.12", default-features = false, features = [
|
||||
"charset",
|
||||
"rustls-tls-webpki-roots",
|
||||
"http2",
|
||||
"json",
|
||||
"cookies",
|
||||
"gzip",
|
||||
"brotli",
|
||||
"zstd",
|
||||
"deflate"
|
||||
] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.45", features = ["macros", "rt-multi-thread"] }
|
||||
anyhow = "1.0"
|
||||
thiserror = "2.0"
|
||||
url = "2.5"
|
||||
```
|
||||
|
||||
## 🏗️ CLIENT BUILDER PATTERN
|
||||
|
||||
### Configurable HTTP Client
|
||||
```rust
|
||||
use reqwest::{Client, ClientBuilder, Response};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
pub struct HttpClient {
|
||||
client: Client,
|
||||
base_url: Url,
|
||||
default_timeout: Duration,
|
||||
}
|
||||
|
||||
impl HttpClient {
|
||||
pub fn builder() -> HttpClientBuilder {
|
||||
HttpClientBuilder::new()
|
||||
}
|
||||
|
||||
pub async fn get<T>(&self, path: &str) -> Result<T, HttpError>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let url = self.base_url.join(path)?;
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(url)
|
||||
.timeout(self.default_timeout)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
self.handle_response(response).await
|
||||
}
|
||||
|
||||
pub async fn post<T, B>(&self, path: &str, body: &B) -> Result<T, HttpError>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
B: Serialize,
|
||||
{
|
||||
let url = self.base_url.join(path)?;
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(url)
|
||||
.json(body)
|
||||
.timeout(self.default_timeout)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
self.handle_response(response).await
|
||||
}
|
||||
|
||||
async fn handle_response<T>(&self, response: Response) -> Result<T, HttpError>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
let status = response.status();
|
||||
|
||||
if status.is_success() {
|
||||
let text = response.text().await?;
|
||||
serde_json::from_str(&text).map_err(|e| HttpError::Deserialization {
|
||||
error: e.to_string(),
|
||||
body: text,
|
||||
})
|
||||
} else {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
Err(HttpError::UnexpectedStatus {
|
||||
status: status.as_u16(),
|
||||
body,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HttpClientBuilder {
|
||||
base_url: Option<String>,
|
||||
timeout: Option<Duration>,
|
||||
user_agent: Option<String>,
|
||||
headers: Vec<(String, String)>,
|
||||
accept_invalid_certs: bool,
|
||||
}
|
||||
|
||||
impl HttpClientBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base_url: None,
|
||||
timeout: Some(Duration::from_secs(30)),
|
||||
user_agent: Some("rust-http-client/1.0".to_string()),
|
||||
headers: Vec::new(),
|
||||
accept_invalid_certs: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn base_url(mut self, url: &str) -> Self {
|
||||
self.base_url = Some(url.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = Some(timeout);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<HttpClient, HttpError> {
|
||||
let base_url = self.base_url
|
||||
.ok_or_else(|| HttpError::Configuration("Base URL is required".to_string()))?;
|
||||
|
||||
let mut client_builder = ClientBuilder::new()
|
||||
.danger_accept_invalid_certs(self.accept_invalid_certs);
|
||||
|
||||
if let Some(timeout) = self.timeout {
|
||||
client_builder = client_builder.timeout(timeout);
|
||||
}
|
||||
|
||||
if let Some(user_agent) = &self.user_agent {
|
||||
client_builder = client_builder.user_agent(user_agent);
|
||||
}
|
||||
|
||||
let client = client_builder.build()?;
|
||||
let parsed_url = Url::parse(&base_url)?;
|
||||
|
||||
Ok(HttpClient {
|
||||
client,
|
||||
base_url: parsed_url,
|
||||
default_timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚨 ERROR HANDLING
|
||||
|
||||
### Comprehensive Error Types
|
||||
```rust
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum HttpError {
|
||||
#[error("HTTP request error: {0}")]
|
||||
Request(#[from] reqwest::Error),
|
||||
|
||||
#[error("URL parsing error: {0}")]
|
||||
UrlParse(#[from] url::ParseError),
|
||||
|
||||
#[error("JSON serialization error: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
#[error("Deserialization error: {error}, body: {body}")]
|
||||
Deserialization { error: String, body: String },
|
||||
|
||||
#[error("Unexpected HTTP status {status}: {body}")]
|
||||
UnexpectedStatus { status: u16, body: String },
|
||||
|
||||
#[error("Configuration error: {0}")]
|
||||
Configuration(String),
|
||||
|
||||
#[error("Timeout occurred")]
|
||||
Timeout,
|
||||
|
||||
#[error("Authentication failed")]
|
||||
Authentication,
|
||||
}
|
||||
|
||||
impl HttpError {
|
||||
pub fn is_retryable(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
HttpError::Timeout
|
||||
| HttpError::UnexpectedStatus { status: 502..=504, .. }
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ✅ HTTP CLIENT CHECKLIST
|
||||
|
||||
```markdown
|
||||
### HTTP Client Implementation Verification
|
||||
- [ ] Uses reqwest with rustls-tls (not native-tls)
|
||||
- [ ] Compression features enabled (gzip, brotli, deflate)
|
||||
- [ ] Proper timeout configuration
|
||||
- [ ] User-Agent header configured
|
||||
- [ ] Structured error handling with retryable errors
|
||||
- [ ] Authentication patterns implemented
|
||||
- [ ] Response type definitions with camelCase
|
||||
- [ ] Base URL configuration pattern
|
||||
- [ ] JSON serialization/deserialization
|
||||
- [ ] Proper status code handling
|
||||
```
|
||||
|
||||
This HTTP client standard ensures robust, secure, and maintainable HTTP communication in Rust applications.
|
||||
File diff suppressed because it is too large
Load Diff
705
.cursor/rules/rust/features/utilities.mdc
Normal file
705
.cursor/rules/rust/features/utilities.mdc
Normal file
@@ -0,0 +1,705 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# 🛠️ UTILITY LIBRARIES BEST PRACTICES
|
||||
|
||||
> **TL;DR:** Essential utility patterns for authentication, CLI tools, data structures, and common development tasks.
|
||||
|
||||
## 🔐 AUTHENTICATION AND SECURITY
|
||||
|
||||
### JWT with jsonwebtoken
|
||||
```toml
|
||||
# Cargo.toml - JWT configuration
|
||||
[dependencies]
|
||||
jsonwebtoken = "9.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
```
|
||||
|
||||
```rust
|
||||
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Claims {
|
||||
pub sub: String, // Subject (user ID)
|
||||
pub exp: i64, // Expiration time
|
||||
pub iat: i64, // Issued at
|
||||
pub user_role: String, // Custom claim
|
||||
pub session_id: String, // Session identifier
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TokenPair {
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
pub expires_in: i64,
|
||||
}
|
||||
|
||||
pub struct JwtService {
|
||||
encoding_key: EncodingKey,
|
||||
decoding_key: DecodingKey,
|
||||
access_token_expiry: i64, // seconds
|
||||
refresh_token_expiry: i64, // seconds
|
||||
}
|
||||
|
||||
impl JwtService {
|
||||
pub fn new(secret: &str) -> Self {
|
||||
Self {
|
||||
encoding_key: EncodingKey::from_secret(secret.as_bytes()),
|
||||
decoding_key: DecodingKey::from_secret(secret.as_bytes()),
|
||||
access_token_expiry: 3600, // 1 hour
|
||||
refresh_token_expiry: 604800, // 7 days
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_token_pair(&self, user_id: &str, role: &str) -> Result<TokenPair, JwtError> {
|
||||
let now = Utc::now().timestamp();
|
||||
let session_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
// Access token
|
||||
let access_claims = Claims {
|
||||
sub: user_id.to_string(),
|
||||
exp: now + self.access_token_expiry,
|
||||
iat: now,
|
||||
user_role: role.to_string(),
|
||||
session_id: session_id.clone(),
|
||||
};
|
||||
|
||||
let access_token = encode(&Header::default(), &access_claims, &self.encoding_key)?;
|
||||
|
||||
// Refresh token (longer expiry, minimal claims)
|
||||
let refresh_claims = Claims {
|
||||
sub: user_id.to_string(),
|
||||
exp: now + self.refresh_token_expiry,
|
||||
iat: now,
|
||||
user_role: "refresh".to_string(),
|
||||
session_id,
|
||||
};
|
||||
|
||||
let refresh_token = encode(&Header::default(), &refresh_claims, &self.encoding_key)?;
|
||||
|
||||
Ok(TokenPair {
|
||||
access_token,
|
||||
refresh_token,
|
||||
expires_in: self.access_token_expiry,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate_token(&self, token: &str) -> Result<Claims, JwtError> {
|
||||
let validation = Validation::new(Algorithm::HS256);
|
||||
let token_data = decode::<Claims>(token, &self.decoding_key, &validation)?;
|
||||
Ok(token_data.claims)
|
||||
}
|
||||
|
||||
pub fn refresh_access_token(&self, refresh_token: &str) -> Result<TokenPair, JwtError> {
|
||||
let claims = self.validate_token(refresh_token)?;
|
||||
|
||||
// Verify it's a refresh token
|
||||
if claims.user_role != "refresh" {
|
||||
return Err(JwtError::InvalidTokenType);
|
||||
}
|
||||
|
||||
// Generate new token pair
|
||||
self.generate_token_pair(&claims.sub, "user") // Default role, should be fetched from DB
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum JwtError {
|
||||
#[error("JWT encoding/decoding error: {0}")]
|
||||
Token(#[from] jsonwebtoken::errors::Error),
|
||||
#[error("Invalid token type")]
|
||||
InvalidTokenType,
|
||||
#[error("Token expired")]
|
||||
Expired,
|
||||
}
|
||||
```
|
||||
|
||||
## 🖥️ COMMAND LINE INTERFACES
|
||||
|
||||
### CLI with clap
|
||||
```toml
|
||||
# Cargo.toml - CLI configuration
|
||||
[dependencies]
|
||||
clap = { version = "4.0", features = ["derive"] }
|
||||
anyhow = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_yaml = "0.9"
|
||||
```
|
||||
|
||||
```rust
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "myapp")]
|
||||
#[command(about = "A comprehensive application with multiple commands")]
|
||||
#[command(version)]
|
||||
pub struct Cli {
|
||||
/// Global configuration file
|
||||
#[arg(short, long, value_name = "FILE")]
|
||||
pub config: Option<PathBuf>,
|
||||
|
||||
/// Verbose output
|
||||
#[arg(short, long, action = clap::ArgAction::Count)]
|
||||
pub verbose: u8,
|
||||
|
||||
/// Output format
|
||||
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
|
||||
pub format: OutputFormat,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// User management commands
|
||||
User {
|
||||
#[command(subcommand)]
|
||||
action: UserAction,
|
||||
},
|
||||
/// Server operations
|
||||
Server {
|
||||
#[command(subcommand)]
|
||||
action: ServerAction,
|
||||
},
|
||||
/// Database operations
|
||||
Database {
|
||||
#[command(subcommand)]
|
||||
action: DatabaseAction,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum UserAction {
|
||||
/// Create a new user
|
||||
Create {
|
||||
/// Username
|
||||
#[arg(short, long)]
|
||||
username: String,
|
||||
/// Email address
|
||||
#[arg(short, long)]
|
||||
email: String,
|
||||
/// User role
|
||||
#[arg(short, long, value_enum, default_value_t = UserRole::User)]
|
||||
role: UserRole,
|
||||
},
|
||||
/// List all users
|
||||
List {
|
||||
/// Maximum number of users to display
|
||||
#[arg(short, long, default_value_t = 50)]
|
||||
limit: usize,
|
||||
/// Filter by role
|
||||
#[arg(short, long)]
|
||||
role: Option<UserRole>,
|
||||
},
|
||||
/// Delete a user
|
||||
Delete {
|
||||
/// User ID or username
|
||||
#[arg(short, long)]
|
||||
identifier: String,
|
||||
/// Force deletion without confirmation
|
||||
#[arg(short, long)]
|
||||
force: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum ServerAction {
|
||||
/// Start the server
|
||||
Start {
|
||||
/// Port to bind to
|
||||
#[arg(short, long, default_value_t = 8080)]
|
||||
port: u16,
|
||||
/// Host to bind to
|
||||
#[arg(long, default_value = "127.0.0.1")]
|
||||
host: String,
|
||||
},
|
||||
/// Stop the server
|
||||
Stop,
|
||||
/// Show server status
|
||||
Status,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum DatabaseAction {
|
||||
/// Run database migrations
|
||||
Migrate {
|
||||
/// Migration direction
|
||||
#[arg(value_enum, default_value_t = MigrationDirection::Up)]
|
||||
direction: MigrationDirection,
|
||||
},
|
||||
/// Seed the database with test data
|
||||
Seed {
|
||||
/// Environment to seed
|
||||
#[arg(short, long, default_value = "development")]
|
||||
env: String,
|
||||
},
|
||||
/// Reset the database
|
||||
Reset {
|
||||
/// Skip confirmation prompt
|
||||
#[arg(short, long)]
|
||||
yes: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Clone)]
|
||||
pub enum OutputFormat {
|
||||
Text,
|
||||
Json,
|
||||
Yaml,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Clone)]
|
||||
pub enum UserRole {
|
||||
Admin,
|
||||
User,
|
||||
Guest,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Clone)]
|
||||
pub enum MigrationDirection {
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
// CLI execution logic
|
||||
pub async fn run_cli() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Initialize logging based on verbosity
|
||||
let log_level = match cli.verbose {
|
||||
0 => "warn",
|
||||
1 => "info",
|
||||
2 => "debug",
|
||||
_ => "trace",
|
||||
};
|
||||
|
||||
std::env::set_var("RUST_LOG", log_level);
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
match cli.command {
|
||||
Commands::User { action } => handle_user_command(action, cli.format).await,
|
||||
Commands::Server { action } => handle_server_command(action, cli.format).await,
|
||||
Commands::Database { action } => handle_database_command(action, cli.format).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_user_command(action: UserAction, format: OutputFormat) -> anyhow::Result<()> {
|
||||
match action {
|
||||
UserAction::Create { username, email, role } => {
|
||||
println!("Creating user: {} ({}) with role: {:?}", username, email, role);
|
||||
// Implementation
|
||||
}
|
||||
UserAction::List { limit, role } => {
|
||||
println!("Listing up to {} users", limit);
|
||||
if let Some(role) = role {
|
||||
println!("Filtering by role: {:?}", role);
|
||||
}
|
||||
// Implementation
|
||||
}
|
||||
UserAction::Delete { identifier, force } => {
|
||||
if !force {
|
||||
println!("Are you sure you want to delete user '{}'? [y/N]", identifier);
|
||||
// Confirmation logic
|
||||
}
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ BUILDER PATTERNS
|
||||
|
||||
### Typed Builder
|
||||
```toml
|
||||
# Cargo.toml - Builder configuration
|
||||
[dependencies]
|
||||
typed-builder = "0.21"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
```
|
||||
|
||||
```rust
|
||||
use typed_builder::TypedBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserConfig {
|
||||
/// Required: User's email address
|
||||
pub email: String,
|
||||
|
||||
/// Required: Username
|
||||
pub username: String,
|
||||
|
||||
/// Optional: Display name (defaults to username)
|
||||
#[builder(default = self.username.clone())]
|
||||
pub display_name: String,
|
||||
|
||||
/// Optional: User role
|
||||
#[builder(default = UserRole::User)]
|
||||
pub role: UserRole,
|
||||
|
||||
/// Optional: Whether user is active
|
||||
#[builder(default = true)]
|
||||
pub is_active: bool,
|
||||
|
||||
/// Optional: User preferences
|
||||
#[builder(default)]
|
||||
pub preferences: UserPreferences,
|
||||
|
||||
/// Optional: Profile image URL
|
||||
#[builder(default, setter(strip_option))]
|
||||
pub avatar_url: Option<String>,
|
||||
|
||||
/// Optional: User tags (for organization)
|
||||
#[builder(default)]
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserPreferences {
|
||||
#[builder(default = String::from("en"))]
|
||||
pub language: String,
|
||||
|
||||
#[builder(default = String::from("UTC"))]
|
||||
pub timezone: String,
|
||||
|
||||
#[builder(default = true)]
|
||||
pub email_notifications: bool,
|
||||
|
||||
#[builder(default = false)]
|
||||
pub dark_mode: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum UserRole {
|
||||
Admin,
|
||||
User,
|
||||
Guest,
|
||||
}
|
||||
|
||||
impl Default for UserRole {
|
||||
fn default() -> Self {
|
||||
Self::User
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UserPreferences {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
language: "en".to_string(),
|
||||
timezone: "UTC".to_string(),
|
||||
email_notifications: true,
|
||||
dark_mode: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage examples
|
||||
pub fn create_user_examples() {
|
||||
// Minimal required fields
|
||||
let user1 = UserConfig::builder()
|
||||
.email("john@example.com".to_string())
|
||||
.username("john_doe".to_string())
|
||||
.build();
|
||||
|
||||
// Full configuration
|
||||
let user2 = UserConfig::builder()
|
||||
.email("admin@example.com".to_string())
|
||||
.username("admin".to_string())
|
||||
.display_name("System Administrator".to_string())
|
||||
.role(UserRole::Admin)
|
||||
.is_active(true)
|
||||
.avatar_url("https://example.com/avatar.jpg".to_string())
|
||||
.tags(vec!["admin".to_string(), "system".to_string()])
|
||||
.preferences(
|
||||
UserPreferences::builder()
|
||||
.language("en".to_string())
|
||||
.timezone("America/New_York".to_string())
|
||||
.email_notifications(false)
|
||||
.dark_mode(true)
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
|
||||
println!("User 1: {:?}", user1);
|
||||
println!("User 2: {:?}", user2);
|
||||
}
|
||||
```
|
||||
|
||||
## 🧮 RANDOM GENERATION AND UTILITIES
|
||||
|
||||
### Random Data Generation
|
||||
```toml
|
||||
# Cargo.toml - Random utilities
|
||||
[dependencies]
|
||||
rand = "0.8"
|
||||
getrandom = "0.3"
|
||||
uuid = { version = "1.17", features = ["v4", "serde"] }
|
||||
base64 = "0.22"
|
||||
```
|
||||
|
||||
```rust
|
||||
use rand::{Rng, thread_rng, distributions::Alphanumeric};
|
||||
use uuid::Uuid;
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
|
||||
pub struct RandomGenerator;
|
||||
|
||||
impl RandomGenerator {
|
||||
/// Generate a secure random string for API keys, tokens, etc.
|
||||
pub fn secure_string(length: usize) -> String {
|
||||
thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(length)
|
||||
.map(char::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Generate a UUID v4
|
||||
pub fn uuid() -> String {
|
||||
Uuid::new_v4().to_string()
|
||||
}
|
||||
|
||||
/// Generate a short ID (URL-safe)
|
||||
pub fn short_id() -> String {
|
||||
let uuid_bytes = Uuid::new_v4().as_bytes();
|
||||
general_purpose::URL_SAFE_NO_PAD.encode(&uuid_bytes[..8])
|
||||
}
|
||||
|
||||
/// Generate a random integer within range
|
||||
pub fn int_range(min: i32, max: i32) -> i32 {
|
||||
thread_rng().gen_range(min..=max)
|
||||
}
|
||||
|
||||
/// Generate random bytes
|
||||
pub fn bytes(length: usize) -> Vec<u8> {
|
||||
let mut bytes = vec![0u8; length];
|
||||
getrandom::getrandom(&mut bytes).expect("Failed to generate random bytes");
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Generate a base64-encoded random string
|
||||
pub fn base64_string(byte_length: usize) -> String {
|
||||
let bytes = Self::bytes(byte_length);
|
||||
general_purpose::STANDARD.encode(&bytes)
|
||||
}
|
||||
|
||||
/// Generate a session ID
|
||||
pub fn session_id() -> String {
|
||||
format!("sess_{}", Self::secure_string(32))
|
||||
}
|
||||
|
||||
/// Generate a API key
|
||||
pub fn api_key() -> String {
|
||||
format!("ak_{}", Self::base64_string(24))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_secure_string_length() {
|
||||
let str32 = RandomGenerator::secure_string(32);
|
||||
assert_eq!(str32.len(), 32);
|
||||
|
||||
let str64 = RandomGenerator::secure_string(64);
|
||||
assert_eq!(str64.len(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uuid_format() {
|
||||
let uuid = RandomGenerator::uuid();
|
||||
assert!(Uuid::parse_str(&uuid).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_short_id_uniqueness() {
|
||||
let id1 = RandomGenerator::short_id();
|
||||
let id2 = RandomGenerator::short_id();
|
||||
assert_ne!(id1, id2);
|
||||
assert!(id1.len() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int_range() {
|
||||
for _ in 0..100 {
|
||||
let val = RandomGenerator::int_range(1, 10);
|
||||
assert!(val >= 1 && val <= 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 ENHANCED DERIVE MACROS
|
||||
|
||||
### Using derive_more
|
||||
```toml
|
||||
# Cargo.toml - Enhanced derives
|
||||
[dependencies]
|
||||
derive_more = { version = "2", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
```
|
||||
|
||||
```rust
|
||||
use derive_more::{Display, Error, From, Into, Constructor, Deref, DerefMut};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Custom string wrapper with validation
|
||||
#[derive(Debug, Clone, Display, From, Into, Deref, Serialize, Deserialize)]
|
||||
#[serde(try_from = "String")]
|
||||
pub struct EmailAddress(String);
|
||||
|
||||
impl TryFrom<String> for EmailAddress {
|
||||
type Error = ValidationError;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
if value.contains('@') && value.len() > 5 {
|
||||
Ok(EmailAddress(value))
|
||||
} else {
|
||||
Err(ValidationError::InvalidEmail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced error types
|
||||
#[derive(Debug, Display, Error)]
|
||||
pub enum ServiceError {
|
||||
#[display(fmt = "User not found: {}", user_id)]
|
||||
UserNotFound { user_id: String },
|
||||
|
||||
#[display(fmt = "Database error: {}", source)]
|
||||
Database {
|
||||
#[error(source)]
|
||||
source: sqlx::Error
|
||||
},
|
||||
|
||||
#[display(fmt = "Validation failed: {}", field)]
|
||||
Validation { field: String },
|
||||
|
||||
#[display(fmt = "Authentication failed")]
|
||||
Authentication,
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Error)]
|
||||
pub enum ValidationError {
|
||||
#[display(fmt = "Invalid email format")]
|
||||
InvalidEmail,
|
||||
|
||||
#[display(fmt = "Field '{}' is required", field)]
|
||||
Required { field: String },
|
||||
|
||||
#[display(fmt = "Value '{}' is too long (max: {})", value, max)]
|
||||
TooLong { value: String, max: usize },
|
||||
}
|
||||
|
||||
// Constructor patterns
|
||||
#[derive(Debug, Clone, Constructor, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserSession {
|
||||
pub user_id: String,
|
||||
pub session_id: String,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub expires_at: chrono::DateTime<chrono::Utc>,
|
||||
#[new(default)]
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
// Wrapper types with automatic conversions
|
||||
#[derive(Debug, Clone, From, Into, Deref, DerefMut, Serialize, Deserialize)]
|
||||
pub struct UserId(String);
|
||||
|
||||
#[derive(Debug, Clone, From, Into, Deref, DerefMut, Serialize, Deserialize)]
|
||||
pub struct SessionToken(String);
|
||||
|
||||
impl UserId {
|
||||
pub fn new() -> Self {
|
||||
Self(RandomGenerator::uuid())
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionToken {
|
||||
pub fn new() -> Self {
|
||||
Self(RandomGenerator::session_id())
|
||||
}
|
||||
}
|
||||
|
||||
// Usage examples
|
||||
pub fn demonstrate_enhanced_types() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Email validation
|
||||
let email = EmailAddress::try_from("user@example.com".to_string())?;
|
||||
println!("Valid email: {}", email);
|
||||
|
||||
// Constructor usage
|
||||
let session = UserSession::new(
|
||||
"user_123".to_string(),
|
||||
"sess_abc".to_string(),
|
||||
chrono::Utc::now(),
|
||||
chrono::Utc::now() + chrono::Duration::hours(24),
|
||||
);
|
||||
println!("Session: {:?}", session);
|
||||
|
||||
// Wrapper types
|
||||
let user_id = UserId::new();
|
||||
let token = SessionToken::new();
|
||||
println!("User ID: {}, Token: {}", *user_id, *token);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 🚨 UTILITIES ANTI-PATTERNS
|
||||
|
||||
### What to Avoid
|
||||
```rust
|
||||
// ❌ Don't use outdated JWT libraries
|
||||
// use frank_jwt; // Use jsonwebtoken instead
|
||||
|
||||
// ❌ Don't use structopt (deprecated)
|
||||
// use structopt::StructOpt; // Use clap with derive instead
|
||||
|
||||
// ❌ Don't manually implement builders
|
||||
// pub struct ConfigBuilder {
|
||||
// field1: Option<String>,
|
||||
// field2: Option<i32>,
|
||||
// } // Use typed-builder instead
|
||||
|
||||
// ❌ Don't use thread_rng() for cryptographic purposes
|
||||
// let password = thread_rng().gen::<u64>().to_string(); // Use getrandom for security
|
||||
|
||||
// ❌ Don't ignore JWT validation
|
||||
// let claims = decode::<Claims>(token, key, &Validation::default()); // Configure properly
|
||||
```
|
||||
|
||||
## ✅ UTILITIES CHECKLIST
|
||||
|
||||
```markdown
|
||||
### Utilities Implementation Verification
|
||||
- [ ] JWT authentication with proper validation and expiry
|
||||
- [ ] CLI with comprehensive subcommands and help text
|
||||
- [ ] Builder patterns using typed-builder
|
||||
- [ ] Enhanced error types with derive_more
|
||||
- [ ] Secure random generation for sensitive data
|
||||
- [ ] Proper validation for wrapper types
|
||||
- [ ] Constructor patterns for complex types
|
||||
- [ ] Base64 encoding for binary data
|
||||
- [ ] UUID generation for identifiers
|
||||
- [ ] Comprehensive error handling
|
||||
- [ ] Input validation and sanitization
|
||||
- [ ] Type safety with wrapper types
|
||||
```
|
||||
|
||||
This utilities standard provides robust patterns for common development tasks while maintaining type safety and security best practices.
|
||||
Reference in New Issue
Block a user