money-unirate-api 0.1.0
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 +7 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE +21 -0
- data/README.md +138 -0
- data/lib/money/bank/uni_rate/version.rb +9 -0
- data/lib/money/bank/uni_rate.rb +167 -0
- data/lib/money-unirate-api.rb +5 -0
- data/money-unirate-api.gemspec +47 -0
- metadata +134 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 99d6c31d3a85db82760f94c04367f876936afcc29c9c4170970cd62c3bd19916
|
|
4
|
+
data.tar.gz: 91ffe4ab7c550ad26cb2e4d6c7400805e71acfbba96bb1c15276c977a0e95935
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6be26a0a659e674438012436dcc29fcdf654c8ed71a07d90fa136fa8e79b686b02d9f3b9a31b7618762cd650dbb82516baa9c73c3bedf5593207583d5755af0e
|
|
7
|
+
data.tar.gz: 2458c612f096945ce4d93d72048fc19ab6c18efe310e8fa6b358a1c0e60a646896e2b65b342912a4fd353273e6af08a23d8e06a45d9a8f1719e34b5d72d02de5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-05-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `Money::Bank::UniRate` — a `Money::Bank::VariableExchange` subclass backed by
|
|
12
|
+
the UniRate API (https://unirateapi.com).
|
|
13
|
+
- Single-base snapshot fetch from `GET /api/rates`; all cross-rates derived on
|
|
14
|
+
demand so a TTL refresh never serves a stale pair.
|
|
15
|
+
- Lazy fetch on first conversion, optional `ttl_in_seconds` refresh, and
|
|
16
|
+
`flush_rates` to force a refresh.
|
|
17
|
+
- Error mapping (`UniRateError`) for auth (401), Pro-gated (403), not-found
|
|
18
|
+
(404), rate-limit (429), network, and malformed-response failures.
|
|
19
|
+
- Zero runtime dependencies beyond the `money` gem (pure stdlib HTTP).
|
|
20
|
+
|
|
21
|
+
[0.1.0]: https://github.com/UniRate-API/money-unirate-api/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Unirate Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# money-unirate-api
|
|
2
|
+
|
|
3
|
+
A [`Money::Bank`](https://github.com/RubyMoney/money) implementation backed by
|
|
4
|
+
the [UniRate API](https://unirateapi.com) — free, real-time currency exchange
|
|
5
|
+
rates for the [`money`](https://rubygems.org/gems/money) gem.
|
|
6
|
+
|
|
7
|
+
- Drop-in `Money::Bank::UniRate` for `Money.default_bank`
|
|
8
|
+
- One HTTP call per snapshot: fetches a single base currency, **derives every
|
|
9
|
+
cross-rate on demand** (so a refresh never serves a stale pair)
|
|
10
|
+
- Lazy fetch on first conversion + optional TTL-based refresh
|
|
11
|
+
- 170+ currencies (fiat + crypto) via UniRate
|
|
12
|
+
- Free tier, no credit card required
|
|
13
|
+
- Zero runtime dependencies beyond `money` (pure stdlib `net/http` + `json`)
|
|
14
|
+
|
|
15
|
+
> **Affiliation:** this gem is maintained by the UniRate team. It talks to the
|
|
16
|
+
> UniRate API. If you only need euro-area rates the ECB feed (e.g.
|
|
17
|
+
> `eu_central_bank`) may suit you better; for a broad multi-currency source on
|
|
18
|
+
> a free tier, UniRate is a good fit.
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Ruby 3.0+
|
|
23
|
+
- `money` 6.13+
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# Gemfile
|
|
29
|
+
gem "money-unirate-api"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bundle install
|
|
34
|
+
# or
|
|
35
|
+
gem install money-unirate-api
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
require "money"
|
|
42
|
+
require "money/bank/uni_rate"
|
|
43
|
+
|
|
44
|
+
Money.default_bank = Money::Bank::UniRate.new(
|
|
45
|
+
api_key: ENV.fetch("UNIRATE_API_KEY")
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
Money.new(100_00, "USD").exchange_to("EUR") # => #<Money fractional:9200 currency:EUR>
|
|
49
|
+
Money.new(50_00, "EUR").exchange_to("GBP") # cross-rate derived from the USD snapshot
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Get a free API key at [unirateapi.com](https://unirateapi.com).
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
Money::Bank::UniRate.new(
|
|
58
|
+
api_key: ENV.fetch("UNIRATE_API_KEY"), # falls back to ENV["UNIRATE_API_KEY"]
|
|
59
|
+
base_currency: "USD", # currency the snapshot is fetched against
|
|
60
|
+
ttl_in_seconds: 3600, # re-fetch interval; nil (default) = fetch once and cache
|
|
61
|
+
timeout: 30 # per-request timeout in seconds
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
| Option | Default | Description |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `api_key` | `ENV["UNIRATE_API_KEY"]` | UniRate API key (required) |
|
|
68
|
+
| `base_currency` | `"USD"` | Currency the single snapshot is fetched against |
|
|
69
|
+
| `ttl_in_seconds` | `nil` | Seconds before an automatic re-fetch; `nil` fetches once |
|
|
70
|
+
| `timeout` | `30` | Per-request open/read timeout |
|
|
71
|
+
|
|
72
|
+
## How it works
|
|
73
|
+
|
|
74
|
+
UniRate's `/api/rates` returns every rate for one base currency in a single
|
|
75
|
+
response. This bank fetches that snapshot and stores `base → currency` rates.
|
|
76
|
+
Any pair you ask for is derived from those:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
rate(FROM, TO) = rate(base, TO) / rate(base, FROM)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Cross-rates are computed **per call**, not cached in the rate store — so when
|
|
83
|
+
the snapshot refreshes (via `ttl_in_seconds` or `flush_rates`), there are no
|
|
84
|
+
stale derived pairs left behind.
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
bank = Money::Bank::UniRate.new(api_key: "...", ttl_in_seconds: 3600)
|
|
88
|
+
|
|
89
|
+
bank.update_rates # warm the cache up front (optional; otherwise lazy)
|
|
90
|
+
bank.get_rate("EUR", "GBP") # derived from the base snapshot
|
|
91
|
+
bank.expired? # => false until the TTL elapses
|
|
92
|
+
bank.flush_rates # force a re-fetch on the next conversion
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Error handling
|
|
96
|
+
|
|
97
|
+
Every failure raises `Money::Bank::UniRateError`:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
begin
|
|
101
|
+
Money.new(100, "USD").exchange_to("EUR", bank)
|
|
102
|
+
rescue Money::Bank::UniRateError => e
|
|
103
|
+
warn "FX lookup failed: #{e.message}"
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Mapped responses: `401` (bad/missing key), `403` (Pro-gated endpoint), `404`
|
|
108
|
+
(unknown currency), `429` (rate limit), network errors, and malformed responses.
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
bundle install
|
|
114
|
+
bundle exec rspec # ~20 WebMock-based mock tests
|
|
115
|
+
bundle exec rubocop
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
<!-- unirate-ecosystem-footer:start -->
|
|
119
|
+
## Other UniRate clients
|
|
120
|
+
|
|
121
|
+
UniRate ships official client libraries and framework integrations across the
|
|
122
|
+
ecosystem. The repos below are all maintained under the
|
|
123
|
+
[UniRate-API](https://github.com/UniRate-API) org.
|
|
124
|
+
|
|
125
|
+
- **Languages:** [Python](https://github.com/UniRate-API/unirate-api-python) · [Node.js / TypeScript](https://github.com/UniRate-API/unirate-api-nodejs) · [Go](https://github.com/UniRate-API/unirate-api-go) · [Rust](https://github.com/UniRate-API/unirate-api-rust) · [Java](https://github.com/UniRate-API/unirate-api-java) · [Ruby](https://github.com/UniRate-API/unirate-api-ruby) · [PHP](https://github.com/UniRate-API/unirate-api-php) · [.NET](https://github.com/UniRate-API/unirate-api-dotnet) · [Swift](https://github.com/UniRate-API/unirate-api-swift)
|
|
126
|
+
- **Web frameworks:** [NestJS](https://github.com/UniRate-API/nestjs-unirate) · [Django / Wagtail](https://github.com/UniRate-API/wagtail-unirate) · [FastAPI](https://github.com/UniRate-API/fastapi-unirate) · [Flask](https://github.com/UniRate-API/flask-unirate) · [React](https://github.com/UniRate-API/react-unirate) · [tRPC](https://github.com/UniRate-API/trpc-unirate)
|
|
127
|
+
- **Static-site generators:** [Astro](https://github.com/UniRate-API/astro-unirate) · [Eleventy](https://github.com/UniRate-API/eleventy-unirate) · [Hugo](https://github.com/UniRate-API/hugo-unirate)
|
|
128
|
+
- **Data / orchestration:** [Airflow](https://github.com/UniRate-API/airflow-provider-unirate) · [dbt](https://github.com/UniRate-API/dbt-unirate) · [LangChain](https://github.com/UniRate-API/langchain-unirate)
|
|
129
|
+
- **Workflow / no-code:** [n8n](https://github.com/UniRate-API/n8n-nodes-unirate) · [Google Sheets](https://github.com/UniRate-API/unirate-sheets) · [MCP server](https://github.com/UniRate-API/unirate-mcp)
|
|
130
|
+
- **Editors / tools:** [VS Code](https://github.com/UniRate-API/vscode-unirate) · [Obsidian](https://github.com/UniRate-API/obsidian-currency)
|
|
131
|
+
- **Specialty bridges:** [NodaMoney (.NET)](https://github.com/UniRate-API/UniRateApi.NodaMoney) · [money gem (Ruby)](https://github.com/UniRate-API/money-unirate-api)
|
|
132
|
+
|
|
133
|
+
Get a free API key at [unirateapi.com](https://unirateapi.com).
|
|
134
|
+
<!-- unirate-ecosystem-footer:end -->
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Standalone version constant. It deliberately lives in its own top-level
|
|
4
|
+
# module so the gemspec can read the version *without* loading the `money`
|
|
5
|
+
# gem: `Money` is a class there, and reopening it as a module at gem-build
|
|
6
|
+
# time (before `money` is installed) would raise a TypeError.
|
|
7
|
+
module MoneyUniRateApi
|
|
8
|
+
VERSION = "0.1.0"
|
|
9
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
require "money"
|
|
9
|
+
require_relative "uni_rate/version"
|
|
10
|
+
|
|
11
|
+
class Money
|
|
12
|
+
module Bank
|
|
13
|
+
# Raised for any UniRate-specific failure: a network problem, an auth
|
|
14
|
+
# error, a Pro-gated endpoint, or an unexpected response shape.
|
|
15
|
+
class UniRateError < StandardError; end
|
|
16
|
+
|
|
17
|
+
# A {Money::Bank} implementation backed by the UniRate API
|
|
18
|
+
# (https://unirateapi.com).
|
|
19
|
+
#
|
|
20
|
+
# It fetches a single-base rate snapshot from `GET /api/rates` and derives
|
|
21
|
+
# every cross-rate from it on demand, so one HTTP call covers all currency
|
|
22
|
+
# pairs. Derived cross-rates are computed per call (not cached in the
|
|
23
|
+
# store) so a TTL refresh can never serve a stale pair.
|
|
24
|
+
#
|
|
25
|
+
# require "money/bank/uni_rate"
|
|
26
|
+
#
|
|
27
|
+
# Money.default_bank = Money::Bank::UniRate.new(api_key: ENV["UNIRATE_API_KEY"])
|
|
28
|
+
# Money.new(100_00, "USD").exchange_to("EUR") # => #<Money fractional:9200 currency:EUR>
|
|
29
|
+
#
|
|
30
|
+
# Rates are fetched lazily on the first conversion and re-fetched once
|
|
31
|
+
# +ttl_in_seconds+ has elapsed. Leave +ttl_in_seconds+ as +nil+ to fetch
|
|
32
|
+
# exactly once and cache for the life of the object; call {#flush_rates}
|
|
33
|
+
# to force a refresh on the next conversion.
|
|
34
|
+
class UniRate < Money::Bank::VariableExchange
|
|
35
|
+
DEFAULT_BASE_URL = "https://api.unirateapi.com"
|
|
36
|
+
DEFAULT_BASE_CURRENCY = "USD"
|
|
37
|
+
DEFAULT_TIMEOUT = 30
|
|
38
|
+
|
|
39
|
+
attr_reader :base_currency, :rates_updated_at
|
|
40
|
+
attr_accessor :ttl_in_seconds
|
|
41
|
+
|
|
42
|
+
# @param api_key [String] UniRate API key (falls back to ENV["UNIRATE_API_KEY"])
|
|
43
|
+
# @param base_currency [String] currency the snapshot is fetched against
|
|
44
|
+
# @param ttl_in_seconds [Integer, nil] re-fetch interval; nil = fetch once
|
|
45
|
+
# @param base_url [String] base URL override (for testing)
|
|
46
|
+
# @param timeout [Integer, Float] per-request timeout in seconds
|
|
47
|
+
# @param store [Money::RatesStore::Memory] rate store (defaults to in-memory)
|
|
48
|
+
def initialize(api_key: ENV.fetch("UNIRATE_API_KEY", nil),
|
|
49
|
+
base_currency: DEFAULT_BASE_CURRENCY,
|
|
50
|
+
ttl_in_seconds: nil,
|
|
51
|
+
base_url: DEFAULT_BASE_URL,
|
|
52
|
+
timeout: DEFAULT_TIMEOUT,
|
|
53
|
+
store: Money::RatesStore::Memory.new,
|
|
54
|
+
&block)
|
|
55
|
+
if api_key.nil? || api_key.to_s.empty?
|
|
56
|
+
raise ArgumentError, "api_key is required (pass api_key: or set UNIRATE_API_KEY)"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@api_key = api_key
|
|
60
|
+
@base_currency = base_currency.to_s.upcase
|
|
61
|
+
@ttl_in_seconds = ttl_in_seconds
|
|
62
|
+
@base_url = base_url
|
|
63
|
+
@timeout = timeout
|
|
64
|
+
@rates_updated_at = nil
|
|
65
|
+
super(store, &block)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Fetch the latest snapshot and store base->currency rates. Returns the
|
|
69
|
+
# raw { "EUR" => BigDecimal, ... } map. Called lazily on the first
|
|
70
|
+
# conversion; safe to call manually to warm the cache.
|
|
71
|
+
def update_rates
|
|
72
|
+
rates = fetch_rates
|
|
73
|
+
add_rate(base_currency, base_currency, 1)
|
|
74
|
+
rates.each { |code, value| add_rate(base_currency, code, value) }
|
|
75
|
+
@rates_updated_at = Time.now
|
|
76
|
+
rates
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# True when rates have never been fetched, or +ttl_in_seconds+ elapsed.
|
|
80
|
+
def expired?
|
|
81
|
+
return true if rates_updated_at.nil?
|
|
82
|
+
return false if ttl_in_seconds.nil?
|
|
83
|
+
|
|
84
|
+
Time.now - rates_updated_at > ttl_in_seconds
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Exchange rate from +from+ to +to+, deriving cross-rates from the
|
|
88
|
+
# single-base snapshot. Auto-refreshes when {#expired?}.
|
|
89
|
+
#
|
|
90
|
+
# @return [BigDecimal]
|
|
91
|
+
def get_rate(from, to, _opts = {})
|
|
92
|
+
from_iso = Money::Currency.wrap(from).iso_code
|
|
93
|
+
to_iso = Money::Currency.wrap(to).iso_code
|
|
94
|
+
|
|
95
|
+
update_rates if expired?
|
|
96
|
+
return BigDecimal(1) if from_iso == to_iso
|
|
97
|
+
|
|
98
|
+
base_rate(to_iso) / base_rate(from_iso)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Force a refresh on the next conversion (does not hit the network now).
|
|
102
|
+
def flush_rates
|
|
103
|
+
@rates_updated_at = nil
|
|
104
|
+
self
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
# base_currency -> iso rate (1 for the base itself). Raises if absent.
|
|
110
|
+
def base_rate(iso)
|
|
111
|
+
return BigDecimal(1) if iso == base_currency
|
|
112
|
+
|
|
113
|
+
rate = store.get_rate(base_currency, iso)
|
|
114
|
+
raise UniRateError, "No UniRate rate available for #{base_currency}->#{iso}" if rate.nil?
|
|
115
|
+
|
|
116
|
+
rate
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def fetch_rates
|
|
120
|
+
body = http_get("/api/rates", "from" => base_currency)
|
|
121
|
+
rates = body["rates"]
|
|
122
|
+
raise UniRateError, 'Unexpected UniRate response: missing "rates"' unless rates.is_a?(Hash)
|
|
123
|
+
|
|
124
|
+
rates.each_with_object({}) do |(code, value), out|
|
|
125
|
+
out[code.to_s.upcase] = BigDecimal(value.to_s)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def http_get(path, params)
|
|
130
|
+
uri = URI.parse(@base_url)
|
|
131
|
+
uri.path = path
|
|
132
|
+
uri.query = URI.encode_www_form(params.merge("api_key" => @api_key))
|
|
133
|
+
|
|
134
|
+
req = Net::HTTP::Get.new(uri)
|
|
135
|
+
req["Accept"] = "application/json"
|
|
136
|
+
req["User-Agent"] = "money-unirate-api/#{MoneyUniRateApi::VERSION}"
|
|
137
|
+
|
|
138
|
+
parse(perform(uri, req))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def perform(uri, req)
|
|
142
|
+
Net::HTTP.start(uri.hostname, uri.port,
|
|
143
|
+
use_ssl: uri.scheme == "https",
|
|
144
|
+
open_timeout: @timeout,
|
|
145
|
+
read_timeout: @timeout) { |http| http.request(req) }
|
|
146
|
+
rescue StandardError => e
|
|
147
|
+
raise UniRateError, "Network error talking to UniRate: #{e.message}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def parse(response)
|
|
151
|
+
status = response.code.to_i
|
|
152
|
+
case status
|
|
153
|
+
when 200..299
|
|
154
|
+
JSON.parse(response.body.to_s)
|
|
155
|
+
when 401 then raise UniRateError, "Missing or invalid UniRate API key (HTTP 401)"
|
|
156
|
+
when 403 then raise UniRateError, "This UniRate endpoint requires a Pro subscription (HTTP 403)"
|
|
157
|
+
when 404 then raise UniRateError, "Currency not found or no data available (HTTP 404)"
|
|
158
|
+
when 429 then raise UniRateError, "UniRate rate limit exceeded (HTTP 429)"
|
|
159
|
+
else
|
|
160
|
+
raise UniRateError, "UniRate API error (HTTP #{status}): #{response.body}"
|
|
161
|
+
end
|
|
162
|
+
rescue JSON::ParserError => e
|
|
163
|
+
raise UniRateError, "Failed to parse UniRate response: #{e.message}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
+
require "money/bank/uni_rate/version"
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |spec|
|
|
8
|
+
spec.name = "money-unirate-api"
|
|
9
|
+
spec.version = MoneyUniRateApi::VERSION
|
|
10
|
+
spec.authors = ["Unirate Team"]
|
|
11
|
+
spec.email = ["admin@unirateapi.com"]
|
|
12
|
+
|
|
13
|
+
spec.summary = "UniRate API exchange-rate bank for the money gem."
|
|
14
|
+
spec.description = "A Money::Bank implementation backed by the UniRate API " \
|
|
15
|
+
"(https://unirateapi.com) — free currency exchange rates " \
|
|
16
|
+
"for RubyMoney. Fetches one base snapshot and derives all " \
|
|
17
|
+
"cross-rates, with optional TTL-based refresh."
|
|
18
|
+
spec.homepage = "https://github.com/UniRate-API/money-unirate-api"
|
|
19
|
+
spec.license = "MIT"
|
|
20
|
+
|
|
21
|
+
spec.required_ruby_version = ">= 3.0"
|
|
22
|
+
|
|
23
|
+
spec.metadata = {
|
|
24
|
+
"homepage_uri" => spec.homepage,
|
|
25
|
+
"source_code_uri" => spec.homepage,
|
|
26
|
+
"bug_tracker_uri" => "#{spec.homepage}/issues",
|
|
27
|
+
"changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md",
|
|
28
|
+
"documentation_uri" => "https://unirateapi.com",
|
|
29
|
+
"rubygems_mfa_required" => "true"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
spec.files = Dir[
|
|
33
|
+
"lib/**/*.rb",
|
|
34
|
+
"README.md",
|
|
35
|
+
"CHANGELOG.md",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"money-unirate-api.gemspec"
|
|
38
|
+
]
|
|
39
|
+
spec.require_paths = ["lib"]
|
|
40
|
+
|
|
41
|
+
spec.add_dependency "money", ">= 6.13", "< 7"
|
|
42
|
+
|
|
43
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
44
|
+
spec.add_development_dependency "rspec", "~> 3.12"
|
|
45
|
+
spec.add_development_dependency "rubocop", "~> 1.60"
|
|
46
|
+
spec.add_development_dependency "webmock", "~> 3.19"
|
|
47
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: money-unirate-api
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Unirate Team
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-30 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: money
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '6.13'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '7'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '6.13'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: rake
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
type: :development
|
|
41
|
+
prerelease: false
|
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: rspec
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.12'
|
|
54
|
+
type: :development
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.12'
|
|
61
|
+
- !ruby/object:Gem::Dependency
|
|
62
|
+
name: rubocop
|
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.60'
|
|
68
|
+
type: :development
|
|
69
|
+
prerelease: false
|
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.60'
|
|
75
|
+
- !ruby/object:Gem::Dependency
|
|
76
|
+
name: webmock
|
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.19'
|
|
82
|
+
type: :development
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.19'
|
|
89
|
+
description: A Money::Bank implementation backed by the UniRate API (https://unirateapi.com)
|
|
90
|
+
— free currency exchange rates for RubyMoney. Fetches one base snapshot and derives
|
|
91
|
+
all cross-rates, with optional TTL-based refresh.
|
|
92
|
+
email:
|
|
93
|
+
- admin@unirateapi.com
|
|
94
|
+
executables: []
|
|
95
|
+
extensions: []
|
|
96
|
+
extra_rdoc_files: []
|
|
97
|
+
files:
|
|
98
|
+
- CHANGELOG.md
|
|
99
|
+
- LICENSE
|
|
100
|
+
- README.md
|
|
101
|
+
- lib/money-unirate-api.rb
|
|
102
|
+
- lib/money/bank/uni_rate.rb
|
|
103
|
+
- lib/money/bank/uni_rate/version.rb
|
|
104
|
+
- money-unirate-api.gemspec
|
|
105
|
+
homepage: https://github.com/UniRate-API/money-unirate-api
|
|
106
|
+
licenses:
|
|
107
|
+
- MIT
|
|
108
|
+
metadata:
|
|
109
|
+
homepage_uri: https://github.com/UniRate-API/money-unirate-api
|
|
110
|
+
source_code_uri: https://github.com/UniRate-API/money-unirate-api
|
|
111
|
+
bug_tracker_uri: https://github.com/UniRate-API/money-unirate-api/issues
|
|
112
|
+
changelog_uri: https://github.com/UniRate-API/money-unirate-api/blob/main/CHANGELOG.md
|
|
113
|
+
documentation_uri: https://unirateapi.com
|
|
114
|
+
rubygems_mfa_required: 'true'
|
|
115
|
+
post_install_message:
|
|
116
|
+
rdoc_options: []
|
|
117
|
+
require_paths:
|
|
118
|
+
- lib
|
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '3.0'
|
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
125
|
+
requirements:
|
|
126
|
+
- - ">="
|
|
127
|
+
- !ruby/object:Gem::Version
|
|
128
|
+
version: '0'
|
|
129
|
+
requirements: []
|
|
130
|
+
rubygems_version: 3.5.22
|
|
131
|
+
signing_key:
|
|
132
|
+
specification_version: 4
|
|
133
|
+
summary: UniRate API exchange-rate bank for the money gem.
|
|
134
|
+
test_files: []
|