ipwho 1.0.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.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE +21 -0
  4. data/README.md +206 -0
  5. data/lib/ipwho.rb +367 -0
  6. metadata +66 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6e879b527d7bf970cb0fd5cd763a753a1caacf26c39943712c69369850d811e3
4
+ data.tar.gz: fc7de3f5d4cd2c1b486923acd619a5a84ccf3efa1446b2c2dc8c885d36d9cb52
5
+ SHA512:
6
+ metadata.gz: f99e60ee24f0a0e9ae4fae090c77b1a4cc091d627febd96688b1d366652ce241faf272c7325b297b12e56afb46b0da180fa85c555c53c114ea21b00634039969
7
+ data.tar.gz: d55b91b8880d2758a77398f6aac7e07a37ac446187184e2da80c818785935a416cd888eb84ca3dc263107772e5c3359990d4c51099f0a16c19142baabbed1845
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # Changelog
2
+
3
+ ## [1.0.0] - 2026-08-15
4
+
5
+ - `IpwhoApi::Client` with `lookup`, `me`, and `bulk` for [api.ipwho.org](https://api.ipwho.org)
6
+ - Full typed Struct models for geo, timezone, flag, currency, connection, security, and user-agent
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 IPWho Contributors
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,206 @@
1
+ # IPWho ([ipwho.org](https://www.ipwho.org)) Ruby SDK
2
+
3
+ [![Gem version](https://img.shields.io/gem/v/ipwho?style=flat-square)](https://rubygems.org/gems/ipwho) [![Ruby version](https://img.shields.io/badge/ruby-2.5+-red.svg)](https://www.ruby-lang.org/) [![license](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/lavrox/SDK-IPWho-IP-Geolocation-Ruby/blob/main/LICENSE)
4
+
5
+ Official Ruby SDK for the [IPWho](https://www.ipwho.org) **IP geolocation API** — geoip lookup, IP location, IP to country / latitude / longitude, ASN/ISP, timezone, currency, flag, and proxy/VPN detection. Works as an IP lookup / ip-geolocation client for IPv4 and IPv6 (`lookup`, `me`, `bulk`).
6
+
7
+ - Product: [ipwho.org](https://www.ipwho.org)
8
+ - API docs: [ipwho.org/docs](https://www.ipwho.org/docs)
9
+ - Get an API key: [ipwho.org/free-plan](https://www.ipwho.org/free-plan) (free [Lavrox](https://lavrox.com) account)
10
+ - Live API host: `https://api.ipwho.org`
11
+
12
+ ## API key
13
+
14
+ Open a free [Lavrox](https://lavrox.com) account to get an API key for [IPWho](https://www.ipwho.org). Create your key at [ipwho.org/free-plan](https://www.ipwho.org/free-plan) — no credit card required.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ gem install ipwho
20
+ ```
21
+
22
+ ```ruby
23
+ gem "ipwho"
24
+ ```
25
+
26
+ Requires Ruby >= 2.5 and Faraday. Client class: `IpwhoApi::Client`.
27
+
28
+ ## Quick Start
29
+
30
+ ```ruby
31
+ require "ipwho"
32
+
33
+ client = IpwhoApi::Client.new(ENV.fetch("IPWHO_API_KEY"))
34
+
35
+ res = client.lookup("8.8.8.8") # GET /ip/{ip}
36
+ me = client.me # GET /me
37
+ bulk = client.bulk(["8.8.8.8", "1.1.1.1"]) # GET /bulk/{a,b,c}
38
+ ```
39
+
40
+ Successful JSON:
41
+
42
+ ```
43
+ IpwhoApi::IpGeoResponse
44
+ ├── success
45
+ ├── message
46
+ └── data (GeoData, or OpenStruct on bulk)
47
+ ├── ip
48
+ ├── geoLocation
49
+ ├── timezone
50
+ ├── flag
51
+ ├── currency
52
+ ├── connection
53
+ ├── security
54
+ ├── userAgent
55
+ └── responseArray # bulk
56
+ ```
57
+
58
+ ## Reading the full response (8.8.8.8)
59
+
60
+ Live [IPWho](https://www.ipwho.org) values: United States, ASN 15169, America/Chicago, dial code +1.
61
+
62
+ ```ruby
63
+ res = client.lookup("8.8.8.8")
64
+ data = res.data
65
+
66
+ puts data.ip # 8.8.8.8
67
+
68
+ geo = data.geoLocation
69
+ puts geo.continent, geo.continentCode # North America, NA
70
+ puts geo.country, geo.countryCode # United States, US
71
+ puts geo.capital, geo.region, geo.regionCode, geo.city
72
+ puts geo.postalCode, geo.dialCode # +1
73
+ puts geo.isInEu, geo.latitude, geo.longitude, geo.accuracyRadius
74
+
75
+ tz = data.timezone
76
+ puts tz.timeZone # America/Chicago
77
+ puts tz.abbr, tz.offset, tz.isDst, tz.utc, tz.currentTime
78
+
79
+ puts data.flag.flagIcon # 🇺🇸
80
+ puts data.flag.flagUnicode # U+1F1FA U+1F1F8
81
+
82
+ puts data.currency.code, data.currency.symbol, data.currency.name
83
+ puts data.currency.namePlural # US dollars
84
+ puts data.currency.hexUnicode
85
+
86
+ conn = data.connection
87
+ puts conn.asnNumber # 15169
88
+ puts conn.asnOrg # Google LLC
89
+ puts conn.isp, conn.org, conn.domain
90
+ puts conn.connectionType # Corporate
91
+
92
+ puts data.security.isVpn, data.security.isTor, data.security.isThreat
93
+
94
+ if data.userAgent
95
+ puts data.userAgent.browser.name, data.userAgent.os.name
96
+ end
97
+
98
+ me = client.me
99
+ puts me.data.ip
100
+
101
+ bulk = client.bulk(["8.8.8.8", "1.1.1.1"])
102
+ Array(bulk.data.responseArray).each do |item|
103
+ puts item.data.ip, item.data.geoLocation.country
104
+ end
105
+ ```
106
+
107
+ ### Example JSON (mapped fields)
108
+
109
+ ```json
110
+ {
111
+ "success": true,
112
+ "data": {
113
+ "ip": "8.8.8.8",
114
+ "geoLocation": {
115
+ "country": "United States",
116
+ "countryCode": "US",
117
+ "dialCode": "+1",
118
+ "isInEu": false
119
+ },
120
+ "timezone": { "timeZone": "America/Chicago" },
121
+ "flag": { "flagIcon": "🇺🇸", "flagUnicode": "U+1F1FA U+1F1F8" },
122
+ "currency": { "code": "USD", "namePlural": "US dollars" },
123
+ "connection": {
124
+ "asnNumber": 15169,
125
+ "asnOrg": "Google LLC",
126
+ "connectionType": "Corporate"
127
+ },
128
+ "security": { "isVpn": false, "isTor": false, "isThreat": "low" }
129
+ }
130
+ }
131
+ ```
132
+
133
+ ## API Reference
134
+
135
+ ### `IpwhoApi::Client.new(api_key, options = {})`
136
+
137
+ Required key (query `apiKey`). `options[:base_url]` default `https://api.ipwho.org`. `options[:timeout]` default `30`.
138
+
139
+ ### `lookup(ip, options = {})`
140
+
141
+ `options[:format]` json|xml|csv. `options[:fields]` comma-separated filter.
142
+
143
+ ### `me(options = {})`
144
+
145
+ ### `bulk(ips)`
146
+
147
+ Per-IP rows on `data.responseArray`.
148
+
149
+ ### Errors
150
+
151
+ `IpwhoApi::InvalidIPError` (404), `RateLimitError` (429), `APIResponseError`.
152
+
153
+ ## Type Definitions
154
+
155
+ Structs (keyword init):
156
+
157
+ - **GeoLocation**: `continent`, `continentCode`, `country`, `countryCode`, `capital`, `region`, `regionCode`, `city`, `postalCode`, `dialCode`, `isInEu`, `latitude`, `longitude`, `accuracyRadius`
158
+ - **Timezone**: `timeZone`, `abbr`, `offset`, `isDst`, `utc`, `currentTime`
159
+ - **Flag**: `flagIcon`, `flagUnicode`
160
+ - **Currency**: `code`, `symbol`, `name`, `namePlural`, `hexUnicode`
161
+ - **Connection**: `asnNumber`, `asnOrg`, `isp`, `org`, `domain`, `connectionType`
162
+ - **Security**: `isVpn`, `isTor`, `isThreat`
163
+ - **UserAgent**: `browser`, `engine`, `os`, `device`, `cpu`
164
+ - **GeoData**: `ip` plus the objects above
165
+ - **IpGeoResponse**: `success`, `data`, `message`
166
+
167
+ Wire keys such as `postal_Code` and `flag_Icon` are mapped onto these members.
168
+
169
+ ## Troubleshooting
170
+
171
+ - Empty key → `APIResponseError`. Key: [ipwho.org](https://www.ipwho.org).
172
+ - HTTP 403: SDK sends `ipwho-ruby-sdk/1.0.0`.
173
+ - HTTP 429 / 404: `RateLimitError` / `InvalidIPError`.
174
+ - Nil nested structs on some IPs.
175
+
176
+ ## Testing
177
+
178
+ ```bash
179
+ IPWHO_API_KEY=your_key ruby test_ipwho.rb
180
+ ```
181
+
182
+ The live check is `test_ipwho.rb`.
183
+
184
+ ## Changelog
185
+
186
+ ### v1.0.0
187
+
188
+ - `lookup`, `me`, `bulk` matching [api.ipwho.org](https://api.ipwho.org)
189
+ - Full typed Struct models
190
+
191
+ ## License
192
+
193
+ MIT License — see [LICENSE](LICENSE).
194
+
195
+ ## Support
196
+
197
+ - Documentation: [ipwho.org/docs](https://www.ipwho.org/docs)
198
+ - Contact: [ipwho.org/contact](https://www.ipwho.org/contact)
199
+ - GitHub Issues: [lavrox/SDK-IPWho-IP-Geolocation-Ruby](https://github.com/lavrox/SDK-IPWho-IP-Geolocation-Ruby/issues)
200
+ - Website: [ipwho.org](https://www.ipwho.org)
201
+
202
+ ---
203
+
204
+ [IPWho](https://www.ipwho.org) — a [Lavrox](https://lavrox.com) network API.
205
+
206
+ [Lavrox](https://lavrox.com) — Independent API infrastructure. Lower latency, lower cost.
data/lib/ipwho.rb ADDED
@@ -0,0 +1,367 @@
1
+ # frozen_string_literal: true
2
+
3
+ # IPWho Ruby SDK v1.0.0
4
+ # ======================
5
+ #
6
+ # Enterprise-grade client for the IPWho IP Geolocation API.
7
+ # Requires Ruby >= 2.5, faraday ~> 1.0, json.
8
+ #
9
+ # Endpoints:
10
+ # - lookup(ip) — geolocation for a specific IPv4/IPv6 address
11
+ # - me() — geolocation for the caller's IP
12
+ # - bulk(ips) — batch lookup for multiple IPs
13
+ #
14
+ # API docs: https://api.ipwho.org
15
+
16
+ require "json"
17
+ require "faraday"
18
+ require "ostruct"
19
+ require "erb"
20
+
21
+ module IpwhoApi
22
+ VERSION = "1.0.0".freeze
23
+
24
+ # ── Exceptions ────────────────────────────────────────────────────
25
+
26
+ class Error < StandardError; end
27
+ class InvalidIPError < Error; end
28
+ class RateLimitError < Error; end
29
+ class APIResponseError < Error; end
30
+
31
+ # ── Data objects (Struct-based, mirror OpenAPI schema) ────────────
32
+
33
+ # All data objects use camelCase keys matching the API response.
34
+
35
+ GeoLocation = Struct.new(
36
+ :continent, :continentCode, :country, :countryCode, :capital,
37
+ :region, :regionCode, :city, :postalCode, :dialCode,
38
+ :isInEu, :latitude, :longitude, :accuracyRadius,
39
+ keyword_init: true
40
+ ) do
41
+ def self.from_hash(hash)
42
+ new(
43
+ continent: hash["continent"],
44
+ continentCode: hash["continentCode"],
45
+ country: hash["country"],
46
+ countryCode: hash["countryCode"],
47
+ capital: hash["capital"],
48
+ region: hash["region"],
49
+ regionCode: hash["regionCode"],
50
+ city: hash["city"],
51
+ postalCode: hash["postal_Code"],
52
+ dialCode: hash["dial_code"],
53
+ isInEu: hash["is_in_eu"],
54
+ latitude: hash["latitude"],
55
+ longitude: hash["longitude"],
56
+ accuracyRadius: hash["accuracy_radius"]
57
+ )
58
+ end
59
+ end
60
+
61
+ Timezone = Struct.new(
62
+ :timeZone, :abbr, :offset, :isDst, :utc, :currentTime,
63
+ keyword_init: true
64
+ ) do
65
+ def self.from_hash(hash)
66
+ new(
67
+ timeZone: hash["time_zone"],
68
+ abbr: hash["abbr"],
69
+ offset: hash["offset"],
70
+ isDst: hash["is_dst"],
71
+ utc: hash["utc"],
72
+ currentTime: hash["current_time"]
73
+ )
74
+ end
75
+ end
76
+
77
+ Flag = Struct.new(:flagIcon, :flagUnicode, keyword_init: true) do
78
+ def self.from_hash(hash)
79
+ new(flagIcon: hash["flag_Icon"], flagUnicode: hash["flag_unicode"])
80
+ end
81
+ end
82
+
83
+ Currency = Struct.new(
84
+ :code, :symbol, :name, :namePlural, :hexUnicode,
85
+ keyword_init: true
86
+ ) do
87
+ def self.from_hash(hash)
88
+ new(
89
+ code: hash["code"],
90
+ symbol: hash["symbol"],
91
+ name: hash["name"],
92
+ namePlural: hash["name_plural"],
93
+ hexUnicode: hash["hex_unicode"]
94
+ )
95
+ end
96
+ end
97
+
98
+ Connection = Struct.new(
99
+ :asnNumber, :asnOrg, :isp, :org, :domain, :connectionType,
100
+ keyword_init: true
101
+ ) do
102
+ def self.from_hash(hash)
103
+ new(
104
+ asnNumber: hash["asn_number"],
105
+ asnOrg: hash["asn_org"],
106
+ isp: hash["isp"],
107
+ org: hash["org"],
108
+ domain: hash["domain"],
109
+ connectionType: hash["connection_type"]
110
+ )
111
+ end
112
+ end
113
+
114
+ Security = Struct.new(:isVpn, :isTor, :isThreat, keyword_init: true) do
115
+ def self.from_hash(hash)
116
+ new(
117
+ isVpn: hash["isVpn"],
118
+ isTor: hash["isTor"],
119
+ isThreat: hash["isThreat"]
120
+ )
121
+ end
122
+ end
123
+
124
+ UserAgentBrowser = Struct.new(:name, :version, keyword_init: true) do
125
+ def self.from_hash(hash)
126
+ new(name: hash["name"], version: hash["version"])
127
+ end
128
+ end
129
+
130
+ UserAgentEngine = Struct.new(:name, :version, keyword_init: true) do
131
+ def self.from_hash(hash)
132
+ new(name: hash["name"], version: hash["version"])
133
+ end
134
+ end
135
+
136
+ UserAgentOS = Struct.new(:name, :version, keyword_init: true) do
137
+ def self.from_hash(hash)
138
+ new(name: hash["name"], version: hash["version"])
139
+ end
140
+ end
141
+
142
+ UserAgentDevice = Struct.new(:type, :vendor, :model, keyword_init: true) do
143
+ def self.from_hash(hash)
144
+ new(type: hash["type"], vendor: hash["vendor"], model: hash["model"])
145
+ end
146
+ end
147
+
148
+ UserAgentCPU = Struct.new(:architecture, keyword_init: true) do
149
+ def self.from_hash(hash)
150
+ new(architecture: hash["architecture"])
151
+ end
152
+ end
153
+
154
+ UserAgent = Struct.new(:browser, :engine, :os, :device, :cpu, keyword_init: true) do
155
+ def self.from_hash(hash)
156
+ new(
157
+ browser: hash["browser"] ? UserAgentBrowser.from_hash(hash["browser"]) : nil,
158
+ engine: hash["engine"] ? UserAgentEngine.from_hash(hash["engine"]) : nil,
159
+ os: hash["os"] ? UserAgentOS.from_hash(hash["os"]) : nil,
160
+ device: hash["device"] ? UserAgentDevice.from_hash(hash["device"]) : nil,
161
+ cpu: hash["cpu"] ? UserAgentCPU.from_hash(hash["cpu"]) : nil
162
+ )
163
+ end
164
+ end
165
+
166
+ GeoData = Struct.new(
167
+ :ip, :geoLocation, :timezone, :flag, :currency, :connection, :security, :userAgent,
168
+ keyword_init: true
169
+ ) do
170
+ def self.from_hash(hash)
171
+ new(
172
+ ip: hash["ip"] || "",
173
+ geoLocation: hash["geoLocation"] ? GeoLocation.from_hash(hash["geoLocation"]) : nil,
174
+ timezone: hash["timezone"] ? Timezone.from_hash(hash["timezone"]) : nil,
175
+ flag: hash["flag"] ? Flag.from_hash(hash["flag"]) : nil,
176
+ currency: hash["currency"] ? Currency.from_hash(hash["currency"]) : nil,
177
+ connection: hash["connection"] ? Connection.from_hash(hash["connection"]) : nil,
178
+ security: hash["security"] ? Security.from_hash(hash["security"]) : nil,
179
+ userAgent: hash["userAgent"] ? UserAgent.from_hash(hash["userAgent"]) : nil
180
+ )
181
+ end
182
+ end
183
+
184
+ ErrorResponse = Struct.new(:success, :message, keyword_init: true) do
185
+ def self.from_hash(hash)
186
+ new(success: hash["success"], message: hash["message"])
187
+ end
188
+ end
189
+
190
+ IpGeoResponse = Struct.new(:success, :data, :message, keyword_init: true) do
191
+ def self.from_hash(hash)
192
+ data_obj = nil
193
+ if hash["data"].is_a?(Hash)
194
+ # Bulk: normalise responseArray
195
+ if hash["data"]["responseArray"].is_a?(Array)
196
+ data_obj = hash["data"].dup
197
+ data_obj["responseArray"] = hash["data"]["responseArray"].map { |item| IpGeoResponse.from_hash(item) }
198
+ data_obj = OpenStruct.new(data_obj)
199
+ else
200
+ data_obj = GeoData.from_hash(hash["data"])
201
+ end
202
+ end
203
+ new(success: hash["success"], data: data_obj, message: hash["message"])
204
+ end
205
+ end
206
+
207
+ # ── Client ──────────────────────────────────────────────────────────
208
+
209
+ class Client
210
+ DEFAULT_BASE_URL = "https://api.ipwho.org".freeze
211
+
212
+ # @param api_key [String] Your IPWho API key (required).
213
+ # @param options [Hash]
214
+ # @option options [String] :base_url Override base URL (default https://api.ipwho.org).
215
+ # @option options [Numeric] :timeout Request timeout in seconds (default 30).
216
+ def initialize(api_key, options = {})
217
+ raise APIResponseError, "api_key is required" if api_key.nil? || api_key.empty?
218
+
219
+ @api_key = api_key
220
+ @base_url = (options[:base_url] || DEFAULT_BASE_URL).sub(%r{/+$}, "")
221
+ @timeout = options[:timeout] || 30
222
+ end
223
+
224
+ # ── public API ──────────────────────────────────────────────────
225
+
226
+ # Look up geolocation for a specific IPv4 or IPv6 address.
227
+ #
228
+ # @param ip [String] The IP address.
229
+ # @param options [Hash]
230
+ # @option options [String] :format json|xml|csv (default json)
231
+ # @option options [String] :fields Comma-separated fields filter
232
+ # @return [IpwhoApi::IpGeoResponse]
233
+ # @raise [IpwhoApi::InvalidIPError] – 404
234
+ # @raise [IpwhoApi::RateLimitError] – 429
235
+ # @raise [IpwhoApi::APIResponseError] – other errors
236
+ def lookup(ip, options = {})
237
+ format = options[:format] || "json"
238
+ params = build_params(format, options[:fields])
239
+ get("/ip/#{ERB::Util.url_encode(ip)}", params, format)
240
+ end
241
+
242
+ # Look up geolocation for the caller's own IP address.
243
+ #
244
+ # @param options [Hash]
245
+ # @option options [String] :format json|xml|csv (default json)
246
+ # @option options [String] :fields Comma-separated fields filter
247
+ # @return [IpwhoApi::IpGeoResponse]
248
+ # @raise [IpwhoApi::APIResponseError]
249
+ def me(options = {})
250
+ format = options[:format] || "json"
251
+ params = build_params(format, options[:fields])
252
+ get("/me", params, format)
253
+ end
254
+
255
+ # Batch-lookup multiple IP addresses.
256
+ #
257
+ # @param ips [Array<String>] List of IPv4 or IPv6 addresses.
258
+ # @return [IpwhoApi::IpGeoResponse] Response with data.responseArray of IpGeoResponse.
259
+ # @raise [IpwhoApi::APIResponseError]
260
+ def bulk(ips)
261
+ raise APIResponseError, "ips must not be empty" if ips.nil? || ips.empty?
262
+
263
+ bulk_param = ips.join(",")
264
+ params = { apiKey: @api_key }
265
+ get("/bulk/#{ERB::Util.url_encode(bulk_param)}", params, "json")
266
+ end
267
+
268
+ private
269
+
270
+ # Build query parameters.
271
+ def build_params(format, fields)
272
+ params = { apiKey: @api_key }
273
+ params[:format] = format if format != "json"
274
+ params[:get] = fields if fields && !fields.empty?
275
+ params
276
+ end
277
+
278
+ # Execute a GET request and return typed response.
279
+ def get(path, params, format)
280
+ conn = Faraday.new(url: @base_url) do |f|
281
+ f.headers["Accept"] = "application/json"
282
+ f.headers["User-Agent"] = "ipwho-ruby-sdk/#{VERSION}"
283
+ f.options.timeout = @timeout
284
+ f.options.open_timeout = @timeout
285
+ f.adapter Faraday.default_adapter
286
+ end
287
+
288
+ response = conn.get(path, params)
289
+ status = response.status
290
+ body = response.body
291
+
292
+ # Rate limit
293
+ raise RateLimitError, error_message(body, status) if status == 429
294
+
295
+ # Not found
296
+ raise InvalidIPError, error_message(body, status) if status == 404
297
+
298
+ # Other HTTP errors
299
+ raise APIResponseError, "HTTP #{status}: #{error_message(body, status)}" unless status >= 200 && status < 300
300
+
301
+ # Non-JSON formats: wrap raw text
302
+ unless format == "json"
303
+ return IpGeoResponse.new(
304
+ success: true,
305
+ data: GeoData.new(ip: body),
306
+ message: nil
307
+ )
308
+ end
309
+
310
+ # JSON success path
311
+ payload = JSON.parse(body)
312
+
313
+ # Logical error in success=false
314
+ if payload["success"] == false
315
+ raise APIResponseError, payload["message"] || "API returned success=false"
316
+ end
317
+
318
+ IpGeoResponse.from_hash(payload)
319
+ rescue Faraday::Error => e
320
+ raise APIResponseError, "Request failed: #{e.message}"
321
+ rescue JSON::ParserError
322
+ raise APIResponseError, "Invalid JSON response"
323
+ end
324
+
325
+ # Best-effort extraction of error message.
326
+ def error_message(body, _status)
327
+ return "Unknown API error" if body.nil? || body.empty?
328
+
329
+ parsed = JSON.parse(body) rescue nil
330
+ return parsed["message"] if parsed.is_a?(Hash) && parsed["message"]
331
+
332
+ body
333
+ end
334
+ end
335
+ end
336
+
337
+ # ═══════════════════════════════════════════════════════════════════════
338
+ # Example usage
339
+ # ═══════════════════════════════════════════════════════════════════════
340
+
341
+ if __FILE__ == $PROGRAM_NAME
342
+ require "json"
343
+ require "faraday"
344
+ require "ostruct"
345
+ require "erb"
346
+
347
+ api_key = ENV["IPWHO_API_KEY"] || "sk.xxxx"
348
+ client = IpwhoApi::Client.new(api_key)
349
+
350
+ # Single lookup
351
+ resp = client.lookup("8.8.8.8")
352
+ if resp.data&.geoLocation
353
+ gl = resp.data.geoLocation
354
+ puts "IP: #{resp.data.ip} | #{gl.city}, #{gl.country} (#{gl.latitude}, #{gl.longitude})"
355
+ end
356
+ if resp.data&.currency
357
+ puts "Currency: #{resp.data.currency.code} (#{resp.data.currency.symbol})"
358
+ end
359
+
360
+ # Self lookup
361
+ me = client.me
362
+ puts "\nMy IP: #{me.data&.ip || 'unknown'}"
363
+
364
+ # Bulk
365
+ bulk = client.bulk(["8.8.8.8", "1.1.1.1"])
366
+ puts "\nBulk: success=#{bulk.success}"
367
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ipwho
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - IPWho SDK Contributors
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: faraday
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.0'
26
+ description: Geoip lookup, IP location, ASN/ISP, timezone, currency, flag, and VPN/proxy
27
+ detection for IPv4 and IPv6 via api.ipwho.org.
28
+ email:
29
+ - info@ipwho.org
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - CHANGELOG.md
35
+ - LICENSE
36
+ - README.md
37
+ - lib/ipwho.rb
38
+ homepage: https://www.ipwho.org
39
+ licenses:
40
+ - MIT
41
+ metadata:
42
+ homepage_uri: https://www.ipwho.org
43
+ source_code_uri: https://github.com/lavrox/SDK-IPWho-IP-Geolocation-Ruby
44
+ changelog_uri: https://github.com/lavrox/SDK-IPWho-IP-Geolocation-Ruby/blob/main/CHANGELOG.md
45
+ bug_tracker_uri: https://github.com/lavrox/SDK-IPWho-IP-Geolocation-Ruby/issues
46
+ documentation_uri: https://www.ipwho.org/docs
47
+ allowed_push_host: https://rubygems.org
48
+ rubygems_mfa_required: 'true'
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 2.5.0
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 4.0.16
64
+ specification_version: 4
65
+ summary: Official Ruby SDK for the IPWho IP geolocation API (lookup, me, bulk).
66
+ test_files: []