Initial commit
This commit is contained in:
477
commands/rust-hex-add-adapter.md
Normal file
477
commands/rust-hex-add-adapter.md
Normal 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
|
||||
264
commands/rust-hex-add-port.md
Normal file
264
commands/rust-hex-add-port.md
Normal 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
346
commands/rust-hex-init.md
Normal 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.
|
||||
Reference in New Issue
Block a user