Skip to content

Commit

Permalink
🎂 2.0.0 Rewrite
Browse files Browse the repository at this point in the history
Use semantic versioning for dependencies.
Take a more object oriented approach, rather than using a bunch of module functions.
Add some badges.
Use Curb and Oj instead of HTTPClient and JSON.

See the changelog for more information.
  • Loading branch information
elifoster committed Apr 15, 2016
1 parent 6b0be9a commit 15c59c4
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 221 deletions.
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,30 @@
# Changelog
## Version 2
### Version 2.0.0
* Complete rewrite of the gem. Includes the following changes:
* SimpleGeolocator is no longer a helper module containing methods for every type of data. Now, you call the
`SimpleGeolocator#get` function, which returns an IPAPIResponse object. This object has instance attributes to
replace almost all of the old SimpleGeolocator module functions. The exception to this is connection, which has
been replaced by the `IPAPIResponse#mobile?` and `IPAPIResponse#proxy?` functions.
* Proper error handling has been introduced. Functions will no longer return error strings. Now,
`IPAPIResponse#initialize` fails with the according errors, to quickly alert the developer or user that something
has gone wrong.
* `zip` is no longer an Integer, because that implies that some math should be done on it. It is a String now. You
can call `#to_i` if you for some reason want it to be an Integer.
* Region (`#region`) and country (`#country`) are now represented by a LOCATION_STRUCT Struct, which has 2 instance
attributes: name and code.
* Longitude and latitude (`#ll`) are no longer represented as an array, but a Pair from the data_types gem. This
makes significantly more conceptual sense.
* `#isp_name` and `#organization_name` are now represented by the new named `isp` and `organization` instance
attributes.
* Changes to the libraries and requirements:
* HTTPClient is no longer used. It had largely too much overhead for a gem this simple. When we do not need
keepalive, complicated requests, or even just a "full" client, I found it didn't make sense to use it. Now, Curb
is used.
* The stdlib JSON library is not used anymore. It has been replaced by Optimized JSON (Oj) for performance reasons.
* data_types is now a library used by SimpleGeolocator.
* Pessimistic version requirements are now used.

## Version 1
### Version 1.3.2
* License as MIT
Expand Down
8 changes: 5 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
source 'https://rubygems.org'

gem('httpclient', '2.7.1')
gem('rainbow', '2.0.0')
gem('string-utility', '2.5.0')
gem('curb', '~> 0.9.x')
gem('data_types', '~> 1.x')
gem('oj', '~> 2.x')
gem('rainbow', '~> 2.x')
gem('string-utility', '~> 2.x')

ruby '2.3.0'
12 changes: 8 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
GEM
remote: https://rubygems.org/
specs:
httpclient (2.7.1)
curb (0.9.1)
data_types (1.1.0)
oj (2.15.0)
rainbow (2.0.0)
string-utility (2.5.0)

PLATFORMS
ruby

DEPENDENCIES
httpclient (= 2.7.1)
rainbow (= 2.0.0)
string-utility (= 2.5.0)
curb (~> 0.9.x)
data_types (~> 1.x)
oj (~> 2.x)
rainbow (~> 2.x)
string-utility (~> 2.5.x)

