ideal_postcodes 0.1.1 → 1.0.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -0
  3. data/README.md +295 -42
  4. data/Rakefile +13 -6
  5. data/ideal-postcodes-ruby.gemspec +9 -10
  6. data/lib/idealpostcodes.rb +30 -9
  7. data/lib/idealpostcodes/address.rb +31 -0
  8. data/lib/idealpostcodes/key.rb +13 -0
  9. data/lib/idealpostcodes/postcode.rb +11 -24
  10. data/lib/idealpostcodes/util.rb +2 -2
  11. data/lib/idealpostcodes/version.rb +1 -1
  12. data/spec/addresses_spec.rb +68 -0
  13. data/spec/idealpostcodes_spec.rb +56 -0
  14. data/spec/keys_spec.rb +25 -0
  15. data/spec/postcodes_spec.rb +66 -0
  16. data/spec/spec_helper.rb +65 -0
  17. data/spec/vcr_cassettes/IdealPostcodes_Address_lookup_raises_an_exception_if_invalid_key.yml +42 -0
  18. data/spec/vcr_cassettes/IdealPostcodes_Address_lookup_raises_an_exception_if_limit_breached.yml +42 -0
  19. data/spec/vcr_cassettes/IdealPostcodes_Address_lookup_raises_an_exception_if_no_lookups_remaining.yml +42 -0
  20. data/spec/vcr_cassettes/IdealPostcodes_Address_lookup_returns_an_address_for_a_valid_UDPRN.yml +74 -0
  21. data/spec/vcr_cassettes/IdealPostcodes_Address_lookup_returns_nil_for_an_invalid_UDPRN.yml +42 -0
  22. data/spec/vcr_cassettes/IdealPostcodes_Address_search_is_sensitive_to_limit.yml +81 -0
  23. data/spec/vcr_cassettes/IdealPostcodes_Address_search_is_sensitive_to_page.yml +369 -0
  24. data/spec/vcr_cassettes/IdealPostcodes_Address_search_raises_an_exception_if_invalid_key.yml +42 -0
  25. data/spec/vcr_cassettes/IdealPostcodes_Address_search_raises_an_exception_if_limit_breached.yml +42 -0
  26. data/spec/vcr_cassettes/IdealPostcodes_Address_search_raises_an_exception_if_no_lookups_remaining.yml +42 -0
  27. data/spec/vcr_cassettes/IdealPostcodes_Address_search_returns_results_in_a_SearchResult_object.yml +273 -0
  28. data/spec/vcr_cassettes/IdealPostcodes_Key_lookup_details_returns_key_details.yml +62 -0
  29. data/spec/vcr_cassettes/IdealPostcodes_Key_lookup_returns_the_availability_status_of_a_key_false_key_.yml +45 -0
  30. data/spec/vcr_cassettes/IdealPostcodes_Key_lookup_returns_the_availability_status_of_a_key_true_key_.yml +45 -0
  31. data/spec/vcr_cassettes/IdealPostcodes_Postcode_find_by_location_is_sensitive_to_limit_parameter.yml +52 -0
  32. data/spec/vcr_cassettes/IdealPostcodes_Postcode_find_by_location_is_sensitive_to_radius_parament.yml +133 -0
  33. data/spec/vcr_cassettes/IdealPostcodes_Postcode_find_by_location_returns_an_array_of_postcodes_and_locations.yml +84 -0
  34. data/spec/vcr_cassettes/IdealPostcodes_Postcode_find_by_location_returns_an_empty_array_if_no_results_are_found.yml +43 -0
  35. data/spec/vcr_cassettes/IdealPostcodes_Postcode_lookup_raises_an_exception_if_invalid_key.yml +42 -0
  36. data/spec/vcr_cassettes/IdealPostcodes_Postcode_lookup_raises_an_exception_if_key_has_run_out_of_balance.yml +42 -0
  37. data/spec/vcr_cassettes/IdealPostcodes_Postcode_lookup_raises_an_exception_if_limit_has_been_reached.yml +42 -0
  38. data/spec/vcr_cassettes/IdealPostcodes_Postcode_lookup_returns_a_list_of_addresses_for_a_postcode.yml +268 -0
  39. data/spec/vcr_cassettes/IdealPostcodes_Postcode_lookup_returns_an_empty_array_if_postcode_does_not_exist.yml +42 -0
  40. data/spec/vcr_cassettes/IdealPostcodes_key_available_returns_false_if_key_is_unavailable.yml +45 -0
  41. data/spec/vcr_cassettes/IdealPostcodes_key_available_returns_true_if_key_is_available.yml +45 -0
  42. data/spec/vcr_cassettes/IdealPostcodes_key_details_raises_an_exception_if_no_secret_is_provided.yml +45 -0
  43. data/spec/vcr_cassettes/IdealPostcodes_key_details_returns_key_information.yml +62 -0
  44. data/spec/vcr_cassettes/IdealPostcodes_request_generates_a_HTTP_request.yml +268 -0
  45. data/spec/vcr_cassettes/IdealPostcodes_request_raises_authentication_error_if_invalid_key_is_provided.yml +42 -0
  46. data/spec/vcr_cassettes/IdealPostcodes_request_raises_limit_reached_error_if_a_limit_has_been_breached.yml +42 -0
  47. data/spec/vcr_cassettes/IdealPostcodes_request_raises_token_exhausted_error_if_key_balance_is_depleted.yml +42 -0
  48. metadata +96 -39
  49. data/test/test_helper.rb +0 -77
  50. data/test/test_ideal_postcodes.rb +0 -78
