rust
SkillMediaStrict set of rules in terms of codebase development, design patterns, and best practices. Use when the user wants to develop a new feature or refactor existing code.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the rust skill
What this skill tells your AI
The instructions your AI receives, as published by databricks-solutions/apx in .claude/skills/rust/SKILL.md and read by ahel’s review.
Principles
Priority: Correctness > Safety > Readability > Performance
- Idiomatic Rust. Follow standard library conventions. If the stdlib does it one way, do it that way.
- Leverage the type system. Encode invariants in types, not runtime checks. Make illegal states unrepresentable.
- Domain-driven design. Name everything in the language of the problem, not the implementation.
- Reuse existing libraries and frameworks when possible.
Safety rules
Adapted from The Power of Ten (Holzmann) for Rust.
- Simple control flow. No nested if-else, no nested loops. Split into single-purpose functions. Max one level of branching per function body.
- Short functions. No function longer than ~60 lines. If it's too long, decompose it.
- Fixed loop bounds. All loops must have a provable upper bound. Prefer iterators (
for x in collection) over manualwhile i < lenwith index arithmetic. - Minimal allocation. Prefer
&stroverString, slices overVec, borrows over clones. Allocate only when you must own. - Assertions at boundaries. Use
debug_assert!for invariants within functions. Validate inputs at public API boundaries withResult/Option, not panics. - Smallest possible scope. Variables, types, and functions should be visible only where needed. Prefer module-private by default; add
pubonly when required. - Handle all return values. Never discard
Result. Use?propagation or explicit handling. Annotate intentional ignores withlet _ =. - Macros sparingly. Prefer generics, traits, and enums over procedural macros. Macros obscure control flow and complicate debugging.
- Domain types over raw primitives. Wrap repetitive low-level operations in a named type instead of scattering the same logic across functions.
// bad — raw index arithmetic repeated everywhere
let mut i = 0;
while i < bytes.len() {
if bytes[i] == target { return i; }
i += 1;
}
// good — domain cursor encapsulates iteration
let mut scanner = Scanner::new(bytes);
while let Some(b) = scanner.peek() {
if b == target { return scanner.position(); }
scanner.advance();
}
- Zero warnings. Code must compile with
#[deny(warnings)]cleanly. Runcargo clippyand address all lints. - Use
#[expect(lint, reason = "...")]instead of#[allow].#[expect]warns when the suppression becomes unnecessary, preventing stale silencing. Always include areasonstring. (M-LINT-OVERRIDE-EXPECT) - Panics mean stop the program. Panics are not exceptions. Never use
panic!,unwrap(), orunreachable!()for recoverable errors — useResult.expect()is acceptable only for proven invariants with a descriptive message. (M-PANIC-IS-STOP) unsafeonly when required by 3rd-party libraries (e.g. PyO3 macros). No other reasons to write unsafe code.
Naming conventions
Sources: Rust API Guidelines C-CONV, C-GETTER; Microsoft M-CONCISE-NAMES
- Conversion prefixes follow ownership semantics:
as_— cheap reference-to-reference (no allocation, no copy)to_— expensive conversion, may allocate (e.g.to_string())into_— consumes self, returns owned value
- No
get_prefix on getters. Usefn name(&self) -> &str, notfn get_name(). - Implement
From<T>, neverInto<T>. The blanket impl gives youIntofor free. - Concise type names. Avoid hollow suffixes:
Service,Manager,Factory,Handler,Processor. If the name needs a suffix, the type does too much. - Named constants over magic literals. Every literal with domain meaning gets a
constwith a doc comment. No bare numbers, bytes, or strings in logic.
// bad
if b == b'\\' { i += 2; }
// good
const ESCAPE_BYTE: u8 = b'\\';
if b == ESCAPE_BYTE { scanner.skip_escaped(); }
Error handling
Sources: Microsoft M-APP-ERROR, M-ERRORS-CANONICAL-STRUCTS
thiserrorfor library crates,anyhow/eyrefor application crates. Libraries expose structured errors; apps just need context chains.- Error messages: lowercase, no trailing punctuation. Matches
stdconvention for composable.context()chains. - Use
?propagation everywhere. AvoidmatchonResultwhen?+.map_err()suffices. - Never
unwrap()in non-test code. Useexpect("reason")only for proven invariants. - Canonical error struct pattern:
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
#[error("invalid token at position {position}")]
InvalidToken { position: usize, token: char },
#[error("unexpected end of input")]
UnexpectedEof,
#[error(transparent)]
Io(#[from] std::io::Error),
}
Domain-driven design
Adapted from Domain-Driven Design (Evans/Fowler) for Rust.
Ubiquitous language
Name types, functions, and modules in the language of the problem domain, not the implementation.
// bad — describes implementation mechanics
fn find_end_offset(s: &str) -> Option<usize>
fn check_string(s: &str) -> bool
// good — describes domain concepts
fn PaymentResult::validate(invoice: &Invoice) -> Option<PaymentResult>
fn ClassName::is_tailwind(token: &str) -> bool
Value objects as structs
Domain values without identity are structs. Functions take &Struct and return new structs — no mutation through output parameters.
// bad — caller provides mutable buffer
fn process(input: &Config, out: &mut String)
// good — function returns a value object
fn process(input: &Config) -> ProcessResult
Enums for closed domain rules
When a domain has a fixed set of variants or checks, use an enum — not trait objects, not loose functions.
// bad — scattered functions, no unifying type
fn is_not_empty(s: &str) -> bool { ... }
fn starts_with_letter(s: &str) -> bool { ... }
// good — enum variants are self-documenting and composable
enum ValidationRule { NonEmpty, StartsWithLetter, ContainsHyphen }
impl ValidationRule {
fn passes(self, input: &str) -> bool { match self { ... } }
}
const RULES: &[ValidationRule] = &[ValidationRule::NonEmpty, ...];
Traits for open abstractions
Use traits when behavior needs to be extended by future implementations. Start with the trait, then implement concrete types.
// bad — parallel standalone functions
fn run_git_cmd() -> Output { ... }
fn run_uv_cmd() -> Output { ... }
// good — shared trait, separate implementations
trait ExternalCommand {
fn execute(&self) -> Result<Output>;
}
impl ExternalCommand for Git { ... }
impl ExternalCommand for Uv { ... }
See also Service & middleware and Trait object plugin in Ecosystem patterns.
Modules as bounded contexts
Each Rust module is a bounded context. Types and functions within a module share a domain model; the module boundary is the public API. Keep internal helpers private.
Sans-I/O: separate protocol logic from transport
Source: sans-io.readthedocs.io
Protocol logic (parsing, validation, state machines, data transformation) must be pure functions or types that take data in and return data out — no sockets, no channels, no async, no file handles. I/O operations (network, channels, disk) live in a thin outer layer that calls the protocol layer.
This makes protocol logic testable without standing up real infrastructure, reusable across different I/O backends (tokio, crossbeam, sync), and composable across boundaries (Rust ↔ Python).
// bad — protocol parsing entangled with channel I/O
impl SlotSend {
fn __call__(&self, py, event: &PyDict) -> PyResult<...> {
let type_val: String = event.get_item("type")?.extract()?;
match type_val.as_str() {
"http.response.start" => {
let status = event.get_item("status")?.extract()?;
*self.status.lock() = Some(status);
// parse headers here too...
}
"http.response.body" => {
let body = event.get_item("body")?.extract()?;
self.outbound_tx.send(OutboundSlot { ... })?; // I/O mixed in
self.body_tx.send(body)?; // I/O mixed in
}
}
}
}
// good — protocol layer is a pure function, I/O layer calls it
enum SendEvent {
Start { status: u16, headers: Vec<(Bytes, Bytes)> },
Body { data: Bytes, more_body: bool },
}
fn parse_send_event(event: &Bound<'_, PyDict>) -> PyResult<SendEvent> {
// pure — no channels, no async, testable with synthetic PyDicts
}
impl SlotSend {
fn __call__(&self, py, event: &PyDict) -> PyResult<...> {
let parsed = parse_send_event(event)?; // protocol
self.dispatch(parsed) // I/O
}
}
The same principle applies to request classification:
// bad — routing decision mixed with hyper I/O
async fn handle(self, req: Request<Incoming>) -> Response<...> {
if req.uri().path() == "/_health/alive" {
return json_response(HEALTH_ALIVE); // mixed: decision + response construction
}
if is_websocket_upgrade(&req) {
return self.dispatch.dispatch_ws(req).await; // mixed: decision + dispatch
}
// ... semaphore, timeout, dispatch ...
}
// good — classification is a pure function
enum RequestKind {
Probe(ProbeKind),
WebSocket,
Http,
}
fn classify(path: &str, headers: &HeaderMap) -> RequestKind {
// pure — no async, no Response construction, testable with strings
}
async fn handle(self, req: Request<Incoming>) -> Response<...> {
match classify(req.uri().path(), req.headers()) {
RequestKind::Probe(kind) => probe_response(kind),
RequestKind::WebSocket => self.dispatch.dispatch_ws(req).await,
RequestKind::Http => self.dispatch_http(req).await,
}
}
Rule of thumb: if a function touches both data transformation AND a channel/socket/file, split it. The data transformation half is the protocol layer; the channel/socket half is the I/O layer. The protocol layer should be testable with #[test] using synthetic inputs — no #[tokio::test], no channels, no Python::attach.
API design
Sources: Microsoft M-INIT-BUILDER, M-IMPL-ASREF, M-IMPL-IO, M-AVOID-WRAPPERS; Rust API Guidelines C-COMMON-TRAITS
- Builder pattern for complex initialization. When a type has 4+ optional configuration fields, provide a builder instead of a constructor with many parameters.
- Accept
impl AsRef<str>/impl AsRef<Path>over concrete&str/String/&Pathin function params when callers may have either type. - Accept
impl Read/impl Writefor I/O functions. Decouples logic from concrete I/O sources — enables testing withCursor<Vec<u8>>. - Avoid smart pointers in public APIs. Don't expose
Arc<Mutex<T>>,Box<T>,Rc<T>— let callers choose their wrapping strategy. - Eagerly implement common traits:
Debug,Clone,PartialEq,Defaulton all public types. (C-COMMON-TRAITS) - All public types must implement
Debug. No exceptions. (M-PUBLIC-DEBUG) - Avoid unnecessary
Copy. Do not derive or implementCopyunless the type genuinely benefits from implicit copy semantics. PreferClonewith explicit.clone()so copies are visible and intentional.
Code patterns
Return values, don't mutate
Functions return domain types instead of writing into &mut parameters. This makes data flow explicit and enables composition via .map(), .fold(), iterators.
// bad — mutation hides data flow
fn transform(input: &str, out: &mut Vec<String>)
// good — return value makes flow explicit
fn transform(input: &str) -> Vec<TransformResult>
Flat validation with early returns
Split complex validation into a scanning step and a checking step. Each is its own function. No nesting beyond one level.
// bad — nested ifs, multiple concerns in one block
if b == CLOSE {
if i + 1 < len && bytes[i + 1] == CLOSE {
if i == 0 { return None; }
return Some(i);
}
return None;
}
// good — scan finds candidate, validate checks it
fn scan(input: &str) -> Option<Boundary> { ... } // walks bytes
fn validate(pos: usize, bytes: &[u8]) -> Option<Boundary> { ... } // checks invariants
Iterator chains over indexed loops
Prefer .iter(), .map(), .filter(), .collect() over for i in 0..len with manual indexing. Iterator chains are bounds-checked by construction.
Cow<'a, str> for conditional ownership
When a function sometimes borrows and sometimes allocates, return Cow<'a, str> instead of always cloning.
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_"))
} else {
Cow::Borrowed(input)
}
}
Pre-allocate when size is known
Use String::with_capacity() / Vec::with_capacity() when the final size is known or estimable. Avoids repeated reallocations.
Protocol functions are pure
If a function parses, validates, classifies, or transforms data — it must not touch I/O (channels, sockets, files, async runtimes). Accept data in, return data out. This makes protocol logic testable with #[test] (no #[tokio::test], no Python::attach, no channel setup).
// bad — needs a channel + tokio runtime to test
async fn handle_response(data: ResponseData, tx: &mpsc::Sender<Bytes>) -> Result<()> {
let status = StatusCode::from_u16(data.status)?;
for chunk in data.chunks { tx.send(chunk).await?; } // I/O inside logic
Ok(())
}
// good — pure transformation, separately testable
fn build_response(data: ResponseData) -> Result<OutboundResponse> {
let status = StatusCode::from_u16(data.status)?;
Ok(OutboundResponse { status, headers: data.headers, body: data.body })
}
Test-first for bugs
When hitting a bug, write a failing test that reproduces it first. Only then write the fix. Tests document the exact failure mode and prevent regressions.
Ecosystem patterns
Production Rust relies on patterns popularized by Tokio, Axum, Tower, and Serde. These bridge the gap between the micro-level code patterns above and full application architecture.
Builder
Source: Rust API Guidelines C-BUILDER
Separate construction from representation. A builder accumulates configuration through method chaining and produces the final value in a terminal .build() call that can validate and fail.
Expands API design rule: "Builder pattern for complex initialization."
pub struct ServerConfig {
bind_addr: SocketAddr,
workers: usize,
tls: Option<TlsConfig>,
}
pub struct ServerConfigBuilder {
bind_addr: Option<SocketAddr>,
workers: usize,
tls: Option<TlsConfig>,
}
impl ServerConfigBuilder {
pub fn new() -> Self {
Self { bind_addr: None, workers: 1, tls: None }
}
pub fn bind_addr(mut self, addr: SocketAddr) -> Self {
self.bind_addr = Some(addr);
self
}
pub fn workers(mut self, n: usize) -> Self {
self.workers = n;
self
}
pub fn tls(mut self, config: TlsConfig) -> Self {
self.tls = Some(config);
self
}
pub fn build(self) -> Result<ServerConfig, ConfigError> {
let bind_addr = self.bind_addr.ok_or(ConfigError::MissingBindAddr)?;
Ok(ServerConfig { bind_addr, workers: self.workers, tls: self.tls })
}
}
Use when:
- A type has 4+ optional configuration fields
- Construction requires validation that can fail
- You want to guide callers through configuration step-by-step
Avoid when:
- A simple
new()with 1-3 required fields suffices - The type is a plain data carrier with no invariants
Newtype
Source: Rust Design Patterns — Newtype
Wrap a primitive in a single-field tuple struct to enforce domain invariants at construction time. The inner value is private; access goes through validated constructors and accessor methods.
Complements Safety rule #9: "Domain types over raw primitives."
// from crates/framework/src/route.rs
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct QualName(String);
#[derive(Debug, thiserror::Error)]
pub enum QualNameError {
#[error("qualified name must not be empty")]
Empty,
#[error("qualified name has empty segment: {0}")]
EmptySegment(String),
#[error("invalid qualified name segment: {0}")]
InvalidSegment(String),
}
impl QualName {
pub fn new(name: impl Into<String>) -> Result<Self, QualNameError> {
let name = name.into();
if name.is_empty() {
return Err(QualNameError::Empty);
}
for segment in name.split('.') {
if segment.is_empty() {
return Err(QualNameError::EmptySegment(name));
}
// ... validate each segment
}
Ok(Self(name))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
Use when:
- A raw type (
String,u64,Vec<u8>) has domain constraints (non-empty, positive, valid format) - You need to prevent mixing two semantically different values of the same underlying type
- The type appears in public APIs and you want compile-time safety
Avoid when:
- The wrapper adds no invariants and just obscures the inner type
- You only need it in one internal function (use a local
letbinding instead)
Extension trait
Source: Rust API Guidelines C-EXT
Add methods to a foreign type (one you don't own) by defining a trait and implementing it for that type. Callers use the trait to get the new methods.
// from crates/mcp/src/tools/mod.rs
pub trait ToolResultExt {
fn from_serializable(value: &impl StructuredObject) -> Self;
fn from_serializable_error(value: &impl StructuredObject) -> Self;
}
impl ToolResultExt for CallToolResult {
fn from_serializable(value: &impl StructuredObject) -> Self {
build_structured_result(value, false)
}
fn from_serializable_error(value: &impl StructuredObject) -> Self {
build_structured_result(value, true)
}
}
// Usage: CallToolResult::from_serializable(&my_response)
Use when:
- You need to add domain-specific methods to a type from an external crate
- Multiple call sites would otherwise repeat the same conversion/construction logic
- The methods form a coherent semantic group (name the trait after the capability, e.g.
*Ext)
Avoid when:
- A free function would be equally clear and doesn't benefit from method syntax
- You own the type — just add methods directly
Type-state
Source: Cliffle — Rust Typestate Pattern
Encode protocol states as zero-sized type parameters. Methods that are only valid in a specific state are only available on that parameterization. State transitions consume the old value and return a new one.
pub struct Disconnected;
pub struct Connected;
pub struct Connection<S> {
addr: SocketAddr,
_state: std::marker::PhantomData<S>,
}
impl Connection<Disconnected> {
pub fn new(addr: SocketAddr) -> Self {
Self { addr, _state: PhantomData }
}
pub async fn connect(self) -> Result<Connection<Connected>, io::Error> {
// ... establish connection ...
Ok(Connection { addr: self.addr, _state: PhantomData })
}
}
impl Connection<Connected> {
pub async fn send(&self, data: &[u8]) -> Result<(), io::Error> {
// only available when connected
Ok(())
}
}
Use when:
- An object has a clear lifecycle with distinct phases (disconnected → connected, unvalidated → validated)
- Calling methods out of order is a logic error you want to catch at compile time
- State transitions are linear and well-defined
Avoid when:
- States are dynamic or user-driven (use an enum instead)
- The number of states or transitions is large — type-state combinatorics explode quickly
Derive macro
Source: The Little Book of Rust Macros
Declarative (macro_rules!) or procedural macros that auto-derive trait implementations or generate boilerplate. Used sparingly, they eliminate repetitive patterns that generics and traits alone cannot.
Controlled exception to Safety rule #8: "Macros sparingly." Derive macros are acceptable when the pattern is mechanical, repeated across many types, and error-prone to write by hand.
// from crates/mcp/src/tools/mod.rs
macro_rules! tool_response {
(
$(#[$meta:meta])*
$vis:vis struct $name:ident {
$( $(#[$field_meta:meta])* $field_vis:vis $field:ident : $ty:ty ),* $(,)?
}
) => {
$(#[$meta])*
#[derive(Debug, serde::Serialize)]
$vis struct $name {
$( $(#[$field_meta])* $field_vis $field : $ty, )*
}
impl $crate::tools::StructuredObject for $name {}
};
}
// Usage — every tool response struct gets Serialize + StructuredObject:
// tool_response! { pub struct MyToolOutput { pub result: String } }
Use when:
- The same trait implementation is mechanically identical across 5+ types
- Forgetting the impl is a common source of bugs (like the
StructuredObjectmarker above) - The macro body is short and readable
Avoid when:
- Generics, blanket impls, or
#[derive(...)]from serde/thiserror already handle it - The macro hides non-trivial control flow or business logic
- Only 1-2 types need the pattern (just write the impls by hand)
Service & middleware
Source: Tower — Service trait
A service is a trait with a single async call method that transforms a request into a response. Middleware wraps an inner service, adding cross-cutting behavior (logging, auth, timeouts) without modifying business logic. Services compose into layered stacks.
Architectural application of DDD "Traits for open abstractions." See also Trait object plugin.
// from crates/framework/src/bridge/dispatch.rs
// The trait defines the service contract:
pub trait HandlerDispatch: Send + Sync + std::fmt::Debug {
fn handle(
&self,
route: Arc<BoundRoute>,
app_state: Arc<AppState>,
request: InboundRequest,
) -> Pin<Box<dyn Future<Output = Result<OutboundResponse, AppError>> + Send>>;
}
// A concrete service implements the trait:
pub struct RequestResponseDispatch;
impl HandlerDispatch for RequestResponseDispatch {
fn handle(
&self,
route: Arc<BoundRoute>,
app_state: Arc<AppState>,
mut request: InboundRequest,
) -> Pin<Box<dyn Future<Output = Result<OutboundResponse, AppError>> + Send>> {
Box::pin(async move {
let ctx = extract_context(&mut request, &route, &app_state).await?;
let result = invoke_handler(&route, &ctx).await?;
Python::attach(|py| serialize_result(py, &result, &route))
})
}
}
// Layered call chain:
// axum handler → transport conversion → HandlerDispatch trait → Python bridge
Use when:
- You have cross-cutting concerns (auth, logging, metrics, rate limiting) that apply to many handlers
- Multiple dispatch strategies share the same request/response contract
- You want to swap or layer behaviors without modifying core logic
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 89
- Forks
- 25
- Last commit
- Apr 2026
Advanced
- Catalog kind
- skill
- Gateway key
rust-databricks-solutions- Source
- github.com/databricks-solutions/apx