currency-converter 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +7 -0
- data/.rubocop.yml +18 -0
- data/CHANGELOG.md +66 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +9 -1
- data/README.md +154 -40
- data/currency-converter.gemspec +5 -2
- data/lib/currency_converter/api_client.rb +86 -6
- data/lib/currency_converter/cache.rb +3 -2
- data/lib/currency_converter/config.rb +3 -2
- data/lib/currency_converter/converter.rb +44 -4
- data/lib/currency_converter/errors.rb +9 -0
- data/lib/currency_converter/version.rb +1 -1
- data/manual_integration_test.rb +157 -0
- data/spec/currency_converter/api_client_spec.rb +161 -10
- data/spec/currency_converter/cache_spec.rb +49 -11
- data/spec/currency_converter/converter_spec.rb +129 -16
- data/spec/currency_converter/version_spec.rb +15 -0
- data/spec/spec_helper.rb +7 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 655e5b8636644851c4c374f9313d0125c96242dfc6f3dbae16f71f48f2f98805
|
|
4
|
+
data.tar.gz: c6ffca01318cdcda1c52633db3a4ab318c3ffe00d6c9d03d2c0d61d2f63ddf39
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d92c18f8f7ba54fd9e6754495b51f22ddac59e4f3f68501853c5ba9dd970e900c078338ab689ce15c6f11cab527836208fbafe650f76ba87cd5d991ed30aabe
|
|
7
|
+
data.tar.gz: cc9a2d4a1291034e96f9ffd1098875a675dc1da41005f413b52e90bc420804f0472da940ac862907a37725041ee31acb05e6733ab909251dcd1b859230600282
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
AllCops:
|
|
2
2
|
TargetRubyVersion: 2.6
|
|
3
|
+
NewCops: enable
|
|
4
|
+
SuggestExtensions: false
|
|
3
5
|
|
|
4
6
|
Style/StringLiterals:
|
|
5
7
|
Enabled: true
|
|
@@ -11,3 +13,19 @@ Style/StringLiteralsInInterpolation:
|
|
|
11
13
|
|
|
12
14
|
Layout/LineLength:
|
|
13
15
|
Max: 120
|
|
16
|
+
|
|
17
|
+
# Allow longer methods for complex logic
|
|
18
|
+
Metrics/MethodLength:
|
|
19
|
+
Max: 15
|
|
20
|
+
Exclude:
|
|
21
|
+
- 'lib/currency_converter/api_client.rb'
|
|
22
|
+
- 'lib/currency_converter/converter.rb'
|
|
23
|
+
|
|
24
|
+
# Allow longer blocks in tests
|
|
25
|
+
Metrics/BlockLength:
|
|
26
|
+
Exclude:
|
|
27
|
+
- 'spec/**/*_spec.rb'
|
|
28
|
+
|
|
29
|
+
# Disable for generated or gem metadata files
|
|
30
|
+
Gemspec/RequireMFA:
|
|
31
|
+
Enabled: false
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
|
+
## [1.2.1] - 2026-01-10
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
- **Gemspec Metadata**: Added metadata links for better RubyGems integration
|
|
5
|
+
- Added changelog_uri pointing to CHANGELOG.md
|
|
6
|
+
- Added bug_tracker_uri pointing to GitHub Issues
|
|
7
|
+
- Added documentation_uri pointing to README.md
|
|
8
|
+
- Added wiki_uri pointing to GitHub Wiki
|
|
9
|
+
- These links now appear on the RubyGems.org page for better discoverability
|
|
10
|
+
- **README Badges**: Added professional badges to README
|
|
11
|
+
- Gem Version badge (shows current version)
|
|
12
|
+
- Downloads badge (shows total download count)
|
|
13
|
+
- License badge (shows MIT license)
|
|
14
|
+
|
|
15
|
+
### Notes
|
|
16
|
+
- No functional changes - this is a metadata-only release
|
|
17
|
+
- Improves gem discoverability and professionalism on RubyGems.org
|
|
18
|
+
|
|
19
|
+
## [1.2.0] - 2026-01-10
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **v6 API Support**: Migrated from deprecated v4 to v6 ExchangeRate-API
|
|
23
|
+
- Dual-mode operation: authenticated (with API key) and open access (without API key)
|
|
24
|
+
- API key is now optional - gem works perfectly without it
|
|
25
|
+
- Improved error handling for v6-specific errors (invalid-key, quota-reached, inactive-account, unsupported-code)
|
|
26
|
+
- Helpful logging to inform users which API mode they're using
|
|
27
|
+
- **Input Validation**: Comprehensive validation for all conversion inputs
|
|
28
|
+
- Validates amounts (rejects nil, negative, and non-numeric values)
|
|
29
|
+
- Validates currency codes (enforces 3-letter uppercase ISO 4217 format)
|
|
30
|
+
- New error classes: `InvalidAmountError` and `InvalidCurrencyError`
|
|
31
|
+
- Clear, actionable error messages for debugging
|
|
32
|
+
- **HTTP Timeout Configuration**: Configurable timeout to prevent hanging requests
|
|
33
|
+
- Default timeout: 10 seconds
|
|
34
|
+
- Prevents indefinite hangs on slow/dead connections
|
|
35
|
+
- New `TimeoutError` exception for timeout scenarios
|
|
36
|
+
- Configurable via `CurrencyConverter.configure { |c| c.timeout = 15 }`
|
|
37
|
+
- **Test Infrastructure**: Achieved 100% code coverage
|
|
38
|
+
- SimpleCov integration for coverage tracking
|
|
39
|
+
- 43 comprehensive test examples covering all code paths
|
|
40
|
+
- Manual integration test script for real API verification
|
|
41
|
+
- Zero RuboCop offenses
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- **Cache Duration Bug**: Cache expiration now works correctly
|
|
45
|
+
- Previously configured `cache_duration` was ignored
|
|
46
|
+
- Cache now properly expires after the configured duration
|
|
47
|
+
- Fixed by passing `expires_in` parameter to ActiveSupport::Cache
|
|
48
|
+
- **API Key Usage**: API key is now properly used in authenticated requests
|
|
49
|
+
- v1.1.0 configured but never used the API key
|
|
50
|
+
- v6 authenticated mode now includes key in URL path
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
- **Breaking Change**: Migrated from v4 to v6 API
|
|
54
|
+
- v4 endpoint: `https://api.exchangerate-api.com/v4/latest/{currency}`
|
|
55
|
+
- v6 authenticated: `https://v6.exchangerate-api.com/v6/{API_KEY}/latest/{currency}`
|
|
56
|
+
- v6 open access: `https://open.er-api.com/v6/latest/{currency}`
|
|
57
|
+
- No breaking changes for existing users - works with or without API key
|
|
58
|
+
- Code quality improvements: All RuboCop offenses fixed
|
|
59
|
+
|
|
60
|
+
### Migration Guide from v1.1.0 to v1.2.0
|
|
61
|
+
- **No code changes required** - fully backward compatible
|
|
62
|
+
- API key is now optional (but recommended for better rate limits)
|
|
63
|
+
- Invalid inputs (nil, negative amounts, invalid currency codes) will now raise validation errors
|
|
64
|
+
- Cache will now properly expire after configured duration (was broken in v1.1.0)
|
|
65
|
+
- Set timeout if needed: `CurrencyConverter.configure { |c| c.timeout = 15 }`
|
|
66
|
+
|
|
1
67
|
## [1.1.0] - 2024-11-13
|
|
2
68
|
### Added
|
|
3
69
|
- Added caching support for better performance.
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
currency-converter (1.1
|
|
4
|
+
currency-converter (1.2.1)
|
|
5
5
|
|
|
6
6
|
GEM
|
|
7
7
|
remote: https://rubygems.org/
|
|
@@ -21,6 +21,7 @@ GEM
|
|
|
21
21
|
bigdecimal
|
|
22
22
|
rexml
|
|
23
23
|
diff-lcs (1.5.1)
|
|
24
|
+
docile (1.4.1)
|
|
24
25
|
hashdiff (1.1.2)
|
|
25
26
|
i18n (1.14.6)
|
|
26
27
|
concurrent-ruby (~> 1.0)
|
|
@@ -63,6 +64,12 @@ GEM
|
|
|
63
64
|
rubocop-ast (1.35.0)
|
|
64
65
|
parser (>= 3.3.1.0)
|
|
65
66
|
ruby-progressbar (1.13.0)
|
|
67
|
+
simplecov (0.22.0)
|
|
68
|
+
docile (~> 1.1)
|
|
69
|
+
simplecov-html (~> 0.11)
|
|
70
|
+
simplecov_json_formatter (~> 0.1)
|
|
71
|
+
simplecov-html (0.13.2)
|
|
72
|
+
simplecov_json_formatter (0.1.4)
|
|
66
73
|
stringio (3.1.2)
|
|
67
74
|
tzinfo (2.0.6)
|
|
68
75
|
concurrent-ruby (~> 1.0)
|
|
@@ -83,6 +90,7 @@ DEPENDENCIES
|
|
|
83
90
|
rake (~> 13.0)
|
|
84
91
|
rspec (~> 3.0)
|
|
85
92
|
rubocop (~> 1.21)
|
|
93
|
+
simplecov
|
|
86
94
|
stringio (~> 3.1.2)
|
|
87
95
|
webmock
|
|
88
96
|
|
data/README.md
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
# CurrencyConverter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://badge.fury.io/rb/currency-converter)
|
|
4
|
+
[](https://rubygems.org/gems/currency-converter)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
The CurrencyConverter gem provides an easy way to perform currency conversions. It allows you to convert amounts between different currencies using real-time exchange rates from ExchangeRate-API, while offering caching for performance improvements and comprehensive error handling for robustness.
|
|
8
|
+
|
|
9
|
+
**Version 1.2.0** brings significant improvements including v6 API support, input validation, HTTP timeout protection, and 100% test coverage.
|
|
10
|
+
|
|
11
|
+
Author: Developed by [Shobhit Jain](https://github.com/shobhits7).
|
|
7
12
|
|
|
8
13
|
## Installation
|
|
9
14
|
|
|
10
15
|
Add this line to your application's Gemfile:
|
|
11
16
|
|
|
12
17
|
```ruby
|
|
13
|
-
gem 'currency-converter', '~>
|
|
18
|
+
gem 'currency-converter', '~> 1.2.0'
|
|
14
19
|
```
|
|
15
20
|
|
|
16
21
|
And then execute:
|
|
@@ -23,101 +28,210 @@ Or install it yourself as:
|
|
|
23
28
|
|
|
24
29
|
## Configuration
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
The gem can be used with or without configuration. For better rate limits and performance, it's recommended to configure it with an API key.
|
|
32
|
+
|
|
33
|
+
### Basic Usage (No Configuration Required)
|
|
34
|
+
|
|
35
|
+
The gem works out of the box using the v6 open access API:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
converter = CurrencyConverter::Converter.new
|
|
39
|
+
amount_in_eur = converter.convert(100, 'USD', 'EUR')
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Advanced Configuration (Recommended)
|
|
27
43
|
|
|
28
|
-
|
|
29
|
-
To get your api key, you can sign up for free at [Exchange Rate API](https://www.exchangerate-api.com/)
|
|
44
|
+
To get your free API key, sign up at [Exchange Rate API](https://www.exchangerate-api.com/)
|
|
30
45
|
|
|
31
46
|
```ruby
|
|
32
47
|
CurrencyConverter.configure do |config|
|
|
33
|
-
config.api_key = 'your_api_key_here' #
|
|
34
|
-
config.cache_duration =
|
|
48
|
+
config.api_key = 'your_api_key_here' # Optional: For better rate limits (v6 authenticated API)
|
|
49
|
+
config.cache_duration = 3600 # Optional: Cache duration in seconds (default: 1 hour)
|
|
50
|
+
config.timeout = 10 # Optional: HTTP timeout in seconds (default: 10)
|
|
35
51
|
config.logger = Logger.new(STDOUT) # Optional: Configure logging
|
|
36
52
|
end
|
|
37
53
|
```
|
|
38
54
|
|
|
39
|
-
|
|
55
|
+
**Configuration Options:**
|
|
40
56
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
- **api_key** (Optional): Your API key for v6 authenticated access with higher rate limits. Without it, the gem uses the v6 open access API (free tier).
|
|
58
|
+
- **cache_duration** (Optional): Duration in seconds for caching exchange rates. Default: 3600 seconds (1 hour).
|
|
59
|
+
- **timeout** (Optional): HTTP request timeout in seconds. Prevents hanging on slow connections. Default: 10 seconds.
|
|
60
|
+
- **logger** (Optional): Logger instance for debugging and monitoring. Default: Logger writing to STDOUT.
|
|
44
61
|
|
|
45
62
|
|
|
46
63
|
## Usage
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
|
|
65
|
+
### Creating an Instance of the Converter
|
|
66
|
+
|
|
67
|
+
Create an instance of the `CurrencyConverter::Converter` class to perform currency conversions:
|
|
49
68
|
|
|
50
69
|
```ruby
|
|
51
|
-
# Initialize the converter with the configured API key
|
|
52
70
|
converter = CurrencyConverter::Converter.new
|
|
53
71
|
```
|
|
54
72
|
|
|
55
73
|
### Converting Currency
|
|
56
74
|
|
|
57
|
-
Use the convert method to convert an amount from one currency to another
|
|
75
|
+
Use the `convert` method to convert an amount from one currency to another:
|
|
58
76
|
|
|
59
77
|
```ruby
|
|
60
78
|
# Convert 100 USD to EUR
|
|
61
79
|
amount_in_eur = converter.convert(100, 'USD', 'EUR')
|
|
62
|
-
puts amount_in_eur
|
|
80
|
+
puts amount_in_eur # => 85.0 (example rate)
|
|
81
|
+
|
|
82
|
+
# Convert with decimal amounts
|
|
83
|
+
amount_in_gbp = converter.convert(99.99, 'USD', 'GBP')
|
|
84
|
+
puts amount_in_gbp # => 75.99 (example rate)
|
|
63
85
|
```
|
|
64
86
|
|
|
65
|
-
|
|
87
|
+
**Parameters:**
|
|
88
|
+
|
|
89
|
+
- **amount** (Numeric): The amount to convert. Must be a positive number (Integer or Float).
|
|
90
|
+
- **from_currency** (String): Source currency code in ISO 4217 format (3 uppercase letters, e.g., "USD").
|
|
91
|
+
- **to_currency** (String): Target currency code in ISO 4217 format (3 uppercase letters, e.g., "EUR").
|
|
66
92
|
|
|
67
|
-
|
|
93
|
+
**Returns:** The converted amount as a Float, rounded to 2 decimal places.
|
|
68
94
|
|
|
69
|
-
|
|
95
|
+
### Input Validation
|
|
96
|
+
|
|
97
|
+
Version 1.2.0 includes comprehensive input validation:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# Valid inputs
|
|
101
|
+
converter.convert(100, 'USD', 'EUR') # ✓ Valid
|
|
102
|
+
converter.convert(0, 'USD', 'EUR') # ✓ Valid (zero is allowed)
|
|
103
|
+
converter.convert(99.99, 'USD', 'GBP') # ✓ Valid (decimals allowed)
|
|
104
|
+
|
|
105
|
+
# Invalid inputs that will raise errors
|
|
106
|
+
converter.convert(nil, 'USD', 'EUR') # ✗ Raises InvalidAmountError
|
|
107
|
+
converter.convert(-100, 'USD', 'EUR') # ✗ Raises InvalidAmountError
|
|
108
|
+
converter.convert('100', 'USD', 'EUR') # ✗ Raises InvalidAmountError
|
|
109
|
+
converter.convert(100, 'usd', 'EUR') # ✗ Raises InvalidCurrencyError (must be uppercase)
|
|
110
|
+
converter.convert(100, 'US', 'EUR') # ✗ Raises InvalidCurrencyError (must be 3 letters)
|
|
111
|
+
converter.convert(100, nil, 'EUR') # ✗ Raises InvalidCurrencyError
|
|
112
|
+
```
|
|
70
113
|
|
|
71
|
-
|
|
114
|
+
## Error Handling
|
|
72
115
|
|
|
73
|
-
|
|
74
|
-
The convert method raises errors in the following situations:
|
|
116
|
+
Version 1.2.0 provides comprehensive error handling with specific exception types:
|
|
75
117
|
|
|
76
|
-
|
|
118
|
+
### Exception Types
|
|
77
119
|
|
|
78
|
-
|
|
120
|
+
- **`InvalidAmountError`**: Raised when the amount is invalid (nil, negative, or non-numeric)
|
|
121
|
+
- **`InvalidCurrencyError`**: Raised when currency codes are invalid (nil, wrong format, not ISO 4217)
|
|
122
|
+
- **`TimeoutError`**: Raised when the API request times out
|
|
123
|
+
- **`APIError`**: Raised for API-related failures (network errors, missing rates, API quota reached)
|
|
124
|
+
- **`RateNotFoundError`**: Raised when a specific currency rate is not available from the API
|
|
79
125
|
|
|
80
|
-
|
|
126
|
+
### Error Handling Examples
|
|
81
127
|
|
|
82
128
|
```ruby
|
|
83
129
|
begin
|
|
84
130
|
amount_in_eur = converter.convert(100, 'USD', 'EUR')
|
|
85
131
|
puts "Converted amount: #{amount_in_eur} EUR"
|
|
132
|
+
rescue CurrencyConverter::InvalidAmountError => e
|
|
133
|
+
puts "Invalid amount: #{e.message}"
|
|
134
|
+
rescue CurrencyConverter::InvalidCurrencyError => e
|
|
135
|
+
puts "Invalid currency: #{e.message}"
|
|
136
|
+
rescue CurrencyConverter::TimeoutError => e
|
|
137
|
+
puts "Request timed out: #{e.message}"
|
|
86
138
|
rescue CurrencyConverter::APIError => e
|
|
87
|
-
puts "
|
|
139
|
+
puts "API error: #{e.message}"
|
|
88
140
|
rescue StandardError => e
|
|
89
|
-
puts "
|
|
141
|
+
puts "Unexpected error: #{e.message}"
|
|
90
142
|
end
|
|
91
143
|
```
|
|
92
144
|
|
|
145
|
+
### Common Error Messages
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
# Invalid amount errors
|
|
149
|
+
"Amount cannot be nil"
|
|
150
|
+
"Amount must be a number"
|
|
151
|
+
"Amount cannot be negative"
|
|
152
|
+
|
|
153
|
+
# Invalid currency errors
|
|
154
|
+
"from_currency cannot be nil"
|
|
155
|
+
"from_currency must be a 3-letter uppercase code (e.g., 'USD')"
|
|
156
|
+
"to_currency must be a 3-letter uppercase code (e.g., 'EUR')"
|
|
157
|
+
|
|
158
|
+
# API errors
|
|
159
|
+
"Request timed out after 10 seconds"
|
|
160
|
+
"Invalid API key provided"
|
|
161
|
+
"API rate limit quota reached"
|
|
162
|
+
"Unsupported currency code: XYZ"
|
|
163
|
+
"EUR rate not available"
|
|
164
|
+
```
|
|
165
|
+
|
|
93
166
|
## Testing
|
|
94
167
|
|
|
95
|
-
The gem
|
|
168
|
+
The gem has **100% test coverage** with comprehensive test suites covering all functionality.
|
|
169
|
+
|
|
170
|
+
### Running Tests
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# Run all tests
|
|
174
|
+
bundle exec rspec
|
|
175
|
+
|
|
176
|
+
# Run with coverage report
|
|
177
|
+
bundle exec rspec
|
|
178
|
+
# View coverage report: open coverage/index.html
|
|
179
|
+
|
|
180
|
+
# Run code quality checks
|
|
181
|
+
bundle exec rubocop
|
|
182
|
+
|
|
183
|
+
# Run manual integration tests (with real API)
|
|
184
|
+
ruby manual_integration_test.rb
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Test Coverage
|
|
188
|
+
|
|
189
|
+
**Version 1.2.0 Test Statistics:**
|
|
190
|
+
- **43 test examples**, 0 failures
|
|
191
|
+
- **100% code coverage** (125/125 lines)
|
|
192
|
+
- **0 RuboCop offenses**
|
|
193
|
+
- All tests use WebMock to stub external API calls for consistency
|
|
194
|
+
- Manual integration test script included for real API verification
|
|
195
|
+
|
|
196
|
+
### Example Test
|
|
96
197
|
|
|
97
|
-
Example Test:
|
|
98
198
|
```ruby
|
|
99
199
|
RSpec.describe CurrencyConverter::Converter do
|
|
100
200
|
let(:converter) { CurrencyConverter::Converter.new }
|
|
101
201
|
|
|
102
202
|
it 'performs currency conversion successfully' do
|
|
203
|
+
# WebMock stubs the API call
|
|
204
|
+
allow_any_instance_of(CurrencyConverter::APIClient)
|
|
205
|
+
.to receive(:get_rate)
|
|
206
|
+
.and_return(0.85)
|
|
207
|
+
|
|
103
208
|
result = converter.convert(100, 'USD', 'EUR')
|
|
104
|
-
expect(result).to
|
|
209
|
+
expect(result).to eq(85.0)
|
|
105
210
|
end
|
|
106
211
|
|
|
107
|
-
it 'raises
|
|
108
|
-
expect { converter.convert(100, 'USD', '
|
|
212
|
+
it 'raises InvalidAmountError for negative amounts' do
|
|
213
|
+
expect { converter.convert(-100, 'USD', 'EUR') }
|
|
214
|
+
.to raise_error(CurrencyConverter::InvalidAmountError)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'raises InvalidCurrencyError for invalid currency codes' do
|
|
218
|
+
expect { converter.convert(100, 'usd', 'EUR') }
|
|
219
|
+
.to raise_error(CurrencyConverter::InvalidCurrencyError)
|
|
109
220
|
end
|
|
110
221
|
end
|
|
111
222
|
```
|
|
112
223
|
|
|
113
|
-
## Test Setup:
|
|
114
|
-
Ensure you have the required API key and any necessary configuration before running the tests. Mocking external API calls in tests is recommended for consistent results.
|
|
115
|
-
|
|
116
224
|
## Changelog
|
|
117
|
-
> v1.0.0
|
|
118
225
|
|
|
119
|
-
|
|
120
|
-
|
|
226
|
+
See [CHANGELOG.md](CHANGELOG.md) for detailed version history and release notes.
|
|
227
|
+
|
|
228
|
+
**Latest Release (v1.2.0 - 2026-01-10):**
|
|
229
|
+
- Migrated to ExchangeRate-API v6 with dual-mode support
|
|
230
|
+
- Added comprehensive input validation
|
|
231
|
+
- Fixed cache duration implementation
|
|
232
|
+
- Added HTTP timeout configuration
|
|
233
|
+
- Achieved 100% test coverage
|
|
234
|
+
- Zero code quality issues
|
|
121
235
|
|
|
122
236
|
## Development
|
|
123
237
|
|
data/currency-converter.gemspec
CHANGED
|
@@ -16,11 +16,14 @@ Gem::Specification.new do |spec|
|
|
|
16
16
|
|
|
17
17
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
18
|
spec.metadata["source_code_uri"] = "https://github.com/shobhits7/currency_converter"
|
|
19
|
-
|
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/shobhits7/currency_converter/blob/main/CHANGELOG.md"
|
|
20
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/shobhits7/currency_converter/issues"
|
|
21
|
+
spec.metadata["documentation_uri"] = "https://github.com/shobhits7/currency_converter/blob/main/README.md"
|
|
22
|
+
spec.metadata["wiki_uri"] = "https://github.com/shobhits7/currency_converter/wiki"
|
|
20
23
|
|
|
21
24
|
# Specify which files should be added to the gem when it is released.
|
|
22
25
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
23
|
-
spec.files = `git ls-files -z`.split("\x0").
|
|
26
|
+
spec.files = `git ls-files -z`.split("\x0").grep_v(/\.gem$/)
|
|
24
27
|
|
|
25
28
|
# spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
26
29
|
# `git ls-files -z`.split("\x0").reject do |f|
|
|
@@ -7,28 +7,108 @@ require_relative "errors"
|
|
|
7
7
|
module CurrencyConverter
|
|
8
8
|
# The APIClient class handles API requests to the external exchange rate provider.
|
|
9
9
|
# Responsible for retrieving exchange rates based on the provided currencies.
|
|
10
|
+
# Supports both v6 authenticated (with API key) and v6 open access (without API key) endpoints.
|
|
10
11
|
class APIClient
|
|
11
|
-
|
|
12
|
+
# v6 authenticated endpoint (requires API key, better rate limits)
|
|
13
|
+
AUTHENTICATED_BASE_URL = "https://v6.exchangerate-api.com/v6"
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
# v6 open access endpoint (no API key required, lower rate limits)
|
|
16
|
+
OPEN_ACCESS_BASE_URL = "https://open.er-api.com/v6/latest"
|
|
17
|
+
|
|
18
|
+
def initialize(api_key, timeout: CurrencyConverter.configuration&.timeout || 10)
|
|
14
19
|
@api_key = api_key
|
|
20
|
+
@timeout = timeout
|
|
21
|
+
@use_authenticated = !api_key.nil? && !api_key.empty?
|
|
22
|
+
|
|
23
|
+
log_api_mode if CurrencyConverter.configuration&.logger
|
|
15
24
|
end
|
|
16
25
|
|
|
17
26
|
# Gets the exchange rate for a currency pair.
|
|
18
27
|
# @param from_currency [String] the source currency code
|
|
19
28
|
# @param to_currency [String] the target currency code
|
|
20
29
|
# @return [Float] the exchange rate
|
|
21
|
-
# @raise [RateNotFoundError, APIError] for invalid responses
|
|
30
|
+
# @raise [RateNotFoundError, APIError, TimeoutError] for invalid responses, network issues, or timeout
|
|
22
31
|
def get_rate(from_currency, to_currency)
|
|
23
|
-
url =
|
|
24
|
-
|
|
32
|
+
url = build_url(from_currency)
|
|
33
|
+
uri = URI(url)
|
|
34
|
+
|
|
35
|
+
response = fetch_with_timeout(uri)
|
|
25
36
|
data = JSON.parse(response)
|
|
26
37
|
|
|
27
|
-
|
|
38
|
+
# Check for API-specific errors in v6 response
|
|
39
|
+
handle_v6_errors(data)
|
|
40
|
+
|
|
41
|
+
# Extract rate from v6 response format
|
|
42
|
+
rate = data.dig("conversion_rates", to_currency) || data.dig("rates", to_currency)
|
|
43
|
+
raise(RateNotFoundError, "#{to_currency} rate not available") unless rate
|
|
44
|
+
|
|
45
|
+
rate
|
|
28
46
|
rescue JSON::ParserError
|
|
29
47
|
raise APIError, "Invalid API response format"
|
|
48
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
49
|
+
raise TimeoutError, "Request timed out after #{@timeout} seconds: #{e.message}"
|
|
30
50
|
rescue Net::HTTPError, SocketError => e
|
|
31
51
|
raise APIError, "Network error: #{e.message}"
|
|
32
52
|
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Builds the appropriate URL based on authentication mode
|
|
57
|
+
# @param from_currency [String] the base currency code
|
|
58
|
+
# @return [String] the complete API URL
|
|
59
|
+
def build_url(from_currency)
|
|
60
|
+
if @use_authenticated
|
|
61
|
+
"#{AUTHENTICATED_BASE_URL}/#{@api_key}/latest/#{from_currency}"
|
|
62
|
+
else
|
|
63
|
+
"#{OPEN_ACCESS_BASE_URL}/#{from_currency}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Handles v6-specific error responses
|
|
68
|
+
# @param data [Hash] the parsed JSON response
|
|
69
|
+
# @raise [APIError] for various v6 API errors
|
|
70
|
+
def handle_v6_errors(data)
|
|
71
|
+
return unless data["result"] == "error"
|
|
72
|
+
|
|
73
|
+
error_type = data["error-type"]
|
|
74
|
+
case error_type
|
|
75
|
+
when "invalid-key"
|
|
76
|
+
raise APIError, "Invalid API key provided"
|
|
77
|
+
when "inactive-account"
|
|
78
|
+
raise APIError, "API account is inactive"
|
|
79
|
+
when "quota-reached"
|
|
80
|
+
raise APIError, "API rate limit quota reached"
|
|
81
|
+
when "unsupported-code"
|
|
82
|
+
raise APIError, "Unsupported currency code"
|
|
83
|
+
else
|
|
84
|
+
raise APIError, "API error: #{error_type || "unknown error"}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Fetches data from the URI with timeout configuration
|
|
89
|
+
# @param uri [URI] the URI to fetch from
|
|
90
|
+
# @return [String] the response body
|
|
91
|
+
def fetch_with_timeout(uri)
|
|
92
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
|
|
93
|
+
open_timeout: @timeout, read_timeout: @timeout) do |http|
|
|
94
|
+
request = Net::HTTP::Get.new(uri)
|
|
95
|
+
response = http.request(request)
|
|
96
|
+
response.body
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Logs the API mode being used
|
|
101
|
+
def log_api_mode
|
|
102
|
+
if @use_authenticated
|
|
103
|
+
CurrencyConverter.configuration.logger.info(
|
|
104
|
+
"CurrencyConverter: Using v6 authenticated API (better rate limits)"
|
|
105
|
+
)
|
|
106
|
+
else
|
|
107
|
+
CurrencyConverter.configuration.logger.info(
|
|
108
|
+
"CurrencyConverter: Using v6 open access API (free tier). " \
|
|
109
|
+
"Configure an API key for better rate limits: https://www.exchangerate-api.com"
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
33
113
|
end
|
|
34
114
|
end
|
|
@@ -13,10 +13,11 @@ module CurrencyConverter
|
|
|
13
13
|
|
|
14
14
|
# Fetches the value from the cache, using the block to provide a default value.
|
|
15
15
|
# @param key [String] the cache key
|
|
16
|
+
# @param expires_in [Integer] the cache expiration time in seconds
|
|
16
17
|
# @param block [Proc] the block to execute if the key is not found in cache
|
|
17
18
|
# @return [Object] the cached value
|
|
18
|
-
def fetch(key, &block)
|
|
19
|
-
@cache.fetch(key, &block)
|
|
19
|
+
def fetch(key, expires_in: nil, &block)
|
|
20
|
+
@cache.fetch(key, expires_in: expires_in, &block)
|
|
20
21
|
end
|
|
21
22
|
end
|
|
22
23
|
end
|
|
@@ -10,12 +10,13 @@ module CurrencyConverter
|
|
|
10
10
|
# - `cache_duration`: Duration for which the exchange rates should be cached.
|
|
11
11
|
# - `logger`: Configurable logger for logging any errors or information.
|
|
12
12
|
class Configuration
|
|
13
|
-
attr_accessor :api_key, :cache_duration, :logger
|
|
13
|
+
attr_accessor :api_key, :cache_duration, :logger, :timeout
|
|
14
14
|
|
|
15
15
|
def initialize
|
|
16
|
-
@api_key = ENV
|
|
16
|
+
@api_key = ENV.fetch("CURRENCY_CONVERTER_API_KEY", nil)
|
|
17
17
|
@cache_duration = 1.hour # Default cache duration is 1 hour
|
|
18
18
|
@logger = Logger.new($stdout)
|
|
19
|
+
@timeout = 10 # Default timeout is 10 seconds
|
|
19
20
|
end
|
|
20
21
|
end
|
|
21
22
|
|