@@ -1,18 +1,24 @@
1
- require 'rest-client'
2
- require 'uri'
3
- require 'multi_json'
4
1
  require 'cgi'
2
+ require 'uri'
3
+ require 'json'
4
+ require 'rest-client'
5
5
  require 'idealpostcodes/version'
6
- require 'idealpostcodes/postcode'
6
+
7
+ # Require utility libraries
7
8
  require 'idealpostcodes/util'
8
9
  require 'idealpostcodes/errors'
9
10
 
11
+ # Require Resources
12
+ require 'idealpostcodes/key'
13
+ require 'idealpostcodes/address'
14
+ require 'idealpostcodes/postcode'
15
+
10
16
  module IdealPostcodes
11
17
  @base_url = 'https://api.ideal-postcodes.co.uk'
12
18
  @version = '1'
13
19
 
14
20
  class << self
15
- attr_accessor :api_key, :base_url, :version
21
+ attr_accessor :api_key, :base_url, :version, :secret
16
22
  end
17
23
 
18
24
  def self.request(method, path, params = {})
@@ -38,11 +44,26 @@ module IdealPostcodes
38
44
  handle_client_error(error)
39
45
  end
40
46
  rescue RestClient::Exception, Errno::ECONNREFUSED => error
41
- handle_client_error(e)
47
+ handle_client_error(error)
42
48
  end
43
49
  parse response.body
44
50
  end
45
51
 
52
+ def self.apply_secret(secret)
53
+ @secret = secret
54
+ end
55
+
56
+ def self.key_available
57
+ response = Key.lookup @api_key
58
+ response[:available]
59
+ end
60
+
61
+ def self.key_details
62
+ raise IdealPostcodes::AuthenticationError.new('No Secret Key provided. ' +
63
+ 'Set your secret key with IdealPostcodes.apply_secret #your_key') if @secret.nil?
64
+ response = Key.lookup_details @api_key, @secret
65
+ end
66
+
46
67
  private
47
68
 
48
69
  def self.resource_url(path='')
@@ -55,8 +76,8 @@ module IdealPostcodes
55
76
 
56
77
  def self.parse(response)
57
78
  begin
