Conditional compilation strategies
Cargo feature flags enable optional functionality at compile time. They allow crates to provide different capabilities without forcing all dependencies on every user.
Without feature flags:
# Cargo.toml - Feature definition
[package]
name = "mylib"
version = "1.0.0"
edition = "2021"
[features]
# Default features enabled when dependency is added
default = ["std"]
# Standard library support (can be disabled for no_std)
std = []
# Async runtime support
async = ["tokio"]
async-std = ["dep:async-std"]
# Serialization support
serde = ["dep:serde", "dep:serde_json"]
# Database backends
postgres = ["sqlx/postgres"]
mysql = ["sqlx/mysql"]
sqlite = ["sqlx/sqlite"]
# TLS implementations (mutually exclusive in practice)
native-tls = ["dep:native-tls"]
rustls = ["dep:rustls"]
# Debugging features
debug = ["tracing/max_level_debug"]
trace = ["tracing/max_level_trace"]
# Kitchen sink - all features
full = ["std", "async", "serde", "postgres", "rustls"]
# Unstable features (require nightly)
unstable = []
[dependencies]
# Always included
thiserror = "1.0"
# Optional dependencies (feature-gated)
tokio = { version = "1.35", features = ["full"], optional = true }
async-std = { version = "1.12", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }
sqlx = { version = "0.7", optional = true }
native-tls = { version = "0.2", optional = true }
rustls = { version = "0.22", optional = true }
tracing = { version = "0.1", optional = true }
[dev-dependencies]
# Test dependencies always available
tokio = { version = "1.35", features = ["full", "test-util"] }
// src/lib.rs - Conditional compilation
#![cfg_attr(not(feature = "std"), no_std)]
// Conditionally use alloc for no_std
#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
// Feature-gated modules
#[cfg(feature = "async")]
pub mod async_client;
#[cfg(feature = "serde")]
pub mod serialization;
#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
pub mod database;
// Core functionality always available
pub mod core;
pub mod error;
// Re-exports based on features
pub use core::*;
pub use error::*;
#[cfg(feature = "async")]
pub use async_client::AsyncClient;
#[cfg(feature = "serde")]
pub use serialization::{FromJson, ToJson};
/// Prelude module adapts to enabled features
pub mod prelude {
pub use crate::core::*;
pub use crate::error::*;
#[cfg(feature = "async")]
pub use crate::async_client::AsyncClient;
#[cfg(feature = "serde")]
pub use serde::{Deserialize, Serialize};
}
// src/core.rs - Core types with optional trait implementations
use crate::error::Error;
/// Core data type
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Data {
pub id: u64,
pub name: String,
pub value: Vec<u8>,
}
impl Data {
pub fn new(id: u64, name: impl Into<String>) -> Self {
Data {
id,
name: name.into(),
value: Vec::new(),
}
}
/// Serialize to JSON (requires serde feature)
#[cfg(feature = "serde")]
pub fn to_json(&self) -> Result<String, Error> {
serde_json::to_string(self).map_err(|e| Error::Serialization(e.to_string()))
}
/// Deserialize from JSON (requires serde feature)
#[cfg(feature = "serde")]
pub fn from_json(json: &str) -> Result<Self, Error> {
serde_json::from_str(json).map_err(|e| Error::Serialization(e.to_string()))
}
}
/// Synchronous client (always available)
pub struct Client {
endpoint: String,
}
impl Client {
pub fn new(endpoint: impl Into<String>) -> Self {
Client {
endpoint: endpoint.into(),
}
}
pub fn get(&self, path: &str) -> Result<Data, Error> {
// Sync implementation
let _ = (path, &self.endpoint);
Ok(Data::new(1, "test"))
}
}
// src/async_client.rs - Async client (feature-gated)
#![cfg(feature = "async")]
use crate::core::Data;
use crate::error::Error;
/// Async client for concurrent operations
///
/// This type is only available when the `async` feature is enabled.
///
/// # Example
///
///rust,ignore
/// use mylib::AsyncClient;
///
/// #[tokio::main]
/// async fn main() -> Result<(), mylib::Error> {
/// let client = AsyncClient::new("https://api.example.com");
/// let data = client.get("/users/1").await?;
/// Ok(())
/// }
///
pub struct AsyncClient {
endpoint: String,
#[cfg(feature = "native-tls")]
tls: native_tls::TlsConnector,
#[cfg(feature = "rustls")]
tls: rustls::ClientConfig,
}
impl AsyncClient {
pub fn new(endpoint: impl Into<String>) -> Self {
AsyncClient {
endpoint: endpoint.into(),
#[cfg(feature = "native-tls")]
tls: native_tls::TlsConnector::new().unwrap(),
#[cfg(feature = "rustls")]
tls: rustls::ClientConfig::builder()
.with_safe_defaults()
.with_native_roots()
.with_no_client_auth(),
}
}
pub async fn get(&self, path: &str) -> Result<Data, Error> {
// Would use tokio for async HTTP
let _ = path;
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
Ok(Data::new(1, "async result"))
}
pub async fn post(&self, path: &str, data: &Data) -> Result<Data, Error> {
let _ = (path, data);
Ok(Data::new(2, "posted"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_async_get() {
let client = AsyncClient::new("https://example.com");
let result = client.get("/test").await;
assert!(result.is_ok());
}
}
// src/database.rs - Database support with multiple backends
#![cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
use crate::core::Data;
use crate::error::Error;
/// Database connection abstraction
pub struct Database {
#[cfg(feature = "postgres")]
pool: sqlx::PgPool,
#[cfg(feature = "mysql")]
pool: sqlx::MySqlPool,
#[cfg(feature = "sqlite")]
pool: sqlx::SqlitePool,
}
impl Database {
/// Connect to PostgreSQL
#[cfg(feature = "postgres")]
pub async fn connect_postgres(url: &str) -> Result<Self, Error> {
let pool = sqlx::PgPool::connect(url)
.await
.map_err(|e| Error::Database(e.to_string()))?;
Ok(Database { pool })
}
/// Connect to MySQL
#[cfg(feature = "mysql")]
pub async fn connect_mysql(url: &str) -> Result<Self, Error> {
let pool = sqlx::MySqlPool::connect(url)
.await
.map_err(|e| Error::Database(e.to_string()))?;
Ok(Database { pool })
}
/// Connect to SQLite
#[cfg(feature = "sqlite")]
pub async fn connect_sqlite(url: &str) -> Result<Self, Error> {
let pool = sqlx::SqlitePool::connect(url)
.await
.map_err(|e| Error::Database(e.to_string()))?;
Ok(Database { pool })
}
pub async fn get(&self, id: u64) -> Result<Data, Error> {
// Query implementation
let _ = id;
Ok(Data::new(id, "from db"))
}
}
// src/error.rs - Error type adapts to features
/// Error type for the library
#[derive(Debug)]
pub enum Error {
/// Generic error
Generic(String),
/// Serialization error (only with serde feature)
#[cfg(feature = "serde")]
Serialization(String),
/// Database error (only with database features)
#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
Database(String),
/// Async error (only with async feature)
#[cfg(feature = "async")]
Async(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Generic(msg) => write!(f, "{}", msg),
#[cfg(feature = "serde")]
Error::Serialization(msg) => write!(f, "serialization error: {}", msg),
#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
Error::Database(msg) => write!(f, "database error: {}", msg),
#[cfg(feature = "async")]
Error::Async(msg) => write!(f, "async error: {}", msg),
}
}
}
impl std::error::Error for Error {}
// tests/feature_tests.rs - Testing features
//! Integration tests that verify feature combinations
#[test]
fn test_core_always_available() {
use mylib::Client;
let client = Client::new("https://example.com");
assert!(client.get("/test").is_ok());
}
#[cfg(feature = "serde")]
#[test]
fn test_serde_serialization() {
use mylib::Data;
let data = Data::new(1, "test");
let json = data.to_json().unwrap();
let restored: Data = Data::from_json(&json).unwrap();
assert_eq!(data.id, restored.id);
}
#[cfg(feature = "async")]
#[tokio::test]
async fn test_async_client() {
use mylib::AsyncClient;
let client = AsyncClient::new("https://example.com");
let result = client.get("/test").await;
assert!(result.is_ok());
}
#[cfg(all(feature = "async", feature = "postgres"))]
#[tokio::test]
async fn test_postgres_integration() {
// Only runs when both features enabled
}
| Principle | Example |
|-----------|---------|
| Additive | Features add functionality, never remove |
| Minimal default | Only essential features enabled by default |
| Documented | Each feature explains what it enables |
| Testable | CI tests common feature combinations |
# Runtime selection
async-tokio = ["tokio"]
async-std = ["async-std"]
# TLS backend selection
native-tls = ["dep:native-tls"]
rustls-tls = ["dep:rustls"]
# no_std support
std = []
alloc = []
# Debugging levels
debug = ["tracing/max_level_debug"]
# Full feature set
full = ["std", "async-tokio", "rustls-tls", "serde"]
# DON'T: Mutually exclusive features that break
[features]
backend-a = ["dep:a"]
backend-b = ["dep:b"]
# What happens if both enabled?
# DO: Document exclusivity or handle both
[features]
backend-a = ["dep:a"]
backend-b = ["dep:b"]
# Docs: "Enable only one backend"
# Or handle both in code with priority
// DON'T: Feature that removes functionality
#[cfg(not(feature = "minimal"))]
pub fn advanced_feature() { }
// DO: Feature that adds functionality
#[cfg(feature = "advanced")]
pub fn advanced_feature() { }
no_std feature that removes std dependencyRun this code in the official Rust Playground