From 8d51c2e6a590fafaceab7c1277251c6e10cd3349 Mon Sep 17 00:00:00 2001
From: Olivier Gonzalez <gonzoyumo@gmail.com>
Date: Wed, 12 Oct 2011 15:58:45 +0200
Subject: [PATCH] add MaxMind support for ip geocoding

---
 README.rdoc                             | 16 +++++++
 lib/geocoder.rb                         |  4 +-
 lib/geocoder/configuration.rb           |  8 ++++
 lib/geocoder/lookups/maxmind.rb         | 38 +++++++++++++++++
 lib/geocoder/results/maxmind.rb         | 55 +++++++++++++++++++++++++
 test/fixtures/maxmind_74_200_247_59.txt |  1 +
 test/fixtures/maxmind_no_results.txt    |  1 +
 test/lookup_test.rb                     |  6 +++
 test/services_test.rb                   | 14 +++++++
 test/test_helper.rb                     | 13 ++++++
 10 files changed, 154 insertions(+), 2 deletions(-)
 create mode 100644 lib/geocoder/lookups/maxmind.rb
 create mode 100644 lib/geocoder/results/maxmind.rb
 create mode 100644 test/fixtures/maxmind_74_200_247_59.txt
 create mode 100644 test/fixtures/maxmind_no_results.txt

diff --git a/README.rdoc b/README.rdoc
index ce88545d..d4210ba3 100644
--- a/README.rdoc
+++ b/README.rdoc
@@ -235,6 +235,12 @@ By default Geocoder uses Google's geocoding API to fetch coordinates and street
   # to use an API key:
   Geocoder::Configuration.api_key = "..."
 
+  # IP geocoding service :
+  Geocoder::Configuration.ip_lookup = :maxmind
+
+  # to use an API key for IP geocoding service:
+  Geocoder::Configuration.ip_lookup_api_key = "..."
+
   # geocoding service request timeout, in seconds (default 3):
   Geocoder::Configuration.timeout = 5
 
@@ -328,6 +334,16 @@ Documentation:: http://github.com/fiorix/freegeoip/blob/master/README.rst
 Terms of Service:: ?
 Limitations:: ?
 
+==== MaxMind City
+
+API key:: required
+Quota:: Requests Packs can be purchased (starting at 50,000 for 20$)
+Region:: world
+SSL support:: no
+Languages:: English
+Documentation:: http://www.maxmind.com/app/web_services
+Terms of Service:: ?
+Limitations:: ?
 
 == Caching
 
diff --git a/lib/geocoder.rb b/lib/geocoder.rb
index 5bce2283..123cb619 100644
--- a/lib/geocoder.rb
+++ b/lib/geocoder.rb
@@ -63,7 +63,7 @@ module Geocoder
   # All IP address lookups, default first.
   #
   def ip_lookups
-    [:freegeoip]
+    [:freegeoip, :maxmind]
   end
 
 
@@ -81,7 +81,7 @@ module Geocoder
   #
   def lookup(query)
     if ip_address?(query)
-      get_lookup(ip_lookups.first)
+      get_lookup(Configuration.ip_lookup || ip_lookups.first)
     else
       get_lookup(Configuration.lookup || street_lookups.first)
     end
diff --git a/lib/geocoder/configuration.rb b/lib/geocoder/configuration.rb
index 44baabee..13d39e36 100644
--- a/lib/geocoder/configuration.rb
+++ b/lib/geocoder/configuration.rb
@@ -25,6 +25,14 @@ module Geocoder
         # for Google Premier use a 3-element array: [key, client, channel]
         [:api_key, nil],
 
+        # name of IP geocoding service (symbol)
+        # FIXME: temporary added for Maxmind support, need clean rewrite
+        [:ip_lookup, nil],
+
+        # API key for IP geocoding service
+        # FIXME: temporary added for Maxmind support, need clean rewrite
+        [:ip_lookup_api_key, nil],
+
         # cache object (must respond to #[], #[]=, and #keys)
         [:cache, nil],
 
