ipscope 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.
@@ -0,0 +1,572 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IpScope
4
+ # Geographic location intelligence block.
5
+ class GeoBlock
6
+ attr_reader :country_code, :country_name, :region_code, :region_name,
7
+ :city, :postal_code, :latitude, :longitude, :timezone,
8
+ :accuracy_radius_km
9
+
10
+ def initialize(
11
+ country_code: nil,
12
+ country_name: nil,
13
+ region_code: nil,
14
+ region_name: nil,
15
+ city: nil,
16
+ postal_code: nil,
17
+ latitude: nil,
18
+ longitude: nil,
19
+ timezone: nil,
20
+ accuracy_radius_km: nil
21
+ )
22
+ @country_code = country_code&.to_s&.freeze
23
+ @country_name = country_name&.to_s&.freeze
24
+ @region_code = region_code&.to_s&.freeze
25
+ @region_name = region_name&.to_s&.freeze
26
+ @city = city&.to_s&.freeze
27
+ @postal_code = postal_code&.to_s&.freeze
28
+ @latitude = latitude.nil? ? nil : latitude.to_f
29
+ @longitude = longitude.nil? ? nil : longitude.to_f
30
+ @timezone = timezone&.to_s&.freeze
31
+ @accuracy_radius_km = accuracy_radius_km.nil? ? nil : accuracy_radius_km.to_i
32
+ freeze
33
+ end
34
+
35
+ def self.from_h(raw)
36
+ h = raw.is_a?(Hash) ? raw : {}
37
+ new(
38
+ country_code: h["country_code"],
39
+ country_name: h["country_name"],
40
+ region_code: h["region_code"],
41
+ region_name: h["region_name"],
42
+ city: h["city"],
43
+ postal_code: h["postal_code"],
44
+ latitude: h["latitude"],
45
+ longitude: h["longitude"],
46
+ timezone: h["timezone"],
47
+ accuracy_radius_km: h["accuracy_radius_km"]
48
+ )
49
+ end
50
+ end
51
+
52
+ # Autonomous System (ASN) and BGP prefix block.
53
+ class NetworkBlock
54
+ attr_reader :asn, :asn_name, :org, :isp, :connection_type, :bgp_prefix, :rir
55
+
56
+ def initialize(
57
+ asn: nil,
58
+ asn_name: nil,
59
+ org: nil,
60
+ isp: nil,
61
+ connection_type: nil,
62
+ bgp_prefix: nil,
63
+ rir: nil
64
+ )
65
+ @asn = asn.nil? ? nil : asn.to_i
66
+ @asn_name = asn_name&.to_s&.freeze
67
+ @org = org&.to_s&.freeze
68
+ @isp = isp&.to_s&.freeze
69
+ @connection_type = connection_type&.to_s&.freeze
70
+ @bgp_prefix = bgp_prefix&.to_s&.freeze
71
+ @rir = rir&.to_s&.freeze
72
+ freeze
73
+ end
74
+
75
+ def self.from_h(raw)
76
+ h = raw.is_a?(Hash) ? raw : {}
77
+ new(
78
+ asn: h["asn"],
79
+ asn_name: h["asn_name"],
80
+ org: h["org"],
81
+ isp: h["isp"],
82
+ connection_type: h["connection_type"],
83
+ bgp_prefix: h["bgp_prefix"],
84
+ rir: h["rir"]
85
+ )
86
+ end
87
+ end
88
+
89
+ # Anonymity, proxy, VPN, Tor, and threat intelligence block.
90
+ class SecurityBlock
91
+ attr_reader :is_vpn, :is_proxy, :is_tor, :is_datacenter, :is_relay,
92
+ :threat_level, :threat_types
93
+
94
+ def initialize(
95
+ is_vpn: false,
96
+ is_proxy: false,
97
+ is_tor: false,
98
+ is_datacenter: false,
99
+ is_relay: false,
100
+ threat_level: "low",
101
+ threat_types: []
102
+ )
103
+ @is_vpn = !!is_vpn
104
+ @is_proxy = !!is_proxy
105
+ @is_tor = !!is_tor
106
+ @is_datacenter = !!is_datacenter
107
+ @is_relay = !!is_relay
108
+ @threat_level = (threat_level || "low").to_s.freeze
109
+ @threat_types = Array(threat_types).map { |t| t.to_s.freeze }.freeze
110
+ freeze
111
+ end
112
+
113
+ def self.from_h(raw)
114
+ h = raw.is_a?(Hash) ? raw : {}
115
+ new(
116
+ is_vpn: h["is_vpn"],
117
+ is_proxy: h["is_proxy"],
118
+ is_tor: h["is_tor"],
119
+ is_datacenter: h["is_datacenter"],
120
+ is_relay: h["is_relay"],
121
+ threat_level: h["threat_level"] || "low",
122
+ threat_types: h["threat_types"].is_a?(Array) ? h["threat_types"] : []
123
+ )
124
+ end
125
+ end
126
+
127
+ # Detailed connection and carrier classification block.
128
+ class ConnectionBlock
129
+ attr_reader :usage_type, :company_type, :carrier_name, :mcc, :mnc,
130
+ :domain, :route, :allocated_date, :registry_status
131
+
132
+ def initialize(
133
+ usage_type: nil,
134
+ company_type: nil,
135
+ carrier_name: nil,
136
+ mcc: nil,
137
+ mnc: nil,
138
+ domain: nil,
139
+ route: nil,
140
+ allocated_date: nil,
141
+ registry_status: nil
142
+ )
143
+ @usage_type = usage_type&.to_s&.freeze
144
+ @company_type = company_type&.to_s&.freeze
145
+ @carrier_name = carrier_name&.to_s&.freeze
146
+ @mcc = mcc&.to_s&.freeze
147
+ @mnc = mnc&.to_s&.freeze
148
+ @domain = domain&.to_s&.freeze
149
+ @route = route&.to_s&.freeze
150
+ @allocated_date = allocated_date&.to_s&.freeze
151
+ @registry_status = registry_status&.to_s&.freeze
152
+ freeze
153
+ end
154
+
155
+ def self.from_h(raw)
156
+ h = raw.is_a?(Hash) ? raw : {}
157
+ new(
158
+ usage_type: h["usage_type"],
159
+ company_type: h["company_type"],
160
+ carrier_name: h["carrier_name"],
161
+ mcc: h["mcc"],
162
+ mnc: h["mnc"],
163
+ domain: h["domain"],
164
+ route: h["route"],
165
+ allocated_date: h["allocated_date"],
166
+ registry_status: h["registry_status"]
167
+ )
168
+ end
169
+ end
170
+
171
+ # Extended threat scoring and provider attribution block.
172
+ class SecurityExtensionsBlock
173
+ attr_reader :vpn_provider, :proxy_type, :threat_score, :is_abuser, :is_bot, :is_bogon
174
+
175
+ def initialize(
176
+ vpn_provider: nil,
177
+ proxy_type: nil,
178
+ threat_score: nil,
179
+ is_abuser: false,
180
+ is_bot: false,
181
+ is_bogon: false
182
+ )
183
+ @vpn_provider = vpn_provider&.to_s&.freeze
184
+ @proxy_type = proxy_type&.to_s&.freeze
185
+ @threat_score = threat_score.nil? ? nil : threat_score.to_i
186
+ @is_abuser = !!is_abuser
187
+ @is_bot = !!is_bot
188
+ @is_bogon = !!is_bogon
189
+ freeze
190
+ end
191
+
192
+ def self.from_h(raw)
193
+ h = raw.is_a?(Hash) ? raw : {}
194
+ new(
195
+ vpn_provider: h["vpn_provider"],
196
+ proxy_type: h["proxy_type"],
197
+ threat_score: h["threat_score"],
198
+ is_abuser: h["is_abuser"],
199
+ is_bot: h["is_bot"],
200
+ is_bogon: h["is_bogon"]
201
+ )
202
+ end
203
+ end
204
+
205
+ # Canonical IP intelligence response returned by `/v1/ip` and `/v1/ip/{ip}`.
206
+ class IpLookupResponse
207
+ attr_reader :ip, :version, :is_bogon, :bogon_type, :geo, :network,
208
+ :security, :connection, :security_extensions, :updated_at
209
+
210
+ def initialize(
211
+ ip:,
212
+ version:,
213
+ is_bogon:,
214
+ bogon_type:,
215
+ geo:,
216
+ network:,
217
+ security:,
218
+ connection: nil,
219
+ security_extensions: nil,
220
+ updated_at: ""
221
+ )
222
+ @ip = ip.to_s.freeze
223
+ @version = version.to_i
224
+ @is_bogon = !!is_bogon
225
+ @bogon_type = bogon_type&.to_s&.freeze
226
+ @geo = geo
227
+ @network = network
228
+ @security = security
229
+ @connection = connection
230
+ @security_extensions = security_extensions
231
+ @updated_at = updated_at.to_s.freeze
232
+ freeze
233
+ end
234
+
235
+ def self.from_h(raw)
236
+ h = raw.is_a?(Hash) ? raw : {}
237
+ new(
238
+ ip: h["ip"] || "",
239
+ version: h["version"] || 4,
240
+ is_bogon: h["is_bogon"] || false,
241
+ bogon_type: h["bogon_type"],
242
+ geo: GeoBlock.from_h(h["geo"]),
243
+ network: NetworkBlock.from_h(h["network"]),
244
+ security: SecurityBlock.from_h(h["security"]),
245
+ connection: h["connection"].is_a?(Hash) ? ConnectionBlock.from_h(h["connection"]) : nil,
246
+ security_extensions: h["security_extensions"].is_a?(Hash) ? SecurityExtensionsBlock.from_h(h["security_extensions"]) : nil,
247
+ updated_at: h["updated_at"] || ""
248
+ )
249
+ end
250
+ end
251
+
252
+ # Batch IP lookup response returned by `POST /v1/ip/batch`.
253
+ class BatchLookupResponse
254
+ attr_reader :count, :results
255
+
256
+ def initialize(count:, results:)
257
+ @results = Array(results).freeze
258
+ @count = count ? count.to_i : @results.length
259
+ freeze
260
+ end
261
+
262
+ def self.from_h(raw)
263
+ h = raw.is_a?(Hash) ? raw : {}
264
+ raw_list = h["results"].is_a?(Array) ? h["results"] : []
265
+ records = raw_list.select { |item| item.is_a?(Hash) }.map { |item| IpLookupResponse.from_h(item) }
266
+ new(
267
+ count: h["count"] || records.length,
268
+ results: records
269
+ )
270
+ end
271
+ end
272
+
273
+ # Canonical BGP/CIDR prefix intelligence record.
274
+ class CanonicalNetworkPrefixRecord
275
+ attr_reader :network_cidr, :ip_version, :geo, :network, :security,
276
+ :connection, :security_extensions, :updated_at
277
+
278
+ def initialize(
279
+ network_cidr:,
280
+ ip_version:,
281
+ geo:,
282
+ network:,
283
+ security:,
284
+ connection: nil,
285
+ security_extensions: nil,
286
+ updated_at: ""
287
+ )
288
+ @network_cidr = network_cidr.to_s.freeze
289
+ @ip_version = ip_version.to_i
290
+ @geo = geo
291
+ @network = network
292
+ @security = security
293
+ @connection = connection
294
+ @security_extensions = security_extensions
295
+ @updated_at = updated_at.to_s.freeze
296
+ freeze
297
+ end
298
+
299
+ def self.from_h(raw)
300
+ h = raw.is_a?(Hash) ? raw : {}
301
+ new(
302
+ network_cidr: h["network_cidr"] || "",
303
+ ip_version: h["ip_version"] || 4,
304
+ geo: GeoBlock.from_h(h["geo"]),
305
+ network: NetworkBlock.from_h(h["network"]),
306
+ security: SecurityBlock.from_h(h["security"]),
307
+ connection: h["connection"].is_a?(Hash) ? ConnectionBlock.from_h(h["connection"]) : nil,
308
+ security_extensions: h["security_extensions"].is_a?(Hash) ? SecurityExtensionsBlock.from_h(h["security_extensions"]) : nil,
309
+ updated_at: h["updated_at"] || ""
310
+ )
311
+ end
312
+ end
313
+
314
+ # Network CIDR lookup response returned by `GET /v1/network/{address}/{prefixLength}`.
315
+ class NetworkPrefixLookupResponse
316
+ attr_reader :requested_cidr, :exact_match, :enclosing_prefix,
317
+ :overlapping_prefixes, :total_matches
318
+
319
+ def initialize(
320
+ requested_cidr:,
321
+ exact_match:,
322
+ enclosing_prefix:,
323
+ overlapping_prefixes:,
324
+ total_matches:
325
+ )
326
+ @requested_cidr = requested_cidr.to_s.freeze
327
+ @exact_match = exact_match
328
+ @enclosing_prefix = enclosing_prefix
329
+ @overlapping_prefixes = Array(overlapping_prefixes).freeze
330
+ @total_matches = total_matches.to_i
331
+ freeze
332
+ end
333
+
334
+ def self.from_h(raw)
335
+ h = raw.is_a?(Hash) ? raw : {}
336
+ overlaps = (h["overlapping_prefixes"].is_a?(Array) ? h["overlapping_prefixes"] : [])
337
+ .select { |item| item.is_a?(Hash) }
338
+ .map { |item| CanonicalNetworkPrefixRecord.from_h(item) }
339
+ new(
340
+ requested_cidr: h["requested_cidr"] || "",
341
+ exact_match: h["exact_match"].is_a?(Hash) ? CanonicalNetworkPrefixRecord.from_h(h["exact_match"]) : nil,
342
+ enclosing_prefix: h["enclosing_prefix"].is_a?(Hash) ? CanonicalNetworkPrefixRecord.from_h(h["enclosing_prefix"]) : nil,
343
+ overlapping_prefixes: overlaps,
344
+ total_matches: h["total_matches"] || overlaps.length
345
+ )
346
+ end
347
+ end
348
+
349
+ # Snapshot metadata for a single IP intelligence dataset source.
350
+ class DatasetSnapshotSummary
351
+ attr_reader :source, :version_tag, :checksum_sha256, :record_count,
352
+ :activated_at, :created_at
353
+
354
+ def initialize(
355
+ source:,
356
+ version_tag:,
357
+ checksum_sha256:,
358
+ record_count:,
359
+ activated_at:,
360
+ created_at:
361
+ )
362
+ @source = source.to_s.freeze
363
+ @version_tag = version_tag.to_s.freeze
364
+ @checksum_sha256 = checksum_sha256.to_s.freeze
365
+ @record_count = record_count.to_i
366
+ @activated_at = activated_at&.to_s&.freeze
367
+ @created_at = created_at.to_s.freeze
368
+ freeze
369
+ end
370
+
371
+ def self.from_h(raw)
372
+ h = raw.is_a?(Hash) ? raw : {}
373
+ new(
374
+ source: h["source"] || "",
375
+ version_tag: h["version_tag"] || "",
376
+ checksum_sha256: h["checksum_sha256"] || "",
377
+ record_count: h["record_count"] || 0,
378
+ activated_at: h["activated_at"],
379
+ created_at: h["created_at"] || ""
380
+ )
381
+ end
382
+ end
383
+
384
+ # Active dataset snapshots metadata returned by `GET /v1/datasets/metadata`.
385
+ class DatasetsMetadataResponse
386
+ attr_reader :count, :active_datasets
387
+
388
+ def initialize(count:, active_datasets:)
389
+ @active_datasets = Array(active_datasets).freeze
390
+ @count = count ? count.to_i : @active_datasets.length
391
+ freeze
392
+ end
393
+
394
+ def self.from_h(raw)
395
+ h = raw.is_a?(Hash) ? raw : {}
396
+ datasets = (h["active_datasets"].is_a?(Array) ? h["active_datasets"] : [])
397
+ .select { |item| item.is_a?(Hash) }
398
+ .map { |item| DatasetSnapshotSummary.from_h(item) }
399
+ new(
400
+ count: h["count"] || datasets.length,
401
+ active_datasets: datasets
402
+ )
403
+ end
404
+ end
405
+
406
+ # Payload for submitting an IP geolocation or anonymity correction (`POST /v1/feedback`).
407
+ class FeedbackSubmissionInput
408
+ attr_reader :ip_or_cidr, :suggested_country_code, :suggested_region_name,
409
+ :suggested_city, :suggested_is_vpn, :notes, :contact_email
410
+
411
+ def initialize(
412
+ ip_or_cidr:,
413
+ suggested_country_code: nil,
414
+ suggested_region_name: nil,
415
+ suggested_city: nil,
416
+ suggested_is_vpn: nil,
417
+ notes: nil,
418
+ contact_email: nil
419
+ )
420
+ @ip_or_cidr = ip_or_cidr.to_s.freeze
421
+ @suggested_country_code = suggested_country_code&.to_s&.freeze
422
+ @suggested_region_name = suggested_region_name&.to_s&.freeze
423
+ @suggested_city = suggested_city&.to_s&.freeze
424
+ @suggested_is_vpn = suggested_is_vpn.nil? ? nil : !!suggested_is_vpn
425
+ @notes = notes&.to_s&.freeze
426
+ @contact_email = contact_email&.to_s&.freeze
427
+ freeze
428
+ end
429
+
430
+ def to_h
431
+ payload = { "ip_or_cidr" => @ip_or_cidr }
432
+ payload["suggested_country_code"] = @suggested_country_code unless @suggested_country_code.nil?
433
+ payload["suggested_region_name"] = @suggested_region_name unless @suggested_region_name.nil?
434
+ payload["suggested_city"] = @suggested_city unless @suggested_city.nil?
435
+ payload["suggested_is_vpn"] = @suggested_is_vpn unless @suggested_is_vpn.nil?
436
+ payload["notes"] = @notes unless @notes.nil?
437
+ payload["contact_email"] = @contact_email unless @contact_email.nil?
438
+ payload
439
+ end
440
+ end
441
+
442
+ # Feedback submission acknowledgment returned by `POST /v1/feedback`.
443
+ class FeedbackSubmissionResponse
444
+ attr_reader :id, :ip_or_cidr, :status, :created_at
445
+
446
+ def initialize(id:, ip_or_cidr:, status:, created_at:)
447
+ @id = id.to_s.freeze
448
+ @ip_or_cidr = ip_or_cidr.to_s.freeze
449
+ @status = status.to_s.freeze
450
+ @created_at = created_at.to_s.freeze
451
+ freeze
452
+ end
453
+
454
+ def self.from_h(raw)
455
+ h = raw.is_a?(Hash) ? raw : {}
456
+ new(
457
+ id: h["id"] || "",
458
+ ip_or_cidr: h["ip_or_cidr"] || "",
459
+ status: h["status"] || "",
460
+ created_at: h["created_at"] || ""
461
+ )
462
+ end
463
+ end
464
+
465
+ # Service health status returned by `GET /v1/status`.
466
+ class ServiceStatusResponse
467
+ attr_reader :status, :service, :version, :uptime_seconds, :timestamp
468
+
469
+ def initialize(status:, service:, version:, uptime_seconds:, timestamp:)
470
+ @status = status.to_s.freeze
471
+ @service = service.to_s.freeze
472
+ @version = version.to_s.freeze
473
+ @uptime_seconds = uptime_seconds.to_f
474
+ @timestamp = timestamp.to_s.freeze
475
+ freeze
476
+ end
477
+
478
+ def self.from_h(raw)
479
+ h = raw.is_a?(Hash) ? raw : {}
480
+ new(
481
+ status: h["status"] || "",
482
+ service: h["service"] || "",
483
+ version: h["version"] || "",
484
+ uptime_seconds: h["uptime_seconds"] || 0.0,
485
+ timestamp: h["timestamp"] || ""
486
+ )
487
+ end
488
+ end
489
+
490
+ # Rate-limit and quota metadata extracted from `x-ratelimit-*` HTTP response headers.
491
+ class RateLimitMetadata
492
+ attr_reader :limit, :remaining, :reset_epoch_seconds, :credits_remaining, :plan_tier
493
+
494
+ def initialize(
495
+ limit: nil,
496
+ remaining: nil,
497
+ reset_epoch_seconds: nil,
498
+ credits_remaining: nil,
499
+ plan_tier: nil
500
+ )
501
+ @limit = limit.nil? ? nil : limit.to_i
502
+ @remaining = remaining.nil? ? nil : remaining.to_i
503
+ @reset_epoch_seconds = reset_epoch_seconds.nil? ? nil : reset_epoch_seconds.to_i
504
+ @credits_remaining = credits_remaining.nil? ? nil : credits_remaining.to_i
505
+ @plan_tier = plan_tier&.to_s&.freeze
506
+ freeze
507
+ end
508
+ end
509
+
510
+ # Wrapper pairing a response payload with rate-limit metadata and cache hit status.
511
+ class IpScopeResultWithMeta
512
+ attr_reader :data, :rate_limit, :from_cache
513
+
514
+ def initialize(data:, rate_limit:, from_cache:)
515
+ @data = data
516
+ @rate_limit = rate_limit
517
+ @from_cache = !!from_cache
518
+ freeze
519
+ end
520
+ end
521
+
522
+ # Snapshot of in-memory LRU cache statistics.
523
+ class CacheStats
524
+ attr_reader :enabled, :size, :max_entries, :hits, :misses, :evictions
525
+
526
+ def initialize(enabled:, size:, max_entries:, hits:, misses:, evictions:)
527
+ @enabled = !!enabled
528
+ @size = size.to_i
529
+ @max_entries = max_entries.to_i
530
+ @hits = hits.to_i
531
+ @misses = misses.to_i
532
+ @evictions = evictions.to_i
533
+ freeze
534
+ end
535
+ end
536
+
537
+ # Retry configuration for transient HTTP errors (`429`, `502`, `503`, `504`).
538
+ class RetryOptions
539
+ attr_reader :max_retries, :initial_delay_seconds, :max_delay_seconds
540
+
541
+ def initialize(max_retries: 2, initial_delay_seconds: 0.25, max_delay_seconds: 4.0)
542
+ @max_retries = max_retries.to_i
543
+ @initial_delay_seconds = initial_delay_seconds.to_f
544
+ @max_delay_seconds = max_delay_seconds.to_f
545
+ freeze
546
+ end
547
+ end
548
+
549
+ # Bounded LRU + TTL cache configuration (`max_entries: 512` default `< 400 KB`).
550
+ class CacheOptions
551
+ attr_reader :enabled, :max_entries, :ttl_seconds
552
+
553
+ def initialize(enabled: true, max_entries: 512, ttl_seconds: 60.0)
554
+ @enabled = !!enabled
555
+ @max_entries = max_entries.to_i
556
+ @ttl_seconds = ttl_seconds.to_f
557
+ freeze
558
+ end
559
+ end
560
+
561
+ # Per-request options.
562
+ class RequestOptions
563
+ attr_reader :bypass_cache, :timeout_seconds, :headers
564
+
565
+ def initialize(bypass_cache: false, timeout_seconds: nil, headers: {})
566
+ @bypass_cache = !!bypass_cache
567
+ @timeout_seconds = timeout_seconds.nil? ? nil : timeout_seconds.to_f
568
+ @headers = (headers || {}).each_with_object({}) { |(k, v), acc| acc[k.to_s] = v.to_s }.freeze
569
+ freeze
570
+ end
571
+ end
572
+ end