vpndetection 5.9.0 → 5.9.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9dd1a9015e11fcd72008303761601cdeb4fb1bf3b9b672e7e6636ce0ee0a540f
4
- data.tar.gz: f8cff5caeff344d0a669fd066423861cbcde845d22a832859e13e0ef7871583e
3
+ metadata.gz: 4b8d521a64e7bfd1680e5e158d30a340602191565c1c6e0332662d546b163caa
4
+ data.tar.gz: 40dce453e2eca0badd693bdf188ab891d9ce43d77dc5dcaccf5b15ba9606cc9a
5
5
  SHA512:
6
- metadata.gz: b290fc24f994a07348fa600ea74bdd4abb75b38d7e9745ce5a2988f0d8556ac0b923e72f39b1f186913dd47b9819e513e33d387f3d22075a0679a19b1ac553d9
7
- data.tar.gz: 75b53d7b79396301334010f6f5893431f6d51ab2bcc4a2e1cee0a862926812e46c9ebfc8ed98398ea7887c9dfcc2f844775fdd7fdfb8de3a30ce8e64f5da6a52
6
+ metadata.gz: 0a52df204a1d0a4fdc8c7bc88c73b37dd26611efbd58708053d72a02f4b01aa1c83d1c2453b13b2e08217e9386e38ecd4042614284fb5e0302032b3e66789f49
7
+ data.tar.gz: be8c767fc9bb2924505c16e89be1db7848fca3b725bcf045e5a5f21903389c4ca613a0dc605d9a2df02a4aafc3b90dabaa6044f3a1b157d646bca7537f419bb2
@@ -345,9 +345,9 @@ module VPNDetection
345
345
  def batch_answer(ip, body)
346
346
  served = body['results'][ip]
347
347
  failed = body['errors'][ip]
348
- if Transport.lookup_answer?(served)
348
+ if Transport.conforms?(served, 'LookupResponse')
349
349
  Result.new(served)
350
- elsif failed.is_a?(Hash)
350
+ elsif Transport.conforms?(failed, 'BatchLookupError')
351
351
  Error.from_entry(failed['status'], failed['error'])
352
352
  else
353
353
  Error.new(:server_error, "the batch answer carried no readable entry for #{ip}", status: 200)
@@ -86,11 +86,18 @@ module VPNDetection
86
86
  # the spec requires. The generated reader raised a RuntimeError, a
87
87
  # JSON::ParserError or an ArgumentError for those, none of them retried, and
88
88
  # handed an empty or non-object body back as nil, as if it were the answer.
89
+ #
90
+ # Every member present is checked against the type the spec gives it, at any
91
+ # depth, before the answer is handed back: the generated reader converts with
92
+ # `to_i`, so `"version":"x"` came back as 0 and `"version":"3"` as 3, and it
93
+ # skips a list member sent as null, so `"versions": null` came back as nil
94
+ # (5.9.0, measured 2026-10-20).
89
95
  def deserialize(response, return_type)
90
96
  data = super
91
- return data unless data.nil?
97
+ raise Error.new(:server_error, "the API answered with no #{return_type}", status: response.code) if data.nil?
92
98
 
93
- raise Error.new(:server_error, "the API answered with no #{return_type}", status: response.code)
99
+ Transport.check_type!(JSON.parse(response.body.to_s), return_type.to_s, 'the answer')
100
+ data
94
101
  rescue Error
95
102
  raise
96
103
  rescue StandardError => e
@@ -182,6 +189,7 @@ module VPNDetection
182
189
 
183
190
  body = parse_object(response)
184
191
  begin
192
+ check_type!(body, 'Entitlement', 'the answer')
185
193
  Entitlement.build_from_hash(body)
186
194
  rescue StandardError => e
