Coding Standards
Rust coding standards for systemprompt.io - idiomatic patterns, mandatory requirements, and anti-patterns to avoid
On this page
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. No exceptions.
Core Principle
Write code that would pass Steve Klabnik's review. Prefer iterator chains, combinators, and pattern matching over imperative control flow.
Validation
Run these commands after every change:
cargo clippy --workspace -- -D warnings
cargo fmt --all
cargo test --workspace
Idiomatic Rust
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();
Anti-Patterns to 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) |
Manual loops building Vec |
Iterator chains with collect() |
File Limits
| Metric | Limit |
|---|---|
| Source file length | 300 lines |
| Cognitive complexity | 15 |
| Function length | 75 lines |
| Parameters | 5 |
Forbidden Constructs
| Construct | Resolution |
|---|---|
unsafe |
Remove - forbidden in this codebase |
unwrap() |
Use ?, ok_or_else(), or expect() with message |
panic!() / todo!() |
Return Result or implement |
Inline comments (//) |
Delete - code documents itself |
Doc comments (///) |
Delete - no rustdoc |
println! in libraries |
Use tracing |
| Raw SQL strings | Use SQLX macros |
Mandatory Patterns
Typed Identifiers
All identifier fields use wrappers from systemprompt_identifiers:
use systemprompt_identifiers::{ContentId, UserId};
pub struct Content {
pub id: ContentId,
pub user_id: UserId,
}
Available types: 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};
tracing::info!(user_id = %user.id, "Created user");
tracing::error!(error = %e, "Operation failed");
Repository Pattern
Services never execute queries directly. All SQL in repositories using SQLX macros:
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
}
SQLX Macros Only
| Allowed | Forbidden |
|---|---|
sqlx::query!() |
sqlx::query() |
sqlx::query_as!() |
sqlx::query_as() |
sqlx::query_scalar!() |
sqlx::query_scalar() |
Error Handling
Use thiserror for domain-specific errors. 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),
}
DateTime
| Layer | Type |
|---|---|
| Rust | DateTime<Utc> |
| PostgreSQL | TIMESTAMPTZ |
Never use NaiveDateTime or TIMESTAMP.
Builder Pattern
Required for types with 3+ fields or mixed required/optional fields:
pub struct CreateContentParams {
pub slug: String,
pub title: String,
pub body: String,
pub description: Option<String>,
}
impl CreateContentParams {
pub fn builder(slug: impl Into<String>, title: impl Into<String>, body: impl Into<String>) -> CreateContentParamsBuilder {
CreateContentParamsBuilder::new(slug, title, body)
}
}
pub struct CreateContentParamsBuilder {
slug: String,
title: String,
body: String,
description: 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,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn build(self) -> CreateContentParams {
CreateContentParams {
slug: self.slug,
title: self.title,
body: self.body,
description: self.description,
}
}
}
Naming Conventions
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 |
Allowed Abbreviations
id, uuid, url, jwt, mcp, a2a, api, http, json, sql, ctx, req, res, msg, err, cfg
Extension Structure
Every extension follows this structure:
extensions/{name}/
├── Cargo.toml
├── schema/
│ └── 001_table.sql
└── src/
├── lib.rs
├── extension.rs # Extension trait impl
└── error.rs
Requirements:
Cargo.tomlwith systemprompt dependenciessrc/extension.rsimplements Extension traitsrc/error.rsimplements ExtensionError trait- Schema files numbered:
schema/001_name.sql - Registered in root
Cargo.tomlworkspace members
Derive Ordering
When deriving traits, use this order:
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MyType { ... }
Order: Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize
Playbooks
For detailed operational guidance:
systemprompt core playbooks show guide_coding-standards
systemprompt core playbooks show build_rust-standards
systemprompt core playbooks show build_extension-checklist