maxmind-geoip2 1.3.0 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d6ddb183c97df1041a902a1f2f98dec87e640b5907ae19b080bdd03c87bc03f
4
- data.tar.gz: 8f0e58e55e605576bc4214e878c28aaab3bb6c0dbd065bb1281ac5f0db65e69e
3
+ metadata.gz: f26229b50b6ba4760df9389e76c5e705dc2b5b53e7fc50eb6be25d2af74f6985
4
+ data.tar.gz: bfcb0fef72edaf05cdfcb1cc55be5ce2d12afbc650912e062ae263649a279736
5
5
  SHA512:
6
- metadata.gz: 2c6d5e20fd3bef3aacde30acb0c22695c8a903626747db02b58067adcaeafd9d2673f9be8189fad8d6a53869028ff15bf9536ce66a435a2da6e86fb4126cf06b
7
- data.tar.gz: 8c79484d19313833856f91deabceed833f2dae00fb1cd4993d57b973b1747541cf83122e07043c1b4925a2f291172195ab024988be39501a922a751c7808cde1
6
+ metadata.gz: 4d3de90ee7df88d6acbef1af76202af2130e9670ad726bea5f9753581bd5e3ae962f859720458f833731f0822cc08345171ac90cde7b446c784ca7c6ca21215c
7
+ data.tar.gz: 34b7484739814ecd6de33435310c8cb29840534c4beed8d063c2ed00f2e0eee46338cec95bef1be5acb2ac2947be937842a2edc1f646b46460d70fa656213f6d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.0 (2025-11-20)
4
+
5
+ * Ruby 3.2+ is now required. If you're using Ruby 3.0 or 3.1, please use
6
+ version 1.3.0 of this gem.
7
+ * A new `anonymizer` object has been added to the `MaxMind::GeoIP2::Model::Insights`
8
+ model. This object indicates whether the IP address is part of an anonymizing
9
+ network, including VPN confidence scoring, provider name detection, and network
10
+ last seen date. This is only available from the GeoIP2 Insights web service.
11
+ * A new `ip_risk_snapshot` method has been added to `MaxMind::GeoIP2::Record::Traits`.
12
+ This field contains the risk associated with the IP address, ranging from 0.01 to
13
+ 99 (a higher score indicates a higher risk). This is only available from the GeoIP2
14
+ Insights web service.
15
+ * The `anonymous?`, `anonymous_vpn?`, `hosting_provider?`, `public_proxy?`,
16
+ `residential_proxy?`, and `tor_exit_node?` methods in
17
+ `MaxMind::GeoIP2::Record::Traits` have been deprecated. Please use the
18
+ corresponding methods in the `anonymizer` object from the GeoIP2 Insights
19
+ response instead.
20
+
21
+
3
22
  ## 1.3.0 (2025-05-06)
4
23
 
5
24
  * Support for the GeoIP Anonymous Plus database has been added. To do a
