converter/README.md

204 lines
7.6 KiB
Markdown
Raw Permalink Normal View History

2025-09-06 12:57:48 +03:00
# Currency Converter CLI
A command-line utility for converting currencies using CoinMarketCap API, built with Go following Clean Architecture principles.
## Quick Start
### Prerequisites
- Go 1.19 or higher
- CoinMarketCap API key (get from https://coinmarketcap.com/api/)
### Installation & Setup
1. Clone the repository
2. Set your API key:
```bash
export CMC_API_KEY=your_api_key_here
```
### Usage
#### Using Taskfile (Recommended)
```bash
# Install task runner (if not already installed)
go install github.com/go-task/task/v3/cmd/task@latest
# Build and run with parameters
task run -- 123.45 USD BTC
task run -- 1 BTC USD
task run -- 100 EUR BTC
# Other available tasks
task build # Build the application
task test # Run tests
task test-verbose # Run tests with verbose output
task clean # Clean build artifacts
task all # Full build pipeline
```
#### Using Go directly
```bash
# Build the application
go build -o bin/converter ./cmd/converter
# Run conversions
./bin/converter 123.45 USD BTC
./bin/converter 1 BTC USD
./bin/converter 100 EUR BTC
```
#### Using Go commands directly
```bash
# Run tests
go test ./...
# Build manually
mkdir -p bin && go build -o bin/converter ./cmd/converter
# Run with API key
CMC_API_KEY=your_key ./bin/converter 50 USD BTC
```
## Project Structure
```
converter/
├── cmd/converter/ # CLI application entry point
│ └── main.go # Main function, argument parsing, DI setup
├── app/ # Application layer (Use Cases)
│ ├── convert_currency.go # Core use case implementation
│ ├── convert_currency_test.go # Use case tests with mocks
│ └── mocks/ # Generated mocks (mockery)
│ └── RateProvider.go # Mock for RateProvider interface
├── domain/ # Domain layer (Business Logic)
│ ├── entities.go # Value objects (Currency, Money, Rate)
│ ├── entities_test.go # Domain entity tests
│ ├── services.go # Domain service (CurrencyConverter)
│ └── services_test.go # Domain service tests
├── infrastructure/ # Infrastructure layer (External APIs)
│ └── coinmarketcap/ # CoinMarketCap API client
│ ├── client.go # HTTP client with retry/fallback logic
│ └── models.go # API response DTOs
├── Taskfile.yml # Task runner configuration
├── go.mod # Go module definition
├── go.sum # Go module checksums
├── TASK.md # Original task requirements (Russian)
├── CLAUDE.md # Project configuration for Claude Code
└── README.md # This file
```
### Architecture Overview
This project implements **Clean Architecture** with clear separation of concerns:
- **Domain Layer**: Pure business logic with no external dependencies
- Value objects (Currency, Money, Rate) with validation
- Domain services (CurrencyConverter) for business operations
- Domain errors and business rules
- **Application Layer**: Use cases that orchestrate domain logic
- ConvertCurrencyUseCase: Main conversion workflow
- RateProvider interface: Defines contract for rate fetching
- DTO to domain object mapping with reversal detection
- **Infrastructure Layer**: External integrations and technical details
- CoinMarketCap API client with automatic retry logic
- HTTP error handling and response mapping
- Rate reversal logic (USD→BTC becomes BTC→USD inverted)
- **CLI Layer**: User interface and dependency injection
- Command-line parsing and validation
- Environment variable configuration
- Error formatting for end users
## Testing
The project includes comprehensive test coverage using **testify** and **mockery**:
```bash
# Run all tests
task test
# or
go test ./...
# Run tests with verbose output
task test-verbose
# or
go test -v ./...
# Run tests with coverage
task test-coverage
# or
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
```
### Test Structure
- **Unit Tests**: Domain entities and services with table-driven tests
- **Use Case Tests**: Application layer with generated mocks using testify/mockery
- **Mock Generation**: Use `task generate-mocks` to regenerate mocks
## Architecture Decisions
### Why decimal.Decimal for Money Handling
We chose `shopspring/decimal` over floating-point numbers and other alternatives for the following critical reasons:
#### Financial Accuracy Requirements
- Floating point arithmetic cannot represent decimal numbers exactly
- Example: `0.1 + 0.2 ≠ 0.3` in floating-point arithmetic
- Currency conversion errors compound with multiple operations
- CoinMarketCap API returns exchange rates with high precision (8+ decimal places)
#### Real-world Impact
```go
// Problematic with float64
Bad: float64(123.45) * float64(0.000016) = 0.001975200000000001
// Accurate with decimal.Decimal
Good: decimal(123.45) * decimal(0.000016) = 0.001975200000000000
```
#### Industry Standards
- Banking and financial systems never use floating-point for monetary calculations
- Major payment APIs (Stripe, PayPal) use string representations to avoid precision loss
- `shopspring/decimal` is the Go standard for financial applications (used by Kubernetes billing, trading systems)
#### Alternative Analysis
- **big.Rat**: Overkill for this use case, overly complex API for basic currency operations
- **Integer scaling**: Requires manual precision management, error-prone across currencies with different decimal places (USD=2, BTC=8, ETH=18)
- **float64**: Unacceptable precision loss for financial data
#### Performance vs Precision Trade-off
- Using integer scaling would significantly increase development complexity with manual precision management across different currency types
- decimal.Decimal adds only microseconds vs potentially costly conversion errors
- Ensures user trust by avoiding strange rounding artifacts
- Better maintainability with clear intent and JSON-friendly serialization
**Conclusion**: The cost of precision errors in financial software far exceeds the minimal performance overhead of using decimal.Decimal.
### Currency Handling Strategy
Our implementation uses CoinMarketCap's `/v1/cryptocurrency/quotes/latest` endpoint with intelligent fallback logic:
- **Direct request**: Try the requested conversion (e.g., `USD → BTC`)
- **Fallback on empty data**: If no data returned, try reverse direction (`BTC → USD`) and invert the rate
- **Error handling**: Distinguish between unknown symbols, invalid parameters, and network issues
This approach handles both crypto-to-fiat and fiat-to-crypto conversions using only the cryptocurrency endpoint:
- **BTC → USD**: Direct API call works
- **USD → BTC**: API call fails (USD not in crypto DB), retry as BTC → USD, then invert rate
The automatic fallback with rate inversion eliminates the need for separate fiat currency handling while maintaining mathematical precision using decimal arithmetic.
### Currency Precision Strategy
To keep the implementation simple, we use a uniform precision rule:
- **All currencies**: 8 decimal places (covers cryptocurrency precision requirements)
This approach handles high-precision cryptocurrencies properly while being acceptable for fiat currencies (even though they typically use 2 decimal places). In a production environment, we would maintain per-currency precision metadata, but 8 decimals covers all practical use cases without overcomplicating the initial implementation.
## Development
This project follows Clean Architecture principles and SOLID design patterns as specified in the requirements.