ibanchecker 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 +12 -0
- data/LICENSE +21 -0
- data/README.md +197 -0
- data/lib/ibanchecker/client.rb +131 -0
- data/lib/ibanchecker/errors.rb +44 -0
- data/lib/ibanchecker/models.rb +219 -0
- data/lib/ibanchecker/transport.rb +81 -0
- data/lib/ibanchecker/version.rb +5 -0
- data/lib/ibanchecker.rb +18 -0
- metadata +58 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5959a043f0fe0044a65aa74d7203b57aeeafd050934efa28b7776dd938f3026b
|
|
4
|
+
data.tar.gz: 891de691976d13de1c3e5a3a9bb50f4a79ffdbae62f3f2d4f45b445ca03ea125
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bce4770bafedfb4fc03437f602200ecc469d8bc7ed2c85615797ad8a743ef98d4ce14510035b55785ab72f79514bdacca6b951772e1fa68715d6ced637188384
|
|
7
|
+
data.tar.gz: 6b6186281e8edb97df2a3fb7b7ea6677df6a7850262265a518a0e06ac1f199d2d1798bf93184e146b66b313b11f20f5f4d91b13c5f649c8fc934d501ff3d7114
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- `validate`, `validate_bulk`, `extract`, `country_format` and `lookup_bic`
|
|
8
|
+
- Typed models for every response, with the raw body kept on `#raw`
|
|
9
|
+
- Typed errors for 400, 401, 404, 429, other statuses and transport failures;
|
|
10
|
+
a malformed IBAN is a result with `valid?` false, never an error
|
|
11
|
+
- No runtime dependencies; ships a `net/http` transport and accepts anything
|
|
12
|
+
responding to `call(method, url, headers, body)` instead
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ibanchecker.cash
|
|
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,197 @@
|
|
|
1
|
+
# ibanchecker
|
|
2
|
+
|
|
3
|
+
Official Ruby client for the [ibanchecker.cash](https://ibanchecker.cash) IBAN validation API.
|
|
4
|
+
|
|
5
|
+
Validate IBANs across 92 countries, validate up to 100 IBANs per request, extract IBANs from free text, look up country format specifications, and resolve SWIFT/BIC codes. No IBAN data is stored or logged; all validation runs in memory at the edge.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
gem install ibanchecker
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or in a Gemfile:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem "ibanchecker"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Ruby 2.7 or newer. There are no runtime dependencies: the client is built on `net/http` and `json` from the standard library.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "ibanchecker"
|
|
25
|
+
|
|
26
|
+
client = IbanChecker::Client.new # no API key needed for light use (100 requests/hour per IP)
|
|
27
|
+
|
|
28
|
+
result = client.validate("DE89 3704 0044 0532 0130 00")
|
|
29
|
+
|
|
30
|
+
if result.valid?
|
|
31
|
+
result.country_name # => "Germany"
|
|
32
|
+
result.bank_name # => "Commerzbank AG Cologne"
|
|
33
|
+
result.bic # => "COBADEFFXXX"
|
|
34
|
+
else
|
|
35
|
+
result.error # human-readable reason
|
|
36
|
+
result.error_code # e.g. "INVALID_COUNTRY"
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Authentication
|
|
41
|
+
|
|
42
|
+
An API key is optional. Without one, requests are limited to 100 per hour per IP. With a key, requests count against your plan quota. Get a free key at [ibanchecker.cash/api-docs](https://ibanchecker.cash/api-docs).
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
client = IbanChecker::Client.new("iban_your_api_key")
|
|
46
|
+
client = IbanChecker::Client.new(ENV["IBANCHECKER_API_KEY"])
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Methods
|
|
50
|
+
|
|
51
|
+
| Method | Description |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `validate(iban)` | Validate a single IBAN. Returns a `ValidationResult`. |
|
|
54
|
+
| `validate_bulk(ibans)` | Validate up to 100 IBANs. Returns a `BatchResult`. |
|
|
55
|
+
| `extract(text)` | Find and validate IBANs in free text (up to 50,000 chars). Returns a `BatchResult`. |
|
|
56
|
+
| `country_format(country)` | IBAN format spec for an ISO country code. Returns a `FormatSpec`. |
|
|
57
|
+
| `lookup_bic(bic)` | Resolve an 8 or 11 character BIC. Returns a `BankRecord`. |
|
|
58
|
+
|
|
59
|
+
`country_format` is the one name that differs from the other ibanchecker clients, where it is `getFormat`. `format` is `Kernel#format`, Ruby's `sprintf`, so a method by that name on this class would shadow it for every line inside the class.
|
|
60
|
+
|
|
61
|
+
### Bulk validation
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
batch = client.validate_bulk([
|
|
65
|
+
"DE89370400440532013000",
|
|
66
|
+
"GB29NWBK60161331926819",
|
|
67
|
+
"XX00"
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
batch.count # => 3
|
|
71
|
+
batch.valid_count # => 2
|
|
72
|
+
batch.invalid_count # => 1
|
|
73
|
+
|
|
74
|
+
batch.each do |result| # results come back in input order
|
|
75
|
+
puts "#{result.iban} #{result.valid? ? 'ok' : result.error_code}"
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`BatchResult` is `Enumerable`, so `map`, `select` and `find` work on it directly. `count` is the API's own count rather than `Enumerable#count`, so reach for `batch.results.count { |r| ... }` when you want the block form.
|
|
80
|
+
|
|
81
|
+
### Extract from text
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
batch = client.extract("Please wire to DE89 3704 0044 0532 0130 00 by Friday.")
|
|
85
|
+
|
|
86
|
+
batch.map { |r| [r.iban, r.bank_name] }
|
|
87
|
+
# => [["DE89370400440532013000", "Commerzbank AG Cologne"]]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Country format and BIC lookup
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
format = client.country_format("DE")
|
|
94
|
+
format.length # => 22
|
|
95
|
+
format.example # => "DE89370400440532013000"
|
|
96
|
+
|
|
97
|
+
format.bban_fields.map { |f| "#{f.label} (#{f.length})" }
|
|
98
|
+
# => ["BLZ (8)", "Account No. (10)"]
|
|
99
|
+
|
|
100
|
+
bank = client.lookup_bic("DEUTDEFF")
|
|
101
|
+
bank.bank_name # => "Deutsche Bank AG Frankfurt"
|
|
102
|
+
bank.city # => "FRANKFURT AM MAIN"
|
|
103
|
+
bank.sepa # => true
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### The national check digit
|
|
107
|
+
|
|
108
|
+
For a number of countries the API also runs the national account check digit on top of the ISO 13616 check, and reports it on `national_check_valid`. It is advisory: an IBAN with `valid?` true is a valid IBAN whatever this says. A `false` usually means a transcription error in the account number. It is `nil` where the country has no such scheme.
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
result = client.validate("DE84100100100532013000")
|
|
112
|
+
|
|
113
|
+
if result.valid? && result.national_check_valid == false
|
|
114
|
+
puts "Valid IBAN, but the account number looks mistyped."
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`valid?` is the only predicate method on the models, because it is the only field that is always a boolean. `national_check_valid` and `sepa` can each be `nil`, and `nil` there means "not known for this IBAN" rather than "no", so they are plain readers and you compare them explicitly.
|
|
119
|
+
|
|
120
|
+
## Error handling
|
|
121
|
+
|
|
122
|
+
A malformed IBAN is **not** an error: `validate` returns a `ValidationResult` with `valid?` false. Errors are raised only for transport, authentication, quota and server-side problems.
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
begin
|
|
126
|
+
bank = client.lookup_bic("ZZZZZZZZ")
|
|
127
|
+
rescue IbanChecker::NotFoundError
|
|
128
|
+
puts "No bank for that BIC"
|
|
129
|
+
rescue IbanChecker::RateLimitError => e
|
|
130
|
+
puts "Slow down: #{e.message}"
|
|
131
|
+
rescue IbanChecker::AuthenticationError
|
|
132
|
+
puts "Check your API key"
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
| Class | Raised when |
|
|
137
|
+
| --- | --- |
|
|
138
|
+
| `IbanChecker::BadRequestError` | HTTP 400, the request was malformed |
|
|
139
|
+
| `IbanChecker::AuthenticationError` | HTTP 401, the API key is missing, invalid or inactive |
|
|
140
|
+
| `IbanChecker::NotFoundError` | HTTP 404, no such country code or BIC |
|
|
141
|
+
| `IbanChecker::RateLimitError` | HTTP 429, hourly limit or monthly quota exceeded |
|
|
142
|
+
| `IbanChecker::APIError` | any other error status, or a body that could not be read |
|
|
143
|
+
| `IbanChecker::TransportError` | the request never reached the API: DNS, TLS, connection, timeout |
|
|
144
|
+
|
|
145
|
+
All of them inherit from `IbanChecker::Error`, so one `rescue IbanChecker::Error` catches everything this gem raises. Each carries `#status`, `#error_code` and `#response`.
|
|
146
|
+
|
|
147
|
+
## Timeouts
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
client = IbanChecker::Client.new(timeout: 3.0) # seconds, applied to connect and read
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Using your own HTTP stack
|
|
154
|
+
|
|
155
|
+
The client ships a `net/http` transport and needs nothing installed. If your application already has an HTTP layer, pass anything that responds to `call(method, url, headers, body)` and returns an object with `#status` and `#body`. This is also how the test suite runs without a network.
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
class MyTransport
|
|
159
|
+
def call(method, url, headers, body)
|
|
160
|
+
# ... your HTTP stack here
|
|
161
|
+
IbanChecker::Transport::Response.new(status, response_body)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
client = IbanChecker::Client.new("iban_your_api_key", transport: MyTransport.new)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The bundled transport follows a redirect only when it stays on the same host, so an `http` base URL upgrading to `https` works, and a redirect elsewhere raises `TransportError` instead of forwarding your API key to another host.
|
|
169
|
+
|
|
170
|
+
## Raw responses
|
|
171
|
+
|
|
172
|
+
Every model keeps the untouched response body on `#raw`, so a field added to the API later is reachable without waiting for a client release.
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
result = client.validate("DE89370400440532013000")
|
|
176
|
+
result.raw["transfer_type"] # => "SEPA+SWIFT"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Tests
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
bundle install
|
|
183
|
+
bundle exec rake test
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Links
|
|
187
|
+
|
|
188
|
+
- Website: https://ibanchecker.cash
|
|
189
|
+
- API documentation: https://ibanchecker.cash/api-docs
|
|
190
|
+
- OpenAPI spec: https://ibanchecker.cash/openapi.json
|
|
191
|
+
- Free online tools: https://ibanchecker.cash/tools
|
|
192
|
+
|
|
193
|
+
Clients for other languages: [Python](https://pypi.org/project/ibanchecker/), [PHP](https://packagist.org/packages/ibanchecker/client), [JavaScript](https://www.npmjs.com/package/@ibanchecker/client), and an [MCP server](https://www.npmjs.com/package/@ibanchecker/mcp).
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module IbanChecker
|
|
6
|
+
# Client for the ibanchecker.cash IBAN validation API.
|
|
7
|
+
#
|
|
8
|
+
# Validate IBANs across 92 countries, validate up to 100 IBANs per request,
|
|
9
|
+
# extract IBANs from free text, look up country format specifications and
|
|
10
|
+
# resolve SWIFT/BIC codes.
|
|
11
|
+
#
|
|
12
|
+
# An API key is optional. Without one, requests are limited to 100 per hour
|
|
13
|
+
# per IP. Get a free key at https://ibanchecker.cash/api-docs.
|
|
14
|
+
#
|
|
15
|
+
# client = IbanChecker::Client.new # or .new("iban_your_key")
|
|
16
|
+
# result = client.validate("DE89 3704 0044 0532 0130 00")
|
|
17
|
+
# puts "#{result.bank_name} #{result.bic}" if result.valid?
|
|
18
|
+
class Client
|
|
19
|
+
DEFAULT_BASE_URL = "https://ibanchecker.cash/api/v1"
|
|
20
|
+
|
|
21
|
+
ERRORS_BY_STATUS = {
|
|
22
|
+
400 => BadRequestError,
|
|
23
|
+
401 => AuthenticationError,
|
|
24
|
+
404 => NotFoundError,
|
|
25
|
+
429 => RateLimitError
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# Everything outside the unreserved set of RFC 3986 is percent-encoded
|
|
29
|
+
# before it goes into a path segment.
|
|
30
|
+
RESERVED = /[^A-Za-z0-9\-._~]/.freeze
|
|
31
|
+
|
|
32
|
+
attr_reader :base_url
|
|
33
|
+
|
|
34
|
+
def initialize(api_key = nil, base_url: DEFAULT_BASE_URL, timeout: 10.0, transport: nil)
|
|
35
|
+
key = api_key.to_s
|
|
36
|
+
@api_key = key.empty? ? nil : key
|
|
37
|
+
@base_url = base_url.to_s.sub(%r{/+\z}, "")
|
|
38
|
+
@transport = transport || Transport::NetHttp.new(timeout: timeout)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Validate a single IBAN.
|
|
42
|
+
#
|
|
43
|
+
# A malformed IBAN is not an error: the result comes back with +valid?+
|
|
44
|
+
# false and an +error+ plus +error_code+ explaining why.
|
|
45
|
+
def validate(iban)
|
|
46
|
+
ValidationResult.from_api(request("POST", "/validate", "iban" => iban.to_s))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Validate up to 100 IBANs in one request. Results come back in the same
|
|
50
|
+
# order as the input.
|
|
51
|
+
def validate_bulk(ibans)
|
|
52
|
+
BatchResult.from_api(
|
|
53
|
+
request("POST", "/validate/bulk", "ibans" => Array(ibans).map(&:to_s))
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Scan free text (emails, invoices) for IBAN-shaped strings and validate
|
|
58
|
+
# each candidate. Up to 50,000 characters per request.
|
|
59
|
+
def extract(text)
|
|
60
|
+
BatchResult.from_api(request("POST", "/extract", "text" => text.to_s))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The IBAN format specification for an ISO 3166-1 alpha-2 country code,
|
|
64
|
+
# for example "DE".
|
|
65
|
+
#
|
|
66
|
+
# Named country_format rather than format because Kernel#format is
|
|
67
|
+
# sprintf, and shadowing it inside this class would be a trap.
|
|
68
|
+
def country_format(country)
|
|
69
|
+
FormatSpec.from_api(request("GET", "/formats/#{escape(country.to_s.downcase)}"))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Resolve an 8 or 11 character ISO 9362 BIC to a bank record.
|
|
73
|
+
def lookup_bic(bic)
|
|
74
|
+
BankRecord.from_api(request("GET", "/swift/#{escape(bic.to_s.upcase)}"))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def request(method, path, payload = nil)
|
|
80
|
+
headers = {
|
|
81
|
+
"Accept" => "application/json",
|
|
82
|
+
"User-Agent" => "ibanchecker-ruby/#{VERSION}"
|
|
83
|
+
}
|
|
84
|
+
headers["Authorization"] = "Bearer #{@api_key}" if @api_key
|
|
85
|
+
|
|
86
|
+
body = nil
|
|
87
|
+
if payload
|
|
88
|
+
body = JSON.generate(payload)
|
|
89
|
+
headers["Content-Type"] = "application/json"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
response = @transport.call(method, @base_url + path, headers, body)
|
|
93
|
+
status = response.status.to_i
|
|
94
|
+
raw = response.body.to_s
|
|
95
|
+
data = parse(raw)
|
|
96
|
+
|
|
97
|
+
raise error_for(status, data) if status >= 400
|
|
98
|
+
return data if data
|
|
99
|
+
|
|
100
|
+
raise APIError.new(
|
|
101
|
+
raw.strip.empty? ? "The API returned an empty body" : "The API returned a body that is not a JSON object",
|
|
102
|
+
status: status
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns the decoded object, or nil when the body was empty, unparseable
|
|
107
|
+
# or not a JSON object. The caller decides what that means: on an error
|
|
108
|
+
# status it just means there is no message to quote.
|
|
109
|
+
def parse(raw)
|
|
110
|
+
return nil if raw.strip.empty?
|
|
111
|
+
|
|
112
|
+
decoded = JSON.parse(raw)
|
|
113
|
+
decoded.is_a?(Hash) ? decoded : nil
|
|
114
|
+
rescue JSON::ParserError
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def error_for(status, data)
|
|
119
|
+
message = data && data["error"].is_a?(String) ? data["error"] : "HTTP #{status}"
|
|
120
|
+
code = data && data["error_code"].is_a?(String) ? data["error_code"] : nil
|
|
121
|
+
|
|
122
|
+
ERRORS_BY_STATUS.fetch(status, APIError).new(
|
|
123
|
+
message, status: status, error_code: code, response: data
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def escape(value)
|
|
128
|
+
value.gsub(RESERVED) { |char| char.each_byte.map { |byte| format("%%%02X", byte) }.join }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IbanChecker
|
|
4
|
+
# Base class for every error this client raises.
|
|
5
|
+
#
|
|
6
|
+
# A malformed IBAN is not an error: Client#validate returns a
|
|
7
|
+
# ValidationResult with +valid?+ false. These are raised for transport,
|
|
8
|
+
# authentication, quota and server-side problems only.
|
|
9
|
+
class Error < StandardError
|
|
10
|
+
# HTTP status that produced this error, when there was one.
|
|
11
|
+
attr_reader :status
|
|
12
|
+
|
|
13
|
+
# Machine-readable code from the API body, for example "BIC_NOT_FOUND".
|
|
14
|
+
attr_reader :error_code
|
|
15
|
+
|
|
16
|
+
# The decoded response body, when the API sent one.
|
|
17
|
+
attr_reader :response
|
|
18
|
+
|
|
19
|
+
def initialize(message, status: nil, error_code: nil, response: nil)
|
|
20
|
+
super(message)
|
|
21
|
+
@status = status
|
|
22
|
+
@error_code = error_code
|
|
23
|
+
@response = response
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The request was malformed (HTTP 400).
|
|
28
|
+
class BadRequestError < Error; end
|
|
29
|
+
|
|
30
|
+
# The API key is missing, invalid or inactive (HTTP 401).
|
|
31
|
+
class AuthenticationError < Error; end
|
|
32
|
+
|
|
33
|
+
# The requested country code or BIC was not found (HTTP 404).
|
|
34
|
+
class NotFoundError < Error; end
|
|
35
|
+
|
|
36
|
+
# The hourly rate limit or the monthly quota was exceeded (HTTP 429).
|
|
37
|
+
class RateLimitError < Error; end
|
|
38
|
+
|
|
39
|
+
# An unexpected server-side error, or a body that could not be read.
|
|
40
|
+
class APIError < Error; end
|
|
41
|
+
|
|
42
|
+
# The request never reached the API: DNS, TLS, connection or timeout.
|
|
43
|
+
class TransportError < Error; end
|
|
44
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IbanChecker
|
|
4
|
+
# One segment of a country's BBAN, in the order it appears in the IBAN.
|
|
5
|
+
class BbanField
|
|
6
|
+
attr_reader :label, :length, :type, :description
|
|
7
|
+
|
|
8
|
+
# The untouched field object from the API response.
|
|
9
|
+
attr_reader :raw
|
|
10
|
+
|
|
11
|
+
def initialize(label: nil, length: nil, type: nil, description: nil, raw: {})
|
|
12
|
+
@label = label
|
|
13
|
+
@length = length
|
|
14
|
+
@type = type
|
|
15
|
+
@description = description
|
|
16
|
+
@raw = raw
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.from_api(data)
|
|
20
|
+
new(
|
|
21
|
+
label: data["label"],
|
|
22
|
+
length: data["length"],
|
|
23
|
+
type: data["type"],
|
|
24
|
+
description: data["description"],
|
|
25
|
+
raw: data
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The result of validating one IBAN.
|
|
31
|
+
#
|
|
32
|
+
# +valid?+ is the primary flag. When it is false only +iban+, +formatted+,
|
|
33
|
+
# +country+, +country_name+, +error+ and +error_code+ are populated.
|
|
34
|
+
#
|
|
35
|
+
# +valid?+ is the only predicate method here because it is the only field
|
|
36
|
+
# that is always a boolean. +sepa+ and +national_check_valid+ can each be
|
|
37
|
+
# nil, so they are plain readers and nil means "not known for this IBAN".
|
|
38
|
+
class ValidationResult
|
|
39
|
+
ATTRIBUTES = %i[
|
|
40
|
+
iban formatted check_digits bban country country_name
|
|
41
|
+
bank_name bank_type bic bank_city bank_code branch_code account_number
|
|
42
|
+
national_check_valid currency currency_name transfer_type sepa flag
|
|
43
|
+
error error_code
|
|
44
|
+
].freeze
|
|
45
|
+
|
|
46
|
+
attr_reader(*ATTRIBUTES)
|
|
47
|
+
|
|
48
|
+
# The untouched response body.
|
|
49
|
+
attr_reader :raw
|
|
50
|
+
|
|
51
|
+
def initialize(valid:, iban:, raw: {}, **rest)
|
|
52
|
+
@valid = valid
|
|
53
|
+
@iban = iban
|
|
54
|
+
@raw = raw
|
|
55
|
+
ATTRIBUTES.each do |name|
|
|
56
|
+
next if name == :iban
|
|
57
|
+
|
|
58
|
+
instance_variable_set("@#{name}", rest[name])
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# True when the IBAN passes the ISO 13616 check digit, the country's
|
|
63
|
+
# length and the country's BBAN structure.
|
|
64
|
+
def valid?
|
|
65
|
+
@valid
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.from_api(data)
|
|
69
|
+
new(
|
|
70
|
+
valid: data["valid"] == true,
|
|
71
|
+
iban: data["iban"].to_s,
|
|
72
|
+
formatted: data["formatted"],
|
|
73
|
+
check_digits: data["check_digits"],
|
|
74
|
+
bban: data["bban"],
|
|
75
|
+
country: data["country"],
|
|
76
|
+
country_name: data["country_name"],
|
|
77
|
+
bank_name: data["bank_name"],
|
|
78
|
+
bank_type: data["bank_type"],
|
|
79
|
+
bic: data["bic"],
|
|
80
|
+
bank_city: data["bank_city"],
|
|
81
|
+
bank_code: data["bank_code"],
|
|
82
|
+
branch_code: data["branch_code"],
|
|
83
|
+
account_number: data["account_number"],
|
|
84
|
+
national_check_valid: data["national_check_valid"],
|
|
85
|
+
currency: data["currency"],
|
|
86
|
+
currency_name: data["currency_name"],
|
|
87
|
+
transfer_type: data["transfer_type"],
|
|
88
|
+
sepa: data["sepa"],
|
|
89
|
+
flag: data["flag"],
|
|
90
|
+
error: data["error"],
|
|
91
|
+
error_code: data["error_code"],
|
|
92
|
+
raw: data
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Kept short on purpose: the default would print the whole raw body, which
|
|
97
|
+
# makes an irb session unreadable.
|
|
98
|
+
def inspect
|
|
99
|
+
detail = valid? ? "valid #{bank_name || 'bank not in directory'}" : "invalid #{error_code}"
|
|
100
|
+
"#<#{self.class} #{iban} #{detail}>"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The result of a bulk validation or a text extraction.
|
|
105
|
+
#
|
|
106
|
+
# The object is enumerable: iterating it walks the per-IBAN results in input
|
|
107
|
+
# order. +count+ is the API's own count rather than Enumerable#count, so use
|
|
108
|
+
# +results.count { ... }+ if you want the block form.
|
|
109
|
+
class BatchResult
|
|
110
|
+
include Enumerable
|
|
111
|
+
|
|
112
|
+
attr_reader :count, :valid_count, :invalid_count, :results
|
|
113
|
+
|
|
114
|
+
# The untouched response body.
|
|
115
|
+
attr_reader :raw
|
|
116
|
+
|
|
117
|
+
def initialize(count: 0, valid_count: 0, invalid_count: 0, results: [], raw: {})
|
|
118
|
+
@count = count
|
|
119
|
+
@valid_count = valid_count
|
|
120
|
+
@invalid_count = invalid_count
|
|
121
|
+
@results = results
|
|
122
|
+
@raw = raw
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def each(&block)
|
|
126
|
+
@results.each(&block)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def size
|
|
130
|
+
@results.size
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.from_api(data)
|
|
134
|
+
rows = data["results"]
|
|
135
|
+
results = rows.is_a?(Array) ? rows.select { |row| row.is_a?(Hash) }.map { |row| ValidationResult.from_api(row) } : []
|
|
136
|
+
|
|
137
|
+
new(
|
|
138
|
+
count: data["count"] || 0,
|
|
139
|
+
valid_count: data["valid_count"] || 0,
|
|
140
|
+
invalid_count: data["invalid_count"] || 0,
|
|
141
|
+
results: results,
|
|
142
|
+
raw: data
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The IBAN format specification for one country.
|
|
148
|
+
class FormatSpec
|
|
149
|
+
attr_reader :country_code, :country_name, :length, :currency, :currency_name,
|
|
150
|
+
:sepa, :swift, :format_string, :example, :bban_fields
|
|
151
|
+
|
|
152
|
+
# The untouched response body.
|
|
153
|
+
attr_reader :raw
|
|
154
|
+
|
|
155
|
+
def initialize(bban_fields: [], raw: {}, **rest)
|
|
156
|
+
@bban_fields = bban_fields
|
|
157
|
+
@raw = raw
|
|
158
|
+
%i[country_code country_name length currency currency_name sepa swift
|
|
159
|
+
format_string example].each do |name|
|
|
160
|
+
instance_variable_set("@#{name}", rest[name])
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self.from_api(data)
|
|
165
|
+
fields = data["bban_fields"]
|
|
166
|
+
bban_fields = fields.is_a?(Array) ? fields.select { |f| f.is_a?(Hash) }.map { |f| BbanField.from_api(f) } : []
|
|
167
|
+
|
|
168
|
+
new(
|
|
169
|
+
country_code: data["country_code"],
|
|
170
|
+
country_name: data["country_name"],
|
|
171
|
+
length: data["length"],
|
|
172
|
+
currency: data["currency"],
|
|
173
|
+
currency_name: data["currency_name"],
|
|
174
|
+
sepa: data["sepa"],
|
|
175
|
+
swift: data["swift"],
|
|
176
|
+
format_string: data["format_string"],
|
|
177
|
+
example: data["example"],
|
|
178
|
+
bban_fields: bban_fields,
|
|
179
|
+
raw: data
|
|
180
|
+
)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# The institution behind a SWIFT/BIC code.
|
|
185
|
+
class BankRecord
|
|
186
|
+
ATTRIBUTES = %i[
|
|
187
|
+
bic bic8 bank_code country_code location_code branch_code
|
|
188
|
+
bank_name city country_name sepa type status
|
|
189
|
+
].freeze
|
|
190
|
+
|
|
191
|
+
attr_reader(*ATTRIBUTES)
|
|
192
|
+
|
|
193
|
+
# The untouched response body.
|
|
194
|
+
attr_reader :raw
|
|
195
|
+
|
|
196
|
+
def initialize(raw: {}, **rest)
|
|
197
|
+
@raw = raw
|
|
198
|
+
ATTRIBUTES.each { |name| instance_variable_set("@#{name}", rest[name]) }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def self.from_api(data)
|
|
202
|
+
new(
|
|
203
|
+
bic: data["bic"],
|
|
204
|
+
bic8: data["bic8"],
|
|
205
|
+
bank_code: data["bank_code"],
|
|
206
|
+
country_code: data["country_code"],
|
|
207
|
+
location_code: data["location_code"],
|
|
208
|
+
branch_code: data["branch_code"],
|
|
209
|
+
bank_name: data["bank_name"],
|
|
210
|
+
city: data["city"],
|
|
211
|
+
country_name: data["country_name"],
|
|
212
|
+
sepa: data["sepa"],
|
|
213
|
+
type: data["type"],
|
|
214
|
+
status: data["status"],
|
|
215
|
+
raw: data
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module IbanChecker
|
|
7
|
+
# The HTTP seam.
|
|
8
|
+
#
|
|
9
|
+
# The client ships NetHttp, built entirely on the standard library, so the
|
|
10
|
+
# gem has no runtime dependencies. Anything that responds to
|
|
11
|
+
# #call(method, url, headers, body) and returns something with #status and
|
|
12
|
+
# #body can be passed in instead. That is how the test suite runs without a
|
|
13
|
+
# network, and how a host application can route calls through its own HTTP
|
|
14
|
+
# stack.
|
|
15
|
+
module Transport
|
|
16
|
+
# What a transport returns.
|
|
17
|
+
Response = Struct.new(:status, :body)
|
|
18
|
+
|
|
19
|
+
# Default transport, built on net/http.
|
|
20
|
+
class NetHttp
|
|
21
|
+
METHODS = {
|
|
22
|
+
"GET" => Net::HTTP::Get,
|
|
23
|
+
"POST" => Net::HTTP::Post
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
def initialize(timeout: 10.0, max_redirects: 3)
|
|
27
|
+
@timeout = timeout
|
|
28
|
+
@max_redirects = max_redirects
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call(method, url, headers, body)
|
|
32
|
+
request_class = METHODS.fetch(method) do
|
|
33
|
+
raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
uri = URI.parse(url)
|
|
37
|
+
redirects = 0
|
|
38
|
+
|
|
39
|
+
loop do
|
|
40
|
+
response = perform(request_class, uri, headers, body)
|
|
41
|
+
location = response.is_a?(Net::HTTPRedirection) ? response["location"] : nil
|
|
42
|
+
|
|
43
|
+
return Response.new(response.code.to_i, response.body.to_s) if location.nil? || redirects >= @max_redirects
|
|
44
|
+
|
|
45
|
+
target = URI.join(uri.to_s, location)
|
|
46
|
+
# Following a redirect to another host would send the Authorization
|
|
47
|
+
# header there, which is how an API key leaks. An upgrade from http
|
|
48
|
+
# to https on the same host is the case worth following.
|
|
49
|
+
unless target.host == uri.host
|
|
50
|
+
raise TransportError,
|
|
51
|
+
"Refusing to follow a redirect from #{uri.host} to #{target.host}: " \
|
|
52
|
+
"that would send the API key to another host"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
uri = target
|
|
56
|
+
redirects += 1
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def perform(request_class, uri, headers, body)
|
|
63
|
+
request = request_class.new(uri)
|
|
64
|
+
headers.each { |name, value| request[name] = value }
|
|
65
|
+
request.body = body if body
|
|
66
|
+
|
|
67
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
68
|
+
http.use_ssl = uri.scheme == "https"
|
|
69
|
+
http.open_timeout = @timeout
|
|
70
|
+
http.read_timeout = @timeout
|
|
71
|
+
http.write_timeout = @timeout if http.respond_to?(:write_timeout=)
|
|
72
|
+
|
|
73
|
+
begin
|
|
74
|
+
http.start { |connection| connection.request(request) }
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
raise TransportError, "Request to #{uri} failed: #{e.class}: #{e.message}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/ibanchecker.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ibanchecker/version"
|
|
4
|
+
require_relative "ibanchecker/errors"
|
|
5
|
+
require_relative "ibanchecker/models"
|
|
6
|
+
require_relative "ibanchecker/transport"
|
|
7
|
+
require_relative "ibanchecker/client"
|
|
8
|
+
|
|
9
|
+
# Official Ruby client for the ibanchecker.cash IBAN validation API.
|
|
10
|
+
#
|
|
11
|
+
# require "ibanchecker"
|
|
12
|
+
#
|
|
13
|
+
# client = IbanChecker::Client.new
|
|
14
|
+
# result = client.validate("DE89370400440532013000")
|
|
15
|
+
# result.valid? # => true
|
|
16
|
+
# result.bank_name # => "Commerzbank AG Cologne"
|
|
17
|
+
module IbanChecker
|
|
18
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ibanchecker
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ibanchecker.cash
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Validate IBANs across 92 countries, validate up to 100 IBANs per request,
|
|
13
|
+
extract IBANs from free text, look up country format specifications and resolve
|
|
14
|
+
SWIFT/BIC codes. No runtime dependencies.
|
|
15
|
+
email:
|
|
16
|
+
- api@ibanchecker.cash
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/ibanchecker.rb
|
|
25
|
+
- lib/ibanchecker/client.rb
|
|
26
|
+
- lib/ibanchecker/errors.rb
|
|
27
|
+
- lib/ibanchecker/models.rb
|
|
28
|
+
- lib/ibanchecker/transport.rb
|
|
29
|
+
- lib/ibanchecker/version.rb
|
|
30
|
+
homepage: https://ibanchecker.cash
|
|
31
|
+
licenses:
|
|
32
|
+
- MIT
|
|
33
|
+
metadata:
|
|
34
|
+
homepage_uri: https://ibanchecker.cash
|
|
35
|
+
documentation_uri: https://ibanchecker.cash/api-docs
|
|
36
|
+
source_code_uri: https://github.com/koraykoylu/ibanchecker-ruby
|
|
37
|
+
changelog_uri: https://github.com/koraykoylu/ibanchecker-ruby/blob/main/CHANGELOG.md
|
|
38
|
+
bug_tracker_uri: https://github.com/koraykoylu/ibanchecker-ruby/issues
|
|
39
|
+
allowed_push_host: https://rubygems.org
|
|
40
|
+
rubygems_mfa_required: 'true'
|
|
41
|
+
rdoc_options: []
|
|
42
|
+
require_paths:
|
|
43
|
+
- lib
|
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: 2.7.0
|
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
requirements: []
|
|
55
|
+
rubygems_version: 4.0.16
|
|
56
|
+
specification_version: 4
|
|
57
|
+
summary: Official Ruby client for the ibanchecker.cash IBAN validation API
|
|
58
|
+
test_files: []
|