Development Patterns
This document defines the standard patterns used across the IceGate codebase for config, errors, HTTP routes, handlers, and services.
1. Config Pattern
File: {module}/config.rs
//! Configuration for {MODULE}.
use serde::{Deserialize, Serialize};
use icegate_common::config::ServerConfig;
const DEFAULT_HOST: &str = "0.0.0.0";
const DEFAULT_PORT: u16 = 4318;
/// Configuration for {MODULE} server.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ModuleConfig {
pub enabled: bool,
pub host: String,
pub port: u16,
}
impl Default for ModuleConfig {
fn default() -> Self {
Self {
enabled: true,
host: DEFAULT_HOST.to_string(),
port: DEFAULT_PORT,
}
}
}
impl ServerConfig for ModuleConfig {
fn name(&self) -> &'static str { "Module Name" }
fn enabled(&self) -> bool { self.enabled }
fn port(&self) -> u16 { self.port }
}
impl ModuleConfig {
pub fn validate(&self) -> Result<()> {
// Validation logic
Ok(())
}
}
Key points:
- Use
#[derive(Debug, Clone, Serialize, Deserialize)] - Use
#[serde(default)]at struct level - Use
#[serde(rename_all = "lowercase")]for enum variants - Implement
ServerConfigtrait for port validation - Implement
validate()method for custom validation - Define constants for default values
2. Crate Error Pattern
File: {crate}/src/error.rs
//! Error types for {CRATE}.
use std::io;
/// Result type for {CRATE} operations.
pub type Result<T> = std::result::Result<T, CrateError>;
/// Errors for {CRATE} operations.
#[derive(Debug, thiserror::Error)]
pub enum CrateError {
#[error("decode error: {0}")]
Decode(String),
#[error("{0}")]
Validation(String),
#[error("not implemented: {0}")]
NotImplemented(String),
#[error("configuration error: {0}")]
Config(String),
#[error("io error: {0}")]
Io(#[from] io::Error),
#[error("iceberg error: {0}")]
Iceberg(#[from] iceberg::Error),
}
// Cross-crate conversion
impl From<icegate_common::error::CommonError> for CrateError {
fn from(err: icegate_common::error::CommonError) -> Self {
use icegate_common::error::CommonError;
match err {
CommonError::Config(msg) => Self::Config(msg),
CommonError::Iceberg(e) => Self::Iceberg(e),
// ... other conversions
}
}
}
Key points:
- Use
thiserror = "2"for derive macro - Define
Result<T>type alias - Use
#[from]for automaticFromimplementations - Implement manual
Fromfor cross-crate error conversion
3. HTTP Transport Error Pattern
File: {module}/error.rs
//! HTTP error handling for {MODULE} API.
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
use super::models::{ErrorResponse, ErrorType};
use crate::error::CrateError;
/// Result type for {MODULE} handlers.
pub type ModuleResult<T> = Result<T, ModuleError>;
/// Newtype wrapper implementing `IntoResponse`.
#[derive(Debug)]
pub struct ModuleError(pub CrateError);
impl From<CrateError> for ModuleError {
fn from(err: CrateError) -> Self { Self(err) }
}
impl IntoResponse for ModuleError {
fn into_response(self) -> Response {
let (status, error_type) = match &self.0 {
CrateError::Decode(_) | CrateError::Validation(_) =>
(StatusCode::BAD_REQUEST, ErrorType::BadData),
CrateError::NotImplemented(_) =>
(StatusCode::NOT_IMPLEMENTED, ErrorType::NotImplemented),
_ => (StatusCode::INTERNAL_SERVER_ERROR, ErrorType::Internal),
};
(status, Json(ErrorResponse::new(error_type, self.0.to_string()))).into_response()
}
}
Key points:
- Create newtype wrapper around crate error
- Implement
From<CrateError>for ergonomic?usage - Implement
IntoResponsefor automatic HTTP response conversion - Map error variants to appropriate HTTP status codes
4. gRPC Transport Error Pattern
File: {module}/error.rs
//! gRPC error handling for {MODULE} API.
use tonic::{Code, Status};
use crate::error::CrateError;
#[derive(Debug)]
pub struct GrpcError(pub CrateError);
impl From<CrateError> for GrpcError {
fn from(err: CrateError) -> Self { Self(err) }
}
impl From<GrpcError> for Status {
fn from(err: GrpcError) -> Self {
let (code, msg) = match &err.0 {
CrateError::Decode(_) | CrateError::Validation(_) =>
(Code::InvalidArgument, err.0.to_string()),
CrateError::NotImplemented(_) =>
(Code::Unimplemented, err.0.to_string()),
_ => (Code::Internal, err.0.to_string()),
};
Self::new(code, msg)
}
}
Key points:
- Create newtype wrapper around crate error
- Implement
From<GrpcError> for Statusfor tonic integration - Map error variants to appropriate gRPC status codes
5. Response Models Pattern
File: {module}/models.rs
//! Response models for {MODULE} API.
use serde::Serialize;
#[derive(Debug, Serialize, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum ResponseStatus { Success, Error }
#[derive(Debug, Serialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum ErrorType { BadData, NotImplemented, Internal }
#[derive(Debug, Serialize)]
pub struct ErrorResponse {
pub error: String,
#[serde(rename = "errorType")]
pub error_type: ErrorType,
}
impl ErrorResponse {
pub fn new(error_type: ErrorType, message: impl Into<String>) -> Self {
Self { error: message.into(), error_type }
}
}
Key points:
- Use typed enums for status and error types
- Use
#[serde(rename_all = "...")]for JSON field naming - Provide constructor methods for ergonomic creation
6. Handler Pattern
File: {module}/handlers.rs
pub async fn handler_name(
State(state): State<ModuleState>,
headers: HeaderMap,
Query(params): Query<ParamsStruct>,
) -> ModuleResult<impl IntoResponse> {
// Extract tenant from headers
let tenant_id = extract_tenant_id(&headers);
// Process request
let result = process(&state, ¶ms).await?;
Ok((StatusCode::OK, Json(Response::success(result))))
}
Key points:
- Use typed extractors:
State<T>,Query<T>,Path<T>,HeaderMap - Return
ModuleResult<impl IntoResponse>for error handling - Use
?operator for error propagation - Return tuple
(StatusCode, Json<T>)for response
7. Server/Routes Pattern
File: {module}/server.rs
#[derive(Clone)]
pub struct ModuleState {
pub resource: Arc<SharedResource>,
}
pub async fn run(
resource: Arc<SharedResource>,
config: ModuleConfig,
cancel_token: CancellationToken,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?;
let state = ModuleState { resource };
let app = super::routes::routes(state);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app)
.with_graceful_shutdown(async move { cancel_token.cancelled().await })
.await?;
Ok(())
}
File: {module}/routes.rs
pub fn routes(state: ModuleState) -> Router {
Router::new()
.route("/v1/endpoint", post(handlers::endpoint_handler))
.route("/health", get(handlers::health))
.with_state(state)
}
Key points:
- State struct must derive
Clone - Use
Arc<T>for shared resources - Use
CancellationTokenfor graceful shutdown - Separate routes into dedicated module
- Use
with_state()to inject state into router
Summary
| Component | Pattern |
|---|---|
| Config | Derives + ServerConfig trait + validate() |
| Crate Errors | thiserror + Result alias + From impls |
| HTTP Errors | Newtype + IntoResponse |
| gRPC Errors | Newtype + Into<Status> |
| Models | Typed response envelopes |
| Handlers | Typed extractors + Result return |
| Routes | Router builder with state |
| Servers | CancellationToken shutdown |
Previous
Next