BUNDLED WITH
1.11.2
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# simple_geolocator
[![Gem Version](https://badge.fury.io/rb/simple_geolocator.svg)](https://badge.fury.io/rb/simple_geolocator)
[![Gem Downloads](https://img.shields.io/gem/dt/simple_geolocator.svg?maxAge=2592000)]()
[![Gemnasium](https://img.shields.io/gemnasium/elifoster/simple_geolocator.svg?maxAge=2592000)]()

A simple Geolocating Ruby Gem utilizing IP-API.com.

Expand All @@ -22,7 +24,7 @@ $ bundle
```

## Usage
You can use it in your code or via IRB as described in the documentation. Or, if you wanna have some real fun, you can use the fancy command line interface added in version 1.3.0!
You can use it in your code as described in the documentation. Or, if you wanna have some real fun, you can use the fancy command line interface added in version 1.3.0!

```
$ simplegeo <ip>
Expand Down
35 changes: 16 additions & 19 deletions bin/simplegeo
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,27 @@ require 'string-utility'
require_relative '../lib/simple_geolocator'

if ARGV.empty?
puts 'You must provide an IP.'
exit
fail 'You must provide an IP.'
end

ip = ARGV[0]
city = SimpleGeolocator.city(ip)
region = SimpleGeolocator.region(ip)[:name]
country = SimpleGeolocator.country(ip)[:name]
zip = SimpleGeolocator.zip(ip)
ll = SimpleGeolocator.ll(ip)
timezone = SimpleGeolocator.timezone(ip)
isp = SimpleGeolocator.isp_name(ip)
org = SimpleGeolocator.organization_name(ip)
connection_attributes = SimpleGeolocator.connection(ip)
response = SimpleGeolocator.get(ip)
city = response.city
region = response.region.name
country = response.country.name
zip = response.zip
ll = response.ll
timezone = response.timezone
isp = response.isp
org = response.organization
mobile = response.mobile?
proxy = response.proxy?

puts "Here is the data for #{ip}:"
puts Rainbow("ISP: #{isp}").color(StringUtility.random_color_six)
puts Rainbow("Organization: #{org}").color(StringUtility.random_color_six)
puts Rainbow("Timezone: #{timezone}").color(StringUtility.random_color_six)
puts Rainbow("Location: #{city}, #{region}, #{country}, #{zip}")
.color(StringUtility.random_color_six)
puts Rainbow("Exact location: #{ll[0]}, #{ll[1]}")
.color(StringUtility.random_color_six)
puts Rainbow('They are using a mobile connection.')
.color(StringUtility.random_color_six) if connection_attributes[:mobile]
puts Rainbow('They are using a proxy.')
.color(StringUtility.random_color_six) if connection_attributes[:proxy]
puts Rainbow("Location: #{city}, #{region}, #{country}, #{zip}").color(StringUtility.random_color_six)
puts Rainbow("Exact location: #{ll.left}, #{ll.right}").color(StringUtility.random_color_six)
puts Rainbow('They are using a mobile connection.').color(StringUtility.random_color_six) if mobile
puts Rainbow('They are using a proxy.').color(StringUtility.random_color_six) if proxy
196 changes: 16 additions & 180 deletions lib/simple_geolocator.rb
Original file line number Diff line number Diff line change
@@ -1,188 +1,24 @@
require 'httpclient'
require 'json'
require 'curb'
require 'oj'
require 'data_types/pair'
require_relative 'simple_geolocator/ipapi_response'

module SimpleGeolocator
extend self
module_function

@client = HTTPClient.new
@cache = {}

# Gets the full JSON response, useful for getting multiple pieces of data in
# a single request.
# @param ip [String] The IP to get data for.
# @return [JSON] A parsed JSON object containing the response.
def get_full_response(ip)
return @cache[ip] unless @cache[ip].nil?
url = "http://ip-api.com/json/#{ip}?fields=258047"
uri = URI.parse(url)
response = @client.get(uri)
@cache[ip] = JSON.parse(response.body)
end

# Gets whether the request failed or not.
# @param response [JSON] The parsed response body (#get_full_response) to
# check.
# @return [Boolean] True if successful, false if errored.
def request_successful?(response)
case response['status']
when 'success'
true
when 'fail'
false
end
end

# Gets the country data for the IP.
# @param ip [String] See #get_full_response
# @return [Hash] A hash containing data formatted as
# { :name => 'United States', :code => 'US' }
# @return [String] A string containing the error message.
def country(ip)
response = get_full_response(ip)
err = error(response)
return err unless err.nil?
ret = {
name: response['country'],
code: response['countryCode']
}
ret
end

# Gets the region data for the IP.
# @param ip [String] See #get_full_response
# @return [Hash] A hash containing data formatted as
# { :name => 'Oregon', :code => 'OR'}
# @return [String] A string containing the error message.
def region(ip)
response = get_full_response(ip)
err = error(response)
return err unless err.nil?
ret = {
name: response['regionName'],
code: response['region']
}
ret
end

# Gets the city name for the IP.
# @param ip [String] See #get_full_response
# @return [String] The name of the city that the IP is located in.
# @return [String] A string containing the error message.
def city(ip)
response = get_full_response(ip)
err = error(response)
return err unless err.nil?

response['city']
end

# Gets the zip code for the IP.
# @param ip [String] See #get_full_response
# @return [Int] The zip code that the IP is located in.
# @return [String] A string containing the error message.
def zip(ip)
response = get_full_response(ip)
err = error(response)
return err unless err.nil?

response['zip'].to_i
end

# Gets the latitude, longitude for the IP.
# @param ip [String] See #get_full_response
# @return [Array] An array of Floats formatted as lat, lon
# @return [String] A string containing the error message.
def ll(ip)
response = get_full_response(ip)
err = error(response)
return err unless err.nil?

ret = [response['lat'], response['lon']]
ret
end

# Gets the timezone for the IP.
# @param ip [String] See #get_full_response
# @return [String] The timezone (UTC, PST, etc.) that the IP is in.
# @return [String] A string containing the error message.
def timezone(ip)
response = get_full_response(ip)
err = error(response)
return err unless err.nil?

response['timezone']
end

# Gets the name of the IP's Internet Service Provider.
# @param ip [String] See #get_full_response
# @return [String] The ISP name, such as Comcast Cable.
# @return [String] A string containing the error message.
def isp_name(ip)
response = get_full_response(ip)
err = error(response)
return err unless err.nil?

response['isp']
end

# Gets the name of the IP's organization. For most people, this is identical
# to their ISP name.
# @param ip [String] See #get_full_response
# @return [String] The organization name, such as Google.
# @return [String] A string containing the error message.
def organization_name(ip)
response = get_full_response(ip)
err = error(response)
return err unless err.nil?
URL_FORMAT = 'http://ip-api.com/json/%s?fields=258047'.freeze

response['org']
end

# Gets the IP connection attributes - if it's a mobile and/or a proxy
# connection.
# @param ip [String] See #get_full_response
# @return [Hash] A hash containing data formatted as
# { :mobile => true, :proxy => true}
# @return [String] A string containing the error message.
def connection(ip)
response = get_full_response(ip)
err = error(response)
return err unless err.nil?
ret = {
mobile: response['mobile'],
proxy: response['proxy']
}
ret
end

# Gets the according description for the semi-ambiguous error returned by the
# API.
# @param error [String] The error message returned by #error
# @return [String] The error description.
# @return [Nil] If you provided an invalid error message.
def get_error_description(error)
case error
when 'private range'
return 'The IP address is part of a private range.'
when 'reserved range'
return 'The IP address is part of a reserved range.'
when 'invalid query'
return 'The IP address or domain name is invalid.'
when 'quota'
return 'You have reached the quota.'
else
return nil
end
end

private

# Gets the error message from a response.
# @param response [JSON] See #request_successful?
# @return [String] The error message.
# @return [Nil] If there was no error message to begin with.
def error(response)
return response['message'] unless request_successful?(response)
nil
# Gets the full response. From here, all the data related to the IP can be accessed. Caches the result in order to
# prevent reaching the rate limit.
# @param ip [String] The IP to get data for.
# @return [IPAPIResponse] The full parsed response object.
def get(ip)
return @cache[ip] if @cache.key?(ip)
url = format(URL_FORMAT, ip)
response = Curl.get(url).body_str
ipapi = SimpleGeolocator::IPAPIResponse.new(Oj.load(response))
@cache[ip] = ipapi
end
end
Loading

0 comments on commit 15c59c4

Please sign in to comment.