This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
GeoIP2-ruby is MaxMind's official Ruby client library for:
- GeoIP/GeoLite Web Services: Country, City Plus, and Insights endpoints
- GeoIP/GeoLite Databases: Local MMDB file reading for various database types (City, Country, ASN, Anonymous IP, Anonymous Plus, ISP, etc.)
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.
Key Technologies:
- Ruby 3.2+ (uses frozen string literals and modern Ruby features)
- MaxMind DB Reader for binary database files
- HTTP gem for web service client functionality
- Minitest for testing
- RuboCop with multiple plugins for code quality
lib/maxmind/geoip2/
├── model/ # Response models (City, Insights, AnonymousIP, etc.)
├── record/ # Data records (City, Location, Traits, etc.)
├── client.rb # HTTP client for MaxMind web services
├── reader.rb # Local MMDB file reader
├── errors.rb # Custom exceptions for error handling
└── version.rb # Version constant
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:
class City < Country
attr_reader :city
attr_reader :location
attr_reader :postal
attr_reader :subdivisions
def initialize(record, locales)
super
@city = MaxMind::GeoIP2::Record::City.new(record['city'], locales)
@location = MaxMind::GeoIP2::Record::Location.new(record['location'])
@postal = MaxMind::GeoIP2::Record::Postal.new(record['postal'])
@subdivisions = create_subdivisions(record['subdivisions'], locales)
end
endKey Points:
- Instance variables are set in the constructor
- Use
attr_readerto expose them - Models and records are initialized from hash data (from JSON/DB)
- Records are composed objects (City contains City record, Location record, etc.)
Models follow clear inheritance patterns:
Country→ base model with country/continent dataCityextendsCountry→ adds city, location, postal, subdivisionsInsightsextendsCity→ adds additional web service fields (web service only)EnterpriseextendsCity→ adds enterprise-specific fields
Records have similar patterns:
Abstract→ base withgetmethod for accessing hash dataPlaceextendsAbstract→ adds names/locales handling- Specific records (
City,Country, etc.) extendPlaceorAbstract
Both models and records use a protected get method to safely access hash data:
def get(key)
if @record.nil? || !@record.key?(key)
return false if key.start_with?('is_')
return nil
end
@record[key]
end- Returns
falsefor missing boolean fields (starting withis_) - Returns
nilfor missing optional fields - Records store the raw hash in
@recordinstance variable
Public methods expose data through the get method:
def anonymizer_confidence
get('anonymizer_confidence')
end
def provider_name
get('provider_name')
endSome fields require parsing and are computed lazily:
def network_last_seen
return @network_last_seen if defined?(@network_last_seen)
date_string = get('network_last_seen')
if !date_string
@network_last_seen = nil
return nil
end
@network_last_seen = Date.parse(date_string)
end- Use
defined?(@variable)to check if already parsed - Parse only once and cache in instance variable
- Handle nil cases before parsing
- Memoize nil results too, so repeated calls do not re-run the lookup
Some models are only used by web services and do not need MaxMind DB support:
Web Service Only Models:
- Models that are exclusive to web service responses
- Simpler implementation, just inherit and define in model hierarchy
- Example:
Insights(extends City but used only for web service)
Database-Supported Models:
- Models used by both web services and database files
- Reader has specific methods (e.g.,
anonymous_ip,anonymous_plus,city) - Must handle MaxMind DB format data structures
- Example:
City,Country,AnonymousIP,AnonymousPlus
# Install dependencies
bundle install
# Run all tests
bundle exec rake test
# Run tests and RuboCop
bundle exec rake # default task
# Run RuboCop only
bundle exec rake rubocop
# Run specific test file
ruby -Ilib:test test/test_reader.rbTests are organized by functionality:
test/test_reader.rb- Database reader teststest/test_client.rb- Web service client teststest/test_model_*.rb- Model-specific teststest/data/- Test fixtures and sample database files
Tests use Minitest with a constant for test data:
class CountryModelTest < Minitest::Test
RAW = {
'continent' => {
'code' => 'NA',
'geoname_id' => 42,
'names' => { 'en' => 'North America' },
},
'country' => {
'geoname_id' => 1,
'iso_code' => 'US',
'names' => { 'en' => 'United States of America' },
},
'traits' => {
'ip_address' => '1.2.3.4',
'prefix_length' => 24,
},
}.freeze
def test_values
model = MaxMind::GeoIP2::Model::Country.new(RAW, ['en'])
assert_equal(42, model.continent.geoname_id)
assert_equal('NA', model.continent.code)
assert_equal('United States of America', model.country.name)
end
endWhen adding new fields to models:
- Update the
RAWconstant to include the new field - Add assertions to verify the field is properly populated
- Test both presence and absence of the field (nil handling)
- Test with different values if applicable
For database models (like AnonymousPlus):
-
Add a public method that calls
get:# A description of the field. # # @return [Type, nil] def field_name get('field_name') end
-
For fields requiring parsing (dates, complex types), use lazy loading:
def network_last_seen return @network_last_seen if defined?(@network_last_seen) date_string = get('network_last_seen') if !date_string @network_last_seen = nil return nil end @network_last_seen = Date.parse(date_string) end
For composed models (like City, Country):
-
Add
attr_readerfor the new record/field:attr_reader :new_field
-
Initialize in constructor:
def initialize(record, locales) super @new_field = record['new_field'] end
-
Provide comprehensive YARD documentation (
@returntags) -
Update tests to include the new field in test data and assertions
-
Update CHANGELOG.md with the change
When creating a new model class:
- Determine if web service only or database-supported
- Follow the pattern from existing similar models
- Extend the appropriate base class (e.g.,
Country,City, or standalone) - Use
attr_readerfor composed record objects - Provide comprehensive YARD documentation for all public methods
- Add corresponding tests with full coverage
- If database-supported, add a method to
Readerclass
When deprecating fields:
-
Use
@deprecatedin YARD doc with version and alternative:# This field is deprecated as of version 2.0.0. # Use the anonymizer object from the Insights response instead. # # @return [Boolean] # @deprecated since 2.0.0 def is_anonymous get('is_anonymous') end
-
Keep deprecated fields functional - don't break existing code
-
Update CHANGELOG.md with deprecation notices
-
Document alternatives in the deprecation message
Always update CHANGELOG.md for user-facing changes.
Important: Do not add a date to changelog entries until release time.
- If there's an existing version entry without a date (e.g.,
1.5.0), add your changes there - If creating a new version entry, don't include a date - it will be added at release time
- Use past tense for descriptions
## 1.5.0
* A new `field_name` method has been added to `MaxMind::GeoIP2::Model::ModelName`.
This method provides information about...
* The `old_field` method in `MaxMind::GeoIP2::Model::ModelName` has been deprecated.
Please use `new_field` instead.Using the wrong nil check can cause unexpected behavior.
Solution: Follow these patterns:
- Use
if !variableorif variable.nil?to check for nil - The
getmethod returnsnilfor missing keys (exceptis_*keys which returnfalse) - Use
defined?(@variable)to check if an instance variable has been set (for lazy loading)
New methods without documentation make the API harder to use.
Solution: Always add YARD documentation:
- Use
@return [Type, nil]for the return type - Add a description of what the method returns
- Use
@deprecated since X.Y.Zfor deprecated methods - Include examples in model class documentation if helpful
Tests fail because fixtures don't include new fields.
Solution: Update all related tests:
- Add field to test
RAWconstant or test data hash - Add assertions for the new field
- Test nil case if field is optional
- Test different data types if applicable
- RuboCop enforced with multiple plugins (minitest, performance, rake, thread_safety)
- Frozen string literals (
# frozen_string_literal: true) in all files - Target Ruby 3.2+
- No metrics cops - AbcSize, ClassLength, MethodLength disabled
- Trailing commas allowed in arrays, hashes, and arguments
- Use
if !conditioninstead ofunless condition(NegatedIf disabled)
Key RuboCop configurations:
- Line length not enforced
- Format string token checks disabled
- Numeric predicates allowed in any style
- Multiple assertions allowed in tests
bundle install# Run tests and linting
bundle exec rake
# Or run separately
bundle exec rake test
bundle exec rake rubocopruby -Ilib:test test/test_reader.rb- Ruby 3.2+ required
- Target compatibility should match current supported Ruby versions (3.2-3.4)