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