heycarsten-gcoder 0.3.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/.document +5 -0
- data/.gitignore +3 -0
- data/README.rdoc +7 -0
- data/Rakefile +58 -0
- data/VERSION.yml +4 -0
- data/gcoder.gemspec +57 -0
- data/lib/gcoder/config.rb +26 -0
- data/lib/gcoder/geocoding_api.rb +167 -0
- data/lib/gcoder/persistence.rb +80 -0
- data/lib/gcoder/resolver.rb +15 -0
- data/lib/gcoder.rb +42 -0
- data/test/config_test.rb +37 -0
- data/test/gcoder_test.rb +26 -0
- data/test/geocoding_api_test.rb +74 -0
- data/test/persistence_test.rb +78 -0
- data/test/resolver_test.rb +36 -0
- data/test/test_helper.rb +58 -0
- metadata +74 -0
data/.document
ADDED
data/.gitignore
ADDED
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'jeweler'
|
7
|
+
Jeweler::Tasks.new do |g|
|
8
|
+
g.name = 'gcoder'
|
9
|
+
g.summary = 'A library for geocoding postal codes via the Google Maps ' \
|
10
|
+
'Geocoding API with a persisted cache through Tokyo Tyrant'
|
11
|
+
g.email = 'heycarsten@gmail.com'
|
12
|
+
g.homepage = 'http://github.com/heycarsten/gcoder'
|
13
|
+
g.authors = ['Carsten Nielsen']
|
14
|
+
end
|
15
|
+
rescue LoadError
|
16
|
+
puts 'Jeweler not available. Install it with: sudo gem install ' \
|
17
|
+
'technicalpickles-jeweler -s http://gems.github.com'
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
require 'rake/testtask'
|
22
|
+
Rake::TestTask.new(:test) do |test|
|
23
|
+
test.libs << 'lib' << 'test'
|
24
|
+
test.pattern = 'test/**/*_test.rb'
|
25
|
+
test.verbose = true
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
begin
|
30
|
+
require 'rcov/rcovtask'
|
31
|
+
Rcov::RcovTask.new do |test|
|
32
|
+
test.libs << 'test'
|
33
|
+
test.pattern = 'test/**/*_test.rb'
|
34
|
+
test.verbose = true
|
35
|
+
end
|
36
|
+
rescue LoadError
|
37
|
+
task :rcov do
|
38
|
+
abort 'RCov is not available. In order to run rcov, you must: sudo gem ' \
|
39
|
+
'install spicycode-rcov'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
task :default => :test
|
45
|
+
|
46
|
+
# require 'rake/rdoctask'
|
47
|
+
# Rake::RDocTask.new do |rdoc|
|
48
|
+
# if File.exist?('VERSION.yml')
|
49
|
+
# config = YAML.load(File.read('VERSION.yml'))
|
50
|
+
# version = [config[:major], config[:minor], config[:patch]].join('.')
|
51
|
+
# else
|
52
|
+
# version = ''
|
53
|
+
# end
|
54
|
+
# rdoc.rdoc_dir = 'rdoc'
|
55
|
+
# rdoc.title = "GCoder #{version}"
|
56
|
+
# rdoc.rdoc_files.include('README*')
|
57
|
+
# rdoc.rdoc_files.include('lib/**/*.rb')
|
58
|
+
# end
|
data/VERSION.yml
ADDED
data/gcoder.gemspec
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{gcoder}
|
5
|
+
s.version = "0.3.0"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Carsten Nielsen"]
|
9
|
+
s.date = %q{2009-06-26}
|
10
|
+
s.email = %q{heycarsten@gmail.com}
|
11
|
+
s.extra_rdoc_files = [
|
12
|
+
"README.rdoc"
|
13
|
+
]
|
14
|
+
s.files = [
|
15
|
+
".document",
|
16
|
+
".gitignore",
|
17
|
+
"README.rdoc",
|
18
|
+
"Rakefile",
|
19
|
+
"VERSION.yml",
|
20
|
+
"gcoder.gemspec",
|
21
|
+
"lib/gcoder.rb",
|
22
|
+
"lib/gcoder/config.rb",
|
23
|
+
"lib/gcoder/geocoding_api.rb",
|
24
|
+
"lib/gcoder/persistence.rb",
|
25
|
+
"lib/gcoder/resolver.rb",
|
26
|
+
"test/config_test.rb",
|
27
|
+
"test/gcoder_test.rb",
|
28
|
+
"test/geocoding_api_test.rb",
|
29
|
+
"test/persistence_test.rb",
|
30
|
+
"test/resolver_test.rb",
|
31
|
+
"test/test_helper.rb"
|
32
|
+
]
|
33
|
+
s.has_rdoc = true
|
34
|
+
s.homepage = %q{http://github.com/heycarsten/gcoder}
|
35
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
36
|
+
s.require_paths = ["lib"]
|
37
|
+
s.rubygems_version = %q{1.3.1}
|
38
|
+
s.summary = %q{A library for geocoding postal codes via the Google Maps Geocoding API with a persisted cache through Tokyo Tyrant}
|
39
|
+
s.test_files = [
|
40
|
+
"test/config_test.rb",
|
41
|
+
"test/gcoder_test.rb",
|
42
|
+
"test/geocoding_api_test.rb",
|
43
|
+
"test/persistence_test.rb",
|
44
|
+
"test/resolver_test.rb",
|
45
|
+
"test/test_helper.rb"
|
46
|
+
]
|
47
|
+
|
48
|
+
if s.respond_to? :specification_version then
|
49
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
50
|
+
s.specification_version = 2
|
51
|
+
|
52
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
53
|
+
else
|
54
|
+
end
|
55
|
+
else
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module GCoder
|
2
|
+
module Config
|
3
|
+
|
4
|
+
# If :tt_port is 0 then :tt_host should point to a Unix socket.
|
5
|
+
@default_settings = {
|
6
|
+
:gmaps_api_key => nil,
|
7
|
+
:gmaps_api_timeout => 2,
|
8
|
+
:tt_host => nil,
|
9
|
+
:tt_port => 0,
|
10
|
+
:append_query => nil,
|
11
|
+
:no_raise_on_connection_fail => false }
|
12
|
+
|
13
|
+
def self.merge(overrides)
|
14
|
+
@default_settings.merge(overrides)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.update(hsh)
|
18
|
+
@default_settings.update(hsh)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.[](key)
|
22
|
+
@default_settings[key]
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
module GCoder
|
2
|
+
module GeocodingAPI
|
3
|
+
|
4
|
+
BASE_URI = 'http://maps.google.com/maps/geo'
|
5
|
+
BASE_PARAMS = {
|
6
|
+
:q => nil,
|
7
|
+
:output => 'json',
|
8
|
+
:oe => 'utf8',
|
9
|
+
:sensor => 'false',
|
10
|
+
:key => nil }
|
11
|
+
|
12
|
+
|
13
|
+
class Request
|
14
|
+
|
15
|
+
def self.get(query, options = {})
|
16
|
+
response = new(query, options).get
|
17
|
+
response.validate!
|
18
|
+
response.to_h
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(query, options = {})
|
22
|
+
unless query
|
23
|
+
raise Errors::BlankRequestError, "query cannot be nil"
|
24
|
+
end
|
25
|
+
unless query.is_a?(String)
|
26
|
+
raise Errors::MalformedQueryError, "query must be String, not: #{query.class}"
|
27
|
+
end
|
28
|
+
@config = Config.merge(options)
|
29
|
+
@query = query
|
30
|
+
validate_state!
|
31
|
+
end
|
32
|
+
|
33
|
+
def params
|
34
|
+
BASE_PARAMS.merge(:key => @config[:gmaps_api_key], :q => query)
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_params
|
38
|
+
params.inject([]) do |array, (key, value)|
|
39
|
+
array << "#{uri_escape key}=#{uri_escape value}"
|
40
|
+
end.join('&')
|
41
|
+
end
|
42
|
+
|
43
|
+
def query
|
44
|
+
@config[:append_query] ? "#{@query} #{@config[:append_query]}" : @query
|
45
|
+
end
|
46
|
+
|
47
|
+
def uri
|
48
|
+
[BASE_URI, '?', to_params].join
|
49
|
+
end
|
50
|
+
|
51
|
+
def get
|
52
|
+
return @json_response if @json_response
|
53
|
+
Timeout.timeout(@config[:gmaps_api_timeout]) do
|
54
|
+
Response.new(http_get)
|
55
|
+
end
|
56
|
+
rescue Timeout::Error
|
57
|
+
raise Errors::RequestTimeoutError, 'The query timed out at ' \
|
58
|
+
"#{@config[:timeout]} second(s)"
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
def http_get
|
64
|
+
open(uri).read
|
65
|
+
end
|
66
|
+
|
67
|
+
# Snaked from Rack::Utils which 'stole' it from Camping.
|
68
|
+
def uri_escape(string)
|
69
|
+
string.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) do
|
70
|
+
'%' + $1.unpack('H2' * $1.size).join('%').upcase
|
71
|
+
end.tr(' ', '+')
|
72
|
+
end
|
73
|
+
|
74
|
+
def validate_state!
|
75
|
+
if '' == query.strip.to_s
|
76
|
+
raise Errors::BlankRequestError, 'You must specifiy a query to resolve.'
|
77
|
+
end
|
78
|
+
unless @config[:gmaps_api_key]
|
79
|
+
raise Errors::NoAPIKeyError, 'You must provide a Google Maps API ' \
|
80
|
+
'key in your configuration. Go to http://code.google.com/apis/maps/' \
|
81
|
+
'signup.html to get one.'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
class Response
|
89
|
+
|
90
|
+
def initialize(raw_response)
|
91
|
+
@response = JSON.parse(raw_response)
|
92
|
+
end
|
93
|
+
|
94
|
+
def status
|
95
|
+
@response['Status']['code']
|
96
|
+
end
|
97
|
+
|
98
|
+
def validate!
|
99
|
+
case status
|
100
|
+
when 400
|
101
|
+
raise Errors::APIMalformedRequestError, 'The GMaps Geo API has ' \
|
102
|
+
'indicated that the request is not formed correctly: ' \
|
103
|
+
"(#{@response.inspect})"
|
104
|
+
when 602
|
105
|
+
raise Errors::APIGeocodingError, 'The GMaps Geo API has indicated ' \
|
106
|
+
"that it is not able to geocode the request: (#{@response.inspect})"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def to_h
|
111
|
+
{ :accuracy => accuracy,
|
112
|
+
:country => {
|
113
|
+
:name => country_name,
|
114
|
+
:code => country_code,
|
115
|
+
:administrative_area => administrative_area },
|
116
|
+
:point => {
|
117
|
+
:longitude => longitude,
|
118
|
+
:latitude => latitude },
|
119
|
+
:box => box }
|
120
|
+
end
|
121
|
+
|
122
|
+
def box
|
123
|
+
{ :north => placemark['ExtendedData']['LatLonBox']['north'],
|
124
|
+
:south => placemark['ExtendedData']['LatLonBox']['south'],
|
125
|
+
:east => placemark['ExtendedData']['LatLonBox']['east'],
|
126
|
+
:west => placemark['ExtendedData']['LatLonBox']['west'] }
|
127
|
+
end
|
128
|
+
|
129
|
+
def accuracy
|
130
|
+
placemark['AddressDetails']['Accuracy']
|
131
|
+
end
|
132
|
+
|
133
|
+
def latitude
|
134
|
+
placemark['Point']['coordinates'][1]
|
135
|
+
end
|
136
|
+
|
137
|
+
def longitude
|
138
|
+
placemark['Point']['coordinates'][0]
|
139
|
+
end
|
140
|
+
|
141
|
+
def latlon_box
|
142
|
+
placemark['ExtendedData']['LatLonBox']
|
143
|
+
end
|
144
|
+
|
145
|
+
def country_name
|
146
|
+
placemark['AddressDetails']['Country']['CountryName']
|
147
|
+
end
|
148
|
+
|
149
|
+
def country_code
|
150
|
+
placemark['AddressDetails']['Country']['CountryNameCode']
|
151
|
+
end
|
152
|
+
|
153
|
+
def administrative_area
|
154
|
+
placemark['AddressDetails']['Country']['AdministrativeArea']['AdministrativeAreaName']
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def placemark
|
160
|
+
@response['Placemark'][0]
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module GCoder
|
2
|
+
module Persistence
|
3
|
+
|
4
|
+
|
5
|
+
class DataStore
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@config = Config.merge(options)
|
9
|
+
unless @config[:tt_host]
|
10
|
+
raise ArgumentError, ':tt_host must be specified when it is not ' \
|
11
|
+
'present in the global configuration.'
|
12
|
+
end
|
13
|
+
@tyrant = Rufus::Tokyo::Tyrant.new(@config[:tt_host], @config[:tt_port])
|
14
|
+
rescue RuntimeError => boom
|
15
|
+
if boom.message.include?('couldn\'t connect to tyrant')
|
16
|
+
errmsg = 'Unable to connect to the Tokyo Tyrant server at ' \
|
17
|
+
"#{@config[:tt_host]} [#{@config[:tt_port]}]"
|
18
|
+
if @config[:no_raise_on_connection_fail]
|
19
|
+
@tyrant = nil
|
20
|
+
STDERR.puts("[GCODER] #{errmsg}")
|
21
|
+
else
|
22
|
+
raise Errors::TTUnableToConnectError, errmsg
|
23
|
+
end
|
24
|
+
else
|
25
|
+
raise boom
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def fetch(key, &block)
|
30
|
+
unless block_given?
|
31
|
+
raise ArgumentError, 'no block was given but one was expected'
|
32
|
+
end
|
33
|
+
value = storage_get(key)
|
34
|
+
return value if value
|
35
|
+
storage_put(key, block.call(key.to_s))
|
36
|
+
end
|
37
|
+
|
38
|
+
def [](key)
|
39
|
+
storage_get(key)
|
40
|
+
end
|
41
|
+
|
42
|
+
def []=(key, value)
|
43
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
44
|
+
raise ArgumentError, "key must be String or Symbol, not: #{key.class}"
|
45
|
+
end
|
46
|
+
storage_put(key, value)
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def storage_get(key)
|
52
|
+
if tyrant
|
53
|
+
value = tyrant[key.to_s]
|
54
|
+
value ? YAML.load(value) : nil
|
55
|
+
else
|
56
|
+
STDERR.puts "[GCODER] Unable to get #{key.inspect} " \
|
57
|
+
'because there is no Tyrant connection.'
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def storage_put(key, value)
|
63
|
+
if tyrant
|
64
|
+
tyrant[key.to_s.downcase] = YAML.dump(value)
|
65
|
+
value # <- We don't want to return YAML in this case.
|
66
|
+
else
|
67
|
+
STDERR.puts "[GCODER] Unable to put #{key.inspect} " \
|
68
|
+
'because there is no Tyrant connection.'
|
69
|
+
value
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def tyrant
|
74
|
+
@tyrant
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
data/lib/gcoder.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rufus/tokyo/tyrant'
|
3
|
+
require 'json'
|
4
|
+
require 'yaml'
|
5
|
+
require 'open-uri'
|
6
|
+
require 'timeout'
|
7
|
+
|
8
|
+
$:.unshift(File.dirname(__FILE__))
|
9
|
+
|
10
|
+
require 'gcoder/config'
|
11
|
+
require 'gcoder/geocoding_api'
|
12
|
+
require 'gcoder/persistence'
|
13
|
+
require 'gcoder/resolver'
|
14
|
+
|
15
|
+
|
16
|
+
module GCoder
|
17
|
+
|
18
|
+
module Errors
|
19
|
+
class Error < StandardError; end
|
20
|
+
class MalformedQueryError < Error; end
|
21
|
+
class BlankRequestError < Error; end
|
22
|
+
class RequestTimeoutError < Error; end
|
23
|
+
class NoAPIKeyError < Error; end
|
24
|
+
class APIMalformedRequestError < Error; end
|
25
|
+
class APIGeocodingError < Error; end
|
26
|
+
class TTUnableToConnectError < Error; end
|
27
|
+
class InvalidStorageValueError < Error; end
|
28
|
+
class UnknownFormatSymbolError < Error; end
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
module ProxyMethods
|
33
|
+
def GCoder.config=(hsh)
|
34
|
+
Config.update(hsh)
|
35
|
+
end
|
36
|
+
|
37
|
+
def GCoder.connect(options = {})
|
38
|
+
Resolver.new(options)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
data/test/config_test.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ConfigTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context 'Config#merge' do
|
6
|
+
setup do
|
7
|
+
@config = GCoder::Config.merge(:gmaps_api_timeout => 3)
|
8
|
+
end
|
9
|
+
|
10
|
+
should 'return a hash of updated config settings' do
|
11
|
+
assert_instance_of Hash, @config
|
12
|
+
assert_equal 6, @config.size
|
13
|
+
assert_equal 3, @config[:gmaps_api_timeout]
|
14
|
+
end
|
15
|
+
|
16
|
+
should 'not change default configuration' do
|
17
|
+
assert_equal 2, GCoder::Config[:gmaps_api_timeout]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'Config#update' do
|
22
|
+
setup do
|
23
|
+
@config = GCoder::Config.update(:gmaps_api_timeout => 1)
|
24
|
+
end
|
25
|
+
|
26
|
+
should 'return a hash of updated config settings' do
|
27
|
+
assert_instance_of Hash, @config
|
28
|
+
assert_equal 6, @config.size
|
29
|
+
assert_equal 1, @config[:gmaps_api_timeout]
|
30
|
+
end
|
31
|
+
|
32
|
+
should 'change default configuration' do
|
33
|
+
assert_equal 1, GCoder::Config[:gmaps_api_timeout]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
data/test/gcoder_test.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class GCoderTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context 'ProxyMethods' do
|
6
|
+
should 'be present in GCoder module' do
|
7
|
+
assert_respond_to GCoder, :config=
|
8
|
+
assert_respond_to GCoder, :connect
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'GCoder.config=' do
|
12
|
+
should 'proxy to Config.update' do
|
13
|
+
GCoder::Config.expects(:update).with({})
|
14
|
+
assert GCoder.config = {}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'GCoder.connect' do
|
19
|
+
should 'proxy to Resolver.new' do
|
20
|
+
GCoder::Resolver.expects(:new).with({}).returns(:it_works)
|
21
|
+
assert_equal :it_works, GCoder.connect
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
|
4
|
+
class GeocodingAPITest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
context 'initialize with incorrect arguments' do
|
7
|
+
should 'fail with no arguments' do
|
8
|
+
assert_raise ArgumentError do
|
9
|
+
GCoder::GeocodingAPI::Request.new
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
should 'fail with any argument other than a string' do
|
14
|
+
assert_raise GCoder::Errors::BlankRequestError do
|
15
|
+
GCoder::GeocodingAPI::Request.new(nil)
|
16
|
+
end
|
17
|
+
assert_raise GCoder::Errors::MalformedQueryError do
|
18
|
+
GCoder::GeocodingAPI::Request.new(0)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
should 'fail when passed a blank string as an argument' do
|
23
|
+
assert_raise GCoder::Errors::BlankRequestError do
|
24
|
+
GCoder::GeocodingAPI::Request.new(' ')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'initialize with no API key present' do
|
30
|
+
should 'fall down, go boom' do
|
31
|
+
assert_raise GCoder::Errors::NoAPIKeyError do
|
32
|
+
GCoder::GeocodingAPI::Request.new('M6R2G5')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'query with correct arguments' do
|
38
|
+
setup do
|
39
|
+
@zip = GCoder::GeocodingAPI::Request.new('M6R2G5',
|
40
|
+
:gmaps_api_key => 'apikey')
|
41
|
+
end
|
42
|
+
|
43
|
+
should 'return parsed and tidied JSON' do
|
44
|
+
@zip.expects(:http_get).returns(PAYLOADS[:json_m6r2g5])
|
45
|
+
response = @zip.get.to_h
|
46
|
+
assert_equal 5, response[:accuracy]
|
47
|
+
assert_equal 'Canada', response[:country][:name]
|
48
|
+
assert_equal 'CA', response[:country][:code]
|
49
|
+
assert_equal 'ON', response[:country][:administrative_area]
|
50
|
+
assert_equal 43.6504650, response[:point][:latitude]
|
51
|
+
assert_equal -79.4449720, response[:point][:longitude]
|
52
|
+
assert_equal 43.6536126, response[:box][:north]
|
53
|
+
assert_equal 43.6473174, response[:box][:south]
|
54
|
+
assert_equal -79.4418244, response[:box][:east]
|
55
|
+
assert_equal -79.4481196, response[:box][:west]
|
56
|
+
end
|
57
|
+
|
58
|
+
should 'raise an error when API returns malformed request' do
|
59
|
+
@zip.expects(:http_get).returns(PAYLOADS[:json_400])
|
60
|
+
assert_raise GCoder::Errors::APIMalformedRequestError do
|
61
|
+
@zip.get.validate!
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
should 'have an appropriate URI' do
|
66
|
+
assert_match /output=json/, @zip.uri
|
67
|
+
assert_match /q=M6R2G5/, @zip.uri
|
68
|
+
assert_match /oe=u/, @zip.uri
|
69
|
+
assert_match /sensor=false/, @zip.uri
|
70
|
+
assert_match /key=apikey/, @zip.uri
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class PersistenceTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context 'DataStore.new' do
|
6
|
+
should 'throw and argument error without :tt_host specified' do
|
7
|
+
assert_raise ArgumentError do
|
8
|
+
GCoder::Persistence::DataStore.new
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
should 'throw a better error if the Tokyo Tyrant connection fails' do
|
13
|
+
assert_raise GCoder::Errors::TTUnableToConnectError do
|
14
|
+
GCoder::Persistence::DataStore.new(:tt_host => '/tmp/fake')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'DataStore' do
|
20
|
+
setup do
|
21
|
+
Rufus::Tokyo::Tyrant.stubs(:new).returns({})
|
22
|
+
@db = GCoder::Persistence::DataStore.new(:tt_host => '/tmp/tttest')
|
23
|
+
end
|
24
|
+
|
25
|
+
context '#fetch' do
|
26
|
+
should 'raise an error without any block specified' do
|
27
|
+
assert_raise ArgumentError do
|
28
|
+
@db.fetch(:stuff)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
should 'get a value that exists' do
|
33
|
+
@db[:test_key] = 'test_value'
|
34
|
+
assert_equal 'test_value', @db.fetch(:test_key) { raise 'ultrafail' }
|
35
|
+
end
|
36
|
+
|
37
|
+
should 'set the value to whatever the block yields if value is nil' do
|
38
|
+
@db.fetch(:test_key) { 'block_value' }
|
39
|
+
assert_equal 'block_value', @db[:test_key]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context '#[]=' do
|
44
|
+
should 'now allow numbers or nil as a key' do
|
45
|
+
assert_raise ArgumentError do
|
46
|
+
@db[nil] = 'extreme fail'
|
47
|
+
end
|
48
|
+
assert_raise ArgumentError do
|
49
|
+
@db[23] = 'massive fail'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context '#[]' do
|
55
|
+
should 'return nil for a key that does not exist' do
|
56
|
+
assert_nil @db['nope']
|
57
|
+
assert_nil @db['']
|
58
|
+
assert_nil @db[0]
|
59
|
+
assert_nil @db[nil]
|
60
|
+
end
|
61
|
+
|
62
|
+
should 'alow retrieval of nil' do
|
63
|
+
@db[:nil_key] = nil
|
64
|
+
assert_nil @db[:nil_key]
|
65
|
+
end
|
66
|
+
|
67
|
+
should 'return parsed JSON for keys that do exist' do
|
68
|
+
@db[:payload_1] = PAYLOADS[:test_string]
|
69
|
+
@db[:payload_2] = PAYLOADS[:test_hash]
|
70
|
+
@db[:payload_3] = PAYLOADS[:test_array]
|
71
|
+
assert_equal PAYLOADS[:test_string], @db[:payload_1]
|
72
|
+
assert_equal PAYLOADS[:test_hash], @db[:payload_2]
|
73
|
+
assert_equal PAYLOADS[:test_array], @db[:payload_3]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ResolverTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
context 'Resolver' do
|
6
|
+
setup do
|
7
|
+
Rufus::Tokyo::Tyrant.stubs(:new).returns({})
|
8
|
+
@db = GCoder::Resolver.new(:tt_host => '/tmp/tttest', :gmaps_api_key => 'testkey')
|
9
|
+
end
|
10
|
+
|
11
|
+
should 'return a hash of information for a new address' do
|
12
|
+
GCoder::GeocodingAPI::Request.any_instance.
|
13
|
+
expects(:http_get).returns(PAYLOADS[:json_m6r2g5])
|
14
|
+
assert_instance_of Hash, @db.resolve('m6r2g5')
|
15
|
+
end
|
16
|
+
|
17
|
+
should 'not call api when a cached postal code is called' do
|
18
|
+
assert_instance_of Hash, @db.resolve('m6r2g5')
|
19
|
+
end
|
20
|
+
|
21
|
+
should 'store the postal code key in the correct format' do
|
22
|
+
assert_instance_of Hash, @db.resolve('M6R2G5')
|
23
|
+
end
|
24
|
+
|
25
|
+
should 'allow [] to resolve with auto-instantiation' do
|
26
|
+
assert_instance_of Hash, @db['M6R2G5']
|
27
|
+
end
|
28
|
+
|
29
|
+
should 'raise malformed postal code error for a nil postal code' do
|
30
|
+
assert_raise GCoder::Errors::BlankRequestError do
|
31
|
+
@db.resolve(nil)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'shoulda'
|
4
|
+
require 'mocha'
|
5
|
+
|
6
|
+
$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
7
|
+
$:.unshift(File.dirname(__FILE__))
|
8
|
+
require 'gcoder'
|
9
|
+
|
10
|
+
class Test::Unit::TestCase
|
11
|
+
|
12
|
+
PAYLOADS = {
|
13
|
+
:json_m6r2g5 => %q<
|
14
|
+
{
|
15
|
+
"name": "M6R2G5",
|
16
|
+
"Status": {
|
17
|
+
"code": 200,
|
18
|
+
"request": "geocode"
|
19
|
+
},
|
20
|
+
"Placemark": [ {
|
21
|
+
"id": "p1",
|
22
|
+
"address": "Ontario M6R 2G5, Canada",
|
23
|
+
"AddressDetails": {"Country": {"CountryNameCode": "CA","CountryName": "Canada","AdministrativeArea": {"AdministrativeAreaName": "ON","PostalCode": {"PostalCodeNumber": "M6R 2G5"}}},"Accuracy": 5},
|
24
|
+
"ExtendedData": {
|
25
|
+
"LatLonBox": {
|
26
|
+
"north": 43.6536126,
|
27
|
+
"south": 43.6473174,
|
28
|
+
"east": -79.4418244,
|
29
|
+
"west": -79.4481196
|
30
|
+
}
|
31
|
+
},
|
32
|
+
"Point": {
|
33
|
+
"coordinates": [ -79.4449720, 43.6504650, 0 ]
|
34
|
+
}
|
35
|
+
} ]
|
36
|
+
}>,
|
37
|
+
:json_602 => %q<
|
38
|
+
{
|
39
|
+
"name": "crashbangboom",
|
40
|
+
"Status": {
|
41
|
+
"code": 602,
|
42
|
+
"request": "geocode"
|
43
|
+
}
|
44
|
+
}>,
|
45
|
+
:json_400 => %q<
|
46
|
+
{
|
47
|
+
"name": "",
|
48
|
+
"Status": {
|
49
|
+
"code": 400,
|
50
|
+
"request": "geocode"
|
51
|
+
}
|
52
|
+
}>,
|
53
|
+
:test_string => "test\nstring",
|
54
|
+
:test_hash => { 'test' => 'value', 100 => 'one hundred' },
|
55
|
+
:test_array => ['test', 1, 3.1415, true, false, nil]
|
56
|
+
}
|
57
|
+
|
58
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: heycarsten-gcoder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Carsten Nielsen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-26 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: heycarsten@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
files:
|
25
|
+
- .document
|
26
|
+
- .gitignore
|
27
|
+
- README.rdoc
|
28
|
+
- Rakefile
|
29
|
+
- VERSION.yml
|
30
|
+
- gcoder.gemspec
|
31
|
+
- lib/gcoder.rb
|
32
|
+
- lib/gcoder/config.rb
|
33
|
+
- lib/gcoder/geocoding_api.rb
|
34
|
+
- lib/gcoder/persistence.rb
|
35
|
+
- lib/gcoder/resolver.rb
|
36
|
+
- test/config_test.rb
|
37
|
+
- test/gcoder_test.rb
|
38
|
+
- test/geocoding_api_test.rb
|
39
|
+
- test/persistence_test.rb
|
40
|
+
- test/resolver_test.rb
|
41
|
+
- test/test_helper.rb
|
42
|
+
has_rdoc: true
|
43
|
+
homepage: http://github.com/heycarsten/gcoder
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options:
|
46
|
+
- --charset=UTF-8
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 1.2.0
|
65
|
+
signing_key:
|
66
|
+
specification_version: 2
|
67
|
+
summary: A library for geocoding postal codes via the Google Maps Geocoding API with a persisted cache through Tokyo Tyrant
|
68
|
+
test_files:
|
69
|
+
- test/config_test.rb
|
70
|
+
- test/gcoder_test.rb
|
71
|
+
- test/geocoding_api_test.rb
|
72
|
+
- test/persistence_test.rb
|
73
|
+
- test/resolver_test.rb
|
74
|
+
- test/test_helper.rb
|