SystemPrompt is a world-class Rust programming brand. Every Rust file in extensions and MCP servers must be instantly recognizable as on-brand, world-class idiomatic Rust as Steve Klabnik would write it. No exceptions. No shortcuts. No compromise.
Checkable, actionable patterns. Run cargo clippy --workspace -- -D warnings and cargo fmt --all after changes.
0. Idiomatic Rust
Write code that would pass Steve Klabnik's review. Prefer iterator chains, combinators, and pattern matching over imperative control flow.
Option/Result Combinators
let name = request.name.as_deref().map(str::trim);
let value = opt.unwrap_or_else(|| compute_default());
let result = input.ok_or_else(|| Error::Missing)?;
Pattern Matching
match request.name.as_deref().map(str::trim) {
Some("") => return Err(ApiError::bad_request("Name cannot be empty")),
Some(name) => name.to_owned(),
None => generate_default(),
}
Iterator Chains
let valid_items: Vec<_> = items
.iter()
.filter(|item| item.is_active())
.map(|item| item.to_dto())
.collect();
Avoid
| Anti-Pattern | Idiomatic |
|---|---|
if let Some(x) = opt { x } else { default } |
opt.unwrap_or(default) |
match opt { Some(x) => Some(f(x)), None => None } |
opt.map(f) |
if condition { Some(x) } else { None } |
condition.then(|| x) |
Nested if let / match |
Combine with and_then, map, ok_or |
Manual loops building Vec |
Iterator chains with collect() |
match with guards when combinators suffice |
filter, map, and_then |
1. Limits
| Metric | Limit |
|---|---|
| Source file length | 300 lines |
| Cognitive complexity | 15 |
| Function length | 75 lines |
| Parameters | 5 |
2. Forbidden Constructs
| Construct | Resolution |
|---|---|
unsafe |
Remove - forbidden in this codebase |
unwrap() |
Use ?, ok_or_else(), or expect() with descriptive message |
panic!() / todo!() / unimplemented!() |
Return Result or implement |
Inline comments (//) |
ZERO TOLERANCE - delete all. Code documents itself through naming and structure |
Doc comments (///, //!) |
ZERO TOLERANCE - no API docs, no rustdoc. Only exception: rare //! module docs at file top when absolutely necessary |
| TODO/FIXME/HACK comments | Fix immediately or don't write |
Tests in source files (#[cfg(test)]) |
Move to separate test crate |
3. Mandatory Patterns
Typed Identifiers
All identifier fields use wrappers from systemprompt_identifiers:
// WRONG
pub struct Content { pub id: String, pub user_id: String }
// RIGHT
use systemprompt_identifiers::{ContentId, UserId};
pub struct Content { pub id: ContentId, pub user_id: UserId }
Available: SessionId, UserId, AgentId, TaskId, ContextId, TraceId, ClientId, AgentName, AiToolCallId, McpExecutionId, SkillId, SourceId, CategoryId, ContentId.
Logging
All logging via tracing. No println! in library code.
use tracing::{info, error, debug, warn};
// Structured fields (preferred over format strings)
tracing::info!(user_id = %user.id, "Created user");
tracing::error!(error = %e, "Operation failed");
// In handlers/services
tracing::info!(item_id = %id, "Item created successfully");
| Forbidden | Resolution |
|---|---|
println! in library code |
Use tracing::info!() |
| Format strings with interpolation | Use structured fields |
Repository Pattern
Services NEVER execute queries directly. All SQL in repositories using SQLX macros:
// Repository - uses sqlx::query_as!
pub async fn find_by_email(&self, email: &str) -> Result<Option<User>, sqlx::Error> {
sqlx::query_as!(User, "SELECT id, email, name FROM users WHERE email = $1", email)
.fetch_optional(&**self.pool)
.await
}
// Service - calls repository
let user = self.user_repository.find_by_email(email).await?;
SQLX Macros Only
| Allowed | Forbidden |
|---|---|
sqlx::query!() |
sqlx::query() |
sqlx::query_as!() |
sqlx::query_as() |
sqlx::query_scalar!() |
sqlx::query_scalar() |
The ! suffix enables compile-time verification. Zero tolerance for runtime query strings.
Repository Constructor
pub struct ContentRepository {
pool: Arc<PgPool>,
}
impl ContentRepository {
pub fn new(pool: Arc<PgPool>) -> Self {
Self { pool }
}
}
Error Handling
Use domain-specific errors with thiserror. anyhow only at application boundaries:
#[derive(Error, Debug)]
pub enum ServiceError {
#[error("Item not found: {0}")]
NotFound(String),
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
}
Log errors once at handling boundary, not at every propagation point.
DateTime
| Layer | Type |
|---|---|
| Rust | DateTime<Utc> |
| PostgreSQL | TIMESTAMPTZ |
Never use NaiveDateTime or TIMESTAMP. Never format as strings for DB operations.
Option
Only valid when absence is a meaningful domain state. Invalid uses:
- "I don't have it yet"
- Avoiding validation
- Default values that should be explicit
Fail Fast
Never return Ok for failed paths. Propagate errors immediately with ?.
Builder Pattern (MANDATORY for Complex Types)
Required for types with 3+ fields OR any type that mixes required and optional fields.
Structure:
pub struct CreateContentParams {
pub slug: String,
pub title: String,
pub body: String,
pub description: Option<String>,
pub image: Option<String>,
}
pub struct CreateContentParamsBuilder {
slug: String,
title: String,
body: String,
description: Option<String>,
image: Option<String>,
}
impl CreateContentParamsBuilder {
pub fn new(slug: impl Into<String>, title: impl Into<String>, body: impl Into<String>) -> Self {
Self {
slug: slug.into(),
title: title.into(),
body: body.into(),
description: None,
image: None,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_image(mut self, image: impl Into<String>) -> Self {
self.image = Some(image.into());
self
}
pub fn build(self) -> CreateContentParams {
CreateContentParams {
slug: self.slug,
title: self.title,
body: self.body,
description: self.description,
image: self.image,
}
}
}
impl CreateContentParams {
pub fn builder(slug: impl Into<String>, title: impl Into<String>, body: impl Into<String>) -> CreateContentParamsBuilder {
CreateContentParamsBuilder::new(slug, title, body)
}
}
Rules:
| Rule | Description |
|---|---|
Required fields in new() |
All non-optional fields MUST be constructor parameters |
Optional fields via with_*() |
Each optional field gets a with_* method |
build() returns owned type |
Builder is consumed, returns final struct |
No Default for complex types |
Explicit construction prevents invalid states |
Static builder() on target type |
Entry point: CreateContentParams::builder(...) |
4. Naming
Functions
| Prefix | Returns |
|---|---|
get_ |
Result<T> - fails if missing |
find_ |
Result<Option<T>> - may not exist |
list_ |
Result<Vec<T>> |
create_ |
Result<T> or Result<Id> |
update_ |
Result<T> or Result<()> |
delete_ |
Result<()> |
is_ / has_ |
bool |
Variables
| Type | Name |
|---|---|
| Database pool | pool |
| Repository | repo or {noun}_repo |
| Service | service or {noun}_service |
Abbreviations
Allowed: id, uuid, url, jwt, mcp, a2a, api, http, json, sql, ctx, req, res, msg, err, cfg
5. Anti-Patterns
| Pattern | Resolution |
|---|---|
| Raw string identifiers | Use typed identifiers |
| Magic numbers/strings | Use constants or enums |
| Direct SQL in services | Move to repository |
Option<Id> for required fields |
Use non-optional |
| Fuzzy strings / hardcoded fallbacks | Use typed constants, enums, or fail explicitly |
| Unused code / dead code | Delete immediately |
| Tech debt / TODO comments | Fix now or don't write it |
| Commented-out code | Delete - git has history |
6. Derive Ordering
When deriving traits, use this order:
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MyType { ... }
Order: Debug, Clone, Copy (if applicable), PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize
7. Extension-Specific Patterns
Schema Embedding
pub const SCHEMA_MY_TABLE: &str = include_str!("../schema/001_my_table.sql");
impl MyExtension {
pub fn schemas() -> Vec<(&'static str, &'static str)> {
vec![("my_table", SCHEMA_MY_TABLE)]
}
}
Job Registration
use systemprompt_traits::{Job, JobContext, JobResult, submit_job};
#[derive(Debug, Clone, Copy, Default)]
pub struct MyJob;
#[async_trait::async_trait]
impl Job for MyJob {
fn name(&self) -> &'static str { "my_job" }
fn description(&self) -> &'static str { "Does something" }
fn schedule(&self) -> &'static str { "0 0 * * * *" }
async fn execute(&self, ctx: &JobContext) -> anyhow::Result<JobResult> {
let pool = ctx.db_pool::<PgPool>()?;
Ok(JobResult::success())
}
}
submit_job!(&MyJob);
Router Factory
impl MyExtension {
pub fn router(&self, pool: Arc<PgPool>) -> Router {
let state = MyState { pool };
Router::new()
.route("/items", get(list_items).post(create_item))
.route("/items/:id", get(get_item).delete(delete_item))
.with_state(state)
}
}
Quick Reference
| Task | Command |
|---|---|
| Lint all | cargo clippy --workspace -- -D warnings |
| Format all | cargo fmt --all |
| Check format | cargo fmt --all -- --check |
| Build all | cargo build --workspace |
| Test all | cargo test --workspace |