gcoder 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|