data/CLAUDE.md ADDED
@@ -0,0 +1,390 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ **GeoIP2-ruby** is MaxMind's official Ruby client library for:
8
+ - **GeoIP2/GeoLite2 Web Services**: Country, City, and Insights endpoints
9
+ - **GeoIP2/GeoLite2 Databases**: Local MMDB file reading for various database types (City, Country, ASN, Anonymous IP, Anonymous Plus, ISP, etc.)
10
+
11
+ The library provides both web service clients and database readers that return strongly-typed model objects containing geographic, ISP, anonymizer, and other IP-related data.
12
+
13
+ **Key Technologies:**
14
+ - Ruby 3.2+ (uses frozen string literals and modern Ruby features)
15
+ - MaxMind DB Reader for binary database files
16
+ - HTTP gem for web service client functionality
17
+ - Minitest for testing
18
+ - RuboCop with multiple plugins for code quality
19
+
20
+ ## Code Architecture
21
+
22
+ ### Package Structure
23
+
24
+ ```
25
+ lib/maxmind/geoip2/
26
+ ├── model/ # Response models (City, Insights, AnonymousIP, etc.)
27
+ ├── record/ # Data records (City, Location, Traits, etc.)
28
+ ├── client.rb # HTTP client for MaxMind web services
29
+ ├── reader.rb # Local MMDB file reader
30
+ ├── errors.rb # Custom exceptions for error handling
31
+ └── version.rb # Version constant
32
+ ```
33
+
34
+ ### Key Design Patterns
35
+
36
+ #### 1. **Attr Reader Pattern for Immutable Data**
37
+
38
+ Models expose data through `attr_reader` attributes that are initialized in the constructor. Unlike PHP's readonly properties, Ruby uses instance variables with attr_reader:
39
+
40
+ ```ruby
41
+ class City < Country
42
+ attr_reader :city
43
+ attr_reader :location
44
+ attr_reader :postal
45
+ attr_reader :subdivisions
46
+
47
+ def initialize(record, locales)
48
+ super
49
+ @city = MaxMind::GeoIP2::Record::City.new(record['city'], locales)
50
+ @location = MaxMind::GeoIP2::Record::Location.new(record['location'])
51
+ @postal = MaxMind::GeoIP2::Record::Postal.new(record['postal'])
52
+ @subdivisions = create_subdivisions(record['subdivisions'], locales)
53
+ end
54
+ end
55
+ ```
56
+
57
+ **Key Points:**
58
+ - Instance variables are set in the constructor
59
+ - Use `attr_reader` to expose them
60
+ - Models and records are initialized from hash data (from JSON/DB)
61
+ - Records are composed objects (City contains City record, Location record, etc.)
62
+
63
+ #### 2. **Inheritance Hierarchies**
64
+
65
+ Models follow clear inheritance patterns:
66
+ - `Country` → base model with country/continent data
67
+ - `City` extends `Country` → adds city, location, postal, subdivisions
68
+ - `Insights` extends `City` → adds additional web service fields (web service only)
69
+ - `Enterprise` extends `City` → adds enterprise-specific fields
70
+
71
+ Records have similar patterns:
72
+ - `Abstract` → base with `get` method for accessing hash data
73
+ - `Place` extends `Abstract` → adds names/locales handling
74
+ - Specific records (`City`, `Country`, etc.) extend `Place` or `Abstract`
75
+
76
+ #### 3. **Get Method Pattern for Data Access**
77
+
78
+ Both models and records use a protected `get` method to safely access hash data:
79
+
80
+ ```ruby
81
+ def get(key)
82
+ if @record.nil? || !@record.key?(key)
83
+ return false if key.start_with?('is_')
84
+ return nil
85
+ end
86
+
87
+ @record[key]
88
+ end
89
+ ```
90
+
91
+ - Returns `false` for missing boolean fields (starting with `is_`)
92
+ - Returns `nil` for missing optional fields
93
+ - Records store the raw hash in `@record` instance variable
94
+
95
+ Public methods expose data through the `get` method:
96
+
97
+ ```ruby
98
+ def anonymizer_confidence
99
+ get('anonymizer_confidence')
100
+ end
101
+
102
+ def provider_name
103
+ get('provider_name')
104
+ end
105
+ ```
106
+
107
+ #### 4. **Lazy Parsing for Special Types**
108
+
109
+ Some fields require parsing and are computed lazily:
110
+
111
+ ```ruby
112
+ def network_last_seen
113
+ return @network_last_seen if defined?(@network_last_seen)
114
+
115
+ date_string = get('network_last_seen')
116
+
117
+ if !date_string
118
+ return nil
119
+ end
120
+
121
+ @network_last_seen = Date.parse(date_string)
122
+ end
123
+ ```
124
+
125
+ - Use `defined?(@variable)` to check if already parsed
126
+ - Parse only once and cache in instance variable
127
+ - Handle nil cases before parsing
128
+
129
+ #### 5. **Web Service Only vs Database Models**
130
+
131
+ Some models are only used by web services and do **not** need MaxMind DB support:
132
+
133
+ **Web Service Only Models**:
134
+ - Models that are exclusive to web service responses
135
+ - Simpler implementation, just inherit and define in model hierarchy
136
+ - Example: `Insights` (extends City but used only for web service)
137
+
138
+ **Database-Supported Models**:
139
+ - Models used by both web services and database files
140
+ - Reader has specific methods (e.g., `anonymous_ip`, `anonymous_plus`, `city`)
141
+ - Must handle MaxMind DB format data structures
142
+ - Example: `City`, `Country`, `AnonymousIP`, `AnonymousPlus`
143
+
144
+ ## Testing Conventions
145
+
146
+ ### Running Tests
147
+
148
+ ```bash
149
+ # Install dependencies
150
+ bundle install
151
+
152
+ # Run all tests
153
+ bundle exec rake test
154
+
155
+ # Run tests and RuboCop
156
+ bundle exec rake # default task
157
+
158
+ # Run RuboCop only
159
+ bundle exec rake rubocop
160
+
161
+ # Run specific test file
162
+ ruby -Ilib:test test/test_reader.rb
163
+ ```
164
+
165
+ ### Test Structure
166
+
167
+ Tests are organized by functionality:
168
+ - `test/test_reader.rb` - Database reader tests
169
+ - `test/test_client.rb` - Web service client tests
170
+ - `test/test_model_*.rb` - Model-specific tests
171
+ - `test/data/` - Test fixtures and sample database files
172
+
173
+ ### Test Patterns
174
+
175
+ Tests use Minitest with a constant for test data:
176
+
177
+ ```ruby
178
+ class CountryModelTest < Minitest::Test
179
+ RAW = {
180
+ 'continent' => {
181
+ 'code' => 'NA',
182
+ 'geoname_id' => 42,
183
+ 'names' => { 'en' => 'North America' },
184
+ },
185
+ 'country' => {
186
+ 'geoname_id' => 1,
187
+ 'iso_code' => 'US',
188
+ 'names' => { 'en' => 'United States of America' },
189
+ },
190
+ 'traits' => {
191
+ 'ip_address' => '1.2.3.4',
192
+ 'prefix_length' => 24,
193
+ },
194
+ }.freeze
195
+
196
+ def test_values
197
+ model = MaxMind::GeoIP2::Model::Country.new(RAW, ['en'])
198
+
199
+ assert_equal(42, model.continent.geoname_id)
200
+ assert_equal('NA', model.continent.code)
201
+ assert_equal('United States of America', model.country.name)
202
+ end
203
+ end
204
+ ```
205
+
206
+ When adding new fields to models:
207
+ 1. Update the `RAW` constant to include the new field
208
+ 2. Add assertions to verify the field is properly populated
209
+ 3. Test both presence and absence of the field (nil handling)
210
+ 4. Test with different values if applicable
211
+
212
+ ## Working with This Codebase
213
+
214
+ ### Adding New Fields to Existing Models
215
+
216
+ For database models (like AnonymousPlus):
217
+
218
+ 1. **Add a public method** that calls `get`:
219
+ ```ruby
220
+ # A description of the field.
221
+ #
222
+ # @return [Type, nil]
223
+ def field_name
224
+ get('field_name')
225
+ end
226
+ ```
227
+
228
+ 2. **For fields requiring parsing** (dates, complex types), use lazy loading:
229
+ ```ruby
230
+ def network_last_seen
231
+ return @network_last_seen if defined?(@network_last_seen)
232
+
233
+ date_string = get('network_last_seen')
234
+
235
+ if !date_string
236
+ return nil
237
+ end
238
+
239
+ @network_last_seen = Date.parse(date_string)
240
+ end
241
+ ```
242
+
243
+ For composed models (like City, Country):
244
+
245
+ 1. **Add `attr_reader`** for the new record/field:
246
+ ```ruby
247
+ attr_reader :new_field
248
+ ```
249
+
250
+ 2. **Initialize in constructor**:
251
+ ```ruby
252
+ def initialize(record, locales)
253
+ super
254
+ @new_field = record['new_field']
255
+ end
256
+ ```
257
+
258
+ 3. **Provide comprehensive YARD documentation** (`@return` tags)
259
+ 4. **Update tests** to include the new field in test data and assertions
260
+ 5. **Update CHANGELOG.md** with the change
261
+
262
+ ### Adding New Models
263
+
264
+ When creating a new model class:
265
+
266
+ 1. **Determine if web service only or database-supported**
267
+ 2. **Follow the pattern** from existing similar models
268
+ 3. **Extend the appropriate base class** (e.g., `Country`, `City`, or standalone)
269
+ 4. **Use `attr_reader`** for composed record objects
270
+ 5. **Provide comprehensive YARD documentation** for all public methods
271
+ 6. **Add corresponding tests** with full coverage
272
+ 7. **If database-supported**, add a method to `Reader` class
273
+
274
+ ### Deprecation Guidelines
275
+
276
+ When deprecating fields:
277
+
278
+ 1. **Use `@deprecated` in YARD doc** with version and alternative:
279
+ ```ruby
280
+ # This field is deprecated as of version 2.0.0.
281
+ # Use the anonymizer object from the Insights response instead.
282
+ #
283
+ # @return [Boolean]
284
+ # @deprecated since 2.0.0
285
+ def is_anonymous
286
+ get('is_anonymous')
287
+ end
288
+ ```
289
+
290
+ 2. **Keep deprecated fields functional** - don't break existing code
291
+ 3. **Update CHANGELOG.md** with deprecation notices
292
+ 4. **Document alternatives** in the deprecation message
293
+
294
+ ### CHANGELOG.md Format
295
+
296
+ Always update `CHANGELOG.md` for user-facing changes.
297
+
298
+ **Important**: Do not add a date to changelog entries until release time.
299
+
300
+ - If there's an existing version entry without a date (e.g., `1.5.0`), add your changes there
301
+ - If creating a new version entry, don't include a date - it will be added at release time
302
+ - Use past tense for descriptions
303
+
304
+ ```markdown
305
+ ## 1.5.0
306
+
307
+ * A new `field_name` method has been added to `MaxMind::GeoIP2::Model::ModelName`.
308
+ This method provides information about...
309
+ * The `old_field` method in `MaxMind::GeoIP2::Model::ModelName` has been deprecated.
310
+ Please use `new_field` instead.
311
+ ```
312
+
313
+ ## Common Pitfalls and Solutions
314
+
315
+ ### Problem: Incorrect Nil Handling
316
+
317
+ Using the wrong nil check can cause unexpected behavior.
318
+
319
+ **Solution**: Follow these patterns:
320
+ - Use `if !variable` or `if variable.nil?` to check for nil
321
+ - The `get` method returns `nil` for missing keys (except `is_*` keys which return `false`)
322
+ - Use `defined?(@variable)` to check if an instance variable has been set (for lazy loading)
323
+
324
+ ### Problem: Missing YARD Documentation
325
+
326
+ New methods without documentation make the API harder to use.
327
+
328
+ **Solution**: Always add YARD documentation:
329
+ - Use `@return [Type, nil]` for the return type
330
+ - Add a description of what the method returns
331
+ - Use `@deprecated since X.Y.Z` for deprecated methods
332
+ - Include examples in model class documentation if helpful
333
+
334
+ ### Problem: Test Failures After Adding Fields
335
+
336
+ Tests fail because fixtures don't include new fields.
337
+
338
+ **Solution**: Update all related tests:
339
+ 1. Add field to test `RAW` constant or test data hash
340
+ 2. Add assertions for the new field
341
+ 3. Test nil case if field is optional
342
+ 4. Test different data types if applicable
343
+
344
+ ## Code Style Requirements
345
+
346
+ - **RuboCop enforced** with multiple plugins (minitest, performance, rake, thread_safety)
347
+ - **Frozen string literals** (`# frozen_string_literal: true`) in all files
348
+ - **Target Ruby 3.2+**
349
+ - **No metrics cops** - AbcSize, ClassLength, MethodLength disabled
350
+ - **Trailing commas allowed** in arrays, hashes, and arguments
351
+ - **Use `if !condition`** instead of `unless condition` (NegatedIf disabled)
352
+
353
+ Key RuboCop configurations:
354
+ - Line length not enforced
355
+ - Format string token checks disabled
356
+ - Numeric predicates allowed in any style
357
+ - Multiple assertions allowed in tests
358
+
359
+ ## Development Workflow
360
+
361
+ ### Setup
362
+ ```bash
363
+ bundle install
364
+ ```
365
+
366
+ ### Before Committing
367
+ ```bash
368
+ # Run tests and linting
369
+ bundle exec rake
370
+
371
+ # Or run separately
372
+ bundle exec rake test
373
+ bundle exec rake rubocop
374
+ ```
375
+
376
+ ### Running Single Test
377
+ ```bash
378
+ ruby -Ilib:test test/test_reader.rb
379
+ ```
380
+
381
+ ### Version Requirements
382
+ - **Ruby 3.2+** required
383
+ - Target compatibility should match current supported Ruby versions (3.2-3.4)
384
+
385
+ ## Additional Resources
386
+
387
+ - [API Documentation](https://www.rubydoc.info/gems/maxmind-geoip2)
388
+ - [GeoIP2 Web Services Docs](https://dev.maxmind.com/geoip/docs/web-services)
389
+ - [MaxMind DB Format](https://maxmind.github.io/MaxMind-DB/)
390
+ - GitHub Issues: https://github.com/maxmind/GeoIP2-ruby/issues
data/Gemfile.lock CHANGED
@@ -1,10 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- maxmind-geoip2 (1.3.0)
4
+ maxmind-geoip2 (1.4.0)
5
5
  connection_pool (~> 2.2)
6
6
  http (>= 4.3, < 6.0)
7
- maxmind-db (~> 1.2)
7
+ maxmind-db (~> 1.4)
8
8
 
9
9
  GEM
10
10
  remote: https://rubygems.org/
@@ -12,10 +12,9 @@ GEM
12
12
  addressable (2.8.7)
13
13
  public_suffix (>= 2.0.2, < 7.0)
14
14
  ast (2.4.3)
15
- base64 (0.2.0)
16
- bigdecimal (3.1.9)
17
- connection_pool (2.5.3)
18
- crack (1.0.0)
15
+ bigdecimal (3.3.1)
16
+ connection_pool (2.5.4)
17
+ crack (1.0.1)
19
18
  bigdecimal
20
19
  rexml
21
20
  domain_name (0.6.20240107)
@@ -33,36 +32,35 @@ GEM
33
32
  ffi-compiler (1.3.2)
34
33
  ffi (>= 1.15.5)
35
34
  rake
36
- hashdiff (1.1.2)
37
- http (5.2.0)
35
+ hashdiff (1.2.1)
36
+ http (5.3.1)
38
37
  addressable (~> 2.8)
39
- base64 (~> 0.1)
40
38
  http-cookie (~> 1.0)
41
39
  http-form_data (~> 2.2)
42
40
  llhttp-ffi (~> 0.5.0)
43
- http-cookie (1.0.8)
41
+ http-cookie (1.1.0)
44
42
  domain_name (~> 0.5)
45
43
  http-form_data (2.3.0)
46
- json (2.11.3)
47
- language_server-protocol (3.17.0.4)
44
+ json (2.16.0)
45
+ language_server-protocol (3.17.0.5)
48
46
  lint_roller (1.1.0)
49
47
  llhttp-ffi (0.5.1)
50
48
  ffi-compiler (~> 1.0)
51
49
  rake (~> 13.0)
52
- maxmind-db (1.3.2)
53
- minitest (5.25.5)
50
+ maxmind-db (1.4.0)
51
+ minitest (5.26.2)
54
52
  parallel (1.27.0)
55
- parser (3.3.8.0)
53
+ parser (3.3.10.0)
56
54
  ast (~> 2.4.1)
57
55
  racc
58
- prism (1.4.0)
56
+ prism (1.6.0)
59
57
  public_suffix (6.0.2)
60
58
  racc (1.8.1)
61
59
  rainbow (3.1.1)
62
- rake (13.2.1)
63
- regexp_parser (2.10.0)
64
- rexml (3.4.1)
65
- rubocop (1.75.5)
60
+ rake (13.3.1)
61
+ regexp_parser (2.11.3)
62
+ rexml (3.4.4)
63
+ rubocop (1.81.7)
66
64
  json (~> 2.3)
67
65
  language_server-protocol (~> 3.17.0.2)
68
66
  lint_roller (~> 1.1.0)
@@ -70,21 +68,32 @@ GEM
70
68
  parser (>= 3.3.0.2)
71
69
  rainbow (>= 2.2.2, < 4.0)
72
70
  regexp_parser (>= 2.9.3, < 3.0)
73
- rubocop-ast (>= 1.44.0, < 2.0)
71
+ rubocop-ast (>= 1.47.1, < 2.0)
74
72
  ruby-progressbar (~> 1.7)
75
73
  unicode-display_width (>= 2.4.0, < 4.0)
76
- rubocop-ast (1.44.1)
74
+ rubocop-ast (1.48.0)
77
75
  parser (>= 3.3.7.2)
78
76
  prism (~> 1.4)
79
- rubocop-performance (1.25.0)
77
+ rubocop-minitest (0.38.2)
80
78
  lint_roller (~> 1.1)
81
79
  rubocop (>= 1.75.0, < 2.0)
82
80
  rubocop-ast (>= 1.38.0, < 2.0)
81
+ rubocop-performance (1.26.1)
82
+ lint_roller (~> 1.1)
83
+ rubocop (>= 1.75.0, < 2.0)
84
+ rubocop-ast (>= 1.47.1, < 2.0)
85
+ rubocop-rake (0.7.1)
86
+ lint_roller (~> 1.1)
87
+ rubocop (>= 1.72.1)
88
+ rubocop-thread_safety (0.7.3)
89
+ lint_roller (~> 1.1)
90
+ rubocop (~> 1.72, >= 1.72.1)
91
+ rubocop-ast (>= 1.44.0, < 2.0)
83
92
  ruby-progressbar (1.13.0)
84
- unicode-display_width (3.1.4)
85
- unicode-emoji (~> 4.0, >= 4.0.4)
86
- unicode-emoji (4.0.4)
87
- webmock (3.25.1)
93
+ unicode-display_width (3.2.0)
94
+ unicode-emoji (~> 4.1)
95
+ unicode-emoji (4.1.0)
96
+ webmock (3.26.1)
88
97
  addressable (>= 2.8.0)
89
98
  crack (>= 0.3.2)
90
99
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -107,8 +116,11 @@ DEPENDENCIES
107
116
  minitest
108
117
  rake
109
118
  rubocop
119
+ rubocop-minitest
110
120
  rubocop-performance
121
+ rubocop-rake
122
+ rubocop-thread_safety
111
123
  webmock
112
124
 
113
125
  BUNDLED WITH
114
- 2.6.7
126
+ 2.6.9
data/README.md CHANGED
@@ -347,7 +347,7 @@ client API, please see [our support page](https://www.maxmind.com/en/support).
347
347
 
348
348
  ## Requirements
349
349
 
350
- This code requires Ruby version 3.0 or higher.
350
+ This code requires Ruby version 3.2 or higher.
351
351
 
352
352
  ## Contributing
353
353
 
data/Rakefile CHANGED
@@ -11,5 +11,4 @@ end
11
11
  RuboCop::RakeTask.new
12
12
 
13
13
  desc 'Run tests and RuboCop'
14
- task default: :test
15
- task default: :rubocop
14
+ task default: %i[test rubocop]
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'maxmind/geoip2/model/city'
4
+ require 'maxmind/geoip2/record/anonymizer'
4
5
 
5
6
  module MaxMind
6
7
  module GeoIP2
@@ -10,6 +11,19 @@ module MaxMind
10
11
  # See https://dev.maxmind.com/geoip/docs/web-services?lang=en for more
11
12
  # details.
12
13
  class Insights < City
14
+ # Data indicating whether the IP address is part of an anonymizing
15
+ # network.
16
+ #
17
+ # @return [MaxMind::GeoIP2::Record::Anonymizer]
18
+ attr_reader :anonymizer
19
+
20
+ # @!visibility private
21
+ def initialize(record, locales)
22
+ super
23
+ @anonymizer = MaxMind::GeoIP2::Record::Anonymizer.new(
24
+ record['anonymizer'],
25
+ )
26
+ end
13
27
  end
14
28
  end
15
29
  end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'maxmind/geoip2/record/abstract'
5
+
6
+ module MaxMind
7
+ module GeoIP2
8
+ module Record
9
+ # Contains data indicating whether an IP address is part of an
10
+ # anonymizing network.
11
+ #
12
+ # This record is returned by the Insights web service.
13
+ class Anonymizer < Abstract
14
+ # A score ranging from 1 to 99 that represents our percent confidence
15
+ # that the network is currently part of an actively used VPN service.
16
+ # This property is only available from Insights.
17
+ #
18
+ # @return [Integer, nil]
19
+ def confidence
20
+ get('confidence')
21
+ end
22
+
23
+ # This is true if the IP address belongs to any sort of anonymous
24
+ # network. This property is only available from Insights.
25
+ #
26
+ # @return [Boolean]
27
+ def anonymous?
28
+ get('is_anonymous')
29
+ end
30
+
31
+ # This is true if the IP address is registered to an anonymous VPN
32
+ # provider. If a VPN provider does not register subnets under names
33
+ # associated with them, we will likely only flag their IP ranges using
34
+ # the hosting_provider? property. This property is only available from
35
+ # Insights.
36
+ #
37
+ # @return [Boolean]
38
+ def anonymous_vpn?
39
+ get('is_anonymous_vpn')
40
+ end
41
+
42
+ # This is true if the IP address belongs to a hosting or VPN provider
43
+ # (see description of the anonymous_vpn? property). This property is
44
+ # only available from Insights.
45
+ #
46
+ # @return [Boolean]
47
+ def hosting_provider?
48
+ get('is_hosting_provider')
49
+ end
50
+
51
+ # This is true if the IP address belongs to a public proxy. This
52
+ # property is only available from Insights.
53
+ #
54
+ # @return [Boolean]
55
+ def public_proxy?
56
+ get('is_public_proxy')
57
+ end
58
+
59
+ # This is true if the IP address is on a suspected anonymizing network
60
+ # and belongs to a residential ISP. This property is only available from
61
+ # Insights.
62
+ #
63
+ # @return [Boolean]
64
+ def residential_proxy?
65
+ get('is_residential_proxy')
66
+ end
67
+
68
+ # This is true if the IP address is a Tor exit node. This property is
69
+ # only available from Insights.
70
+ #
71
+ # @return [Boolean]
72
+ def tor_exit_node?
73
+ get('is_tor_exit_node')
74
+ end
75
+
76
+ # The last day that the network was sighted in our analysis of
77
+ # anonymized networks. This value is parsed lazily. This property is
78
+ # only available from Insights.
79
+ #
80
+ # @return [Date, nil] A Date object representing the last seen date,
81
+ # or nil if the date is not available.
82
+ def network_last_seen
83
+ return @network_last_seen if defined?(@network_last_seen)
84
+
85
+ date_string = get('network_last_seen')
86
+
87
+ if !date_string
88
+ return nil
89
+ end
90
+
91
+ @network_last_seen = Date.parse(date_string)
92
+ end
93
+
94
+ # The name of the VPN provider (e.g., NordVPN, SurfShark, etc.)
95
+ # associated with the network. This property is only available from
96
+ # Insights.
97
+ #
98
+ # @return [String, nil]
99
+ def provider_name
100
+ get('provider_name')
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -76,7 +76,11 @@ module MaxMind
76
76
  # This is true if the IP address belongs to any sort of anonymous network.
77
77
  # This property is only available from Insights.
78
78
  #
79
+ # This method is deprecated as of version 1.4.0. Use the anonymizer object
80
+ # from the Insights response instead.
81
+ #
79
82
  # @return [Boolean]
83
+ # @deprecated since 1.4.0
80
84
  def anonymous?
81
85
  get('is_anonymous')
82
86
  end
@@ -86,7 +90,11 @@ module MaxMind
86
90
  # associated with them, we will likely only flag their IP ranges using the
87
91
  # hosting_provider? property. This property is only available from Insights.
88
92
  #
93
+ # This method is deprecated as of version 1.4.0. Use the anonymizer object
94
+ # from the Insights response instead.
95
+ #
89
96
  # @return [Boolean]
97
+ # @deprecated since 1.4.0
90
98
  def anonymous_vpn?
91
99
  get('is_anonymous_vpn')
92
100
  end
@@ -107,7 +115,11 @@ module MaxMind
107
115
  # description of the anonymous_vpn? property). This property is only
108
116
  # available from Insights.
109
117
  #
118
+ # This method is deprecated as of version 1.4.0. Use the anonymizer object
119
+ # from the Insights response instead.
120
+ #
110
121
  # @return [Boolean]
122
+ # @deprecated since 1.4.0
111
123
  def hosting_provider?
112
124
  get('is_hosting_provider')
113
125
  end
@@ -146,7 +158,11 @@ module MaxMind
146
158
  # This is true if the IP address belongs to a public proxy. This property
147
159
  # is only available from Insights.
148
160
  #
161
+ # This method is deprecated as of version 1.4.0. Use the anonymizer object
162
+ # from the Insights response instead.
163
+ #
149
164
  # @return [Boolean]
165
+ # @deprecated since 1.4.0
150
166
  def public_proxy?
151
167
  get('is_public_proxy')
152
168
  end
@@ -155,7 +171,11 @@ module MaxMind
155
171
  # and belongs to a residential ISP. This property is only available
156
172
  # from Insights.
157
173
  #
174
+ # This method is deprecated as of version 1.4.0. Use the anonymizer object
175
+ # from the Insights response instead.
176
+ #
158
177
  # @return [Boolean]
178
+ # @deprecated since 1.4.0
159
179
  def residential_proxy?
160
180
  get('is_residential_proxy')
161
181
  end
@@ -163,7 +183,11 @@ module MaxMind
163
183
  # This is true if the IP address is a Tor exit node. This property is only
164
184
  # available from Insights.
165
185
  #
186
+ # This method is deprecated as of version 1.4.0. Use the anonymizer object
187
+ # from the Insights response instead.
188
+ #
166
189
  # @return [Boolean]
190
+ # @deprecated since 1.4.0
167
191
  def tor_exit_node?
168
192
  get('is_tor_exit_node')
169
193
  end
@@ -212,6 +236,19 @@ module MaxMind
212
236
  get('user_count')
213
237
  end
214
238
 
239
+ # This field contains the risk associated with the IP address. The value
240
+ # ranges from 0.01 to 99. A higher score indicates a higher risk.
241
+ # Please note that the IP risk score provided in GeoIP products and
242
+ # services is more static than the IP risk score provided in minFraud
243
+ # and is not responsive to traffic on your network. If you need realtime
244
+ # IP risk scoring based on behavioral signals on your own network, please
245
+ # use minFraud. This property is only available from Insights.
246
+ #
247
+ # @return [Float, nil]
248
+ def ip_risk_snapshot
249
+ get('ip_risk_snapshot')
250
+ end
251
+
215
252
  # The user type associated with the IP address. This can be one of the
216
253
  # following values:
217
254
  #
@@ -3,6 +3,6 @@
3
3
  module MaxMind
4
4
  module GeoIP2
5
5
  # The Gem version.
6
- VERSION = '1.3.0'
6
+ VERSION = '1.4.0'
7
7
  end
8
8
  end
@@ -24,15 +24,18 @@ Gem::Specification.new do |s|
24
24
  'rubygems_mfa_required' => 'true',
25
25
  'source_code_uri' => 'https://github.com/maxmind/GeoIP2-ruby',
26
26
  }
27
- s.required_ruby_version = '>= 3.0'
27
+ s.required_ruby_version = '>= 3.2'
28
28
 
29
29
  s.add_dependency 'connection_pool', ['~> 2.2']
30
30
  s.add_dependency 'http', '>= 4.3', '< 6.0'
31
- s.add_dependency 'maxmind-db', ['~> 1.2']
31
+ s.add_dependency 'maxmind-db', ['~> 1.4']
32
32
 
33
33
  s.add_development_dependency 'minitest'
34
34
  s.add_development_dependency 'rake'
35
35
  s.add_development_dependency 'rubocop'
36
+ s.add_development_dependency 'rubocop-minitest'
36
37
  s.add_development_dependency 'rubocop-performance'
38
+ s.add_development_dependency 'rubocop-rake'
39
+ s.add_development_dependency 'rubocop-thread_safety'
37
40
  s.add_development_dependency 'webmock'
38
41
  end
data/test/test_client.rb CHANGED
@@ -28,6 +28,17 @@ class ClientTest < Minitest::Test
28
28
  }.freeze
29
29
 
30
30
  INSIGHTS = {
31
+ 'anonymizer' => {
32
+ 'confidence' => 85,
33
+ 'is_anonymous' => true,
34
+ 'is_anonymous_vpn' => true,
35
+ 'is_hosting_provider' => false,
36
+ 'is_public_proxy' => false,
37
+ 'is_residential_proxy' => true,
38
+ 'is_tor_exit_node' => false,
39
+ 'network_last_seen' => '2025-10-15',
40
+ 'provider_name' => 'NordVPN',
41
+ },
31
42
  'continent' => {
32
43
  'code' => 'NA',
33
44
  'geoname_id' => 42,
@@ -43,6 +54,7 @@ class ClientTest < Minitest::Test
43
54
  },
44
55
  'traits' => {
45
56
  'ip_address' => '1.2.3.40',
57
+ 'ip_risk_snapshot' => 45.5,
46
58
  'is_anycast' => true,
47
59
  'is_residential_proxy' => true,
48
60
  'network' => '1.2.3.0/24',
@@ -66,16 +78,16 @@ class ClientTest < Minitest::Test
66
78
  assert_equal('North America', record.continent.name)
67
79
 
68
80
  assert_equal(1, record.country.geoname_id)
69
- assert_equal(false, record.country.in_european_union?)
81
+ refute(record.country.in_european_union?)
70
82
  assert_equal('US', record.country.iso_code)
71
83
  assert_equal({ 'en' => 'United States of America' }, record.country.names)
72
84
  assert_equal('United States of America', record.country.name)
73
85
 
74
86
  assert_equal(11, record.maxmind.queries_remaining)
75
87
 
76
- assert_equal(false, record.registered_country.in_european_union?)
88
+ refute(record.registered_country.in_european_union?)
77
89
 
78
- assert_equal(true, record.traits.anycast?)
90
+ assert(record.traits.anycast?)
79
91
  assert_equal('1.2.3.0/24', record.traits.network)
80
92
  end
81
93
 
@@ -86,11 +98,24 @@ class ClientTest < Minitest::Test
86
98
 
87
99
  assert_equal(42, record.continent.geoname_id)
88
100
 
89
- assert_equal(true, record.traits.anycast?)
90
- assert_equal(true, record.traits.residential_proxy?)
101
+ # Test anonymizer object
102
+ assert_equal(85, record.anonymizer.confidence)
103
+ assert(record.anonymizer.anonymous?)
104
+ assert(record.anonymizer.anonymous_vpn?)
105
+ refute(record.anonymizer.hosting_provider?)
106
+ refute(record.anonymizer.public_proxy?)
107
+ assert(record.anonymizer.residential_proxy?)
108
+ refute(record.anonymizer.tor_exit_node?)
109
+ assert_equal(Date.parse('2025-10-15'), record.anonymizer.network_last_seen)
110
+ assert_equal('NordVPN', record.anonymizer.provider_name)
111
+
112
+ # Test traits
113
+ assert(record.traits.anycast?)
114
+ assert(record.traits.residential_proxy?)
91
115
  assert_equal('1.2.3.0/24', record.traits.network)
92
- assert_equal(1.3, record.traits.static_ip_score)
116
+ assert_in_delta(1.3, record.traits.static_ip_score)
93
117
  assert_equal(2, record.traits.user_count)
118
+ assert_in_delta(45.5, record.traits.ip_risk_snapshot)
94
119
  end
95
120
 
96
121
  def test_city
@@ -29,20 +29,18 @@ class CountryModelTest < Minitest::Test
29
29
 
30
30
  def test_objects
31
31
  model = MaxMind::GeoIP2::Model::Country.new(RAW, ['en'])
32
- assert_equal(MaxMind::GeoIP2::Model::Country, model.class)
33
- assert_equal(MaxMind::GeoIP2::Record::Continent, model.continent.class)
34
- assert_equal(MaxMind::GeoIP2::Record::Country, model.country.class)
35
- assert_equal(
36
- MaxMind::GeoIP2::Record::Country,
37
- model.registered_country.class,
32
+
33
+ assert_instance_of(MaxMind::GeoIP2::Model::Country, model)
34
+ assert_instance_of(MaxMind::GeoIP2::Record::Continent, model.continent)
35
+ assert_instance_of(MaxMind::GeoIP2::Record::Country, model.country)
36
+ assert_instance_of(
37
+ MaxMind::GeoIP2::Record::Country, model.registered_country,
38
38
  )
39
- assert_equal(
40
- MaxMind::GeoIP2::Record::RepresentedCountry,
41
- model.represented_country.class,
39
+ assert_instance_of(
40
+ MaxMind::GeoIP2::Record::RepresentedCountry, model.represented_country,
42
41
  )
43
- assert_equal(
44
- MaxMind::GeoIP2::Record::Traits,
45
- model.traits.class,
42
+ assert_instance_of(
43
+ MaxMind::GeoIP2::Record::Traits, model.traits,
46
44
  )
47
45
  end
48
46
 
@@ -55,14 +53,14 @@ class CountryModelTest < Minitest::Test
55
53
  assert_equal('North America', model.continent.name)
56
54
 
57
55
  assert_equal(1, model.country.geoname_id)
58
- assert_equal(false, model.country.in_european_union?)
56
+ refute(model.country.in_european_union?)
59
57
  assert_equal('US', model.country.iso_code)
60
58
  assert_equal({ 'en' => 'United States of America' }, model.country.names)
61
59
  assert_equal('United States of America', model.country.name)
62
60
  assert_nil(model.country.confidence)
63
61
 
64
62
  assert_equal(2, model.registered_country.geoname_id)
65
- assert_equal(true, model.registered_country.in_european_union?)
63
+ assert(model.registered_country.in_european_union?)
66
64
  assert_equal('DE', model.registered_country.iso_code)
67
65
  assert_equal({ 'en' => 'Germany' }, model.registered_country.names)
68
66
  assert_equal('Germany', model.registered_country.name)
@@ -91,6 +89,7 @@ class CountryModelTest < Minitest::Test
91
89
  },
92
90
  ['en'],
93
91
  )
92
+
94
93
  assert_equal(42, model.continent.geoname_id)
95
94
  end
96
95
  end
@@ -29,18 +29,21 @@ class ModelNameTest < Minitest::Test
29
29
 
30
30
  def test_fallback
31
31
  model = MaxMind::GeoIP2::Model::Country.new(RAW, %w[ru zh-CN en])
32
+
32
33
  assert_equal('北美洲', model.continent.name)
33
34
  assert_equal('объединяет государства', model.country.name)
34
35
  end
35
36
 
36
37
  def test_two_fallbacks
37
38
  model = MaxMind::GeoIP2::Model::Country.new(RAW, %w[ru jp])
39
+
38
40
  assert_nil(model.continent.name)
39
41
  assert_equal('объединяет государства', model.country.name)
40
42
  end
41
43
 
42
44
  def test_no_fallbacks
43
45
  model = MaxMind::GeoIP2::Model::Country.new(RAW, %w[jp])
46
+
44
47
  assert_nil(model.continent.name)
45
48
  assert_nil(model.country.name)
46
49
  end
data/test/test_reader.rb CHANGED
@@ -13,12 +13,12 @@ class ReaderTest < Minitest::Test
13
13
  ip = '1.2.0.1'
14
14
  record = reader.anonymous_ip(ip)
15
15
 
16
- assert_equal(true, record.anonymous?)
17
- assert_equal(true, record.anonymous_vpn?)
18
- assert_equal(false, record.hosting_provider?)
19
- assert_equal(false, record.public_proxy?)
20
- assert_equal(false, record.residential_proxy?)
21
- assert_equal(false, record.tor_exit_node?)
16
+ assert(record.anonymous?)
17
+ assert(record.anonymous_vpn?)
18
+ refute(record.hosting_provider?)
19
+ refute(record.public_proxy?)
20
+ refute(record.residential_proxy?)
21
+ refute(record.tor_exit_node?)
22
22
  assert_equal(ip, record.ip_address)
23
23
  assert_equal('1.2.0.0/16', record.network)
24
24
 
@@ -32,7 +32,7 @@ class ReaderTest < Minitest::Test
32
32
  ip = '81.2.69.1'
33
33
  record = reader.anonymous_ip(ip)
34
34
 
35
- assert_equal(true, record.residential_proxy?)
35
+ assert(record.residential_proxy?)
36
36
 
37
37
  reader.close
38
38
  end
@@ -45,14 +45,14 @@ class ReaderTest < Minitest::Test
45
45
  record = reader.anonymous_plus(ip)
46
46
 
47
47
  assert_equal(30, record.anonymizer_confidence)
48
- assert_equal(true, record.anonymous?)
49
- assert_equal(true, record.anonymous_vpn?)
50
- assert_equal(false, record.hosting_provider?)
48
+ assert(record.anonymous?)
49
+ assert(record.anonymous_vpn?)
50
+ refute(record.hosting_provider?)
51
51
  assert_equal(Date.new(2025, 4, 14), record.network_last_seen)
52
52
  assert_equal('foo', record.provider_name)
53
- assert_equal(false, record.public_proxy?)
54
- assert_equal(false, record.residential_proxy?)
55
- assert_equal(false, record.tor_exit_node?)
53
+ refute(record.public_proxy?)
54
+ refute(record.residential_proxy?)
55
+ refute(record.tor_exit_node?)
56
56
 
57
57
  assert_equal(ip, record.ip_address)
58
58
  assert_equal('1.2.0.1/32', record.network)
@@ -89,8 +89,8 @@ class ReaderTest < Minitest::Test
89
89
  assert_nil(record.city.confidence)
90
90
 
91
91
  assert_equal(100, record.location.accuracy_radius)
92
- assert_equal(51.75, record.location.latitude)
93
- assert_equal(-1.25, record.location.longitude)
92
+ assert_in_delta(51.75, record.location.latitude)
93
+ assert_in_delta(-1.25, record.location.longitude)
94
94
  assert_equal('Europe/London', record.location.time_zone)
95
95
 
96
96
  assert_equal(2, record.subdivisions.size)
@@ -117,14 +117,16 @@ class ReaderTest < Minitest::Test
117
117
  'zh-CN' => '华盛顿州',
118
118
  },
119
119
  record.subdivisions[0].names,
120
- assert_equal('WA', record.most_specific_subdivision.iso_code)
121
120
  )
122
121
 
122
+ assert_equal('WA', record.most_specific_subdivision.iso_code)
123
+
123
124
  # This IP has is_anycast.
124
125
 
125
126
  ip_address = '214.1.1.0'
126
127
  record = reader.city(ip_address)
127
- assert_equal(true, record.traits.anycast?)
128
+
129
+ assert(record.traits.anycast?)
128
130
 
129
131
  reader.close
130
132
  end
@@ -135,7 +137,7 @@ class ReaderTest < Minitest::Test
135
137
  )
136
138
  record = reader.city('2001:218::')
137
139
 
138
- assert_equal([], record.subdivisions)
140
+ assert_empty(record.subdivisions)
139
141
  assert_nil(record.most_specific_subdivision)
140
142
 
141
143
  reader.close
@@ -179,7 +181,7 @@ class ReaderTest < Minitest::Test
179
181
  assert_equal('Europe', record.continent.name)
180
182
 
181
183
  assert_equal(2_635_167, record.country.geoname_id)
182
- assert_equal(false, record.country.in_european_union?)
184
+ refute(record.country.in_european_union?)
183
185
  assert_equal('GB', record.country.iso_code)
184
186
  assert_equal(
185
187
  {
@@ -197,7 +199,7 @@ class ReaderTest < Minitest::Test
197
199
  assert_equal('United Kingdom', record.country.name)
198
200
 
199
201
  assert_equal(3_017_382, record.registered_country.geoname_id)
200
- assert_equal(true, record.registered_country.in_european_union?)
202
+ assert(record.registered_country.in_european_union?)
201
203
  assert_equal('FR', record.registered_country.iso_code)
202
204
  assert_equal(
203
205
  {
@@ -235,6 +237,7 @@ class ReaderTest < Minitest::Test
235
237
  assert_equal('military', record.represented_country.type)
236
238
 
237
239
  record = reader.country('81.2.69.163')
240
+
238
241
  assert_equal('81.2.69.163', record.traits.ip_address)
239
242
  assert_equal('81.2.69.160/27', record.traits.network)
240
243
 
@@ -242,7 +245,8 @@ class ReaderTest < Minitest::Test
242
245
 
243
246
  ip_address = '214.1.1.0'
244
247
  record = reader.country(ip_address)
245
- assert_equal(true, record.traits.anycast?)
248
+
249
+ assert(record.traits.anycast?)
246
250
 
247
251
  assert_raises(NoMethodError) { record.foo }
248
252
  reader.close
@@ -253,7 +257,8 @@ class ReaderTest < Minitest::Test
253
257
  'test/data/test-data/GeoIP2-Country-Test.mmdb',
254
258
  )
255
259
  record = reader.country('74.209.24.0')
256
- assert_equal(false, record.country.in_european_union?)
260
+
261
+ refute(record.country.in_european_union?)
257
262
 
258
263
  reader.close
259
264
  end
@@ -289,14 +294,14 @@ class ReaderTest < Minitest::Test
289
294
  assert_equal(11, record.city.confidence)
290
295
  assert_equal(99, record.country.confidence)
291
296
  assert_equal(6_252_001, record.country.geoname_id)
292
- assert_equal(false, record.country.in_european_union?)
297
+ refute(record.country.in_european_union?)
293
298
 
294
299
  assert_equal(27, record.location.accuracy_radius)
295
300
 
296
- assert_equal(false, record.registered_country.in_european_union?)
301
+ refute(record.registered_country.in_european_union?)
297
302
 
298
303
  assert_equal('Cable/DSL', record.traits.connection_type)
299
- assert_equal(true, record.traits.legitimate_proxy?)
304
+ assert(record.traits.legitimate_proxy?)
300
305
 
301
306
  assert_equal(ip_address, record.traits.ip_address)
302
307
  assert_equal('74.209.16.0/20', record.traits.network)
@@ -313,7 +318,8 @@ class ReaderTest < Minitest::Test
313
318
 
314
319
  ip_address = '214.1.1.0'
315
320
  record = reader.enterprise(ip_address)
316
- assert_equal(true, record.traits.anycast?)
321
+
322
+ assert(record.traits.anycast?)
317
323
 
318
324
  reader.close
319
325
  end
@@ -352,7 +358,7 @@ class ReaderTest < Minitest::Test
352
358
  assert_equal('2.125.160.216', record.traits.ip_address)
353
359
  assert_equal('2.125.160.216/29', record.traits.network)
354
360
  assert_nil(record.traits.autonomous_system_number)
355
- assert_equal(false, record.traits.anonymous?)
361
+ refute(record.traits.anonymous?)
356
362
 
357
363
  reader.close
358
364
  end
@@ -438,6 +444,7 @@ class ReaderTest < Minitest::Test
438
444
  "test/data/test-data/GeoIP2-#{t['file']}-Test.mmdb",
439
445
  )
440
446
  record = reader.send(t['method'], '81.2.69.160')
447
+
441
448
  assert_equal('United Kingdom', record.country.name)
442
449
  reader.close
443
450
  end
@@ -450,6 +457,7 @@ class ReaderTest < Minitest::Test
450
457
  %w[xx ru pt-BR es en],
451
458
  )
452
459
  record = reader.send(t['method'], '81.2.69.160')
460
+
453
461
  assert_equal('Великобритания', record.country.name)
454
462
  reader.close
455
463
  end
@@ -461,6 +469,7 @@ class ReaderTest < Minitest::Test
461
469
  "test/data/test-data/GeoIP2-#{t['file']}-Test.mmdb",
462
470
  )
463
471
  record = reader.send(t['method'], '81.2.69.163')
472
+
464
473
  assert_equal('81.2.69.163', record.traits.ip_address)
465
474
  assert_equal('81.2.69.160/27', record.traits.network)
466
475
  reader.close
@@ -473,8 +482,9 @@ class ReaderTest < Minitest::Test
473
482
  "test/data/test-data/GeoIP2-#{t['file']}-Test.mmdb",
474
483
  )
475
484
  record = reader.send(t['method'], '81.2.69.160')
476
- assert_equal(false, record.country.in_european_union?)
477
- assert_equal(false, record.registered_country.in_european_union?)
485
+
486
+ refute(record.country.in_european_union?)
487
+ refute(record.registered_country.in_european_union?)
478
488
  reader.close
479
489
  end
480
490
  end
@@ -524,6 +534,7 @@ class ReaderTest < Minitest::Test
524
534
  reader = MaxMind::GeoIP2::Reader.new(
525
535
  'test/data/test-data/GeoIP2-City-Test.mmdb',
526
536
  )
537
+
527
538
  assert_equal('GeoIP2-City', reader.metadata.database_type)
528
539
  reader.close
529
540
  end
@@ -533,6 +544,7 @@ class ReaderTest < Minitest::Test
533
544
  database: 'test/data/test-data/GeoIP2-Country-Test.mmdb',
534
545
  )
