heycarsten-postalcoder 0.1.3

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/README.rdoc ADDED
@@ -0,0 +1,7 @@
1
+ = PostalCoder
2
+
3
+ Instead of letting postal codes turn me into a postal coder, I made this gem!
4
+
5
+ == Copyright
6
+
7
+ Copyright (C) 2009 Carsten Nielsen. See LICENSE for details.
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 = 'postalcoder'
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/postalcoder'
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 = "PostalCoder #{version}"
56
+ # rdoc.rdoc_files.include('README*')
57
+ # rdoc.rdoc_files.include('lib/**/*.rb')
58
+ # end
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 1
3
+ :patch: 3
4
+ :major: 0
@@ -0,0 +1,25 @@
1
+ module PostalCoder
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
+ :accepted_formats => [:ca_postal_code, :us_zip_code] }
11
+
12
+ def self.merge(overrides)
13
+ @default_settings.merge(overrides)
14
+ end
15
+
16
+ def self.update(hsh)
17
+ @default_settings.update(hsh)
18
+ end
19
+
20
+ def self.[](key)
21
+ @default_settings[key]
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,120 @@
1
+ module PostalCoder
2
+ module Formats
3
+
4
+ SYMBOLS_TO_NAMES_MAP = {
5
+ :ca_postal_code => 'CAPostalCode',
6
+ :us_zip_code => 'USZipCode' }
7
+
8
+ def self.symbol_to_class(symbol)
9
+ unless symbol.is_a?(Symbol)
10
+ raise ArgumentError, "expected Symbol, not: #{symbol.class}"
11
+ end
12
+ class_name = SYMBOLS_TO_NAMES_MAP.fetch(symbol) do |sym|
13
+ raise Errors::UnknownFormatSymbolError, 'The format symbol ' \
14
+ "#{sym.inspect} is not one of the reconized symbols, which are: " \
15
+ "#{SYMBOLS_TO_NAMES_MAP.keys.inspect}"
16
+ end
17
+ eval class_name
18
+ end
19
+
20
+ def self.symbols_to_classes(symbols)
21
+ unless symbols.is_a?(Array)
22
+ raise ArgumentError, "symbols must be Array, not: #{symbols.class}"
23
+ end
24
+ if symbols.empty?
25
+ raise ArgumentError, 'symbols must contain format symbols, it is empty.'
26
+ end
27
+ symbols.map { |f| symbol_to_class(f) }
28
+ end
29
+
30
+ def self.instantiate(postal_code, format_symbol = nil)
31
+ unless postal_code.is_a?(String)
32
+ raise Errors::MalformedPostalCodeError, 'postal_code must be a ' \
33
+ "String, not: #{postal_code.class}"
34
+ end
35
+ if format_symbol
36
+ symbol_to_class(format_symbol).new(postal_code)
37
+ else
38
+ auto_instantiate(postal_code)
39
+ end
40
+ end
41
+
42
+ def self.auto_instantiate(postal_code, accepted_formats = Config[:accepted_formats])
43
+ results = symbols_to_classes(accepted_formats).map do |format|
44
+ begin
45
+ format.new(postal_code)
46
+ rescue Errors::MalformedPostalCodeError
47
+ nil
48
+ end
49
+ end.compact
50
+ return results[0] if results.any?
51
+ raise Errors::MalformedPostalCodeError, "The postal code: #{postal_code}" \
52
+ ' did not properly map to any of the accepted formats.'
53
+ end
54
+
55
+
56
+ class AbstractFormat
57
+
58
+ attr_reader :value
59
+
60
+ def initialize(raw_value)
61
+ unless raw_value.is_a?(String)
62
+ raise ArgumentError, "value must be String, not: #{raw_value.class}"
63
+ end
64
+ @value = cleanup(raw_value)
65
+ validate_form!
66
+ end
67
+
68
+ def to_s
69
+ value
70
+ end
71
+
72
+ def cleanup(raw_value)
73
+ raw_value.upcase.gsub(/\s|\-/, '')
74
+ end
75
+
76
+ def has_valid_form?
77
+ raise NotImplementedError, "#{self.class}#has_valid_form? must be " \
78
+ 'implemented.'
79
+ end
80
+
81
+ protected
82
+
83
+ def validate_form!
84
+ return true if has_valid_form?
85
+ raise Errors::MalformedPostalCodeError, "#{@value.inspect} is not a " \
86
+ "properly formed #{self.class}"
87
+ end
88
+
89
+ end
90
+
91
+
92
+ class CAPostalCode < AbstractFormat
93
+
94
+ def has_valid_form?
95
+ value =~ /\A([A-Z][0-9]){3}\Z/
96
+ end
97
+
98
+ end
99
+
100
+
101
+ class USZipCode < AbstractFormat
102
+
103
+ def to_s
104
+ case value.length
105
+ when 5
106
+ value
107
+ when 9
108
+ "#{value[0,5]}-#{value[5,4]}"
109
+ end
110
+ end
111
+
112
+ def has_valid_form?
113
+ value =~ /\A([0-9]{5}|[0-9]{9})\Z/
114
+ end
115
+
116
+ end
117
+
118
+
119
+ end
120
+ end
@@ -0,0 +1,121 @@
1
+ module PostalCoder
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 Query
14
+
15
+ attr_reader :query
16
+
17
+ def self.get(query, options = {})
18
+ new(query, options).to_hash
19
+ end
20
+
21
+ def initialize(query, options = {})
22
+ unless query.is_a?(String)
23
+ raise ArgumentError, "query must be a String, not: #{query.class}"
24
+ end
25
+ @config = Config.merge(options)
26
+ @query = query
27
+ validate_state!
28
+ end
29
+
30
+ def to_hash
31
+ return tidy_response if @response
32
+ parse_json_response(http_get)
33
+ end
34
+
35
+ def params
36
+ BASE_PARAMS.merge(:key => @config[:gmaps_api_key], :q => query)
37
+ end
38
+
39
+ def to_params
40
+ # No need to escape the keys and values because (so far) they do not
41
+ # contain escapable characters. -- CKN
42
+ params.inject([]) { |a, (k, v)| a << "#{k}=#{v}" }.join('&')
43
+ end
44
+
45
+ def uri
46
+ [BASE_URI, '?', to_params].join
47
+ end
48
+
49
+ protected
50
+
51
+ def placemark
52
+ @response['Placemark'][0]
53
+ end
54
+
55
+ def latlon_box
56
+ placemark['ExtendedData']['LatLonBox']
57
+ end
58
+
59
+ def country
60
+ placemark['AddressDetails']['Country']
61
+ end
62
+
63
+ def tidy_response
64
+ { :accuracy => placemark['AddressDetails']['Accuracy'],
65
+ :country => {
66
+ :name => country['CountryName'],
67
+ :code => country['CountryNameCode'],
68
+ :administrative_area => country['AdministrativeArea']['AdministrativeAreaName']
69
+ },
70
+ :point => {
71
+ :latitude => placemark['Point']['coordinates'][0],
72
+ :longitude => placemark['Point']['coordinates'][1]
73
+ },
74
+ :box => {
75
+ :north => latlon_box['north'],
76
+ :south => latlon_box['south'],
77
+ :east => latlon_box['east'],
78
+ :west => latlon_box['west']
79
+ } }
80
+ end
81
+
82
+ def http_get
83
+ return @json_response if @json_response
84
+ Timeout.timeout(@config[:gmaps_api_timeout]) do
85
+ @json_response = open(uri).read
86
+ end
87
+ rescue Timeout::TimeoutError
88
+ raise Errors::QueryTimeoutError, 'The query timed out at ' \
89
+ "#{@config[:timeout]} second(s)"
90
+ end
91
+
92
+ def parse_json_response(json_response)
93
+ @response = JSON.parse(json_response)
94
+ case @response['Status']['code']
95
+ when 200
96
+ tidy_response
97
+ when 400
98
+ raise Errors::APIMalformedRequestError, 'The GMaps Geo API has ' \
99
+ 'indicated that the request is not formed correctly: ' \
100
+ "(#{query.inspect})"
101
+ when 602
102
+ raise Errors::APIGeocodingError, 'The GMaps Geo API has indicated ' \
103
+ "that it is not able to geocode the request: (#{query.inspect})"
104
+ end
105
+ end
106
+
107
+ def validate_state!
108
+ if '' == query.strip.to_s
109
+ raise Errors::BlankQueryError, 'You must specifiy a query to resolve.'
110
+ end
111
+ unless @config[:gmaps_api_key]
112
+ raise Errors::NoAPIKeyError, 'You must provide a Google Maps API ' \
113
+ 'key in your configuration. Go to http://code.google.com/apis/maps/' \
114
+ 'signup.html to get one.'
115
+ end
116
+ end
117
+
118
+ end
119
+
120
+ end
121
+ end
@@ -0,0 +1,62 @@
1
+ module PostalCoder
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
+ raise Errors::TTUnableToConnectError, 'Unable to connect to the ' \
17
+ "Tokyo Tyrant server at #{@config[:tt_host]} [#{@config[:tt_port]}]"
18
+ else
19
+ raise boom
20
+ end
21
+ end
22
+
23
+ def fetch(key, &block)
24
+ unless block_given?
25
+ raise ArgumentError, 'no block was given but one was expected'
26
+ end
27
+ value = storage_get(key)
28
+ return value if value
29
+ storage_put(key, block.call(key.to_s))
30
+ end
31
+
32
+ def [](key)
33
+ storage_get(key)
34
+ end
35
+
36
+ def []=(key, value)
37
+ unless key.is_a?(String) || key.is_a?(Symbol)
38
+ raise ArgumentError, "key must be String or Symbol, not: #{key.class}"
39
+ end
40
+ storage_put(key, value)
41
+ end
42
+
43
+ protected
44
+
45
+ def storage_get(key)
46
+ value = tyrant[key.to_s]
47
+ value ? YAML.load(value) : nil
48
+ end
49
+
50
+ def storage_put(key, value)
51
+ tyrant[key.to_s] = YAML.dump(value)
52
+ value # <- We don't want to return YAML in this case.
53
+ end
54
+
55
+ def tyrant
56
+ @tyrant
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,16 @@
1
+ module PostalCoder
2
+ class Resolver < Persistence::DataStore
3
+
4
+ def resolve(postal_code_value, format_symbol = nil)
5
+ postal_code = Formats.instantiate(postal_code_value, format_symbol)
6
+ fetch(postal_code.to_s) do |code|
7
+ GeocodingAPI::Query.get(code, @config)
8
+ end
9
+ end
10
+
11
+ def [](postal_code)
12
+ resolve(postal_code)
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,43 @@
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 'postalcoder/config'
11
+ require 'postalcoder/formats'
12
+ require 'postalcoder/geocoding_api'
13
+ require 'postalcoder/persistence'
14
+ require 'postalcoder/resolver'
15
+
16
+
17
+ module PostalCoder
18
+
19
+ module Errors
20
+ class Error < StandardError; end
21
+ class MalformedPostalCodeError < Error; end
22
+ class BlankQueryError < Error; end
23
+ class QueryTimeoutError < Error; end
24
+ class NoAPIKeyError < Error; end
25
+ class APIMalformedRequestError < Error; end
26
+ class APIGeocodingError < Error; end
27
+ class TTUnableToConnectError < Error; end
28
+ class InvalidStorageValueError < Error; end
29
+ class UnknownFormatSymbolError < Error; end
30
+ end
31
+
32
+
33
+ module ProxyMethods
34
+ def PostalCoder.config=(hsh)
35
+ Config.update(hsh)
36
+ end
37
+
38
+ def PostalCoder.connect(options = {})
39
+ Resolver.new(options)
40
+ end
41
+ end
42
+
43
+ end
@@ -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 = PostalCoder::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 5, @config.size
13
+ assert_equal 3, @config[:gmaps_api_timeout]
14
+ end
15
+
16
+ should 'not change default configuration' do
17
+ assert_equal 2, PostalCoder::Config[:gmaps_api_timeout]
18
+ end
19
+ end
20
+
21
+ context 'Config#update' do
22
+ setup do
23
+ @config = PostalCoder::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 5, @config.size
29
+ assert_equal 1, @config[:gmaps_api_timeout]
30
+ end
31
+
32
+ should 'change default configuration' do
33
+ assert_equal 1, PostalCoder::Config[:gmaps_api_timeout]
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,163 @@
1
+ require 'test_helper'
2
+
3
+ class FormatsTest < Test::Unit::TestCase
4
+
5
+ context 'Formats.symbol_to_class' do
6
+ should 'throw an argument error unless given a symbol' do
7
+ assert_raise ArgumentError do
8
+ PostalCoder::Formats.symbol_to_class('boom!')
9
+ end
10
+ end
11
+
12
+ should 'throw an error when passed an unknown format symbol' do
13
+ assert_raise PostalCoder::Errors::UnknownFormatSymbolError do
14
+ PostalCoder::Formats.symbol_to_class(:uberfail)
15
+ end
16
+ end
17
+
18
+ should 'return the appropriate class when passed the proper symbol' do
19
+ assert_equal PostalCoder::Formats::CAPostalCode,
20
+ PostalCoder::Formats.symbol_to_class(:ca_postal_code)
21
+ end
22
+ end
23
+
24
+ context 'Formats.symbols_to_classes' do
25
+ should 'throw an argument error unless passed an array' do
26
+ assert_raise ArgumentError do
27
+ PostalCoder::Formats.symbols_to_classes(nil)
28
+ end
29
+ end
30
+
31
+ should 'throw an argument error if passed an empty array' do
32
+ assert_raise ArgumentError do
33
+ PostalCoder::Formats.symbols_to_classes([])
34
+ end
35
+ end
36
+
37
+ should 'return the appropriate classes when passed proper symbols' do
38
+ assert_equal [PostalCoder::Formats::CAPostalCode],
39
+ PostalCoder::Formats.symbols_to_classes([:ca_postal_code])
40
+ end
41
+ end
42
+
43
+ context 'Formats.auto_instantiate' do
44
+ should 'raise a malformed postal code error if no accepted formats match the input' do
45
+ assert_raise PostalCoder::Errors::MalformedPostalCodeError do
46
+ PostalCoder::Formats.auto_instantiate('failtron')
47
+ end
48
+ end
49
+
50
+ should 'return the first matching instance' do
51
+ assert_instance_of PostalCoder::Formats::USZipCode,
52
+ PostalCoder::Formats.auto_instantiate('20037')
53
+ end
54
+ end
55
+
56
+ context 'Formats.instantiate' do
57
+ should 'raise an error if passed nil as a postal code' do
58
+ assert_raise PostalCoder::Errors::MalformedPostalCodeError do
59
+ PostalCoder::Formats.instantiate(nil)
60
+ end
61
+ end
62
+
63
+ should 'auto instantiate to the first matching accepted format if no format is specified' do
64
+ assert_instance_of PostalCoder::Formats::CAPostalCode,
65
+ PostalCoder::Formats.instantiate('m6r2g5')
66
+ end
67
+
68
+ should 'return true if passed a postal code that matches at least one of the accepted formats' do
69
+ assert_instance_of PostalCoder::Formats::CAPostalCode,
70
+ PostalCoder::Formats.instantiate('m6r2g5', :ca_postal_code)
71
+ end
72
+ end
73
+
74
+ context 'Creating an instance of AbstractFormat' do
75
+ should 'throw a not implemented error' do
76
+ assert_raise NotImplementedError do
77
+ PostalCoder::Formats::AbstractFormat.new('test')
78
+ end
79
+ end
80
+ end
81
+
82
+ context 'Creating an instance of USZipCode' do
83
+ should 'fail with the wrong type' do
84
+ assert_raise ArgumentError do
85
+ PostalCoder::Formats::USZipCode.new(nil)
86
+ end
87
+ assert_raise ArgumentError do
88
+ PostalCoder::Formats::USZipCode.new(20037)
89
+ end
90
+ end
91
+
92
+ should 'fail with a string of the wrong format' do
93
+ assert_raise PostalCoder::Errors::MalformedPostalCodeError do
94
+ PostalCoder::Formats::USZipCode.new('2003')
95
+ end
96
+ assert_raise PostalCoder::Errors::MalformedPostalCodeError do
97
+ PostalCoder::Formats::USZipCode.new('20037-8')
98
+ end
99
+ assert_raise PostalCoder::Errors::MalformedPostalCodeError do
100
+ PostalCoder::Formats::USZipCode.new('')
101
+ end
102
+ assert_raise PostalCoder::Errors::MalformedPostalCodeError do
103
+ PostalCoder::Formats::USZipCode.new('BOOM7-8001')
104
+ end
105
+ end
106
+
107
+ should 'work with whitespace present' do
108
+ assert PostalCoder::Formats::USZipCode.new(' 200 37 ')
109
+ end
110
+
111
+ should 'allow ZIP+4 with or without a dash' do
112
+ assert PostalCoder::Formats::USZipCode.new('20037-8001')
113
+ assert PostalCoder::Formats::USZipCode.new('20037 8001')
114
+ assert PostalCoder::Formats::USZipCode.new('200378001')
115
+ end
116
+ end
117
+
118
+ context 'An instance of USZipCode for ZIP+4 address' do
119
+ setup do
120
+ @zip = PostalCoder::Formats::USZipCode.new(' 20037 8001 ')
121
+ end
122
+
123
+ should 'reformat the value to include a dash' do
124
+ assert_equal '20037-8001', @zip.to_s
125
+ end
126
+ end
127
+
128
+ context 'Creating an instance of CAPostalCode' do
129
+ should 'fail with the wrong type' do
130
+ assert_raise ArgumentError do
131
+ PostalCoder::Formats::CAPostalCode.new(123456)
132
+ end
133
+ assert_raise ArgumentError do
134
+ PostalCoder::Formats::CAPostalCode.new(nil)
135
+ end
136
+ end
137
+
138
+ should 'fail with a string of the wrong format' do
139
+ assert_raise PostalCoder::Errors::MalformedPostalCodeError do
140
+ PostalCoder::Formats::CAPostalCode.new('M6R')
141
+ end
142
+ assert_raise PostalCoder::Errors::MalformedPostalCodeError do
143
+ PostalCoder::Formats::CAPostalCode.new('')
144
+ end
145
+ assert_raise PostalCoder::Errors::MalformedPostalCodeError do
146
+ PostalCoder::Formats::CAPostalCode.new('M6R2G5A1')
147
+ end
148
+ assert_raise PostalCoder::Errors::MalformedPostalCodeError do
149
+ PostalCoder::Formats::CAPostalCode.new('M6R205')
150
+ end
151
+ end
152
+
153
+ should 'work with whitespace present' do
154
+ assert PostalCoder::Formats::CAPostalCode.new(' M6R2G5 ')
155
+ assert PostalCoder::Formats::CAPostalCode.new(' M6R 2G5 ')
156
+ end
157
+
158
+ should 'upcase any lowercase characters' do
159
+ assert_equal 'M6R2G5', PostalCoder::Formats::CAPostalCode.new('m6r2g5').to_s
160
+ end
161
+ end
162
+
163
+ end
@@ -0,0 +1,73 @@
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
+ PostalCoder::GeocodingAPI::Query.new
10
+ end
11
+ end
12
+
13
+ should 'fail with any argument other than a string' do
14
+ assert_raise ArgumentError do
15
+ PostalCoder::GeocodingAPI::Query.new(nil)
16
+ end
17
+ assert_raise ArgumentError do
18
+ PostalCoder::GeocodingAPI::Query.new(0)
19
+ end
20
+ end
21
+
22
+ should 'fail when passed a blank string as an argument' do
23
+ assert_raise PostalCoder::Errors::BlankQueryError do
24
+ PostalCoder::GeocodingAPI::Query.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 PostalCoder::Errors::NoAPIKeyError do
32
+ PostalCoder::GeocodingAPI::Query.new('M6R2G5')
33
+ end
34
+ end
35
+ end
36
+
37
+ context 'query with correct arguments' do
38
+ setup do
39
+ @zip = PostalCoder::GeocodingAPI::Query.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
+ assert_equal 5, @zip.to_hash[:accuracy]
46
+ assert_equal 'Canada', @zip.to_hash[:country][:name]
47
+ assert_equal 'CA', @zip.to_hash[:country][:code]
48
+ assert_equal 'ON', @zip.to_hash[:country][:administrative_area]
49
+ assert_equal -79.4449720, @zip.to_hash[:point][:latitude]
50
+ assert_equal 43.6504650, @zip.to_hash[:point][:longitude]
51
+ assert_equal 43.6536126, @zip.to_hash[:box][:north]
52
+ assert_equal 43.6473174, @zip.to_hash[:box][:south]
53
+ assert_equal -79.4418244, @zip.to_hash[:box][:east]
54
+ assert_equal -79.4481196, @zip.to_hash[:box][:west]
55
+ end
56
+
57
+ should 'raise an error when API returns malformed request' do
58
+ @zip.expects(:http_get).returns(PAYLOADS[:json_400])
59
+ assert_raise PostalCoder::Errors::APIMalformedRequestError do
60
+ @zip.to_hash
61
+ end
62
+ end
63
+
64
+ should 'have an appropriate URI' do
65
+ assert_match /output=json/, @zip.uri
66
+ assert_match /q=M6R2G5/, @zip.uri
67
+ assert_match /oe=u/, @zip.uri
68
+ assert_match /sensor=false/, @zip.uri
69
+ assert_match /key=apikey/, @zip.uri
70
+ end
71
+ end
72
+
73
+ 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
+ PostalCoder::Persistence::DataStore.new
9
+ end
10
+ end
11
+
12
+ should 'throw a better error if the Tokyo Tyrant connection fails' do
13
+ assert_raise PostalCoder::Errors::TTUnableToConnectError do
14
+ PostalCoder::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 = PostalCoder::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,26 @@
1
+ require 'test_helper'
2
+
3
+ class PostalCoderTest < Test::Unit::TestCase
4
+
5
+ context 'ProxyMethods' do
6
+ should 'be present in PostalCoder module' do
7
+ assert_respond_to PostalCoder, :config=
8
+ assert_respond_to PostalCoder, :connect
9
+ end
10
+
11
+ context 'PostalCoder.config=' do
12
+ should 'proxy to Config.update' do
13
+ PostalCoder::Config.expects(:update).with({})
14
+ assert PostalCoder.config = {}
15
+ end
16
+ end
17
+
18
+ context 'PostalCoder.connect' do
19
+ should 'proxy to Resolver.new' do
20
+ PostalCoder::Resolver.expects(:new).with({}).returns(:it_works)
21
+ assert_equal :it_works, PostalCoder.connect
22
+ end
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,42 @@
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 = PostalCoder::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
+ PostalCoder::GeocodingAPI::Query.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 malformed postal code' do
30
+ assert_raise PostalCoder::Errors::MalformedPostalCodeError do
31
+ @db.resolve('m6r212')
32
+ end
33
+ end
34
+
35
+ should 'raise malformed postal code error for a nil postal code' do
36
+ assert_raise PostalCoder::Errors::MalformedPostalCodeError do
37
+ @db.resolve(nil)
38
+ end
39
+ end
40
+ end
41
+
42
+ end
@@ -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 'postalcoder'
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-postalcoder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Carsten Nielsen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-05-20 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
+ - README.rdoc
26
+ - Rakefile
27
+ - VERSION.yml
28
+ - lib/postalcoder.rb
29
+ - lib/postalcoder/config.rb
30
+ - lib/postalcoder/formats.rb
31
+ - lib/postalcoder/geocoding_api.rb
32
+ - lib/postalcoder/persistence.rb
33
+ - lib/postalcoder/resolver.rb
34
+ - test/config_test.rb
35
+ - test/formats_test.rb
36
+ - test/geocoding_api_test.rb
37
+ - test/persistence_test.rb
38
+ - test/postalcoder_test.rb
39
+ - test/resolver_test.rb
40
+ - test/test_helper.rb
41
+ has_rdoc: true
42
+ homepage: http://github.com/heycarsten/postalcoder
43
+ post_install_message:
44
+ rdoc_options:
45
+ - --charset=UTF-8
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.2.0
64
+ signing_key:
65
+ specification_version: 2
66
+ summary: A library for geocoding postal codes via the Google Maps Geocoding API with a persisted cache through Tokyo Tyrant
67
+ test_files:
68
+ - test/config_test.rb
69
+ - test/formats_test.rb
70
+ - test/geocoding_api_test.rb
71
+ - test/persistence_test.rb
72
+ - test/postalcoder_test.rb
73
+ - test/resolver_test.rb
74
+ - test/test_helper.rb