#[derive(Clone, std::fmt::Debug)]
struct User {
id: std::string::String,
username: std::string::String,
email: std::string::String,
password_hash: std::string::String,
}
impl hexser::domain::entity::HexEntity for User {
type Id = std::string::String;
}
trait UserRepository: hexser::ports::repository::Repository<User> {
fn find_by_username(
&self,
username: &str,
) -> hexser::result::hex_result::HexResult<std::option::Option<User>>;
fn find_by_email(
&self,
email: &str,
) -> hexser::result::hex_result::HexResult<std::option::Option<User>>;
}
trait SessionPort {
fn create_session(
&self,
user_id: &str,
ttl_secs: u64,
) -> hexser::result::hex_result::HexResult<std::string::String>;
fn validate_session(
&self,
token: &str,
) -> hexser::result::hex_result::HexResult<std::option::Option<std::string::String>>;
fn revoke_session(&self, token: &str) -> hexser::result::hex_result::HexResult<()>;
}
struct InMemoryUserRepository {
users: std::sync::Arc<std::sync::Mutex<std::vec::Vec<User>>>,
}
impl InMemoryUserRepository {
fn new() -> Self {
Self {
users: std::sync::Arc::new(std::sync::Mutex::new(std::vec::Vec::new())),
}
}
}
impl hexser::adapters::adapter::Adapter for InMemoryUserRepository {}
impl hexser::ports::repository::Repository<User> for InMemoryUserRepository {
fn save(&mut self, user: User) -> hexser::result::hex_result::HexResult<()> {
let mut users = self.users.lock().unwrap();
if let std::option::Option::Some(pos) = users.iter().position(|u| u.id == user.id) {
users[pos] = user;
} else {
users.push(user);
}
std::result::Result::Ok(())
}
}
impl UserRepository for InMemoryUserRepository {
fn find_by_username(
&self,
username: &str,
) -> hexser::result::hex_result::HexResult<std::option::Option<User>> {
let users = self.users.lock().unwrap();
std::result::Result::Ok(users.iter().find(|u| u.username == username).cloned())
}
fn find_by_email(
&self,
email: &str,
) -> hexser::result::hex_result::HexResult<std::option::Option<User>> {
let users = self.users.lock().unwrap();
std::result::Result::Ok(users.iter().find(|u| u.email == email).cloned())
}
}
struct InMemorySessionAdapter {
sessions: std::sync::Arc<
std::sync::Mutex<std::collections::HashMap<std::string::String, (std::string::String, u64)>>,
>,
}
impl InMemorySessionAdapter {
fn new() -> Self {
Self {
sessions: std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
}
}
fn current_timestamp() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
}
}
impl hexser::adapters::adapter::Adapter for InMemorySessionAdapter {}
impl SessionPort for InMemorySessionAdapter {
fn create_session(
&self,
user_id: &str,
ttl_secs: u64,
) -> hexser::result::hex_result::HexResult<std::string::String> {
let token = std::format!("token_{}", Self::current_timestamp());
let expires_at = Self::current_timestamp() + ttl_secs;
let mut sessions = self.sessions.lock().unwrap();
sessions.insert(token.clone(), (user_id.to_string(), expires_at));
std::result::Result::Ok(token)
}
fn validate_session(
&self,
token: &str,
) -> hexser::result::hex_result::HexResult<std::option::Option<std::string::String>> {
let sessions = self.sessions.lock().unwrap();
if let std::option::Option::Some((user_id, expires_at)) = sessions.get(token) {
if *expires_at > Self::current_timestamp() {
return std::result::Result::Ok(std::option::Option::Some(user_id.clone()));
}
}
std::result::Result::Ok(std::option::Option::None)
}
fn revoke_session(&self, token: &str) -> hexser::result::hex_result::HexResult<()> {
let mut sessions = self.sessions.lock().unwrap();
sessions.remove(token);
std::result::Result::Ok(())
}
}
struct AppContext {
user_repo: std::boxed::Box<dyn UserRepository>,
session_port: std::boxed::Box<dyn SessionPort>,
}
impl AppContext {
fn new_in_memory() -> Self {
Self {
user_repo: std::boxed::Box::new(InMemoryUserRepository::new()),
session_port: std::boxed::Box::new(InMemorySessionAdapter::new()),
}
}
}
fn main() -> hexser::result::hex_result::HexResult<()> {
std::println!("User Authentication Integration Example\n");
std::println!("{}", "=".repeat(60));
std::println!("\n1. Port Definitions:");
std::println!(" UserRepository: Persistence abstraction");
std::println!(" - find_by_username(&self, username: &str)");
std::println!(" - find_by_email(&self, email: &str)");
std::println!(" SessionPort: Session management abstraction");
std::println!(" - create_session(&self, user_id: &str, ttl_secs: u64)");
std::println!(" - validate_session(&self, token: &str)");
std::println!(" - revoke_session(&self, token: &str)");
std::println!("\n2. Concrete Adapters:");
std::println!(" InMemoryUserRepository: Vec-based storage");
std::println!(" InMemorySessionAdapter: HashMap-based sessions");
std::println!(" (In production: PostgresUserRepository, RedisSessionAdapter)");
std::println!("\n3. Demonstration:");
let mut user_repo = InMemoryUserRepository::new();
let session_adapter = InMemorySessionAdapter::new();
let user = User {
id: std::string::String::from("user-001"),
username: std::string::String::from("alice"),
email: std::string::String::from("alice@example.com"),
password_hash: std::string::String::from("$2b$12$hashedpassword"),
};
<InMemoryUserRepository as hexser::ports::repository::Repository<User>>::save(
&mut user_repo,
user.clone(),
)?;
std::println!(" ✓ User created: {} ({})", user.username, user.email);
let found = user_repo.find_by_username("alice")?;
std::println!(" ✓ User found by username: {:?}", found.is_some());
let token = session_adapter.create_session(&user.id, 3600)?;
std::println!(" ✓ Session created: {token} (TTL: 3600s)");
let validated = session_adapter.validate_session(&token)?;
std::println!(" ✓ Session validated: user_id = {validated:?}");
session_adapter.revoke_session(&token)?;
std::println!(" ✓ Session revoked");
let revalidated = session_adapter.validate_session(&token)?;
std::println!(" ✓ Session no longer valid: {:?}", revalidated.is_none());
std::println!("\n4. Integration Steps:");
std::println!(" Step 1: Define UserRepository and SessionPort traits");
std::println!(" Step 2: Implement InMemoryUserRepository (dev/test)");
std::println!(" Step 3: Implement InMemorySessionAdapter (dev/test)");
std::println!(" Step 4: Wire adapters in AppContext");
std::println!(" Step 5: Swap adapters for production (Postgres + Redis)");
std::println!("\n5. Production Adapters (pseudocode):");
std::println!(" PostgresUserRepository:");
std::println!(" - Uses sqlx::PgPool for database connection");
std::println!(" - Implements find_by_username with SQL query");
std::println!(" - Error mapping: DB_READ_FAILURE, DB_WRITE_FAILURE");
std::println!(" RedisSessionAdapter:");
std::println!(" - Uses redis::Client for session storage");
std::println!(" - SETEX for create_session (atomic TTL)");
std::println!(" - GET for validate_session");
std::println!(" - DEL for revoke_session");
std::println!("\n6. Error Handling:");
std::println!(" - Hexserror::adapter() for infrastructure failures");
std::println!(" - with_source() preserves underlying error chain");
std::println!(" - with_next_steps() provides actionable guidance");
std::println!(" - Example: CONNECTION_FAILURE, DB_READ_FAILURE, DB_WRITE_FAILURE");
std::println!("\n7. Architecture Benefits:");
std::println!(" ✓ Domain logic (User entity) has no infrastructure dependencies");
std::println!(" ✓ Ports define clear contracts for persistence and sessions");
std::println!(" ✓ Adapters can be swapped without changing business logic");
std::println!(" ✓ Easy to test with in-memory implementations");
std::println!(" ✓ Production adapters use real databases/caches");
std::println!("\n{}", "=".repeat(60));
std::println!("Example complete. See hexser/README.md for production adapter code.");
std::result::Result::Ok(())
}