postcodeinfo-client-ruby 0.1.0

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