postcodeinfo-client-ruby 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.
@@ -0,0 +1,16 @@
1
+ module PostcodeInfo
2
+ class APIExceptionWrapper < Exception
3
+ attr_accessor :original_exception, :response
4
+
5
+ def initialize(original_exception, response)
6
+ self.original_exception = original_exception
7
+ self.response = response
8
+ end
9
+ end
10
+
11
+ class ServiceUnavailable < APIExceptionWrapper; end
12
+ class ServerError < APIExceptionWrapper; end
13
+ class UnparseableResponse < APIExceptionWrapper; end
14
+ class InvalidAuthToken < APIExceptionWrapper; end
15
+ class UnrecognisedPostcode < APIExceptionWrapper; end
16
+ end
@@ -0,0 +1,65 @@
1
+ module PostcodeInfo
2
+ class Postcode
3
+ # these attributes are set on construction
4
+ attr_reader :as_given, :normalised
5
+ attr_accessor :client
6
+ # these attributes require lookups
7
+ # attr_reader :latitude, :longitude, :addresses, :local_authority
8
+
9
+ def initialize(given_postcode, client)
10
+ @as_given = given_postcode
11
+ @normalised = self.class.normalise(@as_given)
12
+ @client = client
13
+ @info = nil
14
+ end
15
+
16
+ def self.normalise(postcode)
17
+ postcode.downcase.gsub(/[^a-z0-9]/, '')
18
+ end
19
+
20
+ def latitude
21
+ lookup_info! if @info.nil?
22
+ @latitude
23
+ end
24
+
25
+ def longitude
26
+ lookup_info! if @info.nil?
27
+ @longitude
28
+ end
29
+
30
+ def local_authority
31
+ lookup_info! if @info.nil?
32
+ @local_authority
33
+ end
34
+
35
+ def addresses
36
+ @addresses ||= @client.addresses(@normalised)
37
+ end
38
+ alias_method :lookup_addresses!, :addresses
39
+
40
+ def valid?
41
+ @valid ||= @client.valid?(@normalised)
42
+ end
43
+
44
+ def lookup_info!
45
+ @info = @client.info(@normalised)
46
+ parse_info!
47
+ end
48
+
49
+
50
+ protected
51
+
52
+ def set_coordinates!(info)
53
+ if info[:centre]
54
+ @latitude = info[:centre][:latitude]
55
+ @longitude = info[:centre][:longitude]
56
+ end
57
+ end
58
+
59
+ def parse_info!
60
+ set_coordinates!(@info)
61
+ @local_authority = @info[:local_authority]
62
+ @valid = true
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,80 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+ # stub: postcodeinfo-client-ruby 0.1.0 ruby lib
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "postcodeinfo-client-ruby"
9
+ s.version = "0.1.0"
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib"]
13
+ s.authors = ["Al Davidson"]
14
+ s.date = "2015-05-29"
15
+ s.description = "Provides a convenient interface for looking up postcodes in an instance of the MoJ postcodeinfo API (https://://github.com/ministryofjustice/postcodeinfo).\n Lets you check if a postcode is valid, and ookup:\n * all addresses with that postcode\n * the latitude/longitude of its centre point\n * the name & GSS code of the local authority under which it resides."
16
+ s.email = "alistair.davidson@digital.justice.gov.uk"
17
+ s.extra_rdoc_files = [
18
+ "LICENSE.MD",
19
+ "README.md"
20
+ ]
21
+ s.files = [
22
+ ".document",
23
+ ".rspec",
24
+ ".ruby-gemset",
25
+ ".ruby-version",
26
+ ".simplecov",
27
+ "Gemfile",
28
+ "Gemfile.lock",
29
+ "LICENSE.MD",
30
+ "README.md",
31
+ "Rakefile",
32
+ "VERSION",
33
+ "lib/postcodeinfo-client-ruby.rb",
34
+ "lib/postcodeinfo/client.rb",
35
+ "lib/postcodeinfo/exceptions.rb",
36
+ "lib/postcodeinfo/postcode.rb",
37
+ "postcodeinfo-client-ruby.gemspec",
38
+ "spec/client_spec.rb",
39
+ "spec/postcode_spec.rb",
40
+ "spec/spec_helper.rb"
41
+ ]
42
+ s.homepage = "https://github.com/ministryofjustice/postcodeinfo-client-ruby"
43
+ s.licenses = ["Open Government License"]
44
+ s.rubygems_version = "2.4.3"
45
+ s.summary = "Client for postcodeinfo API (https://://github.com/ministryofjustice/postcodeinfo)"
46
+
47
+ if s.respond_to? :specification_version then
48
+ s.specification_version = 4
49
+
50
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
51
+ s.add_runtime_dependency(%q<json>, [">= 0"])
52
+ s.add_runtime_dependency(%q<rest-client>, [">= 0"])
53
+ s.add_development_dependency(%q<byebug>, [">= 0"])
54
+ s.add_development_dependency(%q<rspec>, [">= 3.2.0"])
55
+ s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
56
+ s.add_development_dependency(%q<bundler>, ["~> 1.0"])
57
+ s.add_development_dependency(%q<jeweler>, ["~> 2.0.1"])
58
+ s.add_development_dependency(%q<simplecov>, [">= 0"])
59
+ else
60
+ s.add_dependency(%q<json>, [">= 0"])
61
+ s.add_dependency(%q<rest-client>, [">= 0"])
62
+ s.add_dependency(%q<byebug>, [">= 0"])
63
+ s.add_dependency(%q<rspec>, [">= 3.2.0"])
64
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
65
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
66
+ s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
67
+ s.add_dependency(%q<simplecov>, [">= 0"])
68
+ end
69
+ else
70
+ s.add_dependency(%q<json>, [">= 0"])
71
+ s.add_dependency(%q<rest-client>, [">= 0"])
72
+ s.add_dependency(%q<byebug>, [">= 0"])
73
+ s.add_dependency(%q<rspec>, [">= 3.2.0"])
74
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
75
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
76
+ s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
77
+ s.add_dependency(%q<simplecov>, [">= 0"])
78
+ end
79
+ end
80
+
@@ -0,0 +1,289 @@
1
+ require 'spec_helper'
2
+
3
+ describe PostcodeInfo::Client do
4
+ let(:args){ {auth_token: '12345'} }
5
+ let(:client){ PostcodeInfo::Client.new(args) }
6
+ let(:postcode){ 'SN15NB' }
7
+
8
+
9
+ describe 'constructing a new client' do
10
+ context 'with no :auth_token' do
11
+ let(:args){ {} }
12
+ it 'does not raise an ArgumentError' do
13
+ expect{ described_class.new }.to_not raise_error
14
+ end
15
+ end
16
+ context 'with an :auth_token' do
17
+ let(:args){ {auth_token: '12345'} }
18
+ let(:client){ PostcodeInfo::Client.new(args) }
19
+
20
+ it 'stores the auth_token in an instance variable' do
21
+ expect( client.auth_token ).to eq('12345')
22
+ end
23
+
24
+ context 'and an :api_url' do
25
+ before{ args[:api_url] = 'http://my/ur.l' }
26
+
27
+ it 'parses the :api_url' do
28
+ expect( URI ).to receive(:parse).with('http://my/ur.l')
29
+ client
30
+ end
31
+ end
32
+
33
+ context 'and no :api_url' do
34
+ let(:mock_urls){
35
+ {
36
+ my_env: 'http://my/env/url',
37
+ foo: 'https://foo/'
38
+ }
39
+ }
40
+ before do
41
+ allow(described_class).to receive(:api_urls).and_return( mock_urls )
42
+ end
43
+
44
+ context 'but an :env' do
45
+ before{ args[:env] = 'my_env' }
46
+
47
+ it 'parses the looked-up :api_url' do
48
+ expect( URI ).to receive(:parse).with('http://my/env/url')
49
+ client
50
+ end
51
+ end
52
+
53
+ context 'and no :env' do
54
+ context 'when ENV["RAILS_ENV"] is present' do
55
+ before do
56
+ allow(ENV).to receive(:[]).with('RAILS_ENV').and_return( 'foo' )
57
+ end
58
+
59
+ it 'parses the looked-up :api_url' do
60
+ expect( URI ).to receive(:parse).with('https://foo/')
61
+ client
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ describe 'setting the api_url' do
70
+ let(:new_url){ 'https://my.dom.ain/' }
71
+
72
+ it 'stores the parsed URI as @api_uri' do
73
+ expect{ client.api_url=new_url }.to change(client, :api_uri).to( URI.parse(new_url) )
74
+ end
75
+ end
76
+
77
+ describe 'getting the api_url' do
78
+ it 'returns a string' do
79
+ expect(client.api_url).to be_a(String)
80
+ end
81
+ end
82
+
83
+ describe 'lookup_postcode' do
84
+ let(:mock_constructed_postcode){ double(PostcodeInfo::Postcode).as_null_object }
85
+ before do
86
+ allow(PostcodeInfo::Postcode).to receive(:new).and_return(mock_constructed_postcode)
87
+ end
88
+
89
+ context 'given a valid postcode' do
90
+
91
+ it 'returns a PostcodeInfo::Postcode' do
92
+ expect(client.lookup_postcode(postcode)).to eq(mock_constructed_postcode)
93
+ end
94
+
95
+ it 'looks-up the addresses' do
96
+ expect(mock_constructed_postcode).to receive(:lookup_addresses!)
97
+ allow(mock_constructed_postcode).to receive(:lookup_info!)
98
+ client.lookup_postcode(postcode)
99
+ end
100
+
101
+ it 'looks-up the info' do
102
+ allow(mock_constructed_postcode).to receive(:lookup_addresses!)
103
+ expect(mock_constructed_postcode).to receive(:lookup_info!)
104
+ client.lookup_postcode(postcode)
105
+ end
106
+ end
107
+
108
+ context 'when the response raises a RestClient::ResourceNotFound exception' do
109
+ before do
110
+ allow(mock_constructed_postcode).to receive(:lookup_info!).and_raise(RestClient::ResourceNotFound)
111
+ end
112
+
113
+ it 'raises PostcodeInfo::UnrecognizedPostcode' do
114
+ expect{client.lookup_postcode(postcode)}.to raise_error( PostcodeInfo::UnrecognisedPostcode )
115
+ end
116
+ end
117
+ end
118
+
119
+ describe 'valid?' do
120
+ let(:mock_response){ double('response').as_null_object }
121
+ before do
122
+ allow(client).to receive(:make_request).with('/postcodes/SN15NB').and_return(mock_response)
123
+ end
124
+
125
+ it 'makes a request to /postcodes/(given_postcode)' do
126
+ expect(client).to receive(:make_request).with('/postcodes/SN15NB').and_return(mock_response)
127
+ client.valid?(postcode)
128
+ end
129
+
130
+ context 'when the response has code 200' do
131
+ let(:mock_response){ double('response', code: 200) }
132
+
133
+ it 'returns true' do
134
+ expect(client.valid?(postcode)).to eq(true)
135
+ end
136
+ end
137
+
138
+ context 'when the response has code 404' do
139
+ let(:mock_response){ double('response', code: 404) }
140
+
141
+ it 'returns false' do
142
+ expect(client.valid?(postcode)).to eq(false)
143
+ end
144
+ end
145
+
146
+
147
+ context 'when the response raises a RestClient::ResourceNotFound exception' do
148
+ before do
149
+ allow(client).to receive(:make_request).and_raise(RestClient::ResourceNotFound)
150
+ end
151
+
152
+ it 'returns false' do
153
+ expect(client.valid?(postcode)).to eq(false)
154
+ end
155
+ end
156
+
157
+ context 'when the response has code 4XX' do
158
+ let(:mock_response){ double('response', code: 418) }
159
+
160
+ it 'returns nil' do
161
+ expect(client.valid?(postcode)).to be_nil
162
+ end
163
+ end
164
+
165
+ context 'when the response has code 5XX' do
166
+ let(:mock_response){ double('response', code: 500) }
167
+
168
+ it 'returns nil' do
169
+ expect(client.valid?(postcode)).to be_nil
170
+ end
171
+ end
172
+ end
173
+
174
+ describe 'addresses' do
175
+ let(:mock_response){ double('response').as_null_object }
176
+
177
+ before do
178
+ allow(client).to receive(:handle_response).and_return('handled response')
179
+ allow(client).to receive(:make_request).with('/addresses/?postcode=SN15NB').and_return(mock_response)
180
+ end
181
+
182
+ it 'makes a request to /addresses/?postcode=(given_postcode)' do
183
+ expect(client).to receive(:make_request).with('/addresses/?postcode=SN15NB').and_return(mock_response)
184
+ client.addresses(postcode)
185
+ end
186
+
187
+ it 'handles the response' do
188
+ expect(client).to receive(:handle_response).with(mock_response)
189
+ client.addresses(postcode)
190
+ end
191
+
192
+ it 'returns the handled response' do
193
+ expect(client.addresses(postcode)).to eq('handled response')
194
+ end
195
+ end
196
+
197
+ describe 'info' do
198
+ let(:mock_response){ double('response').as_null_object }
199
+
200
+ before do
201
+ allow(client).to receive(:handle_response).and_return('handled response')
202
+ allow(client).to receive(:make_request).with('/postcodes/SN15NB').and_return(mock_response)
203
+ end
204
+
205
+ it 'makes a request to /postcodes/(given_postcode)' do
206
+ expect(client).to receive(:make_request).with('/postcodes/SN15NB').and_return(mock_response)
207
+ client.info(postcode)
208
+ end
209
+
210
+ it 'handles the response' do
211
+ expect(client).to receive(:handle_response).with(mock_response)
212
+ client.info(postcode)
213
+ end
214
+
215
+ it 'returns the handled response' do
216
+ expect(client.info(postcode)).to eq('handled response')
217
+ end
218
+ end
219
+
220
+ describe 'make_request' do
221
+ before do
222
+ client.api_uri = URI.parse("http://my.api/")
223
+ allow(RestClient::Request).to receive(:execute).and_return( 'response' )
224
+ end
225
+
226
+ it 'makes a get request to the given endpoint' do
227
+ expect(RestClient::Request).to receive(:execute).with(hash_including(method: :get, url: 'http://my.api/my/endpoint')).and_return('response')
228
+ client.make_request('/my/endpoint')
229
+ end
230
+
231
+ it 'sends the auth_token as an authorization header' do
232
+ expect(RestClient::Request).to receive(:execute).with(hash_including(headers: { 'Authorization' => 'Token 12345' }))
233
+ client.make_request('/my/endpoint')
234
+ end
235
+
236
+ it 'returns the response' do
237
+ expect( client.make_request('endpoint') ).to eq('response')
238
+ end
239
+
240
+ context 'when the get raises a RestClient::Unauthorized exception' do
241
+ before do
242
+ allow(RestClient::Request).to receive(:execute).and_raise( RestClient::Unauthorized.new('some error msg') )
243
+ end
244
+
245
+ it 're-raises a PostcodeInfo::InvalidAuthToken' do
246
+ expect{ client.make_request('endpoint') }.to raise_error( PostcodeInfo::InvalidAuthToken )
247
+ end
248
+ end
249
+
250
+ context 'when the get raises a RestClient::InternalServerError exception' do
251
+ before do
252
+ allow(RestClient::Request).to receive(:execute).and_raise( RestClient::InternalServerError.new('some error msg') )
253
+ end
254
+
255
+ it 're-raises a PostcodeInfo::ServerError' do
256
+ expect{ client.make_request('endpoint') }.to raise_error( PostcodeInfo::ServerError )
257
+ end
258
+ end
259
+
260
+ context 'when the get raises a SocketError exception' do
261
+ before do
262
+ allow(RestClient::Request).to receive(:execute).and_raise( SocketError.new('some error msg') )
263
+ end
264
+
265
+ it 're-raises a PostcodeInfo::ServiceUnavailable' do
266
+ expect{ client.make_request('endpoint') }.to raise_error( PostcodeInfo::ServiceUnavailable )
267
+ end
268
+ end
269
+
270
+ context 'when the get raises any other exception' do
271
+ before do
272
+ allow(RestClient::Request).to receive(:execute).and_raise( ArgumentError.new('some error msg') )
273
+ end
274
+
275
+ it 're-raises the original exception' do
276
+ expect{ client.make_request('endpoint') }.to raise_error( ArgumentError )
277
+ end
278
+ end
279
+ end
280
+
281
+ describe 'handle_response' do
282
+ let(:mock_response){ double('response', body: 'body') }
283
+
284
+ it 'parses the body as JSON, and symbolizes the keys' do
285
+ expect(JSON).to receive(:parse).with('body', symbolize_names: true).and_return('parsed')
286
+ client.send(:handle_response, mock_response)
287
+ end
288
+ end
289
+ end