| name | rust-patterns |
| description | KMS-specific Rust design patterns: newtype wrappers, builder config, command pattern for KMIP ops, trait-based HSM/DB abstraction, key lifecycle state machine. Use as a reference for Rust patterns in this codebase. |
Rust Design Patterns for the KMS Codebase
Reference for KMS-specific Rust patterns. Apply these when implementing new features or refactoring existing code.
Pattern 1 — Newtype Wrappers for Identifiers
Prevent accidental parameter swaps between different string-typed IDs.
async fn get_object(uid: String, owner: String, ...) -> KResult<KmipObject>
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct UniqueIdentifier(pub String);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UserId(pub String);
async fn get_object(uid: &UniqueIdentifier, owner: &UserId, ...) -> KResult<KmipObject>
Use newtypes for:
UniqueIdentifier — KMIP object UIDs
UserId — authenticated user identity
KeyId — key identifiers in HSM contexts
GroupId — access control group identifiers
Pattern 2 — Builder Pattern for Server Configuration
Avoid large constructor argument lists. Use a builder when a config struct has more than 4 fields or optional fields.
let config = ServerConfig::new(db_url, port, tls_cert, tls_key, fips_mode, auth_config, hsm_config, log_level);
let config = ServerConfig::builder()
.database_url(db_url)
.port(9998)
.tls(TlsConfig { cert: cert_path, key: key_path })
.auth(auth_config)
.build()?;
pub struct ServerConfigBuilder { }
impl ServerConfigBuilder {
pub fn database_url(mut self, url: impl Into<String>) -> Self {
self.database_url = Some(url.into());
self
}
pub fn build(self) -> KResult<ServerConfig> {
Ok(ServerConfig {
database_url: self.database_url.ok_or_else(|| kms_error!("database_url required"))?,
})
}
}
Pattern 3 — Command Pattern for KMIP Operations
Encapsulate each KMIP operation as a value to enable uniform dispatch, logging, and access control.
pub struct GetOperation {
pub unique_identifier: UniqueIdentifier,
pub key_format_type: Option<KeyFormatType>,
}
pub trait KmipOperation: Send + Sync {
type Response;
fn operation_type(&self) -> OperationType;
}
impl KmipOperation for GetOperation {
type Response = GetResponse;
fn operation_type(&self) -> OperationType { OperationType::Get }
}
pub async fn dispatch(op: Operation, kms: &KMS, params: &ExtraParams) -> KResult<Operation> {
match op {
Operation::Get(req) => get::execute(kms, req, params).await.map(Operation::GetResponse),
Operation::Create(req) => create::execute(kms, req, params).await.map(Operation::CreateResponse),
}
}
Pattern 4 — Trait-Based Abstraction for Database Backends
The crate/interfaces/ crate defines database traits. Implement them rather than depending on concrete types.
pub async fn store_object(
uid: &UniqueIdentifier,
object: &KmipObject,
db: &dyn ObjectsDb,
) -> KResult<()> {
db.upsert(uid, object).await
}
pub async fn store_object(
uid: &UniqueIdentifier,
object: &KmipObject,
db: &SqliteDb,
) -> KResult<()>
Key traits in crate/interfaces/:
ObjectsDb — KMIP object storage (CRUD)
PermissionsDb — access control (grant/revoke/check)
Hsm — HSM operations (sign, decrypt, generate key)
Pattern 5 — State Machine for Key Lifecycle
KMIP key states are a well-defined state machine. Represent them as types, not strings.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum State {
PreActive,
Active,
Deactivated,
Compromised,
Destroyed,
DestroyedCompromised,
}
impl State {
pub fn can_transition_to(self, next: State) -> bool {
matches!(
(self, next),
(State::PreActive, State::Active)
| (State::Active, State::Deactivated)
| (State::Active, State::Compromised)
| (State::Deactivated, State::Compromised)
| (State::Deactivated, State::Destroyed)
| (State::Compromised, State::Destroyed)
| (State::Compromised, State::DestroyedCompromised)
)
}
}
if !current_state.can_transition_to(target_state) {
return Err(kms_error!("Invalid state transition: {:?} → {:?}", current_state, target_state));
}
Pattern 6 — Macro for KMIP Error Construction
Avoid boilerplate in error creation across operation files:
return Err(KmsError::InvalidRequest(format!("Key {} not found", uid.0)));
return Err(kms_error!("Key {} not found", uid.0));
Pattern 7 — Extension Traits for Cross-Cutting Concerns
Add methods to external types without modifying them:
pub trait KmsResultExt<T> {
fn map_access_denied(self) -> KResult<T>;
}
impl<T> KmsResultExt<T> for Result<T, DbError> {
fn map_access_denied(self) -> KResult<T> {
self.map_err(|e| match e {
DbError::NotFound => KmsError::Unauthorized("object not found or access denied".into()),
other => KmsError::Database(other),
})
}
}
let obj = db.get(&uid).await.map_access_denied()?;
Pattern 8 — macro_rules! for Repetitive Match Dispatch
When multiple operations share identical boilerplate wiring:
macro_rules! impl_kmip_accessor {
($op:ident, $handler:path) => {
Operation::$op(req) => {
trace!("{} operation", stringify!($op));
$handler(kms, req, params).await.map(Operation::$op)
}
};
}
match operation {
impl_kmip_accessor!(Get, get::execute),
impl_kmip_accessor!(Create, create::execute),
impl_kmip_accessor!(Locate, locate::execute),
}
Quick Rules
- Prefer generics over
dyn Trait unless the set of types is open at runtime
- Keep all
use statements at the top of the file — never inline inside function bodies
- Gate non-FIPS code at the function/module level with
#[cfg(feature = "non-fips")]
- Keep functions ≤ 50 lines — extract helpers rather than growing functions
- Use
? for error propagation — never .unwrap() in production code
- Log with correct levels:
trace! per-request, debug! internal state, info! lifecycle, warn!/error! operator-actionable only