58
- Util.keys_to_sym MultiJson.load(response)
59
- rescue MultiJson::DecodeError => e
79
+ Util.keys_to_sym JSON.parse(response)
80
+ rescue JSON::ParserError => e
60
81
  raise handle_client_error(e)
61
82
  end
62
83
  end
@@ -85,7 +106,7 @@ module IdealPostcodes
85
106
  end
86
107
 
87
108
  def self.general_error(response_code, response_body)
88
- IdealPostcodesError.new "Invalid response object", response_code, response_body
109
+ IdealPostcodesError.new 'Invalid response object', response_code, response_body
89
110
  end
90
111
 
91
112
  end
@@ -0,0 +1,31 @@
1
+ module IdealPostcodes
2
+ module Address
3
+ class SearchResult
4
+ attr_reader :page, :limit, :addresses
5
+ def initialize response
6
+ @page = response[:result][:page]
7
+ @limit = response[:result][:limit]
8
+ @addresses = response[:result][:hits]
9
+ end
10
+ end
11
+
12
+ def self.lookup udprn
13
+ begin
14
+ response = IdealPostcodes.request :get, "addresses/#{udprn}"
15
+ address = response[:result]
16
+ rescue IdealPostcodes::IdealPostcodesError => error
17
+ raise error unless error.response_code == 4044
18
+ address = nil
19
+ end
20
+ address
21
+ end
22
+
23
+ def self.search search_term, options = {}
24
+ query = { query: search_term }
25
+ query[:limit] = options[:limit] unless options[:limit].nil?
26
+ query[:page] = options[:page] unless options[:page].nil?
27
+ response = IdealPostcodes.request :get, "addresses", query
28
+ SearchResult.new response
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ module IdealPostcodes
2
+ module Key
3
+ def self.lookup api_key
4
+ response = IdealPostcodes.request :get, "keys/#{api_key}"
5
+ response[:result]
6
+ end
7
+
8
+ def self.lookup_details api_key, secret
9
+ response = IdealPostcodes.request :get, "keys/#{api_key}", { user_token: secret }
10
+ response[:result]
11
+ end
12
+ end
13
+ end
@@ -1,35 +1,22 @@
1
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)
2
+ module Postcode
3
+ def self.lookup postcode
13
4
  begin
14
5
  response = IdealPostcodes.request :get, "postcodes/#{postcode}"
6
+ addresses = response[:result]
15
7
  rescue IdealPostcodes::ResourceNotFoundError => error
16
8
  raise error unless error.response_code == 4040
17
- response = nil
9
+ addresses = []
18
10
  end
19
- new postcode, response
11
+ addresses
20
12
  end
21
13
 
22
- def empty?
23
- @raw.nil?
14
+ def self.find_by_location geolocation
15
+ query = {lonlat: "#{geolocation[:longitude]},#{geolocation[:latitude]}"}
16
+ query[:limit] = geolocation[:limit] unless geolocation[:limit].nil?
17
+ query[:radius] = geolocation[:radius] unless geolocation[:radius].nil?
18
+ response = IdealPostcodes.request :get, 'postcodes', query
19
+ response[:result]
24
20
  end
25
-
26
- def addresses
27
- @addresses
28
- end
29
-
30
- def to_s
31
- addresses.to_s
32
- end
33
-
34
21
  end
35
22
  end
@@ -4,9 +4,9 @@ module IdealPostcodes
4
4
  def self.merge_params(hash)
5
5
  result = []
6
6
  hash.each do |key, value|
7
- result << "#{CGI.escape(key.to_s)}=#{CGI.escape(value)}"
7
+ result << "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
8
8
  end
9
- result.join("&")
9
+ result.join('&')
10
10
  end
11
11
 
12
12
  def self.keys_to_sym(object)
@@ -1,3 +1,3 @@
1
1
  module IdealPostcodes