535
546
  record = reader.country('81.2.69.160')
547
+
536
548
  assert_equal('United Kingdom', record.country.name)
537
549
  reader.close
538
550
  end
@@ -544,6 +556,7 @@ class ReaderTest < Minitest::Test
544
556
  mode: MaxMind::DB::MODE_MEMORY,
545
557
  )
546
558
  record = reader.country('81.2.69.160')
559
+
547
560
  assert_equal('Великобритания', record.country.name)
548
561
  reader.close
549
562
  end
@@ -569,6 +582,7 @@ class ReaderTest < Minitest::Test
569
582
  mode: MaxMind::DB::MODE_MEMORY,
570
583
  )
571
584
  record = reader.country('81.2.69.160')
585
+
572
586
  assert_equal('Великобритания', record.country.name)
573
587
  reader.close
574
588
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: maxmind-geoip2
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - William Storey
@@ -49,14 +49,14 @@ dependencies:
49
49
  requirements:
50
50
  - - "~>"
51
51
  - !ruby/object:Gem::Version
52
- version: '1.2'
52
+ version: '1.4'
53
53
  type: :runtime
54
54
  prerelease: false
55
55
  version_requirements: !ruby/object:Gem::Requirement
56
56
  requirements:
57
57
  - - "~>"
58
58
  - !ruby/object:Gem::Version
