#![allow(dead_code, unused, clippy::type_complexity)]
use std::collections::HashMap;
#[derive(Debug, Clone)]
enum IsolationStrategy {
RowLevel { tenant_column: String },
Schema { schema_prefix: String },
Database { url_template: String },
}
#[derive(Debug, Clone)]
struct TenantContext {
id: String,
name: Option<String>,
metadata: HashMap<String, String>,
}
impl TenantContext {
fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
name: None,
metadata: HashMap::new(),
}
}
fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
}
struct TenantConfig {
strategy: IsolationStrategy,
default_tenant: Option<String>,
require_tenant: bool,
}
impl TenantConfig {
fn builder() -> TenantConfigBuilder {
TenantConfigBuilder::default()
}
}
#[derive(Default)]
struct TenantConfigBuilder {
strategy: Option<IsolationStrategy>,
default_tenant: Option<String>,
require_tenant: bool,
}
impl TenantConfigBuilder {
fn strategy(mut self, strategy: IsolationStrategy) -> Self {
self.strategy = Some(strategy);
self
}
fn default_tenant(mut self, tenant: impl Into<String>) -> Self {
self.default_tenant = Some(tenant.into());
self
}
fn require_tenant(mut self) -> Self {
self.require_tenant = true;
self
}
fn build(self) -> TenantConfig {
TenantConfig {
strategy: self.strategy.unwrap_or(IsolationStrategy::RowLevel {
tenant_column: "tenant_id".to_string(),
}),
default_tenant: self.default_tenant,
require_tenant: self.require_tenant,
}
}
}
struct TenantMiddleware {
config: TenantConfig,
}
impl TenantMiddleware {
fn new(config: TenantConfig) -> Self {
Self { config }
}
}
trait TenantResolver: Send + Sync {
fn resolve(&self, request: &MockRequest) -> Option<TenantContext>;
}
struct HeaderResolver {
header_name: String,
}
impl HeaderResolver {
fn new(header_name: impl Into<String>) -> Self {
Self {
header_name: header_name.into(),
}
}
}
impl TenantResolver for HeaderResolver {
fn resolve(&self, request: &MockRequest) -> Option<TenantContext> {
request
.headers
.get(&self.header_name)
.map(|id| TenantContext::new(id.clone()))
}
}
struct SubdomainResolver;
impl TenantResolver for SubdomainResolver {
fn resolve(&self, request: &MockRequest) -> Option<TenantContext> {
request.host.as_ref().and_then(|host| {
let parts: Vec<&str> = host.split('.').collect();
if parts.len() >= 2 {
Some(TenantContext::new(parts[0].to_string()))
} else {
None
}
})
}
}
struct MockRequest {
headers: HashMap<String, String>,
host: Option<String>,
path: String,
}
struct TenantAwareClient {
tenant: Option<TenantContext>,
config: TenantConfig,
}
impl TenantAwareClient {
fn new(config: TenantConfig) -> Self {
Self {
tenant: None,
config,
}
}
fn with_tenant(&self, tenant: impl Into<TenantContext>) -> Self {
Self {
tenant: Some(tenant.into()),
config: TenantConfig {
strategy: self.config.strategy.clone(),
default_tenant: self.config.default_tenant.clone(),
require_tenant: self.config.require_tenant,
},
}
}
fn user(&self) -> TenantUserQuery {
TenantUserQuery {
tenant: self.tenant.clone(),
strategy: self.config.strategy.clone(),
}
}
fn current_tenant(&self) -> Option<&TenantContext> {
self.tenant.as_ref()
}
}
impl From<String> for TenantContext {
fn from(id: String) -> Self {
TenantContext::new(id)
}
}
impl From<&str> for TenantContext {
fn from(id: &str) -> Self {
TenantContext::new(id)
}
}
#[derive(Debug, Clone)]
struct User {
id: i32,
email: String,
tenant_id: String,
}
struct TenantUserQuery {
tenant: Option<TenantContext>,
strategy: IsolationStrategy,
}
impl TenantUserQuery {
fn find_many(self) -> TenantUserFindMany {
TenantUserFindMany {
tenant: self.tenant,
strategy: self.strategy,
}
}
fn create(self, _data: CreateUserData) -> TenantUserCreate {
TenantUserCreate {
tenant: self.tenant,
strategy: self.strategy,
}
}
}
struct TenantUserFindMany {
tenant: Option<TenantContext>,
strategy: IsolationStrategy,
}
impl TenantUserFindMany {
async fn exec(self) -> Result<Vec<User>, Box<dyn std::error::Error>> {
let tenant_id = self
.tenant
.as_ref()
.map(|t| t.id.clone())
.unwrap_or_else(|| "default".to_string());
match &self.strategy {
IsolationStrategy::RowLevel { tenant_column } => {
println!(
" [RowLevel] Adding WHERE {} = '{}'",
tenant_column, tenant_id
);
}
IsolationStrategy::Schema { schema_prefix } => {
println!(" [Schema] Using schema: {}_{}", schema_prefix, tenant_id);
}
IsolationStrategy::Database { url_template } => {
println!(
" [Database] Connecting to: {}",
url_template.replace("{tenant}", &tenant_id)
);
}
}
Ok(vec![
User {
id: 1,
email: format!("user1@{}.example.com", tenant_id),
tenant_id: tenant_id.clone(),
},
User {
id: 2,
email: format!("user2@{}.example.com", tenant_id),
tenant_id,
},
])
}
}
struct CreateUserData {
email: String,
}
struct TenantUserCreate {
tenant: Option<TenantContext>,
strategy: IsolationStrategy,
}
impl TenantUserCreate {
async fn exec(self) -> Result<User, Box<dyn std::error::Error>> {
let tenant_id = self
.tenant
.as_ref()
.map(|t| t.id.clone())
.unwrap_or_else(|| "default".to_string());
match &self.strategy {
IsolationStrategy::RowLevel { tenant_column } => {
println!(" [RowLevel] Setting {} = '{}'", tenant_column, tenant_id);
}
IsolationStrategy::Schema { schema_prefix } => {
println!(
" [Schema] Inserting into: {}_{}.users",
schema_prefix, tenant_id
);
}
IsolationStrategy::Database { .. } => {
println!(" [Database] Inserting into tenant database");
}
}
Ok(User {
id: 3,
email: format!("new@{}.example.com", tenant_id),
tenant_id,
})
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Prax Multi-Tenant Examples ===\n");
println!("--- Row-Level Tenant Isolation ---");
println!("All tenants share the same tables, filtered by tenant_id column.\n");
let config = TenantConfig::builder()
.strategy(IsolationStrategy::RowLevel {
tenant_column: "tenant_id".to_string(),
})
.require_tenant()
.build();
let client = TenantAwareClient::new(config);
let tenant_client = client.with_tenant("acme-corp");
println!("Querying users for tenant 'acme-corp':");
let users = tenant_client.user().find_many().exec().await?;
for user in &users {
println!(" {} (tenant: {})", user.email, user.tenant_id);
}
println!();
let other_tenant = client.with_tenant("other-corp");
println!("Querying users for tenant 'other-corp':");
let users = other_tenant.user().find_many().exec().await?;
for user in &users {
println!(" {} (tenant: {})", user.email, user.tenant_id);
}
println!();
println!("--- Schema-Based Tenant Isolation ---");
println!("Each tenant has a separate database schema.\n");
let schema_config = TenantConfig::builder()
.strategy(IsolationStrategy::Schema {
schema_prefix: "tenant".to_string(),
})
.build();
let schema_client = TenantAwareClient::new(schema_config);
let tenant_client = schema_client.with_tenant("acme");
println!("Querying users in schema 'tenant_acme':");
let _users = tenant_client.user().find_many().exec().await?;
println!();
println!("--- Database-Per-Tenant Isolation ---");
println!("Each tenant has a separate database.\n");
let db_config = TenantConfig::builder()
.strategy(IsolationStrategy::Database {
url_template: "postgresql://localhost/{tenant}_db".to_string(),
})
.build();
let db_client = TenantAwareClient::new(db_config);
let tenant_client = db_client.with_tenant("acme");
println!("Querying users in database 'acme_db':");
let _users = tenant_client.user().find_many().exec().await?;
println!();
println!("--- Tenant Resolution ---");
println!("Header-based resolver (X-Tenant-ID):");
let header_resolver = HeaderResolver::new("X-Tenant-ID");
let request = MockRequest {
headers: [("X-Tenant-ID".to_string(), "acme-corp".to_string())]
.into_iter()
.collect(),
host: None,
path: "/api/users".to_string(),
};
if let Some(tenant) = header_resolver.resolve(&request) {
println!(" Resolved tenant: {}", tenant.id);
}
println!();
println!("Subdomain-based resolver:");
let subdomain_resolver = SubdomainResolver;
let request = MockRequest {
headers: HashMap::new(),
host: Some("acme.myapp.com".to_string()),
path: "/api/users".to_string(),
};
if let Some(tenant) = subdomain_resolver.resolve(&request) {
println!(" Resolved tenant from 'acme.myapp.com': {}", tenant.id);
}
println!();
println!("--- Tenant Context with Metadata ---");
let tenant = TenantContext::new("acme-corp")
.with_name("Acme Corporation")
.with_metadata("plan", "enterprise")
.with_metadata("region", "us-west");
println!(
"Tenant: {} ({})",
tenant.id,
tenant.name.as_deref().unwrap_or("")
);
println!("Metadata:");
for (key, value) in &tenant.metadata {
println!(" {}: {}", key, value);
}
println!();
println!("--- Creating Records with Tenant Context ---");
let row_config = TenantConfig::builder()
.strategy(IsolationStrategy::RowLevel {
tenant_column: "tenant_id".to_string(),
})
.build();
let client = TenantAwareClient::new(row_config);
let tenant_client = client.with_tenant("acme-corp");
println!("Creating user for tenant 'acme-corp':");
let user = tenant_client
.user()
.create(CreateUserData {
email: "new@acme-corp.example.com".to_string(),
})
.exec()
.await?;
println!(" Created: {} (tenant: {})", user.email, user.tenant_id);
println!();
println!("--- Configuration Reference ---");
println!(
r#"
Multi-tenant configuration in prax.toml:
```toml
[tenant]
# Enable multi-tenant support
enabled = true
# Isolation strategy: "row_level", "schema", or "database"
strategy = "row_level"
# Row-level isolation settings
[tenant.row_level]
tenant_column = "tenant_id"
auto_filter = true
auto_set = true
# Schema-based isolation settings
[tenant.schema]
schema_prefix = "tenant_"
create_on_demand = true
# Database-per-tenant settings
[tenant.database]
url_template = "postgresql://localhost/{{tenant}}_db"
pool_per_tenant = true
max_tenants_cached = 100
# Tenant resolution
[tenant.resolver]
type = "header" # "header", "subdomain", "path", or "custom"
header_name = "X-Tenant-ID"
# Default tenant (optional)
default_tenant = "public"
# Require tenant for all queries
require_tenant = true
```
Usage in code:
```rust
use prax_orm::tenant::{{TenantConfig, IsolationStrategy}};
let config = TenantConfig::builder()
.strategy(IsolationStrategy::RowLevel {{
tenant_column: "tenant_id".into(),
}})
.require_tenant()
.build();
let client = PraxClient::new(database_url)
.await?
.with_tenant_config(config);
// Set tenant for requests
let tenant_client = client.with_tenant("acme-corp");
// All queries are now scoped to this tenant
let users = tenant_client.user().find_many().exec().await?;
```
"#
);
println!("=== All examples completed successfully! ===");
Ok(())
}