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 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IbanChecker
4
+ VERSION = "0.1.0"
5
+ end
@@ -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: []