diff --git a/lib/geocoder/lookups/maxmind.rb b/lib/geocoder/lookups/maxmind.rb
new file mode 100644
index 00000000..334463d5
--- /dev/null
+++ b/lib/geocoder/lookups/maxmind.rb
@@ -0,0 +1,38 @@
+require 'geocoder/lookups/base'
+require 'geocoder/results/maxmind'
+
+module Geocoder::Lookup
+  class Maxmind < Base
+
+    private # ---------------------------------------------------------------
+
+    def results(query, reverse = false)
+      # don't look up a loopback address, just return the stored result
+      return [reserved_result] if loopback_address?(query)
+      begin
+        doc = fetch_data(query, reverse)
+        if doc && doc.size == 10
+          return [doc]
+        else
+          warn "Maxmind error : #{doc[10]}" if doc
+          return []
+        end
+      rescue StandardError => err
+        raise_error(err)
+        return []
+      end
+    end
+
+    def parse_raw_data(raw_data)
+      raw_data.split(',') # Maxmind just returns text/plain
+    end
+
+    def reserved_result
+      ",,,,0,0,0,0,,"
+    end
+
+    def query_url(query, reverse = false)
+      "http://geoip3.maxmind.com/f?l=#{Geocoder::Configuration.ip_lookup_api_key}&i=#{query}"
+    end
+  end
+end
diff --git a/lib/geocoder/results/maxmind.rb b/lib/geocoder/results/maxmind.rb
new file mode 100644
index 00000000..8963ab5c
--- /dev/null
+++ b/lib/geocoder/results/maxmind.rb
@@ -0,0 +1,55 @@
+require 'geocoder/results/base'
+
+module Geocoder::Result
+  class Maxmind < Base
+
+    def address(format = :full)
+      s = state_code.to_s == "" ? "" : ", #{state_code}"
+      "#{city}#{s} #{postal_code}, #{country_code}".sub(/^[ ,]*/, "")
+    end
+
+    def country_code
+      @data[0]
+    end
+
+    def state_code
+      @data[1]
+    end
+
+    def city
+      @data[2]
+    end
+
+    def postal_code
+      @data[3]
+    end
+
+    def coordinates
+      [@data[4].to_f, @data[5].to_f]
+    end
+
+    def metrocode
+      @data[6]
+    end
+
+    def area_code
+      @data[7]
+    end
+
+    def isp
+      @data[8][1,@data[8].length-2]
+    end
+
+    def organization
+      @data[9][1,@data[9].length-2]
+    end
+
+    def country #not given by MaxMind
+      country_code
+    end
+
+    def state #not given by MaxMind
+      state_code
+    end
+  end
+end
diff --git a/test/fixtures/maxmind_74_200_247_59.txt b/test/fixtures/maxmind_74_200_247_59.txt
new file mode 100644
index 00000000..226e7219
--- /dev/null
+++ b/test/fixtures/maxmind_74_200_247_59.txt
@@ -0,0 +1 @@
+US,TX,Plano,75093,33.034698,-96.813400,623,972,"Layered Technologies","Layered Technologies"
\ No newline at end of file
diff --git a/test/fixtures/maxmind_no_results.txt b/test/fixtures/maxmind_no_results.txt
new file mode 100644
index 00000000..c1d1bc2d
--- /dev/null
+++ b/test/fixtures/maxmind_no_results.txt
@@ -0,0 +1 @@
+,,,,,,,,,,IP_NOT_FOUND
\ No newline at end of file
diff --git a/test/lookup_test.rb b/test/lookup_test.rb
index 3c72d05b..75745666 100644
--- a/test/lookup_test.rb
+++ b/test/lookup_test.rb
@@ -27,4 +27,10 @@ class LookupTest < Test::Unit::TestCase
     g = Geocoder::Lookup::Yahoo.new
     assert_match "appid=MY_KEY", g.send(:query_url, "Madison Square Garden, New York, NY  10001, United States")
   end
+
+  def test_maxmind_api_key
+    Geocoder::Configuration.ip_lookup_api_key = "MY_KEY"
+    g = Geocoder::Lookup::Maxmind.new
+    assert_match "l=MY_KEY", g.send(:query_url, "74.200.247.59")
+  end
 end
diff --git a/test/services_test.rb b/test/services_test.rb
index b01b84ab..7c421e81 100644
--- a/test/services_test.rb
+++ b/test/services_test.rb
@@ -94,6 +94,20 @@ class ServicesTest < Test::Unit::TestCase
     assert_equal "Plano, TX 75093, United States", result.address
   end
 
+  # --- MaxMind ---
+
+  def test_maxmind_result_on_ip_address_search
+    Geocoder::Configuration.ip_lookup = :maxmind
+    result = Geocoder.search("74.200.247.59").first
+    assert result.is_a?(Geocoder::Result::Maxmind)
+  end
+
+  def test_maxmind_result_components
+    Geocoder::Configuration.ip_lookup = :maxmind
+    result = Geocoder.search("74.200.247.59").first
+    assert_equal "Plano, TX 75093, US", result.address
+  end
+
 
   # --- Bing ---
 
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 137f756f..e0567689 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -133,6 +133,19 @@ module Geocoder
       end
     end
 
+    class Maxmind < Base
+      private #-----------------------------------------------------------------
+      def fetch_raw_data(query, reverse = false)
+        raise TimeoutError if query == "timeout"
+        raise SocketError if query == "socket_error"
+        file = case query
+          when "no results";  :no_results
+          else                "74_200_247_59"
+        end
+        read_fixture "maxmind_#{file}.txt"
+      end
+    end
+
     class Bing < Base
       private #-----------------------------------------------------------------
       def fetch_raw_data(query, reverse = false)
-- 
GitLab