2
- VERSION = '0.1.1'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+
3
+ describe IdealPostcodes::Address do
4
+ describe '.lookup' do
5
+ it 'returns an address for a valid UDPRN' do
6
+ address = IdealPostcodes::Address.lookup 0
7
+ expect(is_address(address)).to eq(true)
8
+ end
9
+ it 'returns nil for an invalid UDPRN' do
10
+ address = IdealPostcodes::Address.lookup -1
11
+ expect(address).to be_nil
12
+ end
13
+ it 'raises an exception if invalid key' do
14
+ IdealPostcodes.api_key = 'foo'
15
+ expect {
16
+ IdealPostcodes::Address.lookup 0
17
+ }.to raise_error(IdealPostcodes::AuthenticationError)
18
+ end
19
+ it 'raises an exception if no lookups remaining' do
20
+ expect {
21
+ IdealPostcodes::Address.lookup -2
22
+ }.to raise_error(IdealPostcodes::TokenExhaustedError)
23
+ end
24
+ it 'raises an exception if limit breached' do
25
+ expect {
26
+ IdealPostcodes::Address.lookup -3
27
+ }.to raise_error(IdealPostcodes::LimitReachedError)
28
+ end
29
+ end
30
+
31
+ describe '.search' do
32
+ it 'returns results in a SearchResult object' do
33
+ results = IdealPostcodes::Address.search "ID1 1QD"
34
+ expect(results).to be_a(IdealPostcodes::Address::SearchResult)
35
+ expect(results.addresses.length).to be > 0
36
+ results.addresses.each do |address|
37
+ expect(is_address(address)).to eq(true)
38
+ end
39
+ end
40
+ it 'is sensitive to limit' do
41
+ limit = 1
42
+ results = IdealPostcodes::Address.search "High Street", limit: limit
43
+ expect(results.addresses.length).to equal(limit)
44
+ expect(results.limit).to equal(limit)
45
+ end
46
+ it 'is sensitive to page' do
47
+ page = 1
48
+ results = IdealPostcodes::Address.search "High Street", page: page
49
+ expect(results.page).to equal(page)
50
+ end
51
+ it 'raises an exception if invalid key' do
52
+ IdealPostcodes.api_key = 'foo'
53
+ expect {
54
+ results = IdealPostcodes::Address.search "ID1 1QD"
55
+ }.to raise_error(IdealPostcodes::AuthenticationError)
56
+ end
57
+ it 'raises an exception if no lookups remaining' do
58
+ expect {
59
+ results = IdealPostcodes::Address.search "ID1 CLIP"
60
+ }.to raise_error(IdealPostcodes::TokenExhaustedError)
61
+ end
62
+ it 'raises an exception if limit breached' do
63
+ expect {
64
+ results = IdealPostcodes::Address.search "ID1 CHOP"
65
+ }.to raise_error(IdealPostcodes::LimitReachedError)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ describe IdealPostcodes do
4
+ describe '#request' do
5
+ it 'generates a HTTP request' do
6
+ response = IdealPostcodes.request :get, 'postcodes/ID1 1QD'
7
+ expect(response[:code]).to eq(2000)
8
+ end
9
+ it 'raises an error if no key is provided' do
10
+ IdealPostcodes.api_key = nil
11
+ expect do
12
+ response = IdealPostcodes.request :get, 'postcodes/ID1 1QD'
13
+ end.to raise_error(IdealPostcodes::AuthenticationError)
14
+ end
15
+ it 'raises authentication error if invalid key is provided' do
16
+ IdealPostcodes.api_key = 'foo'
17
+ expect do
18
+ response = IdealPostcodes.request :get, 'postcodes/ID1 1QD'
19
+ end.to raise_error(IdealPostcodes::AuthenticationError)
20
+ end
21
+ it 'raises token exhausted error if key balance is depleted' do
22
+ expect do
23
+ response = IdealPostcodes.request :get, 'postcodes/ID1 CLIP'
24
+ end.to raise_error(IdealPostcodes::TokenExhaustedError)
25
+ end
26
+ it 'raises limit reached error if a limit has been breached' do
27
+ expect do
28
+ response = IdealPostcodes.request :get, 'postcodes/ID1 CHOP'
29
+ end.to raise_error(IdealPostcodes::LimitReachedError)
30
+ end
31
+ end
32
+
33
+ describe '.key_available' do
34
+ it 'returns true if key is available' do
35
+ IdealPostcodes.api_key = "iddqd"
36
+ expect(IdealPostcodes.key_available).to equal(true)
37
+ end
38
+ it 'returns false if key is unavailable' do
39
+ IdealPostcodes.api_key = "idkfa"
40
+ expect(IdealPostcodes.key_available).to equal(false)
41
+ end
42
+ end
43
+
44
+ describe '.key_details' do
45
+ it 'raises an exception if no secret is provided' do
46
+ expect {
47
+ IdealPostcodes.key_details
48
+ }.to raise_error(IdealPostcodes::AuthenticationError)
49
+ end
50
+ it 'returns key information' do
51
+ IdealPostcodes.apply_secret(secret_key)
52
+ response = IdealPostcodes.key_details
53
+ expect(is_key(response)).to eq(true)
54
+ end
55
+ end
56
+ end
data/spec/keys_spec.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe IdealPostcodes::Key do
4
+ describe '.lookup' do
5
+ it 'returns the availability status of a key (true key)' do
6
+ result = IdealPostcodes::Key.lookup "iddqd"
7
+ expect(result[:available]).to eq(true)
8
+ end
9
+ it 'returns the availability status of a key (false key)' do
10
+ result = IdealPostcodes::Key.lookup "idkfa"
11
+ expect(result[:available]).to eq(false)
12
+ end
13
+ end
14
+
15
+ describe '.lookup_details' do
16
+ it 'returns key details' do
17
+ result = IdealPostcodes::Key.lookup_details "gandhi", secret_key
18
+ expect(result[:lookups_remaining]).to_not be_nil
19
+ expect(result[:daily_limit]).to_not be_nil
20
+ expect(result[:individual_limit]).to_not be_nil
21
+ expect(result[:allowed_urls]).to_not be_nil
22
+ expect(result[:notifications]).to_not be_nil
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+
3
+ describe IdealPostcodes::Postcode do
4
+ describe '.lookup' do
5
+ it 'returns a list of addresses for a postcode' do
6
+ addresses = IdealPostcodes::Postcode.lookup 'ID1 1QD'
7
+ expect(addresses.length).to be > 0
8
+ addresses.each do |address|
9
+ expect(is_address(address)).to eq(true)
10
+ expect(address[:postcode]).to eq('ID1 1QD')
11
+ end
12
+ end
13
+ it 'returns an empty array if postcode does not exist' do
14
+ addresses = IdealPostcodes::Postcode.lookup 'ID1 KFA'
15
+ expect(addresses.length).to eq(0)
16
+ end
17
+ it 'raises an exception if key has run out of balance' do
18
+ expect {
19
+ IdealPostcodes::Postcode.lookup 'ID1 CLIP'
20
+ }.to raise_error(IdealPostcodes::TokenExhaustedError)
21
+ end
22
+ it 'raises an exception if limit has been reached' do
23
+ expect {
24
+ IdealPostcodes::Postcode.lookup 'ID1 CHOP'
25
+ }.to raise_error(IdealPostcodes::LimitReachedError)
26
+ end
27
+ it 'raises an exception if invalid key' do
28
+ IdealPostcodes.api_key = 'foo'
29
+ expect {
30
+ IdealPostcodes::Postcode.lookup 'ID1 1QD'
31
+ }.to raise_error(IdealPostcodes::AuthenticationError)
32
+ end
33
+ end
34
+ describe '.find_by_location' do
35
+ it 'returns an array of postcodes and locations' do
36
+ lon = 0.6298
37
+ lat = 51.7923
38
+ postcodes = IdealPostcodes::Postcode.find_by_location longitude: lon, latitude: lat
39
+ expect(postcodes.length).to be > 0
40
+ postcodes.each do |postcode|
41
+ expect(is_postcode_location(postcode)).to eq(true)
42
+ end
43
+ end
44
+ it 'returns an empty array if no results are found' do
45
+ lon = 0
46
+ lat = 0
47
+ postcodes = IdealPostcodes::Postcode.find_by_location longitude: lon, latitude: lat
48
+ expect(postcodes).to be_a(Array)
49
+ expect(postcodes.length).to eq(0)
50
+ end
51
+ it 'is sensitive to limit parameter' do
52
+ lon = 0.6298
53
+ lat = 51.7923
54
+ limit = 1
55
+ postcodes = IdealPostcodes::Postcode.find_by_location longitude: lon, latitude: lat, limit: limit
56
+ expect(postcodes.length).to eq(limit)
57
+ end
58
+ it 'is sensitive to radius parament' do
59
+ lon = 0.6298
60
+ lat = 51.7923
61
+ small_radius = IdealPostcodes::Postcode.find_by_location longitude: lon, latitude: lat, radius: 10
62
+ large_radius = IdealPostcodes::Postcode.find_by_location longitude: lon, latitude: lat, radius: 100
63
+ expect(large_radius.length > small_radius.length).to eq(true)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,65 @@
1
+ # Enable VCR
2
+ require 'vcr'
3
+
4
+ VCR.configure do |c|
5
+ c.cassette_library_dir = 'spec/vcr_cassettes'
6
+ c.hook_into :webmock
7
+ end
8
+
9
+ # Configure Ideal Postcodes lib
10
+ require 'ideal_postcodes'
11
+
12
+ RSpec.configure do |c|
13
+ c.before(:each) do
14
+ IdealPostcodes.base_url = 'https://localhost:1337'
15
+ IdealPostcodes.api_key = "gandhi"
16
+ IdealPostcodes.apply_secret nil
17
+ end
18
+
19
+ c.around(:each) do |example|
20
+ VCR.use_cassette(example.metadata[:full_description]) do
21
+ example.run
22
+ end
23
+ end
24
+ end
25
+
26
+ def contains_attributes attribute_list, target
27
+ result = true
28
+ attribute_list.each do |attribute|
29
+ result = false if target[attribute].nil?
30
+ end
31
+ result
32
+ end
33
+
34
+ def postcode_location_elements
35
+ [:postcode, :longitude, :latitude, :northings, :eastings]
36
+ end
37
+
38
+ def is_postcode_location(postcode)
39
+ contains_attributes postcode_location_elements, postcode
40
+ end
41
+
42
+ def address_elements
43
+ [:postcode, :postcode_inward, :postcode_outward, :post_town, :dependant_locality,
44
+ :double_dependant_locality, :thoroughfare, :dependant_thoroughfare,
45
+ :building_number, :building_name, :sub_building_name, :po_box, :department_name,
46
+ :organisation_name, :udprn, :postcode_type, :su_organisation_indicator,
47
+ :delivery_point_suffix, :line_1, :line_2, :line_3, :premise, :county,
48
+ :district, :ward, :longitude, :latitude, :eastings, :northings]
49
+ end
50
+
51
+ def is_address(address)
52
+ contains_attributes address_elements, address
53
+ end
54
+
55
+ def key_elements
56
+ [:lookups_remaining, :daily_limit, :individual_limit, :allowed_urls, :notifications]
57
+ end
58
+
59
+ def is_key(key)
60
+ contains_attributes key_elements, key
61
+ end
62
+
63
+ def secret_key
64
+ "uk_hxp6ouk0rmyXoobVJnehrsQcdvTfb"
65
+ end