Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
Signed-off-by: David Celis <me@davidcel.is>
  • Loading branch information
davidcelis committed Jan 21, 2014
0 parents commit 6efcedc
Show file tree
Hide file tree
Showing 21 changed files with 766 additions and 0 deletions.
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
*.gem
*.rbc
.bundle
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp
2 changes: 2 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--color
--format progress
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
source 'https://rubygems.org'

# Specify your gem's dependencies in geocodio.gemspec
gemspec
22 changes: 22 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Copyright (c) 2014 David Celis

MIT License

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Geocodio

Geocodio is a lightweight Ruby wrapper around [geocod.io][geocod.io]'s API.

## Installation

In your Gemfile:

```ruby
gem 'geocodio'
```

## Usage

The point of entry to geocod.io's API is the `Geocodio::Client` class. Initialize
one by passing your API key or allowing the initializer to automatically use
the `GEOCODIO_API_KEY` environment variable:

```ruby
geocodio = Geocodio::Client.new('0123456789abcdef')

# Or, if you've set GEOCODIO_API_KEY in your environment:
geocodio = Geocodio::Client.new
```

### Geocoding

```ruby
results = geocodio.geocode('1 Infinite Loop, Cupertino, CA 95014')
# => #<Geocodio::AddressSet:0x007fdf23a07f80 @query="1 Infinite Loop, Cupertino, CA 95014", @addresses=[...]>
address = results.first
# => #<Geocodio::Address:0x007fb062e7fb20 @number="1", @street="Infinite", @suffix="Loop", @city="Monta Vista", @state="CA", @zip="95014", @latitude=37.331669, @longitude=-122.03074, @accuracy=1, @formatted_address="1 Infinite Loop, Monta Vista CA, 95014">
puts address
# => 1 Infinite Loop, Cupertino CA, 95014
puts address.latitude # or address.lat
# => 37.331669
puts address.longitude # or address.lng
# => -122.03074
puts address.accuracy
# => 1
```

You can pass multiple addresses to `Geocodio::Client#geocode`:

```ruby
result_sets = geocodio.geocode('1 Infinite Loop, Cupertino, CA 95014', '54 West Colorado Boulevard, Pasadena, CA 91105')
# => [#<Geocodio::AddressSet:0x007fdf23a07f80 @query="1 Infinite Loop, Cupertino, CA 95014", @addresses=[...]>, #<Geocodio::AddressSet:0x007fdf23a07f80 @query="54 West Colorado Boulevard, Pasadena, CA 91105", @addresses=[...]>]
cupertino = result_sets.first.best
# => #<Geocodio::Address:0x007fb062e7fb20 @number="1", @street="Infinite", @suffix="Loop", @city="Monta Vista", @state="CA", @zip="95014", @latitude=37.331669, @longitude=-122.03074, @accuracy=1, @formatted_address="1 Infinite Loop, Monta Vista CA, 95014">
```

Geocoding will return one or more instances of `Geocodio::AddressSet` which represent a collection of addresses returned from geocod.io. Each address in the result set has an associated accuracy. If you just want whichever result was the most accurate, a `#best` convenience method is provided:

```ruby
results = geocodio.geocode('1 Infinite Loop, Cupertino, CA 95014')
# => #<Geocodio::AddressSet:0x007fdf23a07f80 @query="1 Infinite Loop, Cupertino, CA 95014", @addresses=[...]>
results.size
# => 2
results.best
# => #<Geocodio::Address:0x007fb062e7fb20 @number="1", @street="Infinite", @suffix="Loop", @city="Monta Vista", @state="CA", @zip="95014", @latitude=37.331669, @longitude=-122.03074, @accuracy=1, @formatted_address="1 Infinite Loop, Monta Vista CA, 95014">
```

### Parsing

```ruby
address = geocodio.parse('1 Infinite Loop, Cupertino, CA 95014')
# => #<Geocodio::Address:0x007fa3c15f41c0 @number="1", @street="Infinite", @suffix="Loop", @city="Cupertino", @state="CA", @zip="95014", @accuracy=nil, @formatted_address="1 Infinite Loop, Cupertino CA, 95014">
```

Note that this endpoint performs no geocoding; it merely formats a single provided address according to geocod.io's standards.

## Contributing