59
- version: '1.2'
59
+ version: '1.4'
60
60
  - !ruby/object:Gem::Dependency
61
61
  name: minitest
62
62
  requirement: !ruby/object:Gem::Requirement
@@ -99,6 +99,20 @@ dependencies:
99
99
  - - ">="
100
100
  - !ruby/object:Gem::Version
101
101
  version: '0'
102
+ - !ruby/object:Gem::Dependency
103
+ name: rubocop-minitest
104
+ requirement: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ type: :development
110
+ prerelease: false
111
+ version_requirements: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
102
116
  - !ruby/object:Gem::Dependency
103
117
  name: rubocop-performance
104
118
  requirement: !ruby/object:Gem::Requirement
@@ -113,6 +127,34 @@ dependencies:
113
127
  - - ">="
114
128
  - !ruby/object:Gem::Version
115
129
  version: '0'
130
+ - !ruby/object:Gem::Dependency
131
+ name: rubocop-rake
132
+ requirement: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ type: :development
138
+ prerelease: false
139
+ version_requirements: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ - !ruby/object:Gem::Dependency
145
+ name: rubocop-thread_safety
146
+ requirement: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ type: :development
152
+ prerelease: false
153
+ version_requirements: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
116
158
  - !ruby/object:Gem::Dependency
