ideal_postcodes 0.1.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/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # Ideal-Postcodes.co.uk API Wrapper
2
+
3
+ Get a full list of addresses for any given UK postcode using the Ideal-Postcodes.co.uk API. We use the most accurate addressing database in the UK, Royal Mail's Postcode Address File.
4
+
5
+ ## Getting Started
6
+
7
+ __Install it__
8
+
9
+ ```bash
10
+ gem install ideal_postcodes
11
+ ```
12
+
13
+ __Get an API Key__
14
+
15
+ Get a key [Ideal-Postcodes.co.uk](https://ideal-postcodes.co.uk). Try out the service with the test postcode 'ID1 1QD'
16
+
17
+ __Use it__
18
+
19
+ Do address lookups with a few lines of Ruby
20
+
21
+ ```ruby
22
+ require 'ideal_postcodes'
23
+
24
+ IdealPostcodes.api_key = "your_key_goes_here"
25
+
26
+ postcode = IdealPostcodes::Postcode.lookup "ID1 1QD"
27
+
28
+ # postcode.addresses =>
29
+ #
30
+ # [
31
+ # {
32
+ # :postcode=>"ID1 1QD",
33
+ # :post_town=>"LONDON",
34
+ # :line_1=>"Kingsley Hall",
35
+ # :line_2=>"Powis Road",
36
+ # :line_3=>""
37
+ # },
38
+ # ... and so on
39
+ ```
40
+
41
+ ## Registering
42
+
43
+ PAF is licensed from the Royal Mail and is, unfortunately, not free to use. Ideal Postcodes aims to be simple to use and fairly priced to use for web and mobile developers.
44
+
45
+ We charge _2p_ per [external](https://ideal-postcodes.co.uk/termsandconditions#external) lookup.
46
+
47
+ ## Documentation
48
+
49
+ More documentation can be found [here](https://ideal-postcodes.co.uk/documentation/ruby-wrapper)
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/test*.rb']
7
+ t.verbose = true
8
+ end
@@ -0,0 +1,25 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
2
+
3
+ require 'idealpostcodes/version'
4
+
5
+ spec = Gem::Specification.new do |s|
6
+ s.name = 'ideal_postcodes'
7
+ s.version = IdealPostcodes::VERSION
8
+ s.summary = 'Wrapper for the Ideal-Postcodes.co.uk API'
9
+ s.description = 'Ideal Postcodes is a simple postcode lookup API for UK addresses. See https://ideal-postcodes.co.uk'
10
+ s.authors = ['Chris Blanchard']
11
+ s.email = ['cablanchard@gmail.com']
12
+ s.homepage = 'https://ideal-postcodes.co.uk/documentation'
13
+
14
+ s.add_dependency('rest-client', '~> 1.6')
15
+ s.add_dependency('multi_json', '~> 1.7.9')
16
+
17
+ s.add_development_dependency('mocha', '~> 0.14.0')
18
+ s.add_development_dependency('test-unit')
19
+ s.add_development_dependency('shoulda', '~> 3.5.0')
20
+ s.add_development_dependency('rake')
21
+
22
+ s.files = `git ls-files`.split("\n")
23
+ s.test_files = `git ls-files -- test/*`.split("\n")
24
+ s.require_paths = ['lib']
25
+ end
@@ -0,0 +1,2 @@
1
+ # Allows to reference by ideal_postcodes
2
+ require 'idealpostcodes'
@@ -0,0 +1,91 @@
1
+ require 'rest-client'
2
+ require 'uri'
3
+ require 'multi_json'
4
+ require 'cgi'
5
+ require 'idealpostcodes/version'
6
+ require 'idealpostcodes/postcode'
7
+ require 'idealpostcodes/util'
8
+ require 'idealpostcodes/errors'
9
+
10
+ module IdealPostcodes
11
+ @base_url = 'https://api.ideal-postcodes.co.uk'
12
+ @version = '1'
13
+
14
+ class << self
15
+ attr_accessor :api_key, :base_url, :version
16
+ end
17
+
18
+ def self.request(method, path, params = {})
19
+ unless @api_key
20
+ raise IdealPostcodes::AuthenticationError.new('No API Key provided. ' +
21
+ 'Set your key with IdealPostcodes.api_key = #your_key')
22
+ end
23
+
24
+ url = URI.parse(resource_url(path))
25
+ params.merge! api_key: @api_key
26
+ url.query = Util.merge_params(params)
27
+ request_options = {
28
+ method: method.downcase.to_sym,
29
+ url: url.to_s
30
+ }
31
+
32
+ begin
33
+ response = generate_request(request_options)
34
+ rescue RestClient::ExceptionWithResponse => error
35
+ if rcode = error.http_code && rbody = error.http_body
36
+ handle_error(rcode, rbody)
37
+ else
38
+ handle_client_error(error)
39
+ end
40
+ rescue RestClient::Exception, Errno::ECONNREFUSED => error
41
+ handle_client_error(e)
42
+ end
43
+ parse response.body
44
+ end
45
+
46
+ private
47
+
48
+ def self.resource_url(path='')
49
+ URI.escape "#{@base_url}/v#{@version}/#{path}"
50
+ end
51
+
52
+ def self.generate_request(options)
53
+ RestClient::Request.execute(options)
54
+ end
55
+
56
+ def self.parse(response)
57
+ begin
58
+ Util.keys_to_sym MultiJson.load(response)
59
+ rescue MultiJson::DecodeError => e
60
+ raise handle_client_error(e)
61
+ end
62
+ end
63
+
64
+ def self.handle_error(http_code, http_body)
65
+ error = parse http_body
66
+
67
+ ideal_code, ideal_message = error[:code], error[:message]
68
+
69
+ case ideal_code
70
+ when 4010
71
+ raise AuthenticationError.new ideal_message, http_code, http_body, ideal_code
72
+ when 4020
73
+ raise TokenExhaustedError.new ideal_message, http_code, http_body, ideal_code
74
+ when 4021
75
+ raise LimitReachedError.new ideal_message, http_code, http_body, ideal_code
76
+ when 4040
77
+ raise ResourceNotFoundError.new ideal_message, http_code, http_body, ideal_code
78
+ else
79
+ raise IdealPostcodesError.new ideal_message, http_code, http_body, ideal_code
80
+ end
81
+ end
82
+
83
+ def self.handle_client_error(error)
84
+ raise IdealPostcodesError.new("An unexpected error occured: #{error.message})")
85
+ end
86
+
87
+ def self.general_error(response_code, response_body)
88
+ IdealPostcodesError.new "Invalid response object", response_code, response_body
89
+ end
90
+
91
+ end
@@ -0,0 +1,33 @@
1
+ module IdealPostcodes
2
+ class IdealPostcodesError < StandardError
3
+ attr_reader :message
4
+ attr_reader :http_code
5
+ attr_reader :http_body
6
+ attr_reader :response_code
7
+
8
+ def initialize(message = nil, http_code = nil, http_body = nil, response_code = nil)
9
+ @message = message
10
+ @http_code = http_code
11
+ @http_body = http_body
12
+ @response_code = response_code
13
+ end
14
+
15
+ def to_s
16
+ status = @http_code.nil? ? "" : "#{@http_code} error."
17
+ ideal_code = @response_code.nil ? "" : "(#{@response_code})"
18
+ "#{status} error. (#{ideal_code}) #{message}"
19
+ end
20
+ end
21
+
22
+ class AuthenticationError< IdealPostcodesError
23
+ end
24
+
25
+ class TokenExhaustedError < IdealPostcodesError
26
+ end
27
+
28
+ class LimitReachedError < IdealPostcodesError
29
+ end
30
+
31
+ class ResourceNotFoundError < IdealPostcodesError
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ module IdealPostcodes
2
+ class Postcode
3
+
4
+ attr_reader :postcode_data, :postcode, :addresses
5
+
6
+ def initialize(postcode = nil, postcode_data = nil)
7
+ @raw = postcode_data
8
+ @addresses = (postcode_data.nil? || postcode_data[:result].nil?) ? [] : postcode_data[:result]
9
+ @postcode = postcode
10
+ end
11
+
12
+ def self.lookup(postcode)
13
+ begin
14
+ response = IdealPostcodes.request :get, "postcodes/#{postcode}"
15
+ rescue IdealPostcodes::ResourceNotFoundError => error
16
+ raise error unless error.response_code == 4040
17
+ response = nil
18
+ end
19
+ new postcode, response
20
+ end
21
+
22
+ def empty?
23
+ @raw.nil?
24
+ end
25
+
26
+ def addresses
27
+ @addresses
28
+ end
29
+
30
+ def to_s
31
+ addresses.to_s
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ module IdealPostcodes
2
+ class Util
3
+
4
+ def self.merge_params(hash)
5
+ result = []
6
+ hash.each do |key, value|
7
+ result << "#{CGI.escape(key.to_s)}=#{CGI.escape(value)}"
8
+ end
9
+ result.join("&")
10
+ end
11
+
12
+ def self.keys_to_sym(object)
13
+ case object
14
+ when Hash
15
+ temp = {}
16
+ object.each do |key, value|
17
+ key = (key.to_sym rescue key) || key
18
+ temp[key] = keys_to_sym(value)
19
+ end
20
+ temp
21
+ when Array
22
+ object.map { |elem| keys_to_sym(elem) }
23
+ else
24
+ object
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module IdealPostcodes
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,77 @@
1
+ require 'test/unit'
2
+ require 'mocha/setup'
3
+ require 'ideal_postcodes'
4
+ require 'shoulda'
5
+
6
+ module IdealPostcodes
7
+
8
+ attr_accessor :mock
9
+
10
+ def self.mock=(mock)
11
+ @mock = mock
12
+ end
13
+
14
+ def self.generate_request(options)
15
+ method = options[:method]
16
+ url = options[:url]
17
+ @mock.execute method, url
18
+ end
19
+
20
+ end
21
+
22
+ def test_response(body, status=200)
23
+ body = MultiJson.dump(body)
24
+ m = mock
25
+ m.instance_variable_set('@instance_vals', {code: status, body: body})
26
+ def m.body; @instance_vals[:body]; end
27
+ def m.code; @instance_vals[:code]; end
28
+ m
29
+ end
30
+
31
+ def test_postcode
32
+ "ID11QD"
33
+ end
34
+
35
+ def valid_postcode(params={})
36
+ {
37
+ result: [
38
+ {
39
+ postcode: "ID1 1QD",
40
+ post_town: "LONDON",
41
+ line_1: "Kingsley Hall",
42
+ line_2: "Powis Road",
43
+ line_3: ""
44
+ }
45
+ ],
46
+ message: "Success",
47
+ code: 2000
48
+ }
49
+ end
50
+
51
+ def invalid_postcode
52
+ {
53
+ message: "Postcode not found",
54
+ code: 4040
55
+ }
56
+ end
57
+
58
+ def invalid_token
59
+ {
60
+ message: "Invalid key",
61
+ code: 4010
62
+ }
63
+ end
64
+
65
+ def limit_reached
66
+ {
67
+ code: 4021,
68
+ message: "Lookup Limit Reached"
69
+ }
70
+ end
71
+
72
+ def token_exhausted
73
+ {
74
+ code: 4020,
75
+ message: "Token exhausted"
76
+ }
77
+ end
@@ -0,0 +1,78 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class TestIdealPostcodes < Test::Unit::TestCase
4
+ include Mocha
5
+
6
+ context "API" do
7
+
8
+ setup do
9
+ @mock = mock
10
+ IdealPostcodes.mock = @mock
11
+ end
12
+
13
+ teardown do
14
+ IdealPostcodes.mock = nil
15
+ end
16
+
17
+ should "raise an exception if no API key set" do
18
+ IdealPostcodes.api_key = nil
19
+ assert_raises IdealPostcodes::AuthenticationError do
20
+ postcode = IdealPostcodes::Postcode.lookup(test_postcode)
21
+ end
22
+ end
23
+
24
+ should "raise an exception if authentication failed" do
25
+ IdealPostcodes.api_key = "BOGUS"
26
+ response = test_response invalid_token
27
+ assert_raises IdealPostcodes::AuthenticationError do
28
+ @mock.expects(:execute).raises(RestClient::ExceptionWithResponse.new(response, 401))
29
+ IdealPostcodes::Postcode.lookup(test_postcode)
30
+ end
31
+ end
32
+
33
+ context "Valid API key" do
34
+
35
+ setup do
36
+ IdealPostcodes.api_key = "correctkey"
37
+ end
38
+
39
+ teardown do
40
+ IdealPostcodes.api_key = nil
41
+ end
42
+
43
+ should "raise an exception if tokens exhausted" do
44
+ response = test_response token_exhausted
45
+ assert_raises IdealPostcodes::TokenExhaustedError do
46
+ @mock.expects(:execute).raises(RestClient::ExceptionWithResponse.new(response, 402))
47
+ IdealPostcodes::Postcode.lookup(test_postcode)
48
+ end
49
+ end
50
+
51
+ should "raise an exception if limit has been breached" do
52
+ response = test_response limit_reached
53
+ assert_raises IdealPostcodes::LimitReachedError do
54
+ @mock.expects(:execute).raises(RestClient::ExceptionWithResponse.new(response, 402))
55
+ IdealPostcodes::Postcode.lookup(test_postcode)
56
+ end
57
+ end
58
+
59
+ should "return postcode object with addresses for valid postcode" do
60
+ response = test_response valid_postcode
61
+ @mock.expects(:execute).returns(response)
62
+ postcode = IdealPostcodes::Postcode.lookup(test_postcode)
63
+ assert_equal postcode.empty?, false
64
+ assert_equal postcode.addresses, valid_postcode[:result]
65
+ end
66
+
67
+ should "return postcode object with empty array for non-existent postcode" do
68
+ response = test_response invalid_postcode
69
+ @mock.expects(:execute).raises(RestClient::ExceptionWithResponse.new(response, 404))
70
+ postcode = IdealPostcodes::Postcode.lookup(test_postcode)
71
+ assert_equal postcode.empty?, true
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+
78
+ end
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ideal_postcodes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Chris Blanchard
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rest-client
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.6'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.6'
30
+ - !ruby/object:Gem::Dependency
31
+ name: multi_json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 1.7.9
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 1.7.9
46
+ - !ruby/object:Gem::Dependency
47
+ name: mocha
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 0.14.0
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 0.14.0
62
+ - !ruby/object:Gem::Dependency
63
+ name: test-unit
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: shoulda
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 3.5.0
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 3.5.0
94
+ - !ruby/object:Gem::Dependency
95
+ name: rake
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ description: Ideal Postcodes is a simple postcode lookup API for UK addresses. See
111
+ https://ideal-postcodes.co.uk
112
+ email:
113
+ - cablanchard@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - Gemfile
119
+ - README.md
120
+ - Rakefile
121
+ - ideal-postcodes-ruby.gemspec
122
+ - lib/ideal_postcodes.rb
123
+ - lib/idealpostcodes.rb
124
+ - lib/idealpostcodes/errors.rb
125
+ - lib/idealpostcodes/postcode.rb
126
+ - lib/idealpostcodes/util.rb
127
+ - lib/idealpostcodes/version.rb
128
+ - test/test_helper.rb
129
+ - test/test_ideal_postcodes.rb
130
+ homepage: https://ideal-postcodes.co.uk/documentation
131
+ licenses: []
132
+ post_install_message:
133
+ rdoc_options: []
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ none: false
144
+ requirements:
145
+ - - ! '>='
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ requirements: []
149
+ rubyforge_project:
150
+ rubygems_version: 1.8.24
151
+ signing_key:
152
+ specification_version: 3
153
+ summary: Wrapper for the Ideal-Postcodes.co.uk API
154
+ test_files:
155
+ - test/test_helper.rb
156
+ - test/test_ideal_postcodes.rb