7.3 KiB
description
| 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
-
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)
-
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
-
Create the Port Trait
Based on the port type, create the trait in the appropriate file.
For Driven Ports (in
src/ports/driven.rs):/// [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):/// [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), } -
Common Port Patterns
Suggest appropriate methods based on the port name:
Repository Patterns:
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:
async fn send(&self, data: &Data) -> Result<Response, Error>; async fn query(&self, params: QueryParams) -> Result<QueryResult, Error>;Cache Patterns:
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>; -
Create Adapter Scaffold
After creating the port, automatically create a scaffold for an adapter implementation:
For Driven Ports (create in
src/adapters/driven/)://! [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 } } -
Update Module Exports
Add the new port to the appropriate module file:
For driven ports, add to
src/ports/driven.rs: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:pub mod [adapter_name]; -
Provide Usage Example
Show the user how to use the new port:
// 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(()) } } -
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 + Syncbounds 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:
- Add more methods to the port
- Create additional adapters for this port
- Create another port