117
159
  name: webmock
118
160
  requirement: !ruby/object:Gem::Requirement
@@ -135,6 +177,7 @@ extensions: []
135
177
  extra_rdoc_files: []
136
178
  files:
137
179
  - CHANGELOG.md
180
+ - CLAUDE.md
138
181
  - Gemfile
139
182
  - Gemfile.lock
140
183
  - LICENSE-APACHE
@@ -158,6 +201,7 @@ files:
158
201
  - lib/maxmind/geoip2/model/isp.rb
159
202
  - lib/maxmind/geoip2/reader.rb
160
203
  - lib/maxmind/geoip2/record/abstract.rb
204
+ - lib/maxmind/geoip2/record/anonymizer.rb
161
205
  - lib/maxmind/geoip2/record/city.rb
162
206
  - lib/maxmind/geoip2/record/continent.rb
163
207
  - lib/maxmind/geoip2/record/country.rb
@@ -270,14 +314,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
270
314
  requirements:
271
315
  - - ">="
272
316
  - !ruby/object:Gem::Version
273
- version: '3.0'
317
+ version: '3.2'
274
318
  required_rubygems_version: !ruby/object:Gem::Requirement
275
319
  requirements:
276
320
  - - ">="
277
321
  - !ruby/object:Gem::Version
278
322
  version: '0'
279
323
  requirements: []
280
- rubygems_version: 3.6.7
324
+ rubygems_version: 3.6.9
281
325
  specification_version: 4
282
326
  summary: A gem for interacting with the GeoIP2 webservices and databases.
283
327
  test_files: []