Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:25:50 +08:00
commit e8782848b3
10 changed files with 2504 additions and 0 deletions

View File

@@ -0,0 +1,477 @@
---
description: Add a new adapter implementation for an existing port
---
You are helping create a new adapter implementation for an existing port in a Rust hexagonal architecture project.
## Your Task
Create a concrete adapter implementation for a port trait, following best practices for the specific technology being used.
## Steps
1. **List Available Ports**
First, scan the project to find existing ports:
- Check `src/ports/driven.rs` for driven ports
- Check `src/ports/driving.rs` for driving ports
Display them to the user:
```
Available Ports:
Driven Ports (Secondary):
- UserRepository
- PaymentGateway
- EmailService
Driving Ports (Primary):
- CreateUserUseCase
- ProcessOrderUseCase
```
2. **Ask User for Details**
Ask (if not already provided):
- Which port to implement?
- What technology/adapter type? (e.g., PostgreSQL, MongoDB, HTTP, InMemory, Mock)
- Any configuration needed? (connection strings, API keys, etc.)
3. **Create Adapter Implementation**
Based on the technology, create the appropriate adapter.
**Database Adapters (PostgreSQL example)**:
```rust
//! PostgreSQL implementation of [PortName]
//!
//! This adapter implements [PortName] using PostgreSQL via SQLx.
use crate::domain::models::[Entity];
use crate::ports::driven::{[PortName], [PortName]Error};
use async_trait::async_trait;
use sqlx::PgPool;
/// PostgreSQL implementation of [PortName]
pub struct Postgres[PortName] {
pool: PgPool,
}
impl Postgres[PortName] {
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
}
#[async_trait]
impl [PortName] for Postgres[PortName] {
async fn find_by_id(&self, id: &str) -> Result<[Entity], [PortName]Error> {
sqlx::query_as!(
[Entity],
"SELECT * FROM [table_name] WHERE id = $1",
id
)
.fetch_one(&self.pool)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => [PortName]Error::NotFound(id.to_string()),
_ => [PortName]Error::Database(e.to_string()),
})
}
async fn save(&self, entity: &[Entity]) -> Result<(), [PortName]Error> {
sqlx::query!(
"INSERT INTO [table_name] (id, [fields]) VALUES ($1, $2)
ON CONFLICT (id) DO UPDATE SET [fields] = $2",
entity.id(),
// other fields
)
.execute(&self.pool)
.await
.map_err(|e| [PortName]Error::Database(e.to_string()))?;
Ok(())
}
async fn delete(&self, id: &str) -> Result<(), [PortName]Error> {
sqlx::query!("DELETE FROM [table_name] WHERE id = $1", id)
.execute(&self.pool)
.await
.map_err(|e| [PortName]Error::Database(e.to_string()))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
// Recommend using testcontainers for integration tests
#[sqlx::test]
async fn test_find_by_id(pool: PgPool) {
let repo = Postgres[PortName]::new(pool);
// Test implementation
}
}
```
**HTTP Client Adapters**:
```rust
//! HTTP implementation of [PortName]
//!
//! This adapter implements [PortName] using reqwest HTTP client.
use crate::ports::driven::{[PortName], [PortName]Error};
use async_trait::async_trait;
use reqwest::Client;
use serde::{Deserialize, Serialize};
/// HTTP client implementation of [PortName]
pub struct Http[PortName] {
client: Client,
base_url: String,
api_key: Option<String>,
}
impl Http[PortName] {
pub fn new(base_url: String, api_key: Option<String>) -> Self {
Self {
client: Client::new(),
base_url,
api_key,
}
}
}
#[async_trait]
impl [PortName] for Http[PortName] {
async fn [method](&self, [params]) -> Result<[ReturnType], [PortName]Error> {
let url = format!("{}/[endpoint]", self.base_url);
let mut request = self.client.get(&url);
if let Some(key) = &self.api_key {
request = request.header("Authorization", format!("Bearer {}", key));
}
let response = request
.send()
.await
.map_err(|e| [PortName]Error::Network(e.to_string()))?;
if !response.status().is_success() {
return Err([PortName]Error::HttpError(response.status().as_u16()));
}
response
.json::<[ReturnType]>()
.await
.map_err(|e| [PortName]Error::Deserialization(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use wiremock::{MockServer, Mock, ResponseTemplate};
use wiremock::matchers::{method, path};
#[tokio::test]
async fn test_[method]() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/[endpoint]"))
.respond_with(ResponseTemplate::new(200).set_body_json(/* mock response */))
.mount(&mock_server)
.await;
let adapter = Http[PortName]::new(mock_server.uri(), None);
// Test implementation
}
}
```
**In-Memory Adapters (for testing)**:
```rust
//! In-memory implementation of [PortName]
//!
//! This adapter provides an in-memory implementation useful for testing.
use crate::domain::models::[Entity];
use crate::ports::driven::{[PortName], [PortName]Error};
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
/// In-memory implementation of [PortName]
#[derive(Clone)]
pub struct InMemory[PortName] {
storage: Arc<RwLock<HashMap<String, [Entity]>>>,
}
impl InMemory[PortName] {
pub fn new() -> Self {
Self {
storage: Arc::new(RwLock::new(HashMap::new())),
}
}
}
impl Default for InMemory[PortName] {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl [PortName] for InMemory[PortName] {
async fn find_by_id(&self, id: &str) -> Result<[Entity], [PortName]Error> {
let storage = self.storage.read().await;
storage
.get(id)
.cloned()
.ok_or_else(|| [PortName]Error::NotFound(id.to_string()))
}
async fn save(&self, entity: &[Entity]) -> Result<(), [PortName]Error> {
let mut storage = self.storage.write().await;
storage.insert(entity.id().to_string(), entity.clone());
Ok(())
}
async fn delete(&self, id: &str) -> Result<(), [PortName]Error> {
let mut storage = self.storage.write().await;
storage
.remove(id)
.ok_or_else(|| [PortName]Error::NotFound(id.to_string()))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_save_and_find() {
let repo = InMemory[PortName]::new();
// Test implementation
}
}
```
**Redis Cache Adapter**:
```rust
//! Redis implementation of [CacheName]
//!
//! This adapter implements caching using Redis.
use crate::ports::driven::{[CacheName], [CacheName]Error};
use async_trait::async_trait;
use redis::{AsyncCommands, Client};
use serde::{de::DeserializeOwned, Serialize};
/// Redis implementation of [CacheName]
pub struct Redis[CacheName] {
client: Client,
}
impl Redis[CacheName] {
pub fn new(redis_url: &str) -> Result<Self, redis::RedisError> {
let client = Client::open(redis_url)?;
Ok(Self { client })
}
}
#[async_trait]
impl [CacheName] for Redis[CacheName] {
async fn get<T>(&self, key: &str) -> Result<Option<T>, [CacheName]Error>
where
T: DeserializeOwned,
{
let mut conn = self.client.get_async_connection()
.await
.map_err(|e| [CacheName]Error::Connection(e.to_string()))?;
let value: Option<String> = conn.get(key)
.await
.map_err(|e| [CacheName]Error::Operation(e.to_string()))?;
match value {
Some(v) => {
let parsed = serde_json::from_str(&v)
.map_err(|e| [CacheName]Error::Serialization(e.to_string()))?;
Ok(Some(parsed))
}
None => Ok(None),
}
}
async fn set<T>(&self, key: &str, value: &T, ttl_seconds: Option<u64>) -> Result<(), [CacheName]Error>
where
T: Serialize,
{
let mut conn = self.client.get_async_connection()
.await
.map_err(|e| [CacheName]Error::Connection(e.to_string()))?;
let serialized = serde_json::to_string(value)
.map_err(|e| [CacheName]Error::Serialization(e.to_string()))?;
if let Some(ttl) = ttl_seconds {
conn.set_ex(key, serialized, ttl)
.await
.map_err(|e| [CacheName]Error::Operation(e.to_string()))?;
} else {
conn.set(key, serialized)
.await
.map_err(|e| [CacheName]Error::Operation(e.to_string()))?;
}
Ok(())
}
}
```
4. **Update Module Exports**
Add to `src/adapters/driven/mod.rs` (or `driving/mod.rs`):
```rust
pub mod [adapter_name];
```
5. **Update Dependencies**
Check if required dependencies are in Cargo.toml and suggest additions:
For PostgreSQL:
```toml
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-native-tls"] }
```
For HTTP:
```toml
reqwest = { version = "0.11", features = ["json"] }
```
For Redis:
```toml
redis = { version = "0.24", features = ["tokio-comp", "connection-manager"] }
```
6. **Provide Integration Example**
Show how to wire up the adapter in the application:
```rust
// In main.rs or application setup
use crate::adapters::driven::[Adapter];
use crate::domain::services::[Service];
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Setup adapter
let pool = PgPoolOptions::new()
.connect("postgresql://localhost/mydb")
.await?;
let adapter = [Adapter]::new(pool);
// Create domain service with adapter
let service = [Service]::new(adapter);
// Use the service
service.do_work().await?;
Ok(())
}
```
7. **Suggest Testing Approach**
Based on adapter type:
**For Database Adapters**:
```
Testing recommendations:
1. Use `sqlx::test` macro for integration tests
2. Consider testcontainers for isolated test databases
3. Create test fixtures for common scenarios
4. Test error cases (not found, connection errors)
```
**For HTTP Adapters**:
```
Testing recommendations:
1. Use wiremock or mockito for HTTP mocking
2. Test success and error responses
3. Test authentication/authorization
4. Test timeout and retry logic
```
**For In-Memory Adapters**:
```
Testing recommendations:
1. Test concurrent access with multiple threads
2. Verify data consistency
3. Test all CRUD operations
```
8. **Provide Summary**
Tell the user:
```
✅ Adapter '[AdapterName]' created successfully!
## Files Created/Modified:
- `src/adapters/[driving|driven]/[adapter_name].rs` - Adapter implementation
- `src/adapters/[driving|driven]/mod.rs` - Module export
## Dependencies to Add:
[List required Cargo.toml dependencies]
## Next Steps:
1. Add dependencies to Cargo.toml
2. Implement the TODO items in the adapter
3. Write tests for the adapter
4. Integrate adapter in your application setup
## Testing:
```bash
cargo test
```
## Example Integration:
[Show integration example from step 6]
```
## Technology Templates
Maintain templates for common technologies:
- PostgreSQL, MySQL, SQLite (via sqlx)
- MongoDB (via mongodb crate)
- Redis (via redis crate)
- HTTP clients (via reqwest)
- gRPC (via tonic)
- InMemory (HashMap/RwLock)
- Mock (for testing)
## Important Notes
- Always implement `Send + Sync` for thread safety
- Use appropriate error mapping from library errors to port errors
- Include comprehensive tests
- Add documentation comments
- Follow Rust async best practices
- Consider connection pooling for database adapters
## After Completion
Ask the user if they want to:
1. Create another adapter for the same port (e.g., a test double)
2. Add more methods to the adapter
3. Create integration tests

