diff --git a/README.md b/README.md index c35c2f39275439a757cea636f33fabf5148d6e9f..26c76d5f158296fcda48ad572cf1953183dd197a 100644 --- a/README.md +++ b/README.md @@ -394,6 +394,17 @@ Yahoo BOSS is **not a free service**. As of November 17, 2012 Yahoo no longer of * **Terms of Service**: ? * **Limitations**: ? +#### MaxMind Web Services (`:maxmind`) + +* **API key**: required +* **Quota**: Request Packs can be purchased +* **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/exceptions.rb b/lib/geocoder/exceptions.rb index 0ed0eb0d3954d0d2b6457655aff9f66a0c1b3250..7445c662dc7aa548cc065c69452fa8f3573e0bd9 100644 --- a/lib/geocoder/exceptions.rb +++ b/lib/geocoder/exceptions.rb @@ -15,4 +15,7 @@ module Geocoder class InvalidRequest < Error end + class InvalidApiKey < Error + end + end diff --git a/lib/geocoder/lookup.rb b/lib/geocoder/lookup.rb index 4e58888d09ab9f46a4e5287883a4a23075f92e4e..472a164a01193e823eac1cea726dfae4f685f074 100644 --- a/lib/geocoder/lookup.rb +++ b/lib/geocoder/lookup.rb @@ -37,7 +37,7 @@ module Geocoder # All IP address lookup services, default first. # def ip_services - [:freegeoip] + [:freegeoip, :maxmind] end ## diff --git a/lib/geocoder/lookups/maxmind.rb b/lib/geocoder/lookups/maxmind.rb new file mode 100644 index 0000000000000000000000000000000000000000..9ca10504ad317d9b48d48987687a09be7168cea8 --- /dev/null +++ b/lib/geocoder/lookups/maxmind.rb @@ -0,0 +1,50 @@ +require 'geocoder/lookups/base' +require 'geocoder/results/maxmind' +require 'csv' + +module Geocoder::Lookup + class Maxmind < Base + + def name + "MaxMind" + end + + private # --------------------------------------------------------------- + + def results(query) + # don't look up a loopback address, just return the stored result + return [reserved_result] if query.loopback_ip_address? + doc = fetch_data(query) + if doc and doc.is_a?(Array) + if doc.size == 10 + return [doc] + elsif doc.size > 10 and doc[10] == "INVALID_LICENSE_KEY" + raise_error(Geocoder::InvalidApiKey) || + warn("Invalid MaxMind API key.") + end + end + return [] + end + + def parse_raw_data(raw_data) + # Maxmind just returns text/plain as csv format but according to documentation, + # we get ISO-8859-1 encoded string. We need to convert it. + CSV.parse_line raw_data.force_encoding("ISO-8859-1").encode("UTF-8") + end + + def reserved_result + ",,,,0,0,0,0,," + end + + def query_url_params(query) + super.merge( + :l => configuration.api_key, + :i => query.sanitized_text + ) + end + + def query_url(query) + "#{protocol}://geoip3.maxmind.com/f?" + url_query_string(query) + end + end +end diff --git a/lib/geocoder/lookups/yahoo.rb b/lib/geocoder/lookups/yahoo.rb index 43138b62d3ae06879126dd11fd5442871b387f60..77a658b4f3e0a05961eecce51d5e244cdf88036a 100644 --- a/lib/geocoder/lookups/yahoo.rb +++ b/lib/geocoder/lookups/yahoo.rb @@ -34,6 +34,23 @@ module Geocoder::Lookup end end + ## + # Yahoo returns errors as XML even when JSON format is specified. + # Handle that here, without parsing the XML + # (which would add unnecessary complexity). + # + def parse_raw_data(raw_data) + if raw_data.match /^<\?xml/ + if raw_data.include?("Rate Limit Exceeded") + raise_error(Geocoder::OverQueryLimitError) || warn("Over API query limit.") + elsif raw_data.include?("Please provide valid credentials") + raise_error(Geocoder::InvalidApiKey) || warn("Invalid API key.") + end + else + super(raw_data) + end + end + def query_url_params(query) super.merge( :location => query.sanitized_text, diff --git a/lib/geocoder/lookups/yandex.rb b/lib/geocoder/lookups/yandex.rb index 4b306e1ef68d0eb596283c43083a60aec5ffda74..272ecea9725e3da552111841dbe97971b40bfba7 100644 --- a/lib/geocoder/lookups/yandex.rb +++ b/lib/geocoder/lookups/yandex.rb @@ -21,7 +21,11 @@ module Geocoder::Lookup def results(query) return [] unless doc = fetch_data(query) if err = doc['error'] - warn "Yandex Geocoding API error: #{err['status']} (#{err['message']})." + if err["status"] == 401 and err["message"] == "invalid key" + raise_error(Geocoder::InvalidApiKey) || warn("Invalid API key.") + else + warn "Yandex Geocoding API error: #{err['status']} (#{err['message']})." + end return [] end if doc = doc['response']['GeoObjectCollection'] diff --git a/lib/geocoder/results/maxmind.rb b/lib/geocoder/results/maxmind.rb new file mode 100644 index 0000000000000000000000000000000000000000..8963ab5c729ab4d0a13ed24545e8a7211c5570c0 --- /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/bing_madison_square_garden.json b/test/fixtures/bing_madison_square_garden similarity index 100% rename from test/fixtures/bing_madison_square_garden.json rename to test/fixtures/bing_madison_square_garden diff --git a/test/fixtures/bing_no_results.json b/test/fixtures/bing_no_results similarity index 100% rename from test/fixtures/bing_no_results.json rename to test/fixtures/bing_no_results diff --git a/test/fixtures/bing_reverse.json b/test/fixtures/bing_reverse similarity index 100% rename from test/fixtures/bing_reverse.json rename to test/fixtures/bing_reverse diff --git a/test/fixtures/freegeoip_74_200_247_59.json b/test/fixtures/freegeoip_74_200_247_59 similarity index 100% rename from test/fixtures/freegeoip_74_200_247_59.json rename to test/fixtures/freegeoip_74_200_247_59 diff --git a/test/fixtures/freegeoip_no_results.json b/test/fixtures/freegeoip_no_results similarity index 100% rename from test/fixtures/freegeoip_no_results.json rename to test/fixtures/freegeoip_no_results diff --git a/test/fixtures/geocoder_ca_madison_square_garden.json b/test/fixtures/geocoder_ca_madison_square_garden similarity index 100% rename from test/fixtures/geocoder_ca_madison_square_garden.json rename to test/fixtures/geocoder_ca_madison_square_garden diff --git a/test/fixtures/geocoder_ca_no_results.json b/test/fixtures/geocoder_ca_no_results similarity index 100% rename from test/fixtures/geocoder_ca_no_results.json rename to test/fixtures/geocoder_ca_no_results diff --git a/test/fixtures/geocoder_ca_reverse.json b/test/fixtures/geocoder_ca_reverse similarity index 100% rename from test/fixtures/geocoder_ca_reverse.json rename to test/fixtures/geocoder_ca_reverse diff --git a/test/fixtures/google_garbage.json b/test/fixtures/google_garbage similarity index 100% rename from test/fixtures/google_garbage.json rename to test/fixtures/google_garbage diff --git a/test/fixtures/google_madison_square_garden.json b/test/fixtures/google_madison_square_garden similarity index 100% rename from test/fixtures/google_madison_square_garden.json rename to test/fixtures/google_madison_square_garden diff --git a/test/fixtures/google_no_city_data.json b/test/fixtures/google_no_city_data similarity index 100% rename from test/fixtures/google_no_city_data.json rename to test/fixtures/google_no_city_data diff --git a/test/fixtures/google_no_locality.json b/test/fixtures/google_no_locality similarity index 100% rename from test/fixtures/google_no_locality.json rename to test/fixtures/google_no_locality diff --git a/test/fixtures/google_no_results.json b/test/fixtures/google_no_results similarity index 100% rename from test/fixtures/google_no_results.json rename to test/fixtures/google_no_results diff --git a/test/fixtures/mapquest_madison_square_garden.json b/test/fixtures/mapquest_madison_square_garden similarity index 100% rename from test/fixtures/mapquest_madison_square_garden.json rename to test/fixtures/mapquest_madison_square_garden diff --git a/test/fixtures/mapquest_no_results.json b/test/fixtures/mapquest_no_results similarity index 100% rename from test/fixtures/mapquest_no_results.json rename to test/fixtures/mapquest_no_results diff --git a/test/fixtures/maxmind_74_200_247_59 b/test/fixtures/maxmind_74_200_247_59 new file mode 100644 index 0000000000000000000000000000000000000000..22fce3390f59c095a1c844430478794ceac813d8 --- /dev/null +++ b/test/fixtures/maxmind_74_200_247_59 @@ -0,0 +1 @@ +US,TX,Plano,75093,33.034698,-96.813400,623,972,"Layered Technologies , US","Layered Technologies , US" \ No newline at end of file diff --git a/test/fixtures/maxmind_invalid_key b/test/fixtures/maxmind_invalid_key new file mode 100644 index 0000000000000000000000000000000000000000..e216586596bffa02427197ab20653bc8f5d04529 --- /dev/null +++ b/test/fixtures/maxmind_invalid_key @@ -0,0 +1 @@ +,,,,,,,,,,INVALID_LICENSE_KEY diff --git a/test/fixtures/maxmind_no_results b/test/fixtures/maxmind_no_results new file mode 100644 index 0000000000000000000000000000000000000000..c1d1bc2d3195d6084a0e152a2a2dbf193382bc04 --- /dev/null +++ b/test/fixtures/maxmind_no_results @@ -0,0 +1 @@ +,,,,,,,,,,IP_NOT_FOUND \ No newline at end of file diff --git a/test/fixtures/nominatim_madison_square_garden.json b/test/fixtures/nominatim_madison_square_garden similarity index 100% rename from test/fixtures/nominatim_madison_square_garden.json rename to test/fixtures/nominatim_madison_square_garden diff --git a/test/fixtures/nominatim_no_results.json b/test/fixtures/nominatim_no_results similarity index 100% rename from test/fixtures/nominatim_no_results.json rename to test/fixtures/nominatim_no_results diff --git a/test/fixtures/yahoo_error.json b/test/fixtures/yahoo_error similarity index 100% rename from test/fixtures/yahoo_error.json rename to test/fixtures/yahoo_error diff --git a/test/fixtures/yahoo_invalid_key b/test/fixtures/yahoo_invalid_key new file mode 100644 index 0000000000000000000000000000000000000000..c9b7319b05361287a7829d884d051fb397f36e4d --- /dev/null +++ b/test/fixtures/yahoo_invalid_key @@ -0,0 +1,2 @@ +<?xml version='1.0' encoding='UTF-8'?>\n<yahoo:error xmlns:yahoo='http://yahooapis.com/v1/base.rng'\n xml:lang='en-US'> + <yahoo:description>Please provide valid credentials. OAuth oauth_problem="consumer_key_unknown", realm="yahooapis.com"</yahoo:description>\n</yahoo:error> diff --git a/test/fixtures/yahoo_madison_square_garden.json b/test/fixtures/yahoo_madison_square_garden similarity index 100% rename from test/fixtures/yahoo_madison_square_garden.json rename to test/fixtures/yahoo_madison_square_garden diff --git a/test/fixtures/yahoo_no_results.json b/test/fixtures/yahoo_no_results similarity index 100% rename from test/fixtures/yahoo_no_results.json rename to test/fixtures/yahoo_no_results diff --git a/test/fixtures/yahoo_over_limit b/test/fixtures/yahoo_over_limit new file mode 100644 index 0000000000000000000000000000000000000000..7dddadf5f0d5e75bae7fd245d34dac41012e23aa --- /dev/null +++ b/test/fixtures/yahoo_over_limit @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> <yahoo:error xmlns:yahoo="http://yahooapis.com/v1/base.rng\ <http://yahooapis.com/v1/base.rng%5C>" xml:lang="en-US"> + <yahoo:description>Rate Limit Exceeded</yahoo:description> <yahoo:detail>Key has exceeded its configured rate limit.</yahoo:detail> </yahoo:error> diff --git a/test/fixtures/yandex_invalid_key.json b/test/fixtures/yandex_invalid_key similarity index 100% rename from test/fixtures/yandex_invalid_key.json rename to test/fixtures/yandex_invalid_key diff --git a/test/fixtures/yandex_kremlin.json b/test/fixtures/yandex_kremlin similarity index 100% rename from test/fixtures/yandex_kremlin.json rename to test/fixtures/yandex_kremlin diff --git a/test/fixtures/yandex_no_results.json b/test/fixtures/yandex_no_results similarity index 100% rename from test/fixtures/yandex_no_results.json rename to test/fixtures/yandex_no_results diff --git a/test/lookup_test.rb b/test/lookup_test.rb index d4b9fd34ea8b109f1b9754e36c621c2f93fc46b8..6e77e4ba7d0c2b878f129e6748aab46270a2bb5c 100644 --- a/test/lookup_test.rb +++ b/test/lookup_test.rb @@ -3,6 +3,14 @@ require 'test_helper' class LookupTest < Test::Unit::TestCase + def test_responds_to_name_method + Geocoder::Lookup.all_services.each do |l| + lookup = Geocoder::Lookup.get(l) + assert lookup.respond_to?(:name), + "Lookup #{l} does not respond to #name method." + end + end + def test_search_returns_empty_array_when_no_results Geocoder::Lookup.all_services_except_test.each do |l| lookup = Geocoder::Lookup.get(l) diff --git a/test/services_test.rb b/test/services_test.rb index 20a4f403f8e7ac4c1d2cf968b357855d420465b4..9c232a8593f3155658a77f3aa70bf023a43c315e 100644 --- a/test/services_test.rb +++ b/test/services_test.rb @@ -105,10 +105,25 @@ class ServicesTest < Test::Unit::TestCase assert_equal "Madison Square Garden, New York, NY 10001, United States", result.address end + def test_yahoo_raises_exception_when_over_query_limit + Geocoder.configure(:always_raise => [Geocoder::OverQueryLimitError]) + l = Geocoder::Lookup.get(:yahoo) + assert_raises Geocoder::OverQueryLimitError do + l.send(:results, Geocoder::Query.new("over limit")) + end + end + + def test_yahoo_raises_exception_on_invalid_key + Geocoder.configure(:always_raise => [Geocoder::InvalidApiKey]) + l = Geocoder::Lookup.get(:yahoo) + assert_raises Geocoder::InvalidApiKey do + l.send(:results, Geocoder::Query.new("invalid key")) + end + end # --- Yandex --- - def test_yandex_with_invalid_key + def test_yandex_warns_about_invalid_key # keep test output clean: suppress timeout warning orig = $VERBOSE; $VERBOSE = nil Geocoder.configure(:lookup => :yandex) @@ -118,6 +133,14 @@ class ServicesTest < Test::Unit::TestCase $VERBOSE = orig end + def test_yandex_raises_exception_on_invalid_key + Geocoder.configure(:always_raise => [Geocoder::InvalidApiKey]) + l = Geocoder::Lookup.get(:yandex) + assert_raises Geocoder::InvalidApiKey do + l.send(:results, Geocoder::Query.new("invalid key")) + end + end + # --- Geocoder.ca --- @@ -142,6 +165,30 @@ 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 + + def test_maxmind_raises_exception_on_invalid_key + Geocoder.configure( + :always_raise => [Geocoder::InvalidApiKey] + ) + l = Geocoder::Lookup.get(:maxmind) + assert_raises Geocoder::InvalidApiKey do + l.send(:results, Geocoder::Query.new("invalid key")) + end + end + # --- Bing --- diff --git a/test/test_helper.rb b/test/test_helper.rb index 5c93eb1ba3da487e2cd0eaca3ed3980340d34ca9..6fca5010f64d82ecd2dfd1cd5d1ee8bbc34d8c70 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -73,7 +73,11 @@ require "geocoder/lookups/base" module Geocoder module Lookup class Base - private #----------------------------------------------------------------- + private + def fixture_exists?(filename) + File.exist?(File.join("test", "fixtures", filename)) + end + def read_fixture(file) filepath = File.join("test", "fixtures", file) s = File.read(filepath).strip.gsub(/\n\s*/, "") @@ -83,124 +87,56 @@ module Geocoder end s end - end - class Google < Base - private #----------------------------------------------------------------- - def make_api_request(query) - raise TimeoutError if query.text == "timeout" - raise SocketError if query.text == "socket_error" - file = case query.text - when "no results"; :no_results - when "no locality"; :no_locality - when "no city data"; :no_city_data - else :madison_square_garden - end - read_fixture "google_#{file}.json" + def default_fixture_filename + "#{fixture_prefix}_madison_square_garden" end - end - class GooglePremier < Google - end - - class Yahoo < Base - private #----------------------------------------------------------------- - def make_api_request(query) - raise TimeoutError if query.text == "timeout" - raise SocketError if query.text == "socket_error" - file = case query.text - when "no results"; :no_results - when "error"; :error - else :madison_square_garden - end - read_fixture "yahoo_#{file}.json" - end - end - - class Yandex < Base - private #----------------------------------------------------------------- - def make_api_request(query) - raise TimeoutError if query.text == "timeout" - raise SocketError if query.text == "socket_error" - file = case query.text - when "no results"; :no_results - when "invalid key"; :invalid_key - else :kremlin - end - read_fixture "yandex_#{file}.json" + def fixture_prefix + handle end - end - class GeocoderCa < Base - private #----------------------------------------------------------------- def make_api_request(query) raise TimeoutError if query.text == "timeout" raise SocketError if query.text == "socket_error" if query.reverse_geocode? - read_fixture "geocoder_ca_reverse.json" + filename = "#{fixture_prefix}_reverse" + else + filename = "#{fixture_prefix}_#{query.text.gsub(" ", "_")}" + end + if fixture_exists?(filename) + read_fixture "#{filename}" else - file = case query.text - when "no results"; :no_results - else :madison_square_garden - end - read_fixture "geocoder_ca_#{file}.json" + read_fixture default_fixture_filename end end end - class Freegeoip < Base - private #----------------------------------------------------------------- - def make_api_request(query) - raise TimeoutError if query.text == "timeout" - raise SocketError if query.text == "socket_error" - file = case query.text - when "no results"; :no_results - else "74_200_247_59" - end - read_fixture "freegeoip_#{file}.json" + class GooglePremier + private + def fixture_prefix + "google" end end - class Bing < Base - private #----------------------------------------------------------------- - def make_api_request(query) - raise TimeoutError if query.text == "timeout" - raise SocketError if query.text == "socket_error" - if query.reverse_geocode? - read_fixture "bing_reverse.json" - else - file = case query.text - when "no results"; :no_results - else :madison_square_garden - end - read_fixture "bing_#{file}.json" - end + class Yandex + private + def default_fixture_filename + "yandex_kremlin" end end - class Nominatim < Base - private #----------------------------------------------------------------- - def make_api_request(query) - raise TimeoutError if query.text == "timeout" - raise SocketError if query.text == "socket_error" - file = case query.text - when "no results"; :no_results - else :madison_square_garden - end - read_fixture "nominatim_#{file}.json" + class Freegeoip + private + def default_fixture_filename + "freegeoip_74_200_247_59" end end - class Mapquest < Base - private #----------------------------------------------------------------- - def make_api_request(query) - raise TimeoutError if query.text == "timeout" - raise SocketError if query.text == "socket_error" - file = case query.text - when "no results"; :no_results - else :madison_square_garden - end - read_fixture "mapquest_#{file}.json" + class Maxmind + private + def default_fixture_filename + "maxmind_74_200_247_59" end end