From 1b2a3a4c02649418673940e4f9935443746372db Mon Sep 17 00:00:00 2001 From: Tyr Chen Date: Sat, 31 May 2025 14:33:18 -0700 Subject: [PATCH] feature: add protobuf/grpc rule --- .cursor/rules/rust/features/protobuf-grpc.mdc | 1182 +++++++++++++++++ .cursor/rules/rust/main.mdc | 15 + specs/instructions.md | 153 +++ 3 files changed, 1350 insertions(+) create mode 100644 .cursor/rules/rust/features/protobuf-grpc.mdc diff --git a/.cursor/rules/rust/features/protobuf-grpc.mdc b/.cursor/rules/rust/features/protobuf-grpc.mdc new file mode 100644 index 0000000..3313599 --- /dev/null +++ b/.cursor/rules/rust/features/protobuf-grpc.mdc @@ -0,0 +1,1182 @@ +--- +description: +globs: +alwaysApply: false +--- +# 🛜 RUST PROTOBUF & GRPC STANDARDS + +> **TL;DR:** Modern protobuf and gRPC patterns using prost/tonic 0.13+ with clean code generation, Inner data structures, MessageSanitizer trait, gRPC reflection, and simplified service implementations. + +## 🎯 PROTOBUF & GRPC FRAMEWORK REQUIREMENTS + +### Prost/Tonic Configuration +- **Use prost/tonic latest versions** - Modern protobuf and gRPC implementation +- **Clean code generation** - Organized pb module structure with proper imports +- **Inner data structures** - Simplified, optional-free data structures for business logic +- **MessageSanitizer trait** - Consistent data transformation patterns +- **Simplified service methods** - Clean separation between gRPC and business logic + +## 📦 PROTOBUF & GRPC DEPENDENCIES + +```toml +# Cargo.toml - Protobuf & gRPC dependencies +[dependencies] +# Core protobuf and gRPC +prost = "0.13" +prost-types = "0.13" +tonic = { version = "0.13", features = ["gzip", "tls", "tls-roots", "compression"] } + +# Build-time dependencies +[build-dependencies] +prost-build = "0.13" +tonic-build = { version = "0.13", features = ["prost"] } + +# Data structures and serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +typed_builder = "0.18" + +# Error handling +anyhow = "1.0" +thiserror = "2.0" + +# Async runtime +tokio = { version = "1.45", features = ["macros", "rt-multi-thread", "signal"] } +tokio-stream = { version = "0.1", features = ["net"] } + +# Logging and tracing +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } + +# Optional: Additional features +tower = "0.4" # Middleware +tower-http = { version = "0.5", features = ["trace", "cors"] } +uuid = { version = "1.0", features = ["v4", "serde"] } +chrono = { version = "0.4", features = ["serde"] } + +[dev-dependencies] +tonic-health = "0.13" # Health check service +tonic-reflection = "0.13" # gRPC reflection service +``` + +## 🏗️ PROTOBUF & GRPC ARCHITECTURE + +```mermaid +graph TD + Proto["Proto Files"] --> Build["build.rs"] + Build --> Generate["Code Generation"] + Generate --> PbModule["src/pb/ Module"] + + PbModule --> Generated["Generated Structs
(Foo, Bar, etc.)"] + PbModule --> Services["Generated Services
(GreeterService, etc.)"] + + Generated --> Inner["Inner Structs
(FooInner, BarInner)"] + Generated --> Sanitizer["MessageSanitizer
Implementation"] + + Inner --> Business["Business Logic
Methods"] + Services --> Trait["Service Trait
Implementation"] + + Business --> Trait + Sanitizer --> Trait + + Trait --> Server["gRPC Server"] + + style Proto fill:#4da6ff,stroke:#0066cc,color:white + style Build fill:#4dbb5f,stroke:#36873f,color:white + style Inner fill:#ffa64d,stroke:#cc7a30,color:white + style Sanitizer fill:#d94dbb,stroke:#a3378a,color:white +``` + +## 🚀 BUILD CONFIGURATION + +### build.rs Setup + +```rust +// build.rs +use std::path::PathBuf; + +fn main() -> Result<(), Box> { + let pb_dir = PathBuf::from("src/pb"); + + // Ensure pb directory exists + if !pb_dir.exists() { + std::fs::create_dir_all(&pb_dir)?; + } + + // Configure tonic-build with prost_types + let mut tonic_build = tonic_build::configure() + .out_dir(&pb_dir) + .format(true) // Enable code formatting with prettyplease + .build_server(true) + .build_client(true) + .build_transport(true) // Include transport utilities + .emit_rerun_if_changed(false) // We handle this manually + // Use prost_types instead of compile_well_known_types + .extern_path(".google.protobuf.Timestamp", "::prost_types::Timestamp") + .extern_path(".google.protobuf.Duration", "::prost_types::Duration") + .extern_path(".google.protobuf.Empty", "::prost_types::Empty") + .extern_path(".google.protobuf.Any", "::prost_types::Any") + .extern_path(".google.protobuf.Struct", "::prost_types::Struct") + .extern_path(".google.protobuf.Value", "::prost_types::Value"); + + // Compile proto files + let proto_files = [ + "proto/greeting.proto", + "proto/user.proto", + "proto/common.proto", + ]; + + // Generate file descriptor set for reflection + tonic_build + .file_descriptor_set_path(&pb_dir.join("greeter_descriptor.bin")) + .compile(&proto_files, &["proto"])?; + + // Generate mod.rs file + generate_mod_file(&pb_dir)?; + + // Rename generated files for better organization + rename_generated_files(&pb_dir)?; + + // Emit rerun-if-changed directives + println!("cargo:rerun-if-changed=proto/"); + println!("cargo:rerun-if-changed=build.rs"); + + // Emit rerun-if-env-changed for protoc + println!("cargo:rerun-if-env-changed=PROTOC"); + println!("cargo:rerun-if-env-changed=PROTOC_INCLUDE"); + + Ok(()) +} + +fn generate_mod_file(pb_dir: &PathBuf) -> Result<(), Box> { + let mut mod_content = String::new(); + mod_content.push_str("// Auto-generated module file\n"); + mod_content.push_str("// DO NOT EDIT MANUALLY\n\n"); + + // Add file descriptor set for reflection + mod_content.push_str("/// File descriptor set for gRPC reflection\n"); + mod_content.push_str("pub const GREETER_FILE_DESCRIPTOR_SET: &[u8] = include_bytes!(\"greeter_descriptor.bin\");\n\n"); + + // Scan for generated .rs files + for entry in std::fs::read_dir(pb_dir)? { + let entry = entry?; + let path = entry.path(); + + if let Some(extension) = path.extension() { + if extension == "rs" { + if let Some(file_stem) = path.file_stem() { + let module_name = file_stem.to_string_lossy(); + if module_name != "mod" { + mod_content.push_str(&format!("pub mod {};\n", module_name)); + } + } + } + } + } + + // Write mod.rs file + let mod_file_path = pb_dir.join("mod.rs"); + std::fs::write(mod_file_path, mod_content)?; + + Ok(()) +} + +fn rename_generated_files(pb_dir: &PathBuf) -> Result<(), Box> { + // Rename files like "a.b.rs" to "b.rs" or "a/b.rs" based on naming conflicts + for entry in std::fs::read_dir(pb_dir)? { + let entry = entry?; + let path = entry.path(); + + if let Some(file_name) = path.file_name() { + let file_name_str = file_name.to_string_lossy(); + + // Check if file has package prefix (contains dots) + if file_name_str.contains('.') && file_name_str.ends_with(".rs") { + let parts: Vec<&str> = file_name_str + .strip_suffix(".rs") + .unwrap() + .split('.') + .collect(); + + if parts.len() > 1 { + // Use the last part as the new file name + let new_name = format!("{}.rs", parts.last().unwrap()); + let new_path = pb_dir.join(&new_name); + + // Check for conflicts + if !new_path.exists() { + std::fs::rename(&path, &new_path)?; + println!("Renamed {} to {}", file_name_str, new_name); + } else { + // Create subdirectory structure + let package_name = parts[0]; + let package_dir = pb_dir.join(package_name); + std::fs::create_dir_all(&package_dir)?; + + let new_path = package_dir.join(&new_name); + std::fs::rename(&path, &new_path)?; + println!("Moved {} to {}/{}", file_name_str, package_name, new_name); + } + } + } + } + } + + Ok(()) +} +``` + +### Project Structure + +``` +my-grpc-service/ +├── proto/ # Protocol buffer definitions +│ ├── common.proto # Common types and enums +│ ├── greeting.proto # Greeting service definition +│ └── user.proto # User service definition +├── src/ +│ ├── pb/ # Generated protobuf code +│ │ ├── mod.rs # Auto-generated module file +│ │ ├── common.rs # Generated from common.proto +│ │ ├── greeting.rs # Generated from greeting.proto +│ │ └── user.rs # Generated from user.proto +│ ├── inner/ # Inner data structures +│ │ ├── mod.rs +│ │ ├── common.rs # CommonInner types +│ │ ├── greeting.rs # GreetingInner types +│ │ └── user.rs # UserInner types +│ ├── services/ # Service implementations +│ │ ├── mod.rs +│ │ ├── greeting.rs # Greeting service impl +│ │ └── user.rs # User service impl +│ ├── sanitizers/ # MessageSanitizer implementations +│ │ ├── mod.rs +│ │ └── mod_common.rs +│ ├── lib.rs # Library root +│ └── main.rs # Server binary +├── build.rs # Build script +└── Cargo.toml +``` + +## 🏛️ CORE TRAITS AND PATTERNS + +### MessageSanitizer Trait + +```rust +// src/sanitizers/mod.rs +use crate::inner; + +/// Trait for sanitizing protobuf messages into clean Inner types +pub trait MessageSanitizer { + type Output; + + /// Convert protobuf message to clean Inner type with proper defaults + fn sanitize(self) -> Self::Output; +} + +// Use standard From trait instead of custom ToProtobuf trait + +// Common sanitization utilities for complex types only + +pub fn sanitize_timestamp(opt: Option) -> chrono::DateTime { + opt.map(|ts| { + chrono::DateTime::from_timestamp(ts.seconds, ts.nanos as u32) + .unwrap_or_default() + }) + .unwrap_or_else(chrono::Utc::now) +} +``` + +### Example Proto Definition + +```protobuf +// proto/greeting.proto +syntax = "proto3"; + +package greeting.v1; + +import "google/protobuf/timestamp.proto"; +import "common.proto"; + +// Greeting service definition +service GreeterService { + rpc SayHello(HelloRequest) returns (HelloReply); + rpc SayHelloStream(HelloRequest) returns (stream HelloReply); + rpc GetUserGreeting(UserGreetingRequest) returns (UserGreetingReply); +} + +// Request message for saying hello +message HelloRequest { + string name = 1; + optional string language = 2; + optional common.v1.UserContext user_context = 3; + optional google.protobuf.Timestamp request_time = 4; +} + +// Reply message for hello +message HelloReply { + string message = 1; + string language = 2; + optional google.protobuf.Timestamp reply_time = 3; + optional common.v1.ServerInfo server_info = 4; +} + +message UserGreetingRequest { + string user_id = 1; + optional string custom_message = 2; +} + +message UserGreetingReply { + string greeting = 1; + optional common.v1.UserProfile user_profile = 2; +} +``` + +```protobuf +// proto/common.proto +syntax = "proto3"; + +package common.v1; + +message UserContext { + string user_id = 1; + string session_id = 2; + repeated string roles = 3; +} + +message ServerInfo { + string version = 1; + string environment = 2; + string instance_id = 3; +} + +message UserProfile { + string id = 1; + string name = 2; + string email = 3; + bool is_active = 4; +} +``` + +## 📊 INNER DATA STRUCTURES + +### Inner Types Implementation + +```rust +// src/inner/common.rs +use serde::{Deserialize, Serialize}; +use typed_builder::TypedBuilder; +use uuid::Uuid; + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TypedBuilder)] +pub struct UserContextInner { + #[builder(default, setter(into))] + pub user_id: String, + #[builder(default, setter(into))] + pub session_id: String, + #[builder(default)] + pub roles: Vec, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TypedBuilder)] +pub struct ServerInfoInner { + #[builder(default = "1.0.0".to_string(), setter(into))] + pub version: String, + #[builder(default = "development".to_string(), setter(into))] + pub environment: String, + #[builder(default_code = "Uuid::new_v4().to_string()", setter(into))] + pub instance_id: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TypedBuilder)] +pub struct UserProfileInner { + #[builder(default, setter(into))] + pub id: String, + #[builder(default, setter(into))] + pub name: String, + #[builder(default, setter(into))] + pub email: String, + #[builder(default = true)] + pub is_active: bool, +} +``` + +```rust +// src/inner/greeting.rs +use serde::{Deserialize, Serialize}; +use typed_builder::TypedBuilder; +use chrono::{DateTime, Utc}; +use super::common::{UserContextInner, ServerInfoInner, UserProfileInner}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TypedBuilder)] +pub struct HelloRequestInner { + #[builder(setter(into))] + pub name: String, + #[builder(default = "en".to_string(), setter(into))] + pub language: String, + #[builder(default, setter(strip_option))] + pub user_context: Option, + #[builder(default_code = "Utc::now()")] + pub request_time: DateTime, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TypedBuilder)] +pub struct HelloReplyInner { + #[builder(default, setter(into))] + pub message: String, + #[builder(default = "en".to_string(), setter(into))] + pub language: String, + #[builder(default_code = "Utc::now()")] + pub reply_time: DateTime, + #[builder(default, setter(strip_option))] + pub server_info: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TypedBuilder)] +pub struct UserGreetingRequestInner { + #[builder(default, setter(into))] + pub user_id: String, + #[builder(default, setter(into))] + pub custom_message: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TypedBuilder)] +pub struct UserGreetingReplyInner { + #[builder(default, setter(into))] + pub greeting: String, + #[builder(default)] + pub user_profile: UserProfileInner, +} +``` + +## 🔄 MESSAGE SANITIZER IMPLEMENTATIONS + +```rust +// src/sanitizers/greeting.rs +use crate::{ + pb::greeting::*, + inner::{greeting::*, common::*}, + sanitizers::{MessageSanitizer, sanitize_timestamp} +}; +use chrono::{DateTime, Utc}; + +impl MessageSanitizer for HelloRequest { + type Output = HelloRequestInner; + + fn sanitize(self) -> Self::Output { + HelloRequestInner::builder() + .name(self.name) + .language(self.language.unwrap_or_default()) + .user_context(self.user_context.map(|ctx| ctx.sanitize())) + .request_time( + self.request_time + .map(|ts| { + DateTime::from_timestamp(ts.seconds, ts.nanos as u32) + .unwrap_or_else(Utc::now) + }) + .unwrap_or_else(Utc::now) + ) + .build() + } +} + +impl From for HelloRequest { + fn from(inner: HelloRequestInner) -> Self { + Self { + name: inner.name, + language: if inner.language.is_empty() { None } else { Some(inner.language) }, + user_context: inner.user_context.map(|ctx| ctx.into()), + request_time: Some(prost_types::Timestamp { + seconds: inner.request_time.timestamp(), + nanos: inner.request_time.timestamp_subsec_nanos() as i32, + }), + } + } +} + +impl MessageSanitizer for HelloReply { + type Output = HelloReplyInner; + + fn sanitize(self) -> Self::Output { + HelloReplyInner::builder() + .message(self.message) + .language(self.language) + .reply_time( + self.reply_time + .map(|ts| { + DateTime::from_timestamp(ts.seconds, ts.nanos as u32) + .unwrap_or_else(Utc::now) + }) + .unwrap_or_else(Utc::now) + ) + .server_info(self.server_info.map(|info| info.sanitize())) + .build() + } +} + +impl From for HelloReply { + fn from(inner: HelloReplyInner) -> Self { + Self { + message: inner.message, + language: inner.language, + reply_time: Some(prost_types::Timestamp { + seconds: inner.reply_time.timestamp(), + nanos: inner.reply_time.timestamp_subsec_nanos() as i32, + }), + server_info: inner.server_info.map(|info| info.into()), + } + } +} + +impl MessageSanitizer for UserGreetingRequest { + type Output = UserGreetingRequestInner; + + fn sanitize(self) -> Self::Output { + UserGreetingRequestInner::builder() + .user_id(self.user_id) + .custom_message(self.custom_message.unwrap_or_default()) + .build() + } +} + +impl From for UserGreetingRequest { + fn from(inner: UserGreetingRequestInner) -> Self { + Self { + user_id: inner.user_id, + custom_message: if inner.custom_message.is_empty() { + None + } else { + Some(inner.custom_message) + }, + } + } +} + +impl MessageSanitizer for UserGreetingReply { + type Output = UserGreetingReplyInner; + + fn sanitize(self) -> Self::Output { + UserGreetingReplyInner::builder() + .greeting(self.greeting) + .user_profile( + self.user_profile + .map(|profile| profile.sanitize()) + .unwrap_or_default() + ) + .build() + } +} + +impl From for UserGreetingReply { + fn from(inner: UserGreetingReplyInner) -> Self { + Self { + greeting: inner.greeting, + user_profile: Some(inner.user_profile.into()), + } + } +} +``` + +```rust +// src/sanitizers/common.rs +use crate::{ + pb::common::*, + inner::common::*, + sanitizers::MessageSanitizer +}; + +impl MessageSanitizer for UserContext { + type Output = UserContextInner; + + fn sanitize(self) -> Self::Output { + UserContextInner::builder() + .user_id(self.user_id) + .session_id(self.session_id) + .roles(self.roles) + .build() + } +} + +impl From for UserContext { + fn from(inner: UserContextInner) -> Self { + Self { + user_id: inner.user_id, + session_id: inner.session_id, + roles: inner.roles, + } + } +} + +impl MessageSanitizer for ServerInfo { + type Output = ServerInfoInner; + + fn sanitize(self) -> Self::Output { + ServerInfoInner::builder() + .version(self.version) + .environment(self.environment) + .instance_id(self.instance_id) + .build() + } +} + +impl From for ServerInfo { + fn from(inner: ServerInfoInner) -> Self { + Self { + version: inner.version, + environment: inner.environment, + instance_id: inner.instance_id, + } + } +} + +impl MessageSanitizer for UserProfile { + type Output = UserProfileInner; + + fn sanitize(self) -> Self::Output { + UserProfileInner::builder() + .id(self.id) + .name(self.name) + .email(self.email) + .is_active(self.is_active) + .build() + } +} + +impl From for UserProfile { + fn from(inner: UserProfileInner) -> Self { + Self { + id: inner.id, + name: inner.name, + email: inner.email, + is_active: inner.is_active, + } + } +} +``` + +## 🛠️ SERVICE IMPLEMENTATION PATTERN + +### Business Logic Implementation + +```rust +// src/services/greeting.rs +use anyhow::Result; +use tracing::{info, instrument}; +use crate::inner::{greeting::*, common::*}; + +pub struct GreeterService { + server_info: ServerInfoInner, +} + +impl GreeterService { + pub fn new() -> Self { + Self { + server_info: ServerInfoInner::builder() + .version(env!("CARGO_PKG_VERSION").to_string()) + .environment(std::env::var("ENVIRONMENT").unwrap_or_else(|_| "development".to_string())) + .build(), + } + } + + /// Business logic for saying hello - clean and testable + #[instrument(skip(self))] + pub async fn say_hello_inner( + &self, + request: HelloRequestInner, + ) -> Result { + info!("Processing hello request for user: {}", request.name); + + let greeting = match request.language.as_str() { + "es" => format!("¡Hola, {}!", request.name), + "fr" => format!("Bonjour, {}!", request.name), + "de" => format!("Hallo, {}!", request.name), + "ja" => format!("こんにちは、{}さん!", request.name), + _ => format!("Hello, {}!", request.name), + }; + + let reply = HelloReplyInner::builder() + .message(greeting) + .language(request.language.clone()) + .server_info(Some(self.server_info.clone())) + .build(); + + Ok(reply) + } + + /// Business logic for user-specific greeting + #[instrument(skip(self))] + pub async fn get_user_greeting_inner( + &self, + request: UserGreetingRequestInner, + ) -> Result { + info!("Processing user greeting request for user: {}", request.user_id); + + // Simulate user lookup (in real app, this would be a database call) + let user_profile = UserProfileInner::builder() + .id(request.user_id.clone()) + .name(format!("User_{}", request.user_id)) + .email(format!("user_{}@example.com", request.user_id)) + .is_active(true) + .build(); + + let greeting = if !request.custom_message.is_empty() { + format!("{}, {}!", request.custom_message, user_profile.name) + } else { + format!("Welcome back, {}!", user_profile.name) + }; + + let reply = UserGreetingReplyInner::builder() + .greeting(greeting) + .user_profile(user_profile) + .build(); + + Ok(reply) + } +} + +impl Default for GreeterService { + fn default() -> Self { + Self::new() + } +} +``` + +### gRPC Service Trait Implementation + +```rust +// src/services/greeting.rs (continued) +use tonic::{Request, Response, Status, Code}; +use tokio_stream::{wrappers::ReceiverStream, StreamExt}; +use crate::{ + pb::greeting::{ + greeter_service_server::GreeterService as GreeterServiceTrait, + HelloRequest, HelloReply, UserGreetingRequest, UserGreetingReply + }, + sanitizers::{MessageSanitizer, ToProtobuf} +}; + +#[tonic::async_trait] +impl GreeterServiceTrait for GreeterService { + /// gRPC trait implementation - thin wrapper around business logic + async fn say_hello( + &self, + request: Request, + ) -> Result, Status> { + let remote_addr = request.remote_addr(); + let request_inner = request.into_inner().sanitize(); + + match self.say_hello_inner(request_inner).await { + Ok(reply_inner) => { + info!("Successful hello response to {:?}", remote_addr); + let reply = reply_inner.into(); + Ok(Response::new(reply)) + } + Err(e) => { + tracing::error!("Error processing hello request: {}", e); + Err(Status::new(Code::Internal, "Internal server error")) + } + } + } + + /// Streaming response example + type SayHelloStreamStream = ReceiverStream>; + + async fn say_hello_stream( + &self, + request: Request, + ) -> Result, Status> { + let request_inner = request.into_inner().sanitize(); + let (tx, rx) = tokio::sync::mpsc::channel(128); + + // Clone necessary data for the async task + let service = self.clone(); + + tokio::spawn(async move { + for i in 0..5 { + let mut req = request_inner.clone(); + req.name = format!("{} ({})", req.name, i + 1); + + match service.say_hello_inner(req).await { + Ok(reply_inner) => { + let reply = reply_inner.into(); + if tx.send(Ok(reply)).await.is_err() { + break; // Client disconnected + } + } + Err(e) => { + let _ = tx.send(Err(Status::internal(format!("Error: {}", e)))).await; + break; + } + } + + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + } + }); + + Ok(Response::new(ReceiverStream::new(rx))) + } + + async fn get_user_greeting( + &self, + request: Request, + ) -> Result, Status> { + let request_inner = request.into_inner().sanitize(); + + match self.get_user_greeting_inner(request_inner).await { + Ok(reply_inner) => { + let reply = reply_inner.into(); + Ok(Response::new(reply)) + } + Err(e) => { + tracing::error!("Error processing user greeting request: {}", e); + Err(Status::new(Code::Internal, "Internal server error")) + } + } + } +} + +// Make service cloneable for streaming +impl Clone for GreeterService { + fn clone(&self) -> Self { + Self { + server_info: self.server_info.clone(), + } + } +} +``` + +## 🏁 SERVER SETUP + +### Main Server Implementation + +```rust +// src/main.rs +use anyhow::Result; +use tonic::transport::Server; +use tonic_health::server::health_reporter; +use tonic_reflection::server::ServerReflectionServer; +use tower_http::trace::TraceLayer; +use tracing::{info, level_filters::LevelFilter}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + +mod pb; +mod inner; +mod sanitizers; +mod services; + +use pb::greeting::greeter_service_server::GreeterServiceServer; +use services::greeting::GreeterService; + +#[tokio::main] +async fn main() -> Result<()> { + // Initialize tracing with structured logging + tracing_subscriber::registry() + .with( + EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("info")), + ) + .with( + tracing_subscriber::fmt::layer() + .with_target(true) + .with_thread_ids(true) + .with_file(true) + .with_line_number(true) + ) + .init(); + + let addr = "127.0.0.1:50051".parse()?; + info!("Starting gRPC server on {}", addr); + + // Create health reporter + let (mut health_reporter, health_service) = health_reporter(); + + // Create reflection service for service discovery + let reflection_service = ServerReflectionServer::configure() + .register_encoded_file_descriptor_set(pb::GREETER_FILE_DESCRIPTOR_SET) + .build() + .unwrap(); + + // Set service as serving + health_reporter + .set_serving::>() + .await; + + // Create services + let greeter_service = GreeterService::new(); + + // Start server with enhanced configuration + Server::builder() + .layer(TraceLayer::new_for_grpc()) + .timeout(std::time::Duration::from_secs(30)) + .concurrency_limit_per_connection(256) + .tcp_keepalive(Some(std::time::Duration::from_secs(60))) + .add_service(health_service) + .add_service(reflection_service) + .add_service(GreeterServiceServer::new(greeter_service)) + .serve_with_shutdown(addr, shutdown_signal()) + .await?; + + Ok(()) +} + +async fn shutdown_signal() { + let ctrl_c = async { + tokio::signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } + + info!("Signal received, starting graceful shutdown"); +} +``` + +### Library Root + +```rust +// src/lib.rs +pub mod pb; +pub mod inner; +pub mod sanitizers; +pub mod services; + +// Re-export commonly used types +pub use inner::*; +pub use sanitizers::MessageSanitizer; +pub use services::*; + +// Health check endpoint for service discovery +pub async fn health_check() -> Result<(), Box> { + // Basic health check logic + Ok(()) +} +``` + +## 🧪 TESTING PATTERNS + +### Unit Tests for Business Logic + +```rust +// src/services/greeting.rs (test module) +#[cfg(test)] +mod tests { + use super::*; + use crate::inner::{greeting::*, common::*}; + + #[tokio::test] + async fn test_say_hello_english() { + let service = GreeterService::new(); + let request = HelloRequestInner::builder() + .name("World".to_string()) + .language("en".to_string()) + .build(); + + let result = service.say_hello_inner(request).await.unwrap(); + + assert_eq!(result.message, "Hello, World!"); + assert_eq!(result.language, "en"); + assert!(result.server_info.is_some()); + } + + #[tokio::test] + async fn test_say_hello_spanish() { + let service = GreeterService::new(); + let request = HelloRequestInner::builder() + .name("Mundo".to_string()) + .language("es".to_string()) + .build(); + + let result = service.say_hello_inner(request).await.unwrap(); + + assert_eq!(result.message, "¡Hola, Mundo!"); + assert_eq!(result.language, "es"); + } + + #[tokio::test] + async fn test_get_user_greeting_default() { + let service = GreeterService::new(); + let request = UserGreetingRequestInner::builder() + .user_id("123".to_string()) + .build(); + + let result = service.get_user_greeting_inner(request).await.unwrap(); + + assert_eq!(result.greeting, "Welcome back, User_123!"); + assert_eq!(result.user_profile.id, "123"); + assert_eq!(result.user_profile.email, "user_123@example.com"); + assert!(result.user_profile.is_active); + } + + #[tokio::test] + async fn test_get_user_greeting_custom() { + let service = GreeterService::new(); + let request = UserGreetingRequestInner::builder() + .user_id("456".to_string()) + .custom_message("Good morning".to_string()) + .build(); + + let result = service.get_user_greeting_inner(request).await.unwrap(); + + assert_eq!(result.greeting, "Good morning, User_456!"); + } +} +``` + +### Integration Tests for gRPC Service + +```rust +// tests/integration_test.rs +use anyhow::Result; +use tonic::Request; +use my_grpc_service::{ + pb::greeting::{ + greeter_service_server::GreeterServiceServer, + greeter_service_client::GreeterServiceClient, + HelloRequest + }, + services::greeting::GreeterService +}; + +#[tokio::test] +async fn test_grpc_say_hello() -> Result<()> { + // Start test server + let (client, _server) = setup_test_server().await?; + + // Make request + let request = Request::new(HelloRequest { + name: "Integration Test".to_string(), + language: Some("en".to_string()), + user_context: None, + request_time: Some(prost_types::Timestamp { + seconds: chrono::Utc::now().timestamp(), + nanos: 0, + }), + }); + + let response = client.say_hello(request).await?; + let reply = response.into_inner(); + + assert_eq!(reply.message, "Hello, Integration Test!"); + assert_eq!(reply.language, "en"); + + Ok(()) +} + +async fn setup_test_server() -> Result<(GreeterServiceClient, tokio::task::JoinHandle<()>)> { + use tonic::transport::{Server, Channel, Endpoint}; + use std::net::SocketAddr; + + let addr: SocketAddr = "127.0.0.1:0".parse()?; + let greeter_service = GreeterService::new(); + + let listener = tokio::net::TcpListener::bind(addr).await?; + let addr = listener.local_addr()?; + + let server_handle = tokio::spawn(async move { + Server::builder() + .timeout(std::time::Duration::from_secs(10)) + .add_service(GreeterServiceServer::new(greeter_service)) + .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener)) + .await + .unwrap(); + }); + + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + + // Create channel with connection configuration + let channel = Endpoint::from_shared(format!("http://{}", addr))? + .timeout(std::time::Duration::from_secs(5)) + .connect_timeout(std::time::Duration::from_secs(5)) + .connect() + .await?; + let client = GreeterServiceClient::new(channel); + + Ok((client, server_handle)) +} +``` + +## 📝 PROTOBUF & GRPC BEST PRACTICES CHECKLIST + +```markdown +## Protobuf & gRPC Implementation Verification + +### Code Generation +- [ ] Uses prost/tonic 0.13+ versions +- [ ] Generated code placed in src/pb/ directory +- [ ] build.rs includes format(true) with prettyplease +- [ ] File descriptor set generated for reflection +- [ ] Auto-generated mod.rs file references all modules +- [ ] Files renamed from package.name.rs to name.rs format +- [ ] No naming conflicts in generated files +- [ ] Proto3 optional fields properly configured + +### Inner Data Structures +- [ ] Inner structs created for all protobuf messages +- [ ] Inner structs use TypedBuilder for construction +- [ ] Inner structs include proper derives (Debug, Clone, Default, Serialize, Deserialize) +- [ ] Inner structs remove unnecessary Option wrappers +- [ ] Inner structs use appropriate default values + +### MessageSanitizer Implementation +- [ ] All protobuf messages implement MessageSanitizer trait +- [ ] Sanitization handles Option fields properly +- [ ] Default values provided for missing fields +- [ ] Timestamp conversion handled correctly +- [ ] ToProtobuf trait implemented for reverse conversion + +### Service Implementation +- [ ] Business logic methods use Inner types only +- [ ] Business logic methods are easily testable +- [ ] gRPC trait implementation is thin wrapper +- [ ] Error handling converts business errors to gRPC Status +- [ ] Streaming responses handled properly +- [ ] Request/response logging implemented + +### Project Structure +- [ ] Clear separation: pb/, inner/, sanitizers/, services/ +- [ ] Module organization follows domain boundaries +- [ ] build.rs handles code generation properly +- [ ] Proto files organized by service/domain +- [ ] Common types extracted to shared proto files + +### Testing +- [ ] Unit tests for business logic methods +- [ ] Integration tests for gRPC endpoints +- [ ] Tests use Inner types for simple construction +- [ ] Test server setup for integration testing +- [ ] Error cases covered in tests + +### Performance & Reliability +- [ ] Connection pooling for clients (if needed) +- [ ] Proper timeout and keepalive configuration +- [ ] Health check service implemented +- [ ] gRPC reflection service enabled +- [ ] Concurrency limits configured +- [ ] Graceful shutdown handling +- [ ] Structured tracing and logging configured +- [ ] Compression enabled (gzip/deflate/zstd) + +### Security +- [ ] Input validation in business logic +- [ ] Authentication/authorization patterns +- [ ] TLS configuration for production +- [ ] Rate limiting considerations +- [ ] Error messages don't leak sensitive data +``` + +This comprehensive protobuf and gRPC standard ensures clean, testable, and maintainable code following modern Rust patterns with prost/tonic. diff --git a/.cursor/rules/rust/main.mdc b/.cursor/rules/rust/main.mdc index a725d6c..60b17b0 100644 --- a/.cursor/rules/rust/main.mdc +++ b/.cursor/rules/rust/main.mdc @@ -35,12 +35,14 @@ graph TD Features --> Web{"Web Framework
needed?"} Features --> DB{"Database
access needed?"} Features --> CLI{"CLI interface
needed?"} + Features --> GRPC{"gRPC/Protobuf
needed?"} Features --> Concurrent{"Heavy
concurrency?"} Features --> Config{"Complex config
or templating?"} Web -->|Yes| WebRules["Load Axum Rules"] DB -->|Yes| DBRules["Load Database Rules"] CLI -->|Yes| CLIRules["Load CLI Rules"] + GRPC -->|Yes| GRPCRules["Load Protobuf & gRPC Rules"] Concurrent -->|Yes| ConcurrencyRules["Load Concurrency Rules"] Config -->|Yes| ToolsRules["Load Tools & Config Rules"] @@ -90,12 +92,14 @@ graph TD FeatureDetection --> WebFeature{"Web Framework?"} FeatureDetection --> DBFeature{"Database?"} FeatureDetection --> CLIFeature{"CLI Interface?"} + FeatureDetection --> GRPCFeature{"gRPC/Protobuf?"} FeatureDetection --> SerdeFeature{"Serialization?"} FeatureDetection --> BuilderFeature{"Complex Types?"} WebFeature -->|Yes| AxumRules["Axum Framework Rules"] DBFeature -->|Yes| SQLxRules["SQLx Database Rules"] CLIFeature -->|Yes| CLIRules["CLI Application Rules"] + GRPCFeature -->|Yes| GRPCRules["Protobuf & gRPC Rules"] SerdeFeature -->|Yes| SerdeRules["Serde Best Practices"] BuilderFeature -->|Yes| TypedBuilderRules["TypedBuilder Rules"] @@ -206,6 +210,14 @@ sequenceDiagram - [ ] Configuration file support - [ ] → Load CLI rules if YES to any +### Protobuf & gRPC Requirements +- [ ] Protocol buffers for data serialization +- [ ] gRPC service implementation needed +- [ ] Inter-service communication required +- [ ] Schema evolution support needed +- [ ] High-performance RPC required +- [ ] → Load Protobuf & gRPC rules if YES to any + ### HTTP Client Requirements - [ ] External API integration - [ ] HTTP requests needed @@ -230,6 +242,7 @@ Based on project analysis, load specific rule sets: # Web: core + axum + serde + utilities (JWT) # Database: core + sqlx + utilities (error handling) # CLI: core + cli + utilities (enum_dispatch + error handling) +# gRPC: core + protobuf-grpc + utilities (typed_builder + sanitization) # Auth: core + utilities (JWT + validation) ``` @@ -243,6 +256,7 @@ Based on project analysis, load specific rule sets: | **Web** | `features/axum.mdc` | Axum 0.8 patterns, OpenAPI with utoipa | | **Database** | `features/database.mdc` | SQLx patterns, repository design, testing | | **CLI** | `features/cli.mdc` | Clap 4.0+ patterns, subcommands, enum_dispatch | +| **Protobuf & gRPC** | `features/protobuf-grpc.mdc` | Prost/Tonic 0.13+, Inner types, MessageSanitizer, reflection | | **Concurrency** | `features/concurrency.mdc` | Tokio, DashMap, async patterns | | **Tools & Config** | `features/tools-and-config.mdc` | Tracing, YAML config, MiniJinja templates | | **Utilities** | `features/utilities.mdc` | JWT auth, CLI tools, builders, enhanced derives | @@ -261,6 +275,7 @@ Based on project analysis, load specific rule sets: ### Complex Project Examples - **Workflow engines** (multi-node processing systems) - **Multi-service applications** (auth + business + gateway) +- **Microservices with gRPC** (inter-service communication) - **Enterprise applications** (multiple domains) - **Database systems** with multiple engines - **Distributed systems** with event processing diff --git a/specs/instructions.md b/specs/instructions.md index c1b8bf8..146106d 100644 --- a/specs/instructions.md +++ b/specs/instructions.md @@ -1,5 +1,99 @@ # Instructions +请仔细阅读 @/isolation_rules ,根据它的 rule set 体系,比如在不同的场景或条件下加载不同的 rule,通过 mermaid chart 来指导如何使用 rules 等等。请根据类似的思路帮我处理和扩充以下规则,构建一套处理大型 rust 项目的 rule set: + +1. 如果项目规模不大,则使用单 crate;如果项目复杂,则拆分成多个 crate,使用 workspace 管理,每个 crate 都放入 workspace 的 dependency 中 +2. crate 内部要有合理的文件设置,以功能而非类型划分文件,比如: lib.rs, models.rs, handlers.rs,而非 lib.rs, types.rs, traits.rs, impl.rs。每个文件除去 unit test 的代码行数不超过 500 行,否则将其转换成目录,然后在目录下拆分成多个文件。 +3. 每个函数要遵循 DRY / SRP,函数大小不超过 150 行。 +4. 每个 crate 集中使用 errors.rs 定义错误。如果是 lib crate 则使用 thiserror,如果是 bin crate 则使用 anyhow。 +5. bin crate 要保持 main.rs 简洁,核心逻辑放在其他文件中,由 lib.rs 统一管理。 +6. 如果使用 serde,那么遵循 serde best practice,并且使用 serde 的数据结构要 rename all CamelCase,以便生成的 json 适合前端。 +7. 对于复杂的数据结构如果要能够用 new 构造,那么如果 new 的参数复杂(>=4),请引入 typed_builder,对数据结构使用 TypedBuilder,并对每个字段根据情况引入 default, default_code, 以及 setter(strip_option), setter(into), 或者 setter(strip_option, into)。比如 Option 要使用 `#[builder(default, setter(strip_option, into)]`. +8. 如果需要 web framework,那么必须使用 axum,axum 必须构造 AppConfig / AppState,AppConfig 通过 arc_swap 放入 AppState 中。同时,API 和输入输出需要使用 utoipa,让 API 支持 openapi spec,并引入 utoipa swagger 支持 swagger endpoint。 +9. 如果使用 sqlx,那么写入数据库和读取的数据都需要定义合适的类型,并使用 FromRow。使用 sqlx::query_as,不要使用任何 query! 宏。sqlx 相关 unit test 代码使用 sqlx-db-tester。 +10. 在并发场景下,遵循 Rust 并发处理最佳实践。如果是 primitive type,使用 AtomicXXX 类型,否则如果非频繁更新,可以考虑使用 arc_swap,否则如果可以使用 dashmap,则使用 dashmap,不能使用,则可以选择 tokio 下的 Mutex 或者 RwLock。 +11. unit test 必须写在和代码同一文件中,所有公开接口都需要足够正交的 unit test 来覆盖。 +整套规则写入 .cursor/rules/rust 目录,以 .cursor/rules/rust/main.mdc 为入口规则。所有规则都用 English. + +请仔细阅读已有的 @/rust rule set, +@instructions.md 是我的一个 real world rust application 里面使用的所有 prompt,里面包含了一些 rust 项目的 best practice,请抽取这些 best practice 并更新 rust rule sets。 + +刚才更新和生成的 rule 中的示例代码包含特定的系统(比如 workflow / node),请重新审视所有的 rules,确保 rules 是尽可能 general。 + +我这里有一个我经常使用的 crate 的列表,请加入或者更新到 rules 中(这些 deps 根据需要引入): + +```toml +anyhow = "1.0" +async-trait = "0.1" +atomic_enum = "0.3" +axum = { version = "0.8", features = ["macros", "http2"] } +base64 = "0.22" +chrono = { version = "0.4", features = ["serde"] } +clap = { version = "4.0", features = ["derive"] } +dashmap = { version = "6", features = ["serde"] } +derive_more = { version = "2", features = ["full"] } +futures = "0.3" +getrandom = "0.3" +htmd = "0.2" +http = "1" +jsonpath-rust = "1" +jsonwebtoken = "9.0" +minijinja = { version = "2", features = [ +"json", +"loader", +"loop_controls", +"speedups", +] } +rand = "0.8" +regex = "1" +reqwest = { version = "0.12", default-features = false, features = [ +"charset", +"rustls-tls-webpki-roots", +"http2", +"json", +"cookies", +"gzip", +"brotli", +"zstd", +"deflate", +] } +schemars = { version = "0.8", features = ["chrono", "url"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_yaml = "0.9" +sqlx = { version = "0.8", features = [ +"chrono", +"postgres", +"runtime-tokio-rustls", +"sqlite", +"time", +"uuid", +] } +thiserror = "2.0" +time = { version = "0.3", features = ["serde"] } +tokio = { version = "1.45", features = [ +"macros", +"rt-multi-thread", +"signal", +"sync", +] } +tower = { version = "0.5", features = ["util"] } +tower-http = { version = "0.6", features = ["cors", "trace"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +typed-builder = "0.21" +url = "2.5" +utoipa = { version = "5", features = ["axum_extras"] } +utoipa-axum = { version = "0.2" } +utoipa-swagger-ui = { version = "9", features = [ +"axum", +"vendored", +], default-features = false } +uuid = { version = "1.17", features = ["v4", "serde"] } +``` + +@workspace.mdc 里面 workspace 的例子不好,我们应该根据系统的各个子系统来划分 crate,请更新 example + 请构建 Rust CLI 项目的 rules: 1. 如果项目需要使用到 CLI,则引入 clap,使用 derive feature。 @@ -14,3 +108,62 @@ pub trait CommandExecutor { ``` 4. 其他请遵循 clap 最佳实践 + +现在请帮我构建 protobuf / grpc rules,并更新 @main.mdc。当项目需要构建 protobuf / grpc 应用时,需要使用 prost / tonic 最新版本。一些 best practice: + +1. prost / tonic 生成的代码放在 src/pb 中,注意添加 src/pb/mod.rs 引用所有生成的文件,然后在 lib.rs 中 `pub mod pb;` 进行引用。 +2. 使能 format feature,并且设置 format(true)。 +2. 如果生成的代码名为 src/pb/a.b.rs,请在 build.rs 中将其重命名(比如如果不存在重名风险,则 src/pb/b.rs 或者 src/pb/a/b.rs)。注意生成对应的 mod.rs +3. 为 prost/tonic 生成的每个数据结构提供对应的结构,比如 Foo,则生成 FooInner。相对于 Foo,它不包含不必要的 Option,并且包含 #[derive(Debug, Clone, Default, Serialize, Deserialize, TypedBuilder)] 等 attribute。它也实现了 From for Foo 的 trait。 +4. prost/tonic 生成的数据结构要实现一个 MessageSanitizer trait: + +```rust +pub trait MessageSanitizer { + type Output; + fn sanitize(self) -> Self::Output; +} +比如 type 是 Foo,那么 Output 是 FooInner,sanitize 会处理各种 default 场景,比如 Option default 是 None,我们转换时为 FooInner 提供 BarInner 的 default 值。 +5. 对于 grpc service,先为数据结构生成同名的,输入输出更简洁的方法,再在 grpc service trait 的实现中引用这些方法。比如: +```rust +// 不要这样实现 +#[tonic::async_trait] +impl Greeter for MyGreeter { + async fn say_hello( + &self, + request: Request, + ) -> Result, Status> { + println!("Got a request from {:?}", request.remote_addr()); + + let reply = hello_world::HelloReply { + message: format!("Hello {}!", request.into_inner().name), + }; + Ok(Response::new(reply)) + } +} +// 使用如下实现 +impl MyGreeter { +async pub fn say_hello( + &self, + request: HelloRequestInner, + ) -> Result { + println!("Got a request from {:?}", request.remote_addr()); + + let reply = HelloReplyInner { + message: format!("Hello {}!", request.into_inner().name), + }; + Ok(reply) + } +} +// 然后在 impl Greeter for MyGreeter 中调用这个方法(需要转换输入输出) +``` + +通过这种方法,unit test 可以更好地测试数据结构的方法,而避免复杂的输入输出的构建。 + +几处修改: + +1. 使用 From trait,不要额外定义 ToProtobuf。 +2. 在 build.rs 中使用 pb_dir,不要使用 out_dir。 +3. 使用 prost_types,不要使用compile_well_known_types(true) +4. 不要添加 protoc_arg +5. 对 primitive type 不需要 sanitize_otional_xxx。 +6. TypedBuilder 用法遵循:并对每个字段根据情况引入 default, default_code, 以及 setter(strip_option), setter(into), 或者 setter(strip_option, into)。比如 Option 要使用 `#[builder(default, setter(strip_option, into)]`. 不要滥用 default。