microsoft/hve-core

Public

mirrored from https://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
7226b4c1944c0773b20eeb1b3be5a4aebf11d4e9

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

.github/instructions/coding-standards/rust/rust.instructions.md

675lines · modecode

1---
2applyTo: '**/*.rs'
3description: 'Required instructions for Rust research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core'
4---
5
6# Rust Instructions
7
8Conventions for Rust development targeting the 2021 edition.
9
10## Project Structure
11
12Crates follow a standard layout:
13
14```text
15Cargo.toml
16Cargo.lock
17src/
18 main.rs # Binary crate entry point
19 lib.rs # Library crate root
20 module_name.rs # Top-level module
21 module_name/
22 mod.rs # Module with submodules
23 submodule.rs
24tests/
25 integration_test.rs
26```
27
28* `Cargo.toml` and `Cargo.lock` at crate root.
29* Commit `Cargo.lock` for binary crates to ensure reproducible builds. Exclude it from version control for library crates.
30* `src/` contains all source files.
31* Binary crates use `main.rs`; library crates use `lib.rs`.
32* Test projects use a sibling `tests/` directory for integration tests.
33* Keep crate roots thin with module declarations and re-exports.
34
35Project folder organization scales with complexity. Keep all files at root-level modules when fewer than 10 source files exist. When folders become necessary, organize by domain responsibility: `config`, `error`, `handlers`, `models`, `services`.
36
37## Cargo.toml Conventions
38
39### Standard Fields
40
41<!-- <example-cargo-toml> -->
42```toml
43[package]
44name = "service-name"
45version = "0.1.0"
46edition = "2021"
47license = "MIT"
48
49[dependencies]
50tokio = { version = "1", features = ["full"] }
51serde = { version = "1", features = ["derive"] }
52serde_json = "1"
53thiserror = "2"
54tracing = "0.1"
55tracing-subscriber = { version = "0.3", features = ["env-filter"] }
56```
57<!-- </example-cargo-toml> -->
58
59Sections below reference additional crates (`reqwest`, `tokio-retry`, `async-trait`). Add them to `[dependencies]` when using those patterns.
60
61### Dependency Management
62
63* Use caret ranges for stable public crates: `version = "1"`.
64* Pin exact versions for private or unstable SDKs: `version = "=1.1.3"`.
65* Disable default features when targeting WASM or minimal builds: `default-features = false`.
66* Specify only needed feature flags to reduce compile time and binary size.
67
68### Release Profile
69
70<!-- <example-release-profile> -->
71```toml
72[profile.release]
73strip = true
74lto = true
75codegen-units = 1
76panic = "abort"
77```
78<!-- </example-release-profile> -->
79
80Use `strip = true` to remove debug symbols. Enable `lto` and `codegen-units = 1` for optimized builds. Set `panic = "abort"` for smaller binaries when stack unwinding is unnecessary.
81
82## Coding Conventions
83
84### Naming
85
86| Element | Convention | Example |
87|-----------------|----------------------|-------------------------------|
88| Types & Structs | PascalCase | `UserService`, `DeviceConfig` |
89| Traits | PascalCase | `Repository`, `Serializer` |
90| Enum Variants | PascalCase | `Status::Active` |
91| Functions | snake_case | `process_request` |
92| Variables | snake_case | `device_url`, `retry_count` |
93| Constants | SCREAMING_SNAKE_CASE | `DEFAULT_TIMEOUT` |
94| Modules | snake_case | `error_handler` |
95| Crate Names | kebab-case | `my-service` |
96| Feature Flags | kebab-case | `onnx-runtime` |
97
98### Type Naming Suffixes
99
100Apply these suffixes consistently for domain types:
101
102* Error types: `*Error` suffix (`ServiceError`, `ParseError`)
103* Config types: `*Config` suffix (`AppConfig`, `DatabaseConfig`)
104* Builder types: `*Builder` suffix (`RequestBuilder`, `SessionBuilder`)
105* Result type aliases: `pub type Result<T> = std::result::Result<T, ServiceError>;`
106
107### Module Structure
108
109Member ordering within a module:
110
1111. `use` declarations (standard library, external crates, internal modules)
1122. Constants and statics
1133. Type definitions (structs, enums, type aliases)
1144. Trait definitions
1155. Trait implementations
1166. Inherent implementations
1177. Free functions
1188. Test module
119
120Within categories, order: `pub` items before `pub(crate)` before private.
121
122### Variable Declarations
123
124Prefer type inference with `let` when the type is obvious from the right side. Use explicit type annotations when inference is ambiguous or the type aids readability:
125
126```rust
127let service = UserService::new(repo, logger);
128let lookup: HashMap<String, Vec<Item>> = HashMap::new();
129```
130
131Prefer early returns over deep nesting. Use `if let` and `let ... else` for option/result unwrapping at control flow boundaries:
132
133```rust
134let Some(user) = repository.find(id).await? else {
135 return Err(ServiceError::not_found("User not found"));
136};
137```
138
139## Error Handling
140
141### Custom Error Types
142
143Use `thiserror` for library-style error enums. Define a module-scoped `Result` type alias to reduce boilerplate.
144
145<!-- <example-error-handling> -->
146```rust
147use thiserror::Error;
148
149pub type Result<T> = std::result::Result<T, ServiceError>;
150
151#[derive(Error, Debug)]
152pub enum ServiceError {
153 #[error("Not found: {message}")]
154 NotFound { message: String },
155
156 #[error("Invalid input: {message}")]
157 InvalidInput { message: String },
158
159 #[error("IO error: {0}")]
160 Io(#[from] std::io::Error),
161
162 #[error("Serialization error: {0}")]
163 Serialization(#[from] serde_json::Error),
164}
165
166impl ServiceError {
167 pub fn not_found<S: Into<String>>(message: S) -> Self {
168 Self::NotFound { message: message.into() }
169 }
170
171 pub fn invalid_input<S: Into<String>>(message: S) -> Self {
172 Self::InvalidInput { message: message.into() }
173 }
174}
175```
176<!-- </example-error-handling> -->
177
178### Error Handling Rules
179
180* Use `thiserror` for error types in libraries and modules with structured error variants.
181* Use `anyhow` only in application-level `main` functions or CLI tools where error granularity is unnecessary.
182* Prefer `?` operator for error propagation over explicit `match` or `unwrap`.
183* Never use `unwrap()` or `expect()` in production paths. Reserve them for cases with compile-time or initialization guarantees.
184* Initialization paths include startup config loading in `main`, `OnceLock`/`LazyLock` initializers, and one-time setup that runs before the service accepts work. Use `expect()` with a descriptive message in these contexts.
185* Provide context-aware error messages that include relevant state.
186* Implement `#[from]` for delegating errors from external crates.
187* Add helper constructors on error types for ergonomic creation.
188
189## Async Patterns
190
191### Tokio Runtime
192
193Use Tokio as the async runtime. Select the flavor based on workload characteristics:
194
195<!-- <example-tokio-runtime> -->
196```rust
197// Multi-threaded for high-concurrency services
198#[tokio::main]
199async fn main() -> Result<()> {
200 // ...
201}
202
203// Single-threaded for lightweight or resource-constrained services
204#[tokio::main(flavor = "current_thread")]
205async fn main() -> Result<()> {
206 // ...
207}
208```
209<!-- </example-tokio-runtime> -->
210
211### Concurrent Task Management
212
213* Use `tokio::select!` for racing independent tasks that should run concurrently until one completes.
214* Use `tokio::try_join!` for collecting results from concurrent tasks that must all succeed.
215* Use `tokio::spawn` for background tasks that run independently.
216* Handle task cancellation and shutdown gracefully via `CancellationToken` or `tokio::select!`.
217* Never block the async runtime with synchronous operations; use `tokio::task::spawn_blocking` instead.
218
219<!-- <example-concurrency> -->
220```rust
221tokio::select! {
222 result = background_task() => result?,
223 result = server.run() => result?,
224}
225```
226<!-- </example-concurrency> -->
227
228### Async Trait Implementations
229
230Use `async-trait` for trait definitions requiring async methods. The `async-trait` crate is required for the 2021 edition. Native async trait support is available in the 2024 edition and later.
231
232```rust
233use async_trait::async_trait;
234
235#[async_trait]
236pub trait Repository: Send + Sync {
237 async fn find_by_id(&self, id: &str) -> Result<Option<Item>>;
238 async fn save(&self, item: &Item) -> Result<()>;
239}
240```
241
242## Observability
243
244### Structured Logging
245
246Use the `tracing` crate for all logging. Never use `println!` or `eprintln!` in production code.
247
248<!-- <example-tracing> -->
249```rust
250use tracing::{info, warn, error, debug};
251use tracing_subscriber::filter::EnvFilter;
252
253tracing_subscriber::fmt()
254 .with_env_filter(EnvFilter::from_default_env())
255 .init();
256
257info!(
258 endpoint = %endpoint,
259 interval_secs = %interval,
260 "Starting service with configuration"
261);
262
263error!(
264 error_code = "PUBLISH_FAILED",
265 topic = %topic,
266 "Failed to publish message: {:?}", err
267);
268```
269<!-- </example-tracing> -->
270
271### OpenTelemetry Integration
272
273When distributed tracing is required, integrate OpenTelemetry via `tracing-opentelemetry`. Add `tracing-opentelemetry`, `opentelemetry`, `opentelemetry-sdk` (with the `rt-tokio` feature), and `opentelemetry-otlp` to `[dependencies]`.
274
275* Check for `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable before enabling the exporter.
276* Fall back to console-only logging when the variable is absent.
277* Use `TraceContextPropagator` for W3C trace context propagation.
278
279```rust
280use std::env;
281
282use tracing_subscriber::layer::SubscriberExt;
283
284fn init_tracing() {
285 let subscriber = tracing_subscriber::fmt()
286 .with_env_filter(tracing_subscriber::EnvFilter::from_default_env());
287
288 if env::var("OTEL_EXPORTER_OTLP_ENDPOINT").is_ok() {
289 let tracer = opentelemetry_otlp::new_pipeline()
290 .tracing()
291 .install_batch(opentelemetry_sdk::runtime::Tokio)
292 .expect("OpenTelemetry pipeline must initialize at startup");
293
294 let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
295 tracing::subscriber::set_global_default(
296 subscriber.finish().with(telemetry),
297 ).expect("Global subscriber must be set once at startup");
298 } else {
299 subscriber.init();
300 }
301}
302```
303
304## Serialization
305
306### Serde Patterns
307
308* Derive `Serialize` and `Deserialize` using `serde` for all data transfer types.
309* Use `#[serde(rename_all = "camelCase")]` when interfacing with JSON APIs that use camelCase.
310* Implement `Default` for configuration types to provide sensible fallback values.
311* Use `#[serde(default = "...")]` for fields with non-trivial defaults.
312
313<!-- <example-serde> -->
314```rust
315#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
316pub struct AppConfig {
317 pub endpoint: String,
318 pub polling_interval_secs: u64,
319 #[serde(default = "default_timeout")]
320 pub timeout_ms: u64,
321}
322
323fn default_timeout() -> u64 { 5000 }
324
325impl Default for AppConfig {
326 fn default() -> Self {
327 Self {
328 endpoint: String::new(),
329 polling_interval_secs: 10,
330 timeout_ms: default_timeout(),
331 }
332 }
333}
334```
335<!-- </example-serde> -->
336
337## Configuration Management
338
339### Environment Variables
340
341Define environment variable names as constants. Use `env::var` with clear error messages or sensible defaults.
342
343<!-- <example-env-vars> -->
344```rust
345const ENDPOINT_VAR: &str = "SERVICE_ENDPOINT";
346const INTERVAL_VAR: &str = "POLLING_INTERVAL";
347
348let endpoint = env::var(ENDPOINT_VAR)
349 .expect("SERVICE_ENDPOINT must be set");
350
351let interval = env::var(INTERVAL_VAR)
352 .unwrap_or_else(|_| "10".to_string())
353 .parse::<u64>()
354 .expect("POLLING_INTERVAL must be a valid u64");
355```
356<!-- </example-env-vars> -->
357
358### File-Based Configuration
359
360Support YAML and JSON configuration with validation:
361
362```rust
363impl AppConfig {
364 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
365 let content = std::fs::read_to_string(&path)?;
366 let config: Self = serde_json::from_str(&content)?;
367 config.validate()?;
368 Ok(config)
369 }
370
371 pub fn validate(&self) -> Result<()> {
372 if self.endpoint.is_empty() {
373 return Err(ServiceError::invalid_input("Endpoint must not be empty"));
374 }
375 Ok(())
376 }
377}
378```
379
380### Static Initialization
381
382Use `OnceLock` for thread-safe, one-time initialization of global state:
383
384```rust
385use std::sync::OnceLock;
386
387static CONFIG: OnceLock<AppConfig> = OnceLock::new();
388```
389
390## Resilience Patterns
391
392### Retry Logic
393
394Use `tokio_retry` with exponential backoff for transient failures:
395
396<!-- <example-retry> -->
397```rust
398use tokio_retry::{strategy::ExponentialBackoff, Retry};
399use std::time::Duration;
400
401let retry_strategy = ExponentialBackoff::from_millis(2000)
402 .max_delay(Duration::from_secs(10))
403 .take(5);
404
405let result = Retry::spawn(retry_strategy, || async {
406 client.send(&payload).await
407}).await;
408```
409<!-- </example-retry> -->
410
411## Visibility
412
413* Default to private. Expose only what is needed at module boundaries.
414* Use `pub(crate)` for types shared across modules within the same crate.
415* Mark public API types with `pub` only when they form the crate's external contract.
416
417### Feature Flags
418
419Use Cargo feature flags for optional functionality:
420
421```rust
422pub enum Backend {
423 #[cfg(feature = "backend-a")]
424 BackendA(BackendAImpl),
425 #[cfg(feature = "backend-b")]
426 BackendB(BackendBImpl),
427}
428```
429
430Declare features and gate optional dependencies in `Cargo.toml`:
431
432```toml
433[features]
434default = []
435backend-a = ["dep:backend-a-crate"]
436backend-b = ["dep:backend-b-crate"]
437```
438
439Gate heavy dependencies behind features so downstream consumers control binary size.
440
441## Code Documentation
442
443Public and protected items require documentation comments.
444
445Guidelines:
446
447* Use `///` for item-level documentation on public types, functions, and modules.
448* Use `//!` for module-level documentation at the top of `lib.rs` or `mod.rs`.
449* Document parameters and return values in the description when non-obvious.
450* Include `# Examples` sections for public API functions.
451* Include `# Errors` sections for functions returning `Result`.
452* Include `# Panics` sections for functions that can panic.
453
454<!-- <example-documentation> -->
455```rust
456/// Processes the input data and returns the transformed result.
457///
458/// # Arguments
459///
460/// * `input` - Raw input bytes to process.
461/// * `config` - Processing configuration.
462///
463/// # Returns
464///
465/// The processed output as a byte vector.
466///
467/// # Errors
468///
469/// Returns `ServiceError::InvalidInput` if the input cannot be parsed.
470///
471/// # Examples
472///
473/// ```
474/// let result = process(&input, &config)?;
475/// assert!(!result.is_empty());
476/// ```
477pub fn process(input: &[u8], config: &ProcessConfig) -> Result<Vec<u8>> {
478 // ...
479}
480```
481<!-- </example-documentation> -->
482
483## Clippy and Formatting
484
485### Clippy
486
487* Run `cargo clippy` and resolve all warnings before committing.
488* Do not suppress Clippy lints without documented justification.
489* Use `#[allow(...)]` on specific items rather than crate-wide `#![allow(...)]` when suppression is necessary.
490
491### Formatting
492
493* Run `cargo fmt` before committing.
494* Use default `rustfmt` configuration unless the project includes a `rustfmt.toml`.
495
496## Additional Conventions
497
498* Prefer `&str` over `String` in function parameters when ownership is not needed.
499* Use `impl Into<String>` for constructors and builders accepting string arguments.
500* Use `Cow<'_, str>` when a function may or may not need to allocate:
501
502```rust
503use std::borrow::Cow;
504
505fn normalize_name(name: &str) -> Cow<'_, str> {
506 if name.contains(' ') {
507 Cow::Owned(name.replace(' ', "_"))
508 } else {
509 Cow::Borrowed(name)
510 }
511}
512```
513
514* Use iterators and combinators over manual loops where readability is maintained.
515* Prefer `Vec<u8>` and slices over raw pointer manipulation.
516
517## Patterns to Avoid
518
519* `println!` or `eprintln!` in production code (use `tracing` macros).
520* `unwrap()` or `expect()` in production paths without compile-time guarantees.
521* Shared mutable state without synchronization primitives (`Mutex`, `RwLock`, `OnceLock`).
522* Blocking operations inside async contexts (use `tokio::task::spawn_blocking`).
523* Overly broad feature sets on dependencies (minimize with `default-features = false`).
524* Global mutable statics without `OnceLock`, `LazyLock`, or equivalent.
525* Suppressing Clippy lints without documented justification.
526* `unsafe` blocks without a `// SAFETY:` comment explaining the invariant.
527
528## Complete Example
529
530Demonstrates naming, structure, error handling, async patterns, configuration, observability, and testing:
531
532```rust
533// Rust uses modules, not namespaces — see the mod declarations below.
534
535use std::env;
536use std::time::Duration;
537
538use serde::{Deserialize, Serialize};
539use thiserror::Error;
540use tokio_retry::{strategy::ExponentialBackoff, Retry};
541use tracing::{error, info};
542use tracing_subscriber::filter::EnvFilter;
543
544const ENDPOINT_VAR: &str = "SERVICE_ENDPOINT";
545const INTERVAL_VAR: &str = "POLLING_INTERVAL";
546
547// --- Error ---
548
549pub type Result<T> = std::result::Result<T, ServiceError>;
550
551#[derive(Error, Debug)]
552pub enum ServiceError {
553 #[error("Not found: {message}")]
554 NotFound { message: String },
555
556 #[error("Request failed: {message}")]
557 Request { message: String },
558
559 #[error("IO error: {0}")]
560 Io(#[from] std::io::Error),
561
562 #[error("JSON error: {0}")]
563 Json(#[from] serde_json::Error),
564}
565
566impl ServiceError {
567 pub fn not_found<S: Into<String>>(message: S) -> Self {
568 Self::NotFound { message: message.into() }
569 }
570
571 pub fn request<S: Into<String>>(message: S) -> Self {
572 Self::Request { message: message.into() }
573 }
574}
575
576// --- Config ---
577
578#[derive(Debug, Clone, Serialize, Deserialize)]
579pub struct AppConfig {
580 pub endpoint: String,
581 #[serde(default = "default_interval")]
582 pub polling_interval_secs: u64,
583}
584
585fn default_interval() -> u64 { 10 }
586
587impl AppConfig {
588 pub fn from_env() -> Self {
589 Self {
590 endpoint: env::var(ENDPOINT_VAR)
591 .expect("SERVICE_ENDPOINT must be set"),
592 polling_interval_secs: env::var(INTERVAL_VAR)
593 .unwrap_or_else(|_| "10".to_string())
594 .parse()
595 .expect("POLLING_INTERVAL must be a valid u64"),
596 }
597 }
598}
599
600// --- Service ---
601
602/// Polls an endpoint and processes responses.
603pub struct PollingService {
604 config: AppConfig,
605 client: reqwest::Client,
606}
607
608impl PollingService {
609 pub fn new(config: AppConfig) -> Self {
610 Self {
611 config,
612 client: reqwest::Client::new(),
613 }
614 }
615
616 /// Starts the polling loop.
617 ///
618 /// # Errors
619 ///
620 /// Returns `ServiceError::Request` on repeated fetch failures.
621 pub async fn run(&self) -> Result<()> {
622 let interval = Duration::from_secs(self.config.polling_interval_secs);
623
624 loop {
625 match self.fetch_with_retry().await {
626 Ok(data) => info!(items = data.len(), "Fetched data"),
627 Err(err) => error!(?err, "Fetch failed after retries"),
628 }
629 tokio::time::sleep(interval).await;
630 }
631 }
632
633 async fn fetch_with_retry(&self) -> Result<Vec<u8>> {
634 let strategy = ExponentialBackoff::from_millis(1000)
635 .max_delay(Duration::from_secs(8))
636 .take(3);
637
638 Retry::spawn(strategy, || self.fetch()).await
639 }
640
641 async fn fetch(&self) -> Result<Vec<u8>> {
642 let response = self.client
643 .get(&self.config.endpoint)
644 .send()
645 .await
646 .map_err(|e| ServiceError::request(e.to_string()))?;
647
648 if !response.status().is_success() {
649 return Err(ServiceError::request(
650 format!("HTTP {}", response.status()),
651 ));
652 }
653
654 response.bytes()
655 .await
656 .map(|b| b.to_vec())
657 .map_err(|e| ServiceError::request(e.to_string()))
658 }
659}
660
661// --- Entry Point ---
662
663#[tokio::main]
664async fn main() -> Result<()> {
665 tracing_subscriber::fmt()
666 .with_env_filter(EnvFilter::from_default_env())
667 .init();
668
669 let config = AppConfig::from_env();
670 info!(endpoint = %config.endpoint, "Starting service");
671
672 let service = PollingService::new(config);
673 service.run().await
674}
675```