feature: add protobuf/grpc rule

This commit is contained in:
Tyr Chen
2025-05-31 14:33:18 -07:00
parent 606628b977
commit 1b2a3a4c02
3 changed files with 1350 additions and 0 deletions

View File

@@ -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<String> 要使用 `#[builder(default, setter(strip_option, into)]`.
8. 如果需要 web framework那么必须使用 axumaxum 必须构造 AppConfig / AppStateAppConfig 通过 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<FooInner> for Foo 的 trait。
4. prost/tonic 生成的数据结构要实现一个 MessageSanitizer trait:
```rust
pub trait MessageSanitizer {
type Output;
fn sanitize(self) -> Self::Output;
}
比如 type Foo,那么 Output FooInnersanitize 会处理各种 default 场景,比如 Option<Bar> 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<HelloRequest>,
) -> Result<Response<HelloReply>, 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<HelloReplyInner, Error> {
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<String> 要使用 `#[builder(default, setter(strip_option, into)]`. 不要滥用 default。