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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +57 -0
- data/lib/ipscope/cache.rb +97 -0
- data/lib/ipscope/client.rb +310 -0
- data/lib/ipscope/errors.rb +73 -0
- data/lib/ipscope/models.rb +572 -0
- data/lib/ipscope/transport.rb +348 -0
- data/lib/ipscope/validator.rb +152 -0
- data/lib/ipscope/version.rb +5 -0
- data/lib/ipscope.rb +9 -0
- metadata +59 -0
|
@@ -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
|