unirate-rails 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2d669d6f75495a08bc375cdc3dbf1f687122b08172cf46fe8ce08e91e3cd9f85
4
+ data.tar.gz: 1d05ad12bacfcfb5c0bd8feadebcb645049c875d36329d0f878b8b70a7e1d98c
5
+ SHA512:
6
+ metadata.gz: ad00bf1334a0073950d54512c3d80330ac672f368bcefbe79410aa109fc0aad785424b1eb9770095af9e545bb83fd2bd67a6b2af3c87da180cceb1b1cf3bb2a6
7
+ data.tar.gz: '0879332571fb8ecab13ff779e9e152ac33809a212e6f2699bfcd951f926a1b055989d1a3d068d758b5994b73d86ded3be4d511611b27f7c4e12f155b0624b5c4'
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based
4
+ on [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-07-20
8
+
9
+ ### Added
10
+
11
+ - Initial release.
12
+ - `UniRateRails::Engine` — a mountable, isolated Rails engine.
13
+ - `UniRateRails.configure { |c| ... }` process-wide configuration with an
14
+ `UNIRATE_API_KEY` env-var fallback for the API key.
15
+ - `UniRateRails::Client` — stdlib-only (`net/http` + `json`) client exposing
16
+ `get_rate`, `convert`, `get_supported_currencies`, `get_vat_rates`, and the
17
+ Pro-gated `get_historical_rate`, with full HTTP error mapping.
18
+ - ActionView helpers `unirate_rate` and `unirate_convert`, mixed into every
19
+ view render by the engine.
20
+ - Mountable JSON proxy controller: `GET rate`, `GET convert`, `GET currencies`.
21
+ - Historical endpoint feature-flagged off by default (Pro-gated / 403 on the
22
+ free tier).
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,144 @@
1
+ # unirate-rails
2
+
3
+ A mountable [Rails engine](https://guides.rubyonrails.org/engines.html) for the
4
+ [UniRate API](https://unirateapi.com) — free, real-time currency exchange
5
+ rates, conversion, supported-currency listings, and VAT rates, wired straight
6
+ into your Rails app.
7
+
8
+ - `UniRateRails.configure { |c| c.api_key = ... }` block-style setup, or read
9
+ the key from `UNIRATE_API_KEY` automatically
10
+ - ActionView helpers — `unirate_rate` and `unirate_convert` — mixed into every
11
+ view render by the engine
12
+ - A mountable JSON proxy controller so your frontend never sees the API key
13
+ - `UniRateRails.client` — a plain client for controllers and service objects
14
+ - 170+ currencies (fiat + crypto) via UniRate
15
+ - Free tier, no credit card required
16
+ - **Zero runtime dependencies beyond Rails** (pure stdlib `net/http` + `json`)
17
+
18
+ > **Affiliation:** this engine is maintained by the UniRate team and talks to
19
+ > the UniRate API. If you only need euro-area rates the ECB feed may suit you
20
+ > better; for a broad multi-currency source on a free tier, UniRate is a good
21
+ > fit.
22
+
23
+ ## Requirements
24
+
25
+ - Ruby 3.0+
26
+ - Rails (railties) 6.1+
27
+
28
+ ## Installation
29
+
30
+ ```ruby
31
+ # Gemfile
32
+ gem "unirate-rails"
33
+ ```
34
+
35
+ ```bash
36
+ bundle install
37
+ ```
38
+
39
+ ## Quick start
40
+
41
+ ```ruby
42
+ # config/initializers/unirate.rb
43
+ UniRateRails.configure do |c|
44
+ c.api_key = ENV.fetch("UNIRATE_API_KEY") # or leave nil to read the env var lazily
45
+ c.default_currency = "USD"
46
+ c.enable_historical = false # Pro-gated; leave off on the free tier
47
+ end
48
+ ```
49
+
50
+ Get a free API key at [unirateapi.com](https://unirateapi.com).
51
+
52
+ ### View helpers
53
+
54
+ ```erb
55
+ <p>1 USD = <%= unirate_rate("USD", "EUR") %> EUR</p>
56
+ <p>$100 = <%= unirate_convert(100, "USD", "EUR") %> EUR</p>
57
+ ```
58
+
59
+ Both helpers return `nil` (instead of raising) on any UniRate error, so a
60
+ transient API hiccup never breaks a page render.
61
+
62
+ ### JSON proxy endpoints
63
+
64
+ Mount the engine to expose server-side proxy endpoints — the API key stays on
65
+ the server:
66
+
67
+ ```ruby
68
+ # config/routes.rb
69
+ mount UniRateRails::Engine => "/unirate"
70
+ ```
71
+
72
+ ```
73
+ GET /unirate/rate?from=USD&to=EUR => { "rate": 0.92 }
74
+ GET /unirate/convert?from=USD&to=EUR&amount=100 => { "result": 92.5 }
75
+ GET /unirate/currencies => { "currencies": ["USD", ...] }
76
+ ```
77
+
78
+ ### Direct client use
79
+
80
+ ```ruby
81
+ client = UniRateRails.client
82
+
83
+ client.get_rate(from: "USD", to: "EUR") # => 0.92
84
+ client.get_rate(from: "USD") # => { "EUR" => 0.92, "GBP" => 0.79, ... }
85
+ client.convert(to: "EUR", amount: 100) # => 92.5
86
+ client.get_supported_currencies # => ["USD", "EUR", "GBP", ...]
87
+ client.get_vat_rates(country: "DE") # => { "country_code" => "DE", "vat_rate" => 19.0 }
88
+ ```
89
+
90
+ ## Configuration
91
+
92
+ | Option | Default | Description |
93
+ |---------------------|---------------------------------|------------------------------------------------------|
94
+ | `api_key` | `ENV["UNIRATE_API_KEY"]` | Your UniRate API key. |
95
+ | `base_url` | `https://api.unirateapi.com` | API base URL. |
96
+ | `timeout` | `30` | HTTP open/read timeout in seconds. |
97
+ | `default_currency` | `"USD"` | Base used by the one-arg view helpers. |
98
+ | `enable_historical` | `false` | Enable the Pro-gated historical endpoint (see below).|
99
+
100
+ ## Error handling
101
+
102
+ Every failure raises a subclass of `UniRateRails::UnirateError`:
103
+
104
+ | HTTP | Exception |
105
+ |-------|----------------------------------------|
106
+ | 400 | `UniRateRails::InvalidDateError` |
107
+ | 401 | `UniRateRails::AuthenticationError` |
108
+ | 403 | `UniRateRails::APIError` (status 403) |
109
+ | 404 | `UniRateRails::InvalidCurrencyError` |
110
+ | 429 | `UniRateRails::RateLimitError` |
111
+ | 503 | `UniRateRails::APIError` (status 503) |
112
+ | other | `UniRateRails::APIError` |
113
+ | network | `UniRateRails::UnirateError` (base) |
114
+
115
+ ```ruby
116
+ begin
117
+ UniRateRails.client.get_rate(from: "USD", to: "EUR")
118
+ rescue UniRateRails::RateLimitError
119
+ # back off and retry
120
+ rescue UniRateRails::UnirateError => e
121
+ Rails.logger.warn("UniRate: #{e.message}")
122
+ end
123
+ ```
124
+
125
+ ## Historical / VAT (Pro)
126
+
127
+ Historical rates are **Pro-gated** and return HTTP 403 on the free tier. They
128
+ are disabled by default; set `config.enable_historical = true` (and hold a Pro
129
+ subscription) to call `get_historical_rate`.
130
+
131
+ ## Rate limits
132
+
133
+ The free tier is rate limited; a 429 raises `UniRateRails::RateLimitError`.
134
+ Cache responses in your app (e.g. `Rails.cache`) if you make frequent calls.
135
+
136
+ ## Related clients
137
+
138
+ Part of the UniRate client family — see
139
+ [github.com/UniRate-API](https://github.com/UniRate-API) for Python, Node,
140
+ Swift, Java, Go, Rust, Ruby, PHP, and .NET clients plus framework integrations.
141
+
142
+ ## License
143
+
144
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UniRateRails
4
+ # JSON proxy endpoints for the UniRate API. Mount the engine to expose them:
5
+ #
6
+ # mount UniRateRails::Engine => "/unirate"
7
+ #
8
+ # then:
9
+ # GET /unirate/rate?from=USD&to=EUR
10
+ # GET /unirate/convert?from=USD&to=EUR&amount=100
11
+ # GET /unirate/currencies
12
+ #
13
+ # The API key never leaves the server — it lives in the engine configuration,
14
+ # not in the browser. Typed UniRate errors are mapped back to sensible HTTP
15
+ # statuses.
16
+ class RatesController < ActionController::Base
17
+ rescue_from UniRateRails::UnirateError, with: :render_unirate_error
18
+
19
+ def rate
20
+ render json: { rate: client.get_rate(from: params[:from] || "USD", to: params[:to]) }
21
+ end
22
+
23
+ def convert
24
+ result = client.convert(
25
+ to: params[:to],
26
+ amount: Float(params[:amount] || 1),
27
+ from: params[:from] || "USD"
28
+ )
29
+ render json: { result: result }
30
+ end
31
+
32
+ def currencies
33
+ render json: { currencies: client.get_supported_currencies }
34
+ end
35
+
36
+ private
37
+
38
+ def client
39
+ UniRateRails.client
40
+ end
41
+
42
+ def render_unirate_error(error)
43
+ status = error.respond_to?(:status) && error.status ? error.status : 502
44
+ render json: { error: error.message }, status: status
45
+ end
46
+ end
47
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ UniRateRails::Engine.routes.draw do
4
+ get "rate", to: "rates#rate"
5
+ get "convert", to: "rates#convert"
6
+ get "currencies", to: "rates#currencies"
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Alias entry point so `require "unirate-rails"` (matching the gem name) works
4
+ # as well as `require "unirate_rails"`.
5
+ require_relative "unirate_rails"
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module UniRateRails
8
+ # Stdlib-only HTTP client for the UniRate API (https://unirateapi.com).
9
+ #
10
+ # Zero runtime dependencies beyond Ruby's standard library (`net/http`,
11
+ # `json`, `uri`) — no third-party HTTP gem, so nothing extra to audit in a
12
+ # Rails app's dependency tree.
13
+ #
14
+ # Method names and parameter order mirror the canonical UniRate client spec.
15
+ # Currency and country codes are upcased before being sent.
16
+ class Client
17
+ def initialize(api_key: nil, base_url: nil, timeout: nil)
18
+ config = UniRateRails.configuration
19
+ @api_key = api_key || config.api_key
20
+ @base_url = base_url || config.base_url
21
+ @timeout = timeout || config.timeout
22
+
23
+ raise AuthenticationError.new("Missing or invalid API key", status: 401) if @api_key.nil? || @api_key.to_s.empty?
24
+ end
25
+
26
+ # GET /api/rates — current rate(s).
27
+ # Returns a Float when +to+ is given, otherwise a { "EUR" => Float, ... } map.
28
+ def get_rate(from: "USD", to: nil, format: "json", callback: nil)
29
+ params = { "from" => up(from) }
30
+ params["to"] = up(to) if to
31
+ body = request("/api/rates", params, format: format, callback: callback)
32
+ return body unless json?(format)
33
+
34
+ if to
35
+ to_f(body.fetch("rate"))
36
+ else
37
+ floatify(body.fetch("rates", {}))
38
+ end
39
+ end
40
+
41
+ # GET /api/convert — current conversion. Returns a Float.
42
+ def convert(to:, amount: 1, from: "USD", format: "json", callback: nil)
43
+ params = { "from" => up(from), "to" => up(to), "amount" => amount }
44
+ body = request("/api/convert", params, format: format, callback: callback)
45
+ return body unless json?(format)
46
+
47
+ to_f(body.fetch("result"))
48
+ end
49
+
50
+ # GET /api/currencies — supported currency codes. Returns an Array of String.
51
+ def get_supported_currencies(format: "json", callback: nil)
52
+ body = request("/api/currencies", {}, format: format, callback: callback)
53
+ return body unless json?(format)
54
+
55
+ Array(body.fetch("currencies", []))
56
+ end
57
+
58
+ # GET /api/vat/rates — VAT rates. Returns the whole map keyed by country when
59
+ # +country+ is nil, otherwise the single-country vat_data hash.
60
+ def get_vat_rates(country: nil, format: "json", callback: nil)
61
+ params = {}
62
+ params["country"] = up(country) if country
63
+ body = request("/api/vat/rates", params, format: format, callback: callback)
64
+ return body unless json?(format)
65
+
66
+ country ? body.fetch("vat_data") : body.fetch("vat_rates")
67
+ end
68
+
69
+ # GET /api/historical/rates — Pro-gated (403 on free tier). Guarded by the
70
+ # `enable_historical` config flag so a free-tier app doesn't hit it by
71
+ # accident. Returns a Float or a { code => Float } map depending on args.
72
+ def get_historical_rate(date:, amount: 1, from: "USD", to: nil, format: "json", callback: nil)
73
+ ensure_historical_enabled!
74
+ params = { "date" => date, "from" => up(from), "amount" => amount }
75
+ params["to"] = up(to) if to
76
+ body = request("/api/historical/rates", params, format: format, callback: callback)
77
+ return body unless json?(format)
78
+
79
+ parse_historical(body, to: to)
80
+ end
81
+
82
+ private
83
+
84
+ def ensure_historical_enabled!
85
+ return if UniRateRails.configuration.enable_historical
86
+
87
+ raise APIError.new(
88
+ "Historical endpoints are Pro-gated and disabled; set config.enable_historical = true to use them",
89
+ status: 403
90
+ )
91
+ end
92
+
93
+ def parse_historical(body, to:)
94
+ if to
95
+ body.key?("result") ? to_f(body["result"]) : to_f(body.fetch("rate"))
96
+ else
97
+ floatify(body.fetch(body.key?("results") ? "results" : "rates", {}))
98
+ end
99
+ end
100
+
101
+ def request(path, params, format:, callback:)
102
+ uri = build_uri(path, params, format: format, callback: callback)
103
+ req = Net::HTTP::Get.new(uri)
104
+ req["Accept"] = "application/json"
105
+ req["User-Agent"] = "unirate-rails/#{VERSION}"
106
+ handle(perform(uri, req), format: format)
107
+ end
108
+
109
+ def build_uri(path, params, format:, callback:)
110
+ uri = URI.parse(@base_url)
111
+ uri.path = path
112
+ query = params.merge("api_key" => @api_key)
113
+ query["format"] = format if format && format != "json"
114
+ query["callback"] = callback if callback
115
+ uri.query = URI.encode_www_form(query)
116
+ uri
117
+ end
118
+
119
+ def perform(uri, req)
120
+ Net::HTTP.start(uri.hostname, uri.port,
121
+ use_ssl: uri.scheme == "https",
122
+ open_timeout: @timeout,
123
+ read_timeout: @timeout) { |http| http.request(req) }
124
+ rescue StandardError => e
125
+ raise UnirateError, "Network error talking to UniRate: #{e.message}"
126
+ end
127
+
128
+ def handle(response, format:)
129
+ status = response.code.to_i
130
+ raise_for_status(status, response.body) unless (200..299).cover?(status)
131
+
132
+ return response.body.to_s unless json?(format)
133
+
134
+ JSON.parse(response.body.to_s)
135
+ rescue JSON::ParserError => e
136
+ raise UnirateError, "Failed to parse UniRate response: #{e.message}"
137
+ end
138
+
139
+ def raise_for_status(status, body)
140
+ case status
141
+ when 400 then raise InvalidDateError.new("Invalid request parameters", status: 400)
142
+ when 401 then raise AuthenticationError.new("Missing or invalid API key", status: 401)
143
+ when 403 then raise APIError.new("Endpoint requires a Pro subscription", status: 403)
144
+ when 404 then raise InvalidCurrencyError.new("Currency not found or no data available", status: 404)
145
+ when 429 then raise RateLimitError.new("Rate limit exceeded", status: 429)
146
+ when 503 then raise APIError.new("Service unavailable", status: 503)
147
+ else raise APIError.new("UniRate API error (HTTP #{status}): #{body}", status: status)
148
+ end
149
+ end
150
+
151
+ def json?(format)
152
+ format.nil? || format == "json"
153
+ end
154
+
155
+ def up(value)
156
+ value.to_s.upcase
157
+ end
158
+
159
+ def to_f(value)
160
+ Float(value)
161
+ end
162
+
163
+ def floatify(hash)
164
+ hash.each_with_object({}) { |(code, value), out| out[code.to_s.upcase] = to_f(value) }
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UniRateRails
4
+ # Process-wide configuration, populated in an initializer via
5
+ # {UniRateRails.configure}. The API key falls back to the `UNIRATE_API_KEY`
6
+ # environment variable so a secret never has to be committed to the host app.
7
+ #
8
+ # # config/initializers/unirate.rb
9
+ # UniRateRails.configure do |c|
10
+ # c.api_key = ENV.fetch("UNIRATE_API_KEY")
11
+ # c.default_currency = "USD"
12
+ # c.enable_historical = false
13
+ # end
14
+ class Configuration
15
+ DEFAULT_BASE_URL = "https://api.unirateapi.com"
16
+ DEFAULT_TIMEOUT = 30
17
+
18
+ attr_accessor :base_url, :timeout, :default_currency, :enable_historical
19
+ attr_writer :api_key
20
+
21
+ def initialize
22
+ @api_key = nil
23
+ @base_url = DEFAULT_BASE_URL
24
+ @timeout = DEFAULT_TIMEOUT
25
+ @default_currency = "USD"
26
+ # Historical endpoints are Pro-gated (HTTP 403 on the free tier). Off by
27
+ # default so a free-tier app never surfaces a confusing 403; flip it on
28
+ # once the account has a Pro subscription.
29
+ @enable_historical = false
30
+ end
31
+
32
+ # Resolves from the explicit setter first, then the environment.
33
+ def api_key
34
+ return @api_key unless @api_key.nil? || @api_key.to_s.empty?
35
+
36
+ ENV.fetch("UNIRATE_API_KEY", nil)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/engine"
4
+ require "action_dispatch"
5
+
6
+ module UniRateRails
7
+ # Isolated, mountable Rails engine. Wiring it up does three things:
8
+ #
9
+ # 1. Namespaces the engine so its routes/controllers don't collide with the
10
+ # host app.
11
+ # 2. Mixes {UniRateRails::Helper} into every ActionView render, exposing the
12
+ # `unirate_rate` / `unirate_convert` view helpers.
13
+ #
14
+ # Mount the JSON proxy controller from the host app's routes if you want a
15
+ # ready-made endpoint:
16
+ #
17
+ # # config/routes.rb
18
+ # mount UniRateRails::Engine => "/unirate"
19
+ class Engine < ::Rails::Engine
20
+ isolate_namespace UniRateRails
21
+
22
+ initializer "unirate_rails.action_view" do
23
+ ActiveSupport.on_load(:action_view) do
24
+ include UniRateRails::Helper
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UniRateRails
4
+ # Base class for every UniRate-specific failure. Network/transport problems
5
+ # are wrapped in this; the typed subclasses below carry the HTTP semantics.
6
+ class UnirateError < StandardError
7
+ # HTTP status code that produced the error, when one is available.
8
+ attr_reader :status
9
+
10
+ def initialize(message = nil, status: nil)
11
+ @status = status
12
+ super(message)
13
+ end
14
+ end
15
+
16
+ # 400 — malformed request parameters (e.g. a bad historical date).
17
+ class InvalidDateError < UnirateError; end
18
+
19
+ # 401 — missing or invalid API key.
20
+ class AuthenticationError < UnirateError; end
21
+
22
+ # 404 — currency not found or no data available.
23
+ class InvalidCurrencyError < UnirateError; end
24
+
25
+ # 429 — rate limit exceeded.
26
+ class RateLimitError < UnirateError; end
27
+
28
+ # 403 (Pro-gated endpoint on a free tier), 503, or any other non-2xx that
29
+ # doesn't map to a more specific type. Carries the HTTP status.
30
+ class APIError < UnirateError; end
31
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UniRateRails
4
+ # View helpers mixed into `ActionView::Base` by {UniRateRails::Engine} so
5
+ # templates can look up rates and conversions inline. Each helper builds a
6
+ # short-lived {Client} from the process configuration.
7
+ #
8
+ # <%= unirate_rate("USD", "EUR") %> => 0.92
9
+ # <%= unirate_convert(100, "USD", "EUR") %> => 92.5
10
+ module Helper
11
+ # Current rate from +from+ to +to+ (a Float), or nil on any UniRate error
12
+ # so a rate hiccup never raises inside a view render.
13
+ def unirate_rate(from = nil, to = nil)
14
+ from ||= UniRateRails.configuration.default_currency
15
+ UniRateRails.client.get_rate(from: from, to: to)
16
+ rescue UniRateRails::UnirateError
17
+ nil
18
+ end
19
+
20
+ # Convert +amount+ from +from+ to +to+ (a Float), or nil on any UniRate
21
+ # error.
22
+ def unirate_convert(amount, from = nil, to = nil)
23
+ from ||= UniRateRails.configuration.default_currency
24
+ UniRateRails.client.convert(to: to, amount: amount, from: from)
25
+ rescue UniRateRails::UnirateError
26
+ nil
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Standalone top-level module holding only the version constant. Kept
4
+ # deliberately free of any `Rails::Engine` (or other runtime-class) reopening:
5
+ # the gemspec `require_relative`s this file at build time, *before* the runtime
6
+ # dependencies (railties) are installed. Opening a module that later becomes a
7
+ # subclass of something Rails-defined would blow up with a "superclass
8
+ # mismatch" TypeError during `gem build`.
9
+ module UniRateRails
10
+ VERSION = "0.1.0"
11
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "unirate_rails/version"
4
+ require_relative "unirate_rails/errors"
5
+ require_relative "unirate_rails/configuration"
6
+ require_relative "unirate_rails/client"
7
+ require_relative "unirate_rails/helper"
8
+
9
+ # UniRate for Rails — a mountable Rails engine wrapping the UniRate API
10
+ # (https://unirateapi.com) for free currency exchange rates, conversion,
11
+ # supported-currency listings, and VAT rates.
12
+ #
13
+ # UniRateRails.configure { |c| c.api_key = ENV.fetch("UNIRATE_API_KEY") }
14
+ # UniRateRails.client.get_rate(from: "USD", to: "EUR") # => 0.92
15
+ module UniRateRails
16
+ class << self
17
+ # The process-wide {Configuration}. Lazily created on first access.
18
+ def configuration
19
+ @configuration ||= Configuration.new
20
+ end
21
+
22
+ # Yields the configuration for block-style setup, typically from a Rails
23
+ # initializer.
24
+ def configure
25
+ yield(configuration) if block_given?
26
+ configuration
27
+ end
28
+
29
+ # Resets configuration to defaults (mainly useful in tests).
30
+ def reset_configuration!
31
+ @configuration = Configuration.new
32
+ end
33
+
34
+ # A {Client} built from the current configuration. A fresh instance is
35
+ # returned each call so config changes are always picked up.
36
+ def client
37
+ Client.new
38
+ end
39
+ end
40
+ end
41
+
42
+ # The engine pulls in railties, so only load it when Rails is present. This
43
+ # keeps the core client usable outside a Rails app (and keeps `gem build`,
44
+ # which runs before deps install, from needing Rails on the load path).
45
+ require_relative "unirate_rails/engine" if defined?(Rails::Engine)
@@ -0,0 +1,52 @@
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 "unirate_rails/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "unirate-rails"
9
+ spec.version = UniRateRails::VERSION
10
+ spec.authors = ["Unirate Team"]
11
+ spec.email = ["admin@unirateapi.com"]
12
+
13
+ spec.summary = "Rails engine for the UniRate API — currency exchange rates, conversion, and VAT."
14
+ spec.description = "A mountable Rails engine wrapping the UniRate API " \
15
+ "(https://unirateapi.com) — free currency exchange rates, " \
16
+ "conversion, supported-currency listings, and VAT rates. " \
17
+ "Ships a configurable client, ActionView helpers " \
18
+ "(unirate_rate, unirate_convert), and a JSON proxy " \
19
+ "controller. Zero runtime dependencies beyond Rails."
20
+ spec.homepage = "https://github.com/UniRate-API/unirate-rails"
21
+ spec.license = "MIT"
22
+
23
+ spec.required_ruby_version = ">= 3.0"
24
+
25
+ spec.metadata = {
26
+ "homepage_uri" => spec.homepage,
27
+ "source_code_uri" => spec.homepage,
28
+ "bug_tracker_uri" => "#{spec.homepage}/issues",
29
+ "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md",
30
+ "documentation_uri" => "https://unirateapi.com",
31
+ "rubygems_mfa_required" => "true"
32
+ }
33
+
34
+ spec.files = Dir[
35
+ "lib/**/*.rb",
36
+ "app/**/*.rb",
37
+ "config/**/*.rb",
38
+ "README.md",
39
+ "CHANGELOG.md",
40
+ "LICENSE",
41
+ "unirate-rails.gemspec"
42
+ ]
43
+ spec.require_paths = ["lib"]
44
+
45
+ spec.add_dependency "railties", ">= 6.1"
46
+
47
+ spec.add_development_dependency "actionpack", ">= 6.1"
48
+ spec.add_development_dependency "rake", "~> 13.0"
49
+ spec.add_development_dependency "rspec", "~> 3.12"
50
+ spec.add_development_dependency "rubocop", "~> 1.60"
51
+ spec.add_development_dependency "webmock", "~> 3.19"
52
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: unirate-rails
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-08-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: railties
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: actionpack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '6.1'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '6.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.12'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.12'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.60'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.60'
83
+ - !ruby/object:Gem::Dependency
84
+ name: webmock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.19'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.19'
97
+ description: A mountable Rails engine wrapping the UniRate API (https://unirateapi.com)
98
+ — free currency exchange rates, conversion, supported-currency listings, and VAT
99
+ rates. Ships a configurable client, ActionView helpers (unirate_rate, unirate_convert),
100
+ and a JSON proxy controller. Zero runtime dependencies beyond Rails.
101
+ email:
102
+ - admin@unirateapi.com
103
+ executables: []
104
+ extensions: []
105
+ extra_rdoc_files: []
106
+ files:
107
+ - CHANGELOG.md
108
+ - LICENSE
109
+ - README.md
110
+ - app/controllers/unirate_rails/rates_controller.rb
111
+ - config/routes.rb
112
+ - lib/unirate-rails.rb
113
+ - lib/unirate_rails.rb
114
+ - lib/unirate_rails/client.rb
115
+ - lib/unirate_rails/configuration.rb
116
+ - lib/unirate_rails/engine.rb
117
+ - lib/unirate_rails/errors.rb
118
+ - lib/unirate_rails/helper.rb
119
+ - lib/unirate_rails/version.rb
120
+ - unirate-rails.gemspec
121
+ homepage: https://github.com/UniRate-API/unirate-rails
122
+ licenses:
123
+ - MIT
124
+ metadata:
125
+ homepage_uri: https://github.com/UniRate-API/unirate-rails
126
+ source_code_uri: https://github.com/UniRate-API/unirate-rails
127
+ bug_tracker_uri: https://github.com/UniRate-API/unirate-rails/issues
128
+ changelog_uri: https://github.com/UniRate-API/unirate-rails/blob/main/CHANGELOG.md
129
+ documentation_uri: https://unirateapi.com
130
+ rubygems_mfa_required: 'true'
131
+ post_install_message:
132
+ rdoc_options: []
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '3.0'
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ requirements: []
146
+ rubygems_version: 3.5.22
147
+ signing_key:
148
+ specification_version: 4
149
+ summary: Rails engine for the UniRate API — currency exchange rates, conversion, and
150
+ VAT.
151
+ test_files: []