View File

@@ -0,0 +1,264 @@
---
description: Add a new port (interface) to your hexagonal architecture
---
You are helping add a new port (interface/trait) to a Rust hexagonal architecture project.
## Your Task
Guide the user through creating a new port trait and provide the implementation scaffold.
## Steps
1. **Ask User for Port Details**
Ask the following questions (if not already provided):
- Port name (e.g., "UserRepository", "PaymentGateway", "EmailService")
- Port type: "driving" (primary - what domain offers) or "driven" (secondary - what domain needs)
- Brief description of what this port does
- Required methods (you can suggest common ones based on the name)
2. **Determine Port Type**
**Driving Ports** (Primary):
- Use cases, application services
- What the application offers to external actors
- Usually have an "execute" or similar method
- Example: `CreateUserUseCase`, `GetOrderDetails`
**Driven Ports** (Secondary):
- Repositories, gateways, external services
- What the domain needs from infrastructure
- Usually CRUD or external API operations
- Example: `UserRepository`, `EmailGateway`, `PaymentService`
3. **Create the Port Trait**
Based on the port type, create the trait in the appropriate file.
**For Driven Ports** (in `src/ports/driven.rs`):
```rust
/// [Description of what this port does]
///
/// This port is implemented by adapters that provide [functionality].
#[async_trait]
pub trait [PortName]: Send + Sync {
/// [Method description]
async fn [method_name](&self, [params]) -> Result<[ReturnType], [ErrorType]>;
// Add more methods as needed
}
/// Error type for [PortName]
#[derive(Debug, thiserror::Error)]
pub enum [PortName]Error {
#[error("Not found: {0}")]
NotFound(String),
#[error("Operation failed: {0}")]
OperationFailed(String),
#[error("Unknown error: {0}")]
Unknown(String),
}
```
**For Driving Ports** (in `src/ports/driving.rs`):
```rust
/// [Description of use case]
///
/// This use case [what it does for the user].
#[async_trait]
pub trait [UseCaseName]: Send + Sync {
async fn execute(&self, input: [UseCaseName]Input) -> Result<[UseCaseName]Output, [UseCaseName]Error>;
}
/// Input for [UseCaseName]
#[derive(Debug, Clone)]
pub struct [UseCaseName]Input {
// Input fields
}
/// Output for [UseCaseName]
#[derive(Debug, Clone)]
pub struct [UseCaseName]Output {
// Output fields
}
/// Error type for [UseCaseName]
#[derive(Debug, thiserror::Error)]
pub enum [UseCaseName]Error {
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Use case failed: {0}")]
Failed(String),
}
```
4. **Common Port Patterns**
Suggest appropriate methods based on the port name:
**Repository Patterns**:
```rust
async fn find_by_id(&self, id: &str) -> Result<Entity, Error>;
async fn find_all(&self) -> Result<Vec<Entity>, Error>;
async fn save(&self, entity: &Entity) -> Result<(), Error>;
async fn update(&self, entity: &Entity) -> Result<(), Error>;
async fn delete(&self, id: &str) -> Result<(), Error>;
```
**Service/Gateway Patterns**:
```rust
async fn send(&self, data: &Data) -> Result<Response, Error>;
async fn query(&self, params: QueryParams) -> Result<QueryResult, Error>;
```
**Cache Patterns**:
```rust
async fn get(&self, key: &str) -> Result<Option<Value>, Error>;
async fn set(&self, key: &str, value: Value) -> Result<(), Error>;
async fn delete(&self, key: &str) -> Result<(), Error>;
```
5. **Create Adapter Scaffold**
After creating the port, automatically create a scaffold for an adapter implementation:
**For Driven Ports** (create in `src/adapters/driven/`):
```rust
//! [PortName] adapter implementation
//!
//! This adapter implements the [PortName] port using [technology].
use crate::ports::driven::[PortName];
use async_trait::async_trait;
/// [Technology] implementation of [PortName]
pub struct [TechnologyName][PortName] {
// Configuration fields
// e.g., connection pool, client, config
}
impl [TechnologyName][PortName] {
pub fn new(/* config params */) -> Self {
Self {
// Initialize fields
}
}
}
#[async_trait]
impl [PortName] for [TechnologyName][PortName] {
async fn [method_name](&self, [params]) -> Result<[ReturnType], [ErrorType]> {
// TODO: Implement using [technology]
todo!("Implement [method_name]")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_[method_name]() {
// TODO: Add tests
}
}
```
6. **Update Module Exports**
Add the new port to the appropriate module file:
For driven ports, add to `src/ports/driven.rs`:
```rust
pub use self::[port_name]::*;
mod [port_name];
```
Or add to the existing file if it's small.
For adapters, add to `src/adapters/driven/mod.rs`:
```rust
pub mod [adapter_name];
```
7. **Provide Usage Example**
Show the user how to use the new port:
```rust
// In a domain service
use crate::ports::driven::[PortName];
pub struct MyService<R>
where
R: [PortName],
{
[port_field]: R,
}
impl<R> MyService<R>
where
R: [PortName],
{
pub fn new([port_field]: R) -> Self {
Self { [port_field] }
}
pub async fn do_something(&self) -> Result<(), Error> {
self.[port_field].[method]().await?;
Ok(())
}
}
```
8. **Suggest Next Steps**
Tell the user:
```
✅ Port '[PortName]' created successfully!
## Files Created/Modified:
- `src/ports/[driving|driven].rs` - Port trait definition
- `src/adapters/[driving|driven]/[adapter_name].rs` - Adapter scaffold
## Next Steps:
1. Review the port trait and adjust methods as needed
2. Implement the adapter for your specific technology
3. Add the adapter to your application's dependency injection
4. Write tests for the adapter
## Example Usage:
[Show usage example from step 7]
## Implement Adapter:
To implement the adapter for a specific technology (e.g., PostgreSQL, HTTP):
- Use `/rust-hex-add-adapter` to create additional implementations
- Or manually edit `src/adapters/[driving|driven]/[adapter_name].rs`
```
## Port Naming Conventions
- **Repositories**: `[Entity]Repository` (e.g., `UserRepository`, `OrderRepository`)
- **Gateways**: `[Service]Gateway` (e.g., `PaymentGateway`, `EmailGateway`)
- **Services**: `[Domain]Service` (e.g., `AuthenticationService`, `NotificationService`)
- **Use Cases**: `[Action][Entity]` (e.g., `CreateUser`, `GetOrderDetails`)
## Important Notes
- Always use `#[async_trait]` for async trait methods
- Include `Send + Sync` bounds for thread safety
- Define custom error types using `thiserror`
- Add documentation comments explaining the port's purpose
- Follow Rust naming conventions (PascalCase for traits, snake_case for methods)
## After Completion
Confirm with the user that the port was created successfully and ask if they want to:
1. Add more methods to the port
2. Create additional adapters for this port
3. Create another port

