gcoder 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +3 -0
- data/Gemfile +3 -0
- data/LICENSE +18 -0
- data/README.md +144 -0
- data/Rakefile +48 -0
- data/gcoder.gemspec +22 -0
- data/lib/gcoder/geocoder.rb +135 -0
- data/lib/gcoder/resolver.rb +42 -0
- data/lib/gcoder/storage.rb +107 -0
- data/lib/gcoder/version.rb +3 -0
- data/lib/gcoder.rb +38 -0
- data/spec/gcoder/geocoder_spec.rb +53 -0
- data/spec/gcoder/resolver_spec.rb +47 -0
- data/spec/gcoder/storage_spec.rb +65 -0
- data/spec/gcoder_spec.rb +7 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/requests/1.json +96 -0
- data/spec/support/requests/2.json +364 -0
- data/spec/support/requests/3.json +4 -0
- data/spec/support/requests/4.json +4 -0
- data/spec/support/requests/5.json +4 -0
- data/spec/support/requests/6.json +4 -0
- data/spec/support/requests.yml +18 -0
- metadata +112 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2010 Carsten Nielsen
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
8
|
+
so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
15
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
16
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
# GCoder
|
2
|
+
|
3
|
+
GCoder geocodes stuff using the Google Geocoding API (V3) and caches the
|
4
|
+
results somewhere, if you want. _(Need something more bulldozer-like? Check out
|
5
|
+
[Geokit](http://github.com/andre/geokit-gem).)_
|
6
|
+
|
7
|
+
# Bon Usage
|
8
|
+
|
9
|
+
require 'gcoder'
|
10
|
+
|
11
|
+
G = GCoder.connect(:storage => :heap)
|
12
|
+
|
13
|
+
G['dundas and sorauren', :region => :ca] # ... it geocodes!
|
14
|
+
G[[41.87, -74.16]] # ... and reverse-geocodes!
|
15
|
+
|
16
|
+
The returned value is the 'results' portion of the Google Geocoding API
|
17
|
+
[response](http://code.google.com/apis/maps/documentation/geocoding/#JSON).
|
18
|
+
|
19
|
+
## Configuration Options
|
20
|
+
|
21
|
+
These can be applied globally by setting `GCoder.config` or on a per-connection
|
22
|
+
basis by passing them to `GCoder.connect`.
|
23
|
+
|
24
|
+
### `:append`
|
25
|
+
|
26
|
+
Specifies a string to append to the end of all queries.
|
27
|
+
|
28
|
+
### `:region`
|
29
|
+
|
30
|
+
Tells the Geocoder to favour results in a specific region. More info
|
31
|
+
[here](http://code.google.com/apis/maps/documentation/geocoding/#RegionCodes).
|
32
|
+
|
33
|
+
### `:language`
|
34
|
+
|
35
|
+
By default this is whatever Google thinks it is, you can set it to something
|
36
|
+
if you'd like. More info
|
37
|
+
[here](http://code.google.com/apis/maps/documentation/geocoding/#GeocodingRequests).
|
38
|
+
|
39
|
+
### `:bounds`
|
40
|
+
|
41
|
+
Specifies a bounding box in which to favour results from. Described as an array
|
42
|
+
of two latitude / longitude pairs. The first describes the Northeast corner of
|
43
|
+
the box and the second describes the Southwest corner of the box. Here is an
|
44
|
+
example input:
|
45
|
+
|
46
|
+
[[50.09, -94.88], [41.87, -74.16]]
|
47
|
+
|
48
|
+
More info [here](http://code.google.com/apis/maps/documentation/geocoding/#Viewports).
|
49
|
+
|
50
|
+
### `:storage`
|
51
|
+
|
52
|
+
Defines the storage adapter to use for caching results from the geocoder to
|
53
|
+
limit unnecessary throughput. See "Storage Adapters" below for more information.
|
54
|
+
|
55
|
+
### `:storage_config`
|
56
|
+
|
57
|
+
Passed on to the selected adapter as configuration options. See
|
58
|
+
"Storage Adapters" below for more information.
|
59
|
+
|
60
|
+
## Storage Adapters
|
61
|
+
|
62
|
+
GCoder comes with two adapters: `:heap`, and `:redis`. You can check out
|
63
|
+
`lib/gcoder/storage.rb` for examples of how these are implemented. You can
|
64
|
+
select either of these, or pass `nil` (or `false`) as the `:storage` option to
|
65
|
+
disable caching of results.
|
66
|
+
|
67
|
+
* `:storage => nil` - Disable caching (default.)
|
68
|
+
* `:storage => :heap` - Saves cached values in an in-memory Hash.
|
69
|
+
* `:storage => :redis` - Saves cached values within Redis.
|
70
|
+
|
71
|
+
### Adapter Configuration
|
72
|
+
|
73
|
+
Some adapters have configuration settings that can be passed. The contents of
|
74
|
+
`:storage_config` are passed to the adapter when it is instantiated.
|
75
|
+
|
76
|
+
**HeapAdapter (:heap)**
|
77
|
+
|
78
|
+
The Heap adapter has no options.
|
79
|
+
|
80
|
+
**RedisAdapter (:redis)**
|
81
|
+
|
82
|
+
The Redis adapter has the following options, none are required.
|
83
|
+
|
84
|
+
* `:connection` - Passed to `Redis.connect`.
|
85
|
+
* `:keyspace` - Prefixed to all keys generated by GCoder.
|
86
|
+
* `:key_ttl` - A time-to-live in seconds before cached results expire.
|
87
|
+
|
88
|
+
### Roll Your Own Adapter
|
89
|
+
|
90
|
+
Making your own adapter is pretty easy. Your adapter needs to respond to four
|
91
|
+
instance methods: `connect`, `set`, `get`, and `clear`. Let's make an adapter
|
92
|
+
for Sequel.
|
93
|
+
|
94
|
+
class SequelAdapter < GCoder::Storage::Adapter
|
95
|
+
def connect
|
96
|
+
@db = Sequel.connect(config[:connection])
|
97
|
+
@table_name = (config[:table_name] || :gcoder_results)
|
98
|
+
end
|
99
|
+
|
100
|
+
# The methods `nkey` and `nval` are used to "normalize" keys and values,
|
101
|
+
# respectively. You are encouraged to use them.
|
102
|
+
def set(key, value)
|
103
|
+
if get(key)
|
104
|
+
table.filter(:id => nkey(key)).update(:value => nval(value))
|
105
|
+
else
|
106
|
+
table.insert(:id => nkey(key), :value => nval(value))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def get(key)
|
111
|
+
if (row = table.filter(:id => nkey(key)).first)
|
112
|
+
row[:value]
|
113
|
+
else
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def clear
|
119
|
+
table.delete_all
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def table
|
125
|
+
@db[@table_name]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
GCoder::Storage.register(:sequel, SequelAdapter)
|
130
|
+
|
131
|
+
Now we can use our adapter as a caching layer by specifying it like this:
|
132
|
+
|
133
|
+
G = GCoder.connect \
|
134
|
+
:storage => :sequel,
|
135
|
+
:storage_config => {
|
136
|
+
:connection => 'sqlite://geo.db',
|
137
|
+
:table_name => :locations
|
138
|
+
}
|
139
|
+
|
140
|
+
## Notes
|
141
|
+
|
142
|
+
Tested with Ruby 1.9.2 (MRI) and nothing else, fork it. See
|
143
|
+
[LICENSE](http://github.com/heycarsten/gcoder/blob/master/LICENSE) for details
|
144
|
+
about that jazz.
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rubygems/specification' unless defined?(Gem::Specification)
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
def gemspec
|
6
|
+
@gemspec ||= begin
|
7
|
+
Gem::Specification.load(File.expand_path('gcoder.gemspec'))
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
task :default => :spec
|
12
|
+
|
13
|
+
desc 'Start an irb console'
|
14
|
+
task :console do
|
15
|
+
system 'irb -I lib -r gcoder'
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'Validates the gemspec'
|
19
|
+
task :gemspec do
|
20
|
+
gemspec.validate
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'Displays the current version'
|
24
|
+
task :version do
|
25
|
+
puts "Current version: #{gemspec.version}"
|
26
|
+
end
|
27
|
+
|
28
|
+
desc 'Installs the gem locally'
|
29
|
+
task :install => :package do
|
30
|
+
sh "gem install pkg/#{gemspec.name}-#{gemspec.version}"
|
31
|
+
end
|
32
|
+
|
33
|
+
desc 'Release the gem'
|
34
|
+
task :release => :package do
|
35
|
+
sh "gem push pkg/#{gemspec.name}-#{gemspec.version}.gem"
|
36
|
+
end
|
37
|
+
|
38
|
+
Rake::GemPackageTask.new(gemspec) do |pkg|
|
39
|
+
pkg.gem_spec = gemspec
|
40
|
+
end
|
41
|
+
task :gem => :gemspec
|
42
|
+
task :package => :gemspec
|
43
|
+
|
44
|
+
Rake::TestTask.new(:spec) do |t|
|
45
|
+
t.libs += %w[gcoder spec]
|
46
|
+
t.test_files = FileList['spec/**/*.rb']
|
47
|
+
t.verbose = true
|
48
|
+
end
|
data/gcoder.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require File.expand_path("../lib/gcoder/version", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'gcoder'
|
6
|
+
s.version = GCoder::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ['Carsten Nielsen']
|
9
|
+
s.email = ['heycarsten@gmail.com']
|
10
|
+
s.homepage = 'http://github.com/heycarsten/gcoder'
|
11
|
+
s.summary = %q{A nice library for geocoding stuff with Google Geocoder API}
|
12
|
+
s.description = %q{Uses Google Geocoder API to geocode stuff and optionally caches the results somewhere}
|
13
|
+
|
14
|
+
s.required_rubygems_version = '>= 1.3.6'
|
15
|
+
s.rubyforge_project = 'gcoder'
|
16
|
+
|
17
|
+
s.add_dependency 'hashie'
|
18
|
+
|
19
|
+
s.files = `git ls-files`.split(?\n)
|
20
|
+
s.test_files = `git ls-files -- {test,spec}/*`.split(?\n)
|
21
|
+
s.require_paths = ['lib']
|
22
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module GCoder
|
2
|
+
module Geocoder
|
3
|
+
|
4
|
+
HOST = 'maps.googleapis.com'
|
5
|
+
PATH = '/maps/api/geocode/json'
|
6
|
+
|
7
|
+
class Request
|
8
|
+
def self.u(string)
|
9
|
+
string.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
|
10
|
+
'%' + $1.unpack('H2' * $1.size).join('%').upcase
|
11
|
+
}.tr(' ', '+')
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.to_query(params)
|
15
|
+
params.map { |key, val| "#{u key}=#{u val}" }.join('&')
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.stubs
|
19
|
+
@stubs ||= {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.stub(uri, body)
|
23
|
+
stubs[uri] = body
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(query, opts = {})
|
27
|
+
@config = GCoder.config.merge(opts)
|
28
|
+
detect_and_set_query(query)
|
29
|
+
end
|
30
|
+
|
31
|
+
def params
|
32
|
+
p = { :sensor => 'false' }
|
33
|
+
p[:address] = address if @address
|
34
|
+
p[:latlng] = latlng if @latlng
|
35
|
+
p[:language] = @config[:language] if @config[:language]
|
36
|
+
p[:region] = @config[:region] if @config[:region]
|
37
|
+
p[:bounds] = bounds if @config[:bounds]
|
38
|
+
p
|
39
|
+
end
|
40
|
+
|
41
|
+
def path
|
42
|
+
"#{PATH}?#{self.class.to_query(params)}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def uri
|
46
|
+
"http://#{HOST}#{path}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def get
|
50
|
+
Timeout.timeout(@config[:timeout]) do
|
51
|
+
Response.new(uri, http_get)
|
52
|
+
end
|
53
|
+
rescue Timeout::Error
|
54
|
+
raise TimeoutError, "Query timeout after #{@config[:timeout]} second(s)"
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def detect_and_set_query(query)
|
60
|
+
if query.is_a?(Array)
|
61
|
+
case
|
62
|
+
when query.size != 2
|
63
|
+
raise BadQueryError, "Unable to geocode lat/lng pair that is not " \
|
64
|
+
"two elements long: #{query.inspect}"
|
65
|
+
when query.any? { |q| '' == q.to_s.strip }
|
66
|
+
raise BadQueryError, "Unable to geocode lat/lng pair with blank " \
|
67
|
+
"elements: #{query.inspect}"
|
68
|
+
else
|
69
|
+
@latlng = query
|
70
|
+
end
|
71
|
+
else
|
72
|
+
if '' == query.to_s.strip
|
73
|
+
raise BadQueryError, "Unable to geocode a blank query: " \
|
74
|
+
"#{query.inspect}"
|
75
|
+
else
|
76
|
+
@address = query
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def http_get
|
82
|
+
self.class.stubs[uri] || Net::HTTP.get(HOST, path)
|
83
|
+
end
|
84
|
+
|
85
|
+
def latlng
|
86
|
+
@latlng.join(',')
|
87
|
+
end
|
88
|
+
|
89
|
+
def bounds
|
90
|
+
@config[:bounds].map { |point| point.join(',') }.join('|')
|
91
|
+
end
|
92
|
+
|
93
|
+
def address
|
94
|
+
@config[:append] ? "#{@address} #{@config[:append]}" : @address
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
class Response
|
100
|
+
attr_reader :body, :uri
|
101
|
+
|
102
|
+
def initialize(uri, body)
|
103
|
+
@uri = uri
|
104
|
+
@body = body
|
105
|
+
@response = Hashie::Mash.new(JSON.parse(@body))
|
106
|
+
validate_status!
|
107
|
+
end
|
108
|
+
|
109
|
+
def as_mash
|
110
|
+
@response
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def validate_status!
|
116
|
+
case @response.status
|
117
|
+
when 'OK'
|
118
|
+
# All is well!
|
119
|
+
when 'ZERO_RESULTS'
|
120
|
+
raise NoResultsError, "Geocoding API returned no results: (#{@uri})"
|
121
|
+
when 'OVER_QUERY_LIMIT'
|
122
|
+
raise OverLimitError, 'Rate limit for Geocoding API exceeded!'
|
123
|
+
when 'REQUEST_DENIED'
|
124
|
+
raise GeocoderError, "Request denied by the Geocoding API: (#{@uri})"
|
125
|
+
when 'INVALID_REQUEST'
|
126
|
+
raise GeocoderError, "An invalid request was made: (#{@uri})"
|
127
|
+
else
|
128
|
+
raise GeocoderError, 'No status in Geocoding API response: ' \
|
129
|
+
"(#{@uri})\n\n#{@body}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module GCoder
|
2
|
+
class Resolver
|
3
|
+
|
4
|
+
def initialize(opts = {})
|
5
|
+
@config = GCoder.config.merge(opts)
|
6
|
+
if (adapter_name = @config[:storage])
|
7
|
+
@conn = Storage[adapter_name].new(@config[:storage_config])
|
8
|
+
else
|
9
|
+
@conn = nil
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](*args)
|
14
|
+
geocode *args
|
15
|
+
end
|
16
|
+
|
17
|
+
def geocode(query, opts = {})
|
18
|
+
fetch([query, opts].join) do
|
19
|
+
Geocoder::Request.new(query, opts).get.as_mash
|
20
|
+
end.results
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch(key)
|
24
|
+
raise ArgumentError, 'block required' unless block_given?
|
25
|
+
Hashie::Mash.new((val = get(key)) ? JSON.parse(val) : set(key, yield))
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def get(query)
|
31
|
+
return nil unless @conn
|
32
|
+
@conn.get(query)
|
33
|
+
end
|
34
|
+
|
35
|
+
def set(key, value)
|
36
|
+
return value unless @conn
|
37
|
+
@conn.set(key, value.to_json)
|
38
|
+
value
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module GCoder
|
2
|
+
module Storage
|
3
|
+
|
4
|
+
def self.adapters
|
5
|
+
@adapters ||= {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.[](name)
|
9
|
+
adapters[name.to_sym]
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.register(name, mod)
|
13
|
+
adapters[name.to_sym] = mod
|
14
|
+
end
|
15
|
+
|
16
|
+
class Adapter
|
17
|
+
def initialize(opts = {})
|
18
|
+
@config = (opts || {})
|
19
|
+
connect
|
20
|
+
end
|
21
|
+
|
22
|
+
def config
|
23
|
+
@config
|
24
|
+
end
|
25
|
+
|
26
|
+
def connect
|
27
|
+
raise NotImplementedError, 'This adapter needs to implement #connect'
|
28
|
+
end
|
29
|
+
|
30
|
+
def clear
|
31
|
+
raise NotImplementedError, 'This adapter needs to implement #clear'
|
32
|
+
end
|
33
|
+
|
34
|
+
def get(key)
|
35
|
+
raise NotImplementedError, 'This adapter needs to implement #get'
|
36
|
+
end
|
37
|
+
|
38
|
+
def set(key, val)
|
39
|
+
raise NotImplementedError, 'This adapter needs to implement #set'
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
def nval(value)
|
45
|
+
value.to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
def nkey(key)
|
49
|
+
Digest::SHA1.hexdigest(key.to_s.downcase)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
class HeapAdapter < Adapter
|
55
|
+
def connect
|
56
|
+
@heap = {}
|
57
|
+
end
|
58
|
+
|
59
|
+
def clear
|
60
|
+
@heap = {}
|
61
|
+
end
|
62
|
+
|
63
|
+
def get(key)
|
64
|
+
@heap[nkey(key)]
|
65
|
+
end
|
66
|
+
|
67
|
+
def set(key, value)
|
68
|
+
@heap[nkey(key)] = nval(value)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
class RedisAdapter < Adapter
|
74
|
+
def connect
|
75
|
+
require 'redis'
|
76
|
+
@rdb = Redis.connect(*[config[:connection]].compact)
|
77
|
+
@keyspace = "#{config[:keyspace] || 'gcoder'}:"
|
78
|
+
end
|
79
|
+
|
80
|
+
def clear
|
81
|
+
@rdb.keys(@keyspace + '*').each { |key| @rdb.del(key) }
|
82
|
+
end
|
83
|
+
|
84
|
+
def get(key)
|
85
|
+
@rdb.get(keyns(key))
|
86
|
+
end
|
87
|
+
|
88
|
+
def set(key, value)
|
89
|
+
if (ttl = config[:key_ttl])
|
90
|
+
@rdb.setex(keyns(key), ttl, nval(value))
|
91
|
+
else
|
92
|
+
@rdb.set(keyns(key), nval(value))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def keyns(key)
|
99
|
+
"#{@keyspace}#{nkey(key)}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
register :heap, HeapAdapter
|
104
|
+
register :redis, RedisAdapter
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
data/lib/gcoder.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'hashie'
|
3
|
+
require 'net/http'
|
4
|
+
require 'timeout'
|
5
|
+
require 'digest/sha1'
|
6
|
+
|
7
|
+
$:.unshift(File.dirname(__FILE__))
|
8
|
+
|
9
|
+
require 'gcoder/version'
|
10
|
+
require 'gcoder/geocoder'
|
11
|
+
require 'gcoder/storage'
|
12
|
+
require 'gcoder/resolver'
|
13
|
+
|
14
|
+
module GCoder
|
15
|
+
class NoResultsError < StandardError; end
|
16
|
+
class OverLimitError < StandardError; end
|
17
|
+
class GeocoderError < StandardError; end
|
18
|
+
class BadQueryError < StandardError; end
|
19
|
+
class NotImplementedError < StandardError; end
|
20
|
+
class TimeoutError < StandardError; end
|
21
|
+
|
22
|
+
DEFAULT_CONFIG = {
|
23
|
+
:timeout => 5,
|
24
|
+
:append => nil,
|
25
|
+
:region => nil,
|
26
|
+
:bounds => nil,
|
27
|
+
:storage => nil,
|
28
|
+
:storage_config => nil
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
def self.config
|
32
|
+
@config ||= DEFAULT_CONFIG.dup
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.connect(options = {})
|
36
|
+
Resolver.new(options)
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GCoder::Geocoder::Request do
|
4
|
+
it 'should raise an error when passed nil' do
|
5
|
+
-> {
|
6
|
+
GCoder::Geocoder::Request.new(nil)
|
7
|
+
}.must_raise GCoder::BadQueryError
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should raise an error when passed a blank string' do
|
11
|
+
-> {
|
12
|
+
GCoder::Geocoder::Request.new(' ')
|
13
|
+
}.must_raise GCoder::BadQueryError
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should raise an error when passed incorrect lat/lng pair' do
|
17
|
+
GCoder::Geocoder::Request.tap do |req|
|
18
|
+
-> { req.new([]) }.must_raise GCoder::BadQueryError
|
19
|
+
-> { req.new([43.64]) }.must_raise GCoder::BadQueryError
|
20
|
+
-> { req.new([43.64, nil]) }.must_raise GCoder::BadQueryError
|
21
|
+
-> { req.new(['', 43.64]) }.must_raise GCoder::BadQueryError
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should URI encode a string' do
|
26
|
+
GCoder::Geocoder::Request.u('hello world').must_equal 'hello+world'
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should create a query string' do
|
30
|
+
q = GCoder::Geocoder::Request.to_query(:q => 'hello world', :a => 'test')
|
31
|
+
q.must_equal 'q=hello+world&a=test'
|
32
|
+
end
|
33
|
+
|
34
|
+
it '(when passed a bounds option) should generate correct query params' do
|
35
|
+
GCoder::Geocoder::Request.new('q', :bounds => [[1,2], [3,4]]).tap do |req|
|
36
|
+
req.params[:bounds].must_equal '1,2|3,4'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it '(when passed a lat/lng pair) should generate correct query params' do
|
41
|
+
GCoder::Geocoder::Request.new([43.64, -79.39]).tap do |req|
|
42
|
+
req.params[:latlng].must_equal '43.64,-79.39'
|
43
|
+
req.params[:address].must_be_nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it '(when passed a geocodable string) should generate correct query params' do
|
48
|
+
GCoder::Geocoder::Request.new('queen and spadina').tap do |req|
|
49
|
+
req.params[:latlng].must_be_nil
|
50
|
+
req.params[:address].must_equal 'queen and spadina'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'GCoder::Resolver (with caching)' do
|
4
|
+
before do
|
5
|
+
@g = GCoder.connect(:storage => :heap, :region => :us)
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'should resolve geocodable queries' do
|
9
|
+
r = @g.geocode('queen and spadina', :region => :ca)
|
10
|
+
r.must_be_instance_of Array
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should resolve cached queries' do
|
14
|
+
r1 = @g.geocode('queen and spadina', :region => :ca)
|
15
|
+
r2 = @g.geocode('queen and spadina', :region => :ca)
|
16
|
+
[r1, r2].each { |r| r.must_be_instance_of Array }
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should resolve reverse-geocodeable queries' do
|
20
|
+
r = @g.geocode([43.6487606, -79.3962415], :region => nil)
|
21
|
+
r.must_be_instance_of Array
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should raise an error for queries with no results' do
|
25
|
+
-> { @g['noresults', :region => nil] }.must_raise GCoder::NoResultsError
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should raise an error for denied queries' do
|
29
|
+
-> { @g['denied', :region => nil] }.must_raise GCoder::GeocoderError
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should raise an error when the query limit is exceeded' do
|
33
|
+
-> { @g['overlimit', :region => nil] }.must_raise GCoder::OverLimitError
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should raise an error when the request is invalid' do
|
37
|
+
-> { @g['denied', :region => nil] }.must_raise GCoder::GeocoderError
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'GCoder::Resolver (without caching)' do
|
42
|
+
it 'should resolve queries' do
|
43
|
+
g = GCoder.connect(:storage => nil)
|
44
|
+
r = g['queen and spadina', :region => :ca]
|
45
|
+
r.must_be_instance_of Array
|
46
|
+
end
|
47
|
+
end
|