1. Fork it ( http://github.com/[my-github-username]/geocodio/fork )
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request

[geocod.io]: http://geocod.io/
26 changes: 26 additions & 0 deletions geocodio.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# encoding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'geocodio/version'

Gem::Specification.new do |s|
s.name = 'geocodio'
s.version = Geocodio::VERSION
s.authors = ['David Celis']
s.email = ['me@davidcel.is']

s.summary = %q{An unofficial Ruby client library for geocod.io}
s.description = %q{Geocodio is a geocoding service that aims to fill a void in the community by allowing developers to geocode large amounts of addresses without worrying about daily limits and high costs.}
s.homepage = 'https://github.com/davidcelis/geocodio'
s.license = 'MIT'

s.files = Dir['lib/**/*.rb']
s.test_files = Dir['spec/**/*']
s.require_paths = ['lib']

s.add_dependency 'json'

s.add_development_dependency 'rspec'
s.add_development_dependency 'webmock'
s.add_development_dependency 'vcr'
end
8 changes: 8 additions & 0 deletions lib/geocodio.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'geocodio/address'
require 'geocodio/address_set'
require 'geocodio/client'

require 'geocodio/version'

module Geocodio
end
47 changes: 47 additions & 0 deletions lib/geocodio/address.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
module Geocodio
class Address
attr_accessor :number
attr_accessor :street
attr_accessor :suffix
attr_accessor :city
attr_accessor :state
attr_accessor :zip

attr_accessor :latitude
attr_accessor :longitude
alias :lat :latitude
alias :lng :longitude

# How accurage geocod.io deemed this result to be given the original query.
#
# @return [Float] a number between 0 and 1
attr_accessor :accuracy

def initialize(payload = {})
if payload['address_components']
@number = payload['address_components']['number']
@street = payload['address_components']['street']
@suffix = payload['address_components']['suffix']
@city = payload['address_components']['city']
@state = payload['address_components']['state']
@zip = payload['address_components']['zip']
end

if payload['location']
@latitude = payload['location']['lat']
@longitude = payload['location']['lng']
end

@accuracy = payload['accuracy']

@formatted_address = payload['formatted_address']
end

# Formats the address in the standard way.
#
# @return [String] a formatted address
def to_s
@formatted_address
end
end
end
37 changes: 37 additions & 0 deletions lib/geocodio/address_set.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# AddressSet is a collection of Geocodio::Address objects that get returned from
# a query to geocod.io. Because one query can return multiple results, each with
# an accuracy, a collection to manage them is needed. Most of the time, the user
# should only need to use the #best method.
module Geocodio
class AddressSet
include Enumerable

# Returns the query that retrieved this result set.
#
# @return [String] the original query
attr_reader :query

def initialize(query, *addresses)
@query = query
@addresses = addresses
end

def each(&block)
@addresses.each(&block)
end

# Returns the result that geocod.io deemed the most accurate for the query.
#
# @return [Geocodio::Address] the most accurate address
def best
max_by(&:accuracy)
end

# Returns the number of addresses contained in this result set.
#
# @return [Integer] the number of addresses
def size
@addresses.size
end
end
end
110 changes: 110 additions & 0 deletions lib/geocodio/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
require 'net/http'
require 'json'
require 'cgi'

require 'geocodio/client/error'
require 'geocodio/client/response'

module Geocodio
class Client
CONTENT_TYPE = 'application/json'
METHODS = {
:get => Net::HTTP::Get,
:post => Net::HTTP::Post,
:put => Net::HTTP::Put,
:delete => Net::HTTP::Delete
}
HOST = 'api.geocod.io'
BASE_PATH = '/v1'
PORT = 80

def initialize(api_key = ENV['GEOCODIO_API_KEY'])
@api_key = api_key
end

# Geocodes one or more addresses. If one address is specified, a GET request
# is submitted to http://api.geocod.io/v1/geocode. Multiple addresses will
# instead submit a POST request.
#
# @param addresses [Array] one or more String addresses
# @return [Geocodio::Address, Array<Geocodio::AddressSet>] One or more Address Sets
def geocode(*addresses)
addresses = addresses.first if addresses.first.is_a?(Array)

if addresses.size < 1
raise ArgumentError, 'You must provide at least one address to geocode.'
elsif addresses.size == 1
geocode_single(addresses.first)
else
geocode_batch(addresses)
end
end

# Sends a GET request to http://api.geocod.io/v1/parse to correctly dissect
# an address into individual parts. As this endpoint does not do any
# geocoding, parts missing from the passed address will be missing from the
# result.
#
# @param address [String] the full or partial address to parse
# @return [Geocodio::Address] a parsed and formatted Address
def parse(address)
Address.new get('/parse', q: address).body
end

private

METHODS.each do |method, _|
define_method(method) do |path, params = {}, options = {}|
request method, path, options.merge(params: params)
end
end

def geocode_single(address)
response = get '/geocode', q: address
results = response.body['results']
query = response.body['input']['formatted_address']
addresses = results.map { |result| Address.new(result) }

AddressSet.new(query, *addresses)
end

def geocode_batch(addresses)
response = post '/geocode', {}, body: addresses
result_sets = response.body['results']

result_sets.map do |result_set|
query = result_set['response']['input']['formatted_address']
results = result_set['response']['results']
addresses = results.map { |result| Address.new(result) }

AddressSet.new(query, *addresses)
end
end

def request(method, path, options)
path += "?api_key=#{@api_key}"

if params = options[:params] and !params.empty?
q = params.map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }
path += "&#{q.join('&')}"
end

req = METHODS[method].new(BASE_PATH + path, 'Accept' => CONTENT_TYPE)

if options.key?(:body)
req['Content-Type'] = CONTENT_TYPE
req.body = options[:body] ? JSON.dump(options[:body]) : ''
end

http = Net::HTTP.new HOST, PORT
res = http.start { http.request(req) }

case res
when Net::HTTPSuccess
return Response.new(res)
else
raise Error, res
end
end
end
end
15 changes: 15 additions & 0 deletions lib/geocodio/client/error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Geocodio
class Client
class Error < RuntimeError
attr_reader :response

def initialize(response)
@response, @json = response, JSON.parse(response.body)
end

def body() @json end
def message() body['error'] end
alias :error :message
end
end
end
13 changes: 13 additions & 0 deletions lib/geocodio/client/response.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
require 'json'

module Geocodio
class Client
class Response
attr_reader :body

def initialize(response)
@body = JSON.parse(response.body)
end
end
end
end
Loading

0 comments on commit 6efcedc

Please sign in to comment.