346
commands/rust-hex-init.md Normal file
View File

@@ -0,0 +1,346 @@
---
description: Initialize a hexagonal architecture project structure for Rust
---
You are helping initialize a Rust project with hexagonal architecture (ports and adapters pattern).
## Your Task
Create a complete hexagonal architecture directory structure with example files to help the user get started.
## Steps
1. **Verify or Create Base Directory Structure**
Create the following structure:
```
src/
├── domain/
│ ├── mod.rs
│ ├── models.rs
│ └── services.rs
├── ports/
│ ├── mod.rs
│ ├── driving.rs
│ └── driven.rs
├── adapters/
│ ├── mod.rs
│ ├── driving/
│ │ └── mod.rs
│ └── driven/
│ └── mod.rs
└── lib.rs (or main.rs if it exists)
```
2. **Create Domain Layer Files**
**src/domain/mod.rs**:
```rust
//! Domain layer - Core business logic
//!
//! This layer contains:
//! - Domain models (entities, value objects)
//! - Business rules and validations
//! - Domain services
//!
//! The domain layer has NO dependencies on ports or adapters.
pub mod models;
pub mod services;
```
**src/domain/models.rs**:
```rust
//! Domain models and entities
//!
//! Define your business entities here with their behaviors.
use serde::{Deserialize, Serialize};
/// Example domain entity
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExampleEntity {
id: String,
name: String,
}
impl ExampleEntity {
pub fn new(id: String, name: String) -> Result<Self, ValidationError> {
if name.is_empty() {
return Err(ValidationError::EmptyName);
}
Ok(Self { id, name })
}
pub fn id(&self) -> &str {
&self.id
}
pub fn name(&self) -> &str {
&self.name
}
}
#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
#[error("Name cannot be empty")]
EmptyName,
}
```
**src/domain/services.rs**:
```rust
//! Domain services - Business logic orchestration
//!
//! Domain services coordinate between entities and use ports
//! for external dependencies.
use super::models::{ExampleEntity, ValidationError};
use crate::ports::driven::ExampleRepository;
/// Example domain service
pub struct ExampleService<R>
where
R: ExampleRepository,
{
repository: R,
}
impl<R> ExampleService<R>
where
R: ExampleRepository,
{
pub fn new(repository: R) -> Self {
Self { repository }
}
pub async fn get_entity(&self, id: &str) -> Result<ExampleEntity, ServiceError> {
self.repository
.find_by_id(id)
.await
.map_err(ServiceError::Repository)
}
pub async fn create_entity(&self, name: String) -> Result<ExampleEntity, ServiceError> {
let entity = ExampleEntity::new(uuid::Uuid::new_v4().to_string(), name)
.map_err(ServiceError::Validation)?;
self.repository
.save(&entity)
.await
.map_err(ServiceError::Repository)?;
Ok(entity)
}
}
#[derive(Debug, thiserror::Error)]
pub enum ServiceError {
#[error("Validation error: {0}")]
Validation(#[from] ValidationError),
#[error("Repository error: {0}")]
Repository(#[from] crate::ports::driven::RepositoryError),
}
```
3. **Create Ports Layer Files**
**src/ports/mod.rs**:
```rust
//! Ports layer - Interfaces for adapters
//!
//! Ports define the contracts between the domain and the outside world:
//! - Driving ports: What the domain offers to the outside
//! - Driven ports: What the domain needs from the outside
pub mod driving;
pub mod driven;
```
**src/ports/driving.rs**:
```rust
//! Driving ports (Primary ports)
//!
//! These are the interfaces that the domain exposes to the outside world.
//! Driving adapters (like REST APIs, CLI) will use these interfaces.
use crate::domain::models::ExampleEntity;
use async_trait::async_trait;
/// Example driving port - what the application offers
#[async_trait]
pub trait ExampleUseCase: Send + Sync {
async fn execute(&self, input: UseCaseInput) -> Result<ExampleEntity, UseCaseError>;
}
pub struct UseCaseInput {
pub name: String,
}
#[derive(Debug, thiserror::Error)]
pub enum UseCaseError {
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Service error: {0}")]
Service(#[from] crate::domain::services::ServiceError),
}
```
**src/ports/driven.rs**:
```rust
//! Driven ports (Secondary ports)
//!
//! These are the interfaces that the domain needs from the outside world.
//! Driven adapters (like database repositories, HTTP clients) implement these.
use crate::domain::models::ExampleEntity;
use async_trait::async_trait;
/// Example repository port - what the domain needs
#[async_trait]
pub trait ExampleRepository: Send + Sync {
async fn find_by_id(&self, id: &str) -> Result<ExampleEntity, RepositoryError>;
async fn save(&self, entity: &ExampleEntity) -> Result<(), RepositoryError>;
async fn delete(&self, id: &str) -> Result<(), RepositoryError>;
}
#[derive(Debug, thiserror::Error)]
pub enum RepositoryError {
#[error("Not found: {0}")]
NotFound(String),
#[error("Database error: {0}")]
Database(String),
#[error("Unknown error: {0}")]
Unknown(String),
}
```
4. **Create Adapters Layer Files**
**src/adapters/mod.rs**:
```rust
//! Adapters layer - Implementations of ports
//!
//! Adapters connect the domain to the outside world:
//! - Driving adapters: REST API, CLI, gRPC
//! - Driven adapters: Database, HTTP clients, file systems
pub mod driving;
pub mod driven;
```
**src/adapters/driving/mod.rs**:
```rust
//! Driving adapters
//!
//! These adapters expose the application to the outside world.
//! Examples: REST API, CLI, gRPC server, GraphQL
// Example: REST API adapter would go here
// pub mod rest_api;
```
**src/adapters/driven/mod.rs**:
```rust
//! Driven adapters
//!
//! These adapters implement the ports needed by the domain.
//! Examples: PostgreSQL repository, HTTP client, Redis cache
// Example adapter implementations would go here
// pub mod postgres_repository;
// pub mod http_client;
```
5. **Update lib.rs or main.rs**
Add to the top of `src/lib.rs` (or `src/main.rs` if no lib.rs exists):
```rust
//! Hexagonal Architecture Application
//!
//! This application follows the hexagonal architecture pattern:
//! - Domain: Core business logic
//! - Ports: Interfaces (traits)
//! - Adapters: Implementations
pub mod domain;
pub mod ports;
pub mod adapters;
```
6. **Update Cargo.toml**
Ensure the following dependencies are in Cargo.toml:
```toml
[dependencies]
# Async runtime
tokio = { version = "1", features = ["full"] }
async-trait = "0.1"
# Error handling
thiserror = "1.0"
anyhow = "1.0"
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# UUID generation
uuid = { version = "1.0", features = ["v4", "serde"] }
# Example: Database (uncomment if needed)
# sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-native-tls"] }
# Example: HTTP (uncomment if needed)
# axum = "0.7"
# reqwest = { version = "0.11", features = ["json"] }
```
7. **Provide Usage Instructions**
After creating all files, tell the user:
```
✅ Hexagonal architecture structure initialized!
## Next Steps:
1. Review the generated structure in `src/`
2. Define your domain models in `src/domain/models.rs`
3. Implement business logic in `src/domain/services.rs`
4. Create port traits in `src/ports/` for external dependencies
5. Implement adapters in `src/adapters/` for each port
## Example Commands:
- Add a new port: `/rust-hex-add-port`
- Add an adapter: `/rust-hex-add-adapter`
- Get architecture help: Ask the `rust-hex-architect` agent
## Running the Code:
```bash
cargo build
cargo test
```
## Architecture Layers:
- **Domain**: Pure business logic (no external dependencies)
- **Ports**: Trait definitions (interfaces)
- **Adapters**: Concrete implementations
Dependencies flow: Adapters → Ports → Domain
```
## Important Notes
- Create directories only if they don't exist
- Don't overwrite existing files without asking the user first
- If files already exist, ask if they want to merge or skip
- Use proper Rust formatting and conventions
- Add helpful comments explaining the architecture
## After Completion
Tell the user about the structure created and suggest next steps for their specific use case.