187
195
  raise Error.new(:server_error, "could not read the answer as Entitlement: #{e.message}",
@@ -194,15 +202,21 @@ module VPNDetection
194
202
  raise Error.from_status(response.code, response.headers, response.body) unless response.success?
195
203
 
196
204
  body = parse_object(response)
197
- return Result.new(body) if lookup_answer?(body)
198
-
199
- raise Error.new(:server_error, 'the answer carried no ip and is_vpn', status: response.code)
205
+ begin
206
+ check_type!(body, 'LookupResponse', 'the answer')
207
+ rescue TypeError => e
208
+ raise Error.new(:server_error, "could not read the answer as LookupResponse: #{e.message}",
209
+ status: response.code)
210
+ end
211
+ Result.new(body)
200
212
  end
201
213
 
202
- # Whether a parsed body is a lookup's answer: every plan's carries `ip` and
203
- # `is_vpn`, so one without them is a 2xx the call cannot read.
204
- def self.lookup_answer?(body)
205
- body.is_a?(Hash) && body['ip'].is_a?(String) && [true, false].include?(body['is_vpn'])
214
+ # Whether a parsed value is the generated `type`: a batch entry that is not
215
+ # is that address's error alone.
216
+ def self.conforms?(value, type)
217
+ check_type!(value, type, 'the entry')
218
+ rescue TypeError
219
+ false
206
220
  end
207
221
 
208
222
  # The two maps of a batch answer, each present even when empty. An answer
@@ -269,5 +283,81 @@ module VPNDetection
269
283
  end
270
284
 
271
285
  private_class_method :transport_failure?, :parse_object, :oauth_refusal
286
+
287
+ # The members the spec requires of each answer a lookup, a batch, the
288
+ # entitlement or a database call reads. One that is not nullable must be
289
+ # there and not null. Every plan's lookup carries `ip` and `is_vpn`, and a
290
+ # detail's members are as optional as the plan makes them.
291
+ # test/client_test.rb holds this to spec/openapi.yaml.
292
+ REQUIRED = {
293
+ 'LookupResponse' => %i[ip is_vpn],
294
+ 'VpnDetail' => [],
295
+ 'ClassDetail' => [],
296
+ 'ProxyDetail' => [],
297
+ 'PubproxyDetail' => [],
298
+ 'BatchLookupError' => %i[status error],
299
+ 'Entitlement' => %i[org_id apikey plan usage],
300
+ 'EntitlementApikey' => %i[id expires allowed_cidrs],
301
+ 'EntitlementPlan' => %i[key tier],
302
+ 'EntitlementUsage' => %i[requests quota hard_limit window_start window_end],
303
+ 'DatabaseList' => %i[databases],
304
+ 'Database' => %i[base name summary license_type starts expires renews_at notice_due_at in_term standing
305
+ versions],
306
+ 'DatabaseVersion' => %i[id version formats],
307
+ 'DatabaseFormatSize' => %i[format bytes],
308
+ 'DatabaseMetadata' => %i[id updated entries schema],
309
+ 'DatabaseMetadataColumn' => %i[name type],
310
+ 'DatabaseChecksumsResponse' => %i[id format checksums],
311
+ 'DbChecksums' => %i[md5 sha1 sha256 sha512],
312
+ 'DownloadList' => %i[downloads],
313
+ 'Download' => %i[dataset_id format outcome sample bytes http_status apikey_id client_ip user_agent created],
314
+ }.freeze
315
+
316
+ # True when the parsed `value` has the generated `type`, a model's members
317
+ # recursively; raises TypeError naming where it does not. No list entry or
318
+ # map value the spec describes may be null: through 5.9.0 one came back as
319
+ # nil, or as 0 in `size`, and a lookup's `"is_hosting": "yes"` came back as
320
+ # the answer.
321
+ def self.check_type!(value, type, where)
322
+ raise TypeError, "#{where} is null" if value.nil?
323
+
324
+ typed =
325
+ case type
326
+ when 'String', 'Date', 'Time' then value.is_a?(String)
327
+ when 'Integer' then value.is_a?(Integer)
328
+ when 'Float' then value.is_a?(Numeric)
329
+ when 'Boolean' then [true, false].include?(value)
330
+ when 'Object' then true
331
+ when /\AArray<(?<inner>.+)>\z/
332
+ inner = Regexp.last_match(:inner)
333
+ value.is_a?(Array) && value.each_with_index.all? { |v, i| check_type!(v, inner, "#{where}[#{i}]") }
334
+ when /\AHash<String, ?(?<inner>.+)>\z/
335
+ inner = Regexp.last_match(:inner)
336
+ value.is_a?(Hash) && value.all? { |k, v| check_type!(v, inner, "#{where}.#{k}") }
337
+ else
338
+ model = VPNDetection.const_get(type)
339
+ if model.respond_to?(:openapi_types)
340
+ value.is_a?(Hash) && model.openapi_types.all? { |attr, member| check_member!(model, attr, member, value, where) }
341
+ else
342
+ value.is_a?(String) # an enum
343
+ end
344
+ end
345
+ return true if typed
346
+
347
+ raise TypeError, "#{where} is not a #{type}"
348
+ end
349
+
350
+ # A member absent or null reads as absent, unless the spec requires it and
351
+ # does not make it nullable.
352
+ def self.check_member!(model, attr, member, value, where)
353
+ key = model.attribute_map[attr].to_s
354
+ return check_type!(value[key], member.to_s, "#{where}.#{key}") unless value[key].nil?
355
+ return true unless REQUIRED.fetch(model.name.split('::').last).include?(attr)
356
+ return true if model.openapi_nullable.include?(attr)
357
+
358
+ raise TypeError, "#{where}.#{key} is #{value.key?(key) ? 'null' : 'missing'}"
359
+ end
360
+
361
+ private_class_method :check_member!
272
362
  end
273
363
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module VPNDetection
4
- VERSION = '5.9.0'
4
+ VERSION = '5.9.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vpndetection
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.9.0
4
+ version: 5.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mslm Dev