firstclasspostcodes 0.0.1
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.
- checksums.yaml +7 -0
- data/.dependabot/config.yml +12 -0
- data/.github/workflows/gem.yml +53 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.rubocop.yml +78 -0
- data/Gemfile +25 -0
- data/LICENSE +21 -0
- data/Rakefile +11 -0
- data/firstclasspostcodes.gemspec +54 -0
- data/lib/firstclasspostcodes.rb +33 -0
- data/lib/firstclasspostcodes/client.rb +81 -0
- data/lib/firstclasspostcodes/configuration.rb +156 -0
- data/lib/firstclasspostcodes/events.rb +27 -0
- data/lib/firstclasspostcodes/operations.rb +4 -0
- data/lib/firstclasspostcodes/operations/get_lookup.rb +52 -0
- data/lib/firstclasspostcodes/operations/get_postcode.rb +40 -0
- data/lib/firstclasspostcodes/operations/methods/format_address.rb +38 -0
- data/lib/firstclasspostcodes/operations/methods/list_addresses.rb +29 -0
- data/lib/firstclasspostcodes/response_error.rb +29 -0
- data/lib/firstclasspostcodes/version.rb +5 -0
- data/spec/firstclasspostcodes/client_spec.rb +94 -0
- data/spec/firstclasspostcodes/events_spec.rb +41 -0
- data/spec/firstclasspostcodes/operations/get_lookup_spec.rb +103 -0
- data/spec/firstclasspostcodes/operations/get_postcode_spec.rb +58 -0
- data/spec/firstclasspostcodes/operations/methods/format_address_spec.rb +106 -0
- data/spec/firstclasspostcodes/operations/methods/list_addresses_spec.rb +75 -0
- data/spec/firstclasspostcodes/response_error_spec.rb +43 -0
- data/spec/firstclasspostcodes/version_spec.rb +9 -0
- data/spec/firstclasspostcodes_spec.rb +108 -0
- data/spec/spec_helper.rb +42 -0
- data/spec/support/events_examples.rb +11 -0
- metadata +123 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Firstclasspostcodes
|
4
|
+
module Events
|
5
|
+
def on(event_name, &handler)
|
6
|
+
event_symbol = event_name.to_sym
|
7
|
+
events[event_symbol] ||= []
|
8
|
+
events[event_symbol].push(handler)
|
9
|
+
handler.object_id
|
10
|
+
end
|
11
|
+
|
12
|
+
def off(event_name, handler_id)
|
13
|
+
events[event_name.to_sym]&.filter! do |handler|
|
14
|
+
handler.object_id != handler_id
|
15
|
+
end
|
16
|
+
handler_id
|
17
|
+
end
|
18
|
+
|
19
|
+
def emit(event_name, *args)
|
20
|
+
events[event_name.to_sym]&.each { |handler| handler.call(*args) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def events
|
24
|
+
@events ||= {}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Firstclasspostcodes
|
4
|
+
module Operations
|
5
|
+
module GetLookup
|
6
|
+
def get_lookup(params)
|
7
|
+
raise StandError, "Expected hash, received: #{params}" unless params.is_a?(Hash)
|
8
|
+
|
9
|
+
error_object = nil
|
10
|
+
|
11
|
+
parse_f = ->(val) { val.to_f.to_s == val ? val.to_f : nil }
|
12
|
+
|
13
|
+
within = ->(lat, lng) { (lat >= -90 && lat <= 90) && (lng >= -180 && lng <= 180) }
|
14
|
+
|
15
|
+
unless params[:latitude] || params[:longitude]
|
16
|
+
error_object = {
|
17
|
+
message: "Missing required parameters, expected { latitude, longitude }.",
|
18
|
+
docUrl: "https://docs.firstclasspostcodes.com/operation/getLookup",
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
latitude = parse_f.call(params[:latitude])
|
23
|
+
longitude = parse_f.call(params[:longitude])
|
24
|
+
radius = parse_f.call(params[:radius]) || 0.1
|
25
|
+
|
26
|
+
query_params = { latitude: latitude, longitude: longitude, radius: radius }
|
27
|
+
|
28
|
+
unless latitude && longitude && within.call(latitude, longitude)
|
29
|
+
error_object = {
|
30
|
+
message: "Parameter is invalid: #{query_params}",
|
31
|
+
docUrl: "https://docs.firstclasspostcodes.com/operation/getLookup",
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
request_params = { path: "/lookup", method: :get, query_params: query_params }
|
36
|
+
|
37
|
+
@config.logger.debug("Executing operation getLookup: #{request_params}") if @config.debug?
|
38
|
+
|
39
|
+
emit("operation:getLookup", request_params)
|
40
|
+
|
41
|
+
if error_object
|
42
|
+
error = StandardError.new(error_object)
|
43
|
+
@config.logger.debug("Encountered ParameterValidationError: #{error}")
|
44
|
+
emit("error", error)
|
45
|
+
raise error
|
46
|
+
end
|
47
|
+
|
48
|
+
request(request_params)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "methods/list_addresses"
|
4
|
+
require_relative "methods/format_address"
|
5
|
+
|
6
|
+
module Firstclasspostcodes
|
7
|
+
module Operations
|
8
|
+
module GetPostcode
|
9
|
+
def get_postcode(postcode)
|
10
|
+
error_object = nil
|
11
|
+
|
12
|
+
if !postcode.is_a?(String) || postcode.empty?
|
13
|
+
error_object = {
|
14
|
+
message: "Unexpected postcode parameter: '#{postcode}'",
|
15
|
+
docUrl: "https://docs.firstclasspostcodes.com/operation/getPostcode",
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
request_params = { method: :get, path: "/postcode", query_params: { search: postcode } }
|
20
|
+
|
21
|
+
@config.logger.debug("Executing operation getPostcode: #{request_params}") if @config.debug?
|
22
|
+
|
23
|
+
emit("operation:getPostcode", request_params)
|
24
|
+
|
25
|
+
if error_object
|
26
|
+
error = StandardError.new(error_object)
|
27
|
+
@config.logger.debug("Encountered ParameterValidationError: #{error}")
|
28
|
+
emit("error", error)
|
29
|
+
raise error
|
30
|
+
end
|
31
|
+
|
32
|
+
response = request(request_params)
|
33
|
+
|
34
|
+
response.extend(Methods::ListAddresses, Methods::FormatAddress) unless @config.geo_json?
|
35
|
+
|
36
|
+
response
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Firstclasspostcodes
|
4
|
+
module Operations
|
5
|
+
module Methods
|
6
|
+
module FormatAddress
|
7
|
+
def format_address(index)
|
8
|
+
type, element = index.split(":")
|
9
|
+
|
10
|
+
data = {
|
11
|
+
locality: self[:city] || self[:locality],
|
12
|
+
region: self[:county] || self[:region],
|
13
|
+
postcode: self[:postcode],
|
14
|
+
country: self[:country],
|
15
|
+
}
|
16
|
+
|
17
|
+
return data if type == "postcode"
|
18
|
+
|
19
|
+
list = self[type.to_sym]
|
20
|
+
|
21
|
+
index = element.to_i
|
22
|
+
|
23
|
+
raise StandardError, `Received index "#{index}" but no #{type} data.` if list.empty?
|
24
|
+
|
25
|
+
address = if type == "numbers"
|
26
|
+
component = list[index]
|
27
|
+
join = ->(*args) { args.compact.reject(&:empty?).join(", ") }
|
28
|
+
join.call(component[:number], component[:building], component[:street])
|
29
|
+
else
|
30
|
+
list[index]
|
31
|
+
end
|
32
|
+
|
33
|
+
data.merge(address: address)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Firstclasspostcodes
|
4
|
+
module Operations
|
5
|
+
module Methods
|
6
|
+
module ListAddresses
|
7
|
+
def list_addresses
|
8
|
+
join = ->(*args) { args.compact.reject(&:empty?).join(", ") }
|
9
|
+
|
10
|
+
suffix = join.call(self[:city] || self[:locality], self[:postcode])
|
11
|
+
|
12
|
+
if self[:numbers]&.any?
|
13
|
+
return self[:numbers].each_with_index.map do |number, i|
|
14
|
+
%W[numbers:#{i} #{join.call(number[:number], number[:building], number[:street], suffix)}]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
if self[:streets]&.any?
|
19
|
+
return self[:streets].each_with_index.map do |street, i|
|
20
|
+
%W[streets:#{i} #{join.call(street, suffix)}]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
[%W[postcode:0 #{suffix}]]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
DOC_URL = "https://docs.firstclasspostcodes.com/ruby/errors"
|
4
|
+
|
5
|
+
module Firstclasspostcodes
|
6
|
+
class ResponseError < StandardError
|
7
|
+
attr_reader :doc_url, :type
|
8
|
+
|
9
|
+
def initialize(obj, type = nil)
|
10
|
+
if obj.is_a?(Hash)
|
11
|
+
super(obj[:message])
|
12
|
+
@doc_url = obj[:docUrl]
|
13
|
+
@type = obj[:type]
|
14
|
+
return
|
15
|
+
end
|
16
|
+
super(obj)
|
17
|
+
@doc_url = "#{DOC_URL}/#{type}"
|
18
|
+
@type = type
|
19
|
+
end
|
20
|
+
|
21
|
+
def message
|
22
|
+
<<-MSG
|
23
|
+
The following "#{type}" error was encountered:
|
24
|
+
#{super}
|
25
|
+
=> See: #{doc_url}
|
26
|
+
MSG
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
require_relative "../support/events_examples.rb"
|
7
|
+
|
8
|
+
describe Firstclasspostcodes::Client do
|
9
|
+
subject { Firstclasspostcodes::Client.new }
|
10
|
+
|
11
|
+
it_behaves_like "a class that emits events" do
|
12
|
+
subject { Firstclasspostcodes::Client.new }
|
13
|
+
end
|
14
|
+
|
15
|
+
specify { expect(subject).to respond_to(:get_postcode) }
|
16
|
+
|
17
|
+
specify { expect(subject).to respond_to(:get_lookup) }
|
18
|
+
|
19
|
+
describe "#config" do
|
20
|
+
specify { expect(subject.config).to be_instance_of(Firstclasspostcodes::Configuration) }
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#user_agent" do
|
24
|
+
specify { expect(subject).not_to respond_to(:user_agent=) }
|
25
|
+
|
26
|
+
specify { expect(subject.user_agent).to include Firstclasspostcodes::VERSION }
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#request" do
|
30
|
+
describe "when the request is successful" do
|
31
|
+
let(:response) { double(body: '{"a": 1 }') }
|
32
|
+
|
33
|
+
let(:request_params) { { method: :get, path: "/" } }
|
34
|
+
|
35
|
+
before(:each) { allow(subject).to receive(:call_request).with(kind_of(String), kind_of(Hash)).and_return(response) }
|
36
|
+
|
37
|
+
it "returns a parsed response" do
|
38
|
+
expect(subject.request(request_params)).to eq(a: 1)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "#call_request" do
|
44
|
+
describe "successful request" do
|
45
|
+
let(:stubbed_response) { Typhoeus::Response.new(code: 200, body: "test") }
|
46
|
+
|
47
|
+
before(:each) { Typhoeus.stub(/test/).and_return(stubbed_response) }
|
48
|
+
|
49
|
+
specify { expect(subject.call_request("www.test.com")).to eq(stubbed_response) }
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "unsuccessful request" do
|
53
|
+
let(:stubbed_response) { Typhoeus::Response.new(code: 403, body: "forbidden") }
|
54
|
+
|
55
|
+
before(:each) { Typhoeus.stub(/test/).and_return(stubbed_response) }
|
56
|
+
|
57
|
+
before(:each) { instance_double }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "#handle_request_error" do
|
62
|
+
describe "when request is timed out" do
|
63
|
+
let(:response) { double(timed_out?: true) }
|
64
|
+
|
65
|
+
specify { expect { subject.handle_request_error(response) }.to raise_error(Firstclasspostcodes::ResponseError) }
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "when there was a libcurl error" do
|
69
|
+
let(:response) { double(timed_out?: false, code: 0, return_message: "message") }
|
70
|
+
|
71
|
+
specify { expect { subject.handle_request_error(response) }.to raise_error(Firstclasspostcodes::ResponseError) }
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "when there was an api error" do
|
75
|
+
describe "when the response is JSON" do
|
76
|
+
let(:response) { double(timed_out?: false, code: 500, body: '{ "err": "message" }') }
|
77
|
+
|
78
|
+
specify { expect { subject.handle_request_error(response) }.to raise_error(Firstclasspostcodes::ResponseError) }
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "when the response cannot be parsed" do
|
82
|
+
let(:response) { double(timed_out?: false, code: 500, body: "error message") }
|
83
|
+
|
84
|
+
specify { expect { subject.handle_request_error(response) }.to raise_error(Firstclasspostcodes::ResponseError) }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "#build_request_url" do
|
90
|
+
let(:path) { "/test" }
|
91
|
+
|
92
|
+
specify { expect(subject.build_request_url(path)).to include path }
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
describe Firstclasspostcodes::Events do
|
7
|
+
class TestEvents
|
8
|
+
include Firstclasspostcodes::Events
|
9
|
+
end
|
10
|
+
|
11
|
+
subject { TestEvents.new }
|
12
|
+
|
13
|
+
let(:event_name) { "test" }
|
14
|
+
|
15
|
+
let(:handler) { proc { nil } }
|
16
|
+
|
17
|
+
describe "#on" do
|
18
|
+
it "should add a handler for a specific event" do
|
19
|
+
expect(subject.on(event_name, &handler)).to eq(handler.object_id)
|
20
|
+
expect(subject.events[event_name.to_sym]).to eq([handler])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#off" do
|
25
|
+
before(:each) { subject.on(event_name, &handler) }
|
26
|
+
|
27
|
+
it "should remove a handler for a specific event" do
|
28
|
+
subject.off(event_name, handler.object_id)
|
29
|
+
expect(subject.events[event_name.to_sym]).to eq([])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#emit" do
|
34
|
+
it "should emit an event" do
|
35
|
+
expect do |b|
|
36
|
+
subject.on(event_name, &b)
|
37
|
+
subject.emit(event_name, 1)
|
38
|
+
end.to yield_with_args(1)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe Firstclasspostcodes::Operations::GetLookup do
|
6
|
+
class TestGetLookup
|
7
|
+
attr_accessor :config
|
8
|
+
|
9
|
+
include Firstclasspostcodes::Operations::GetLookup
|
10
|
+
|
11
|
+
def emit(event_name, properties); end
|
12
|
+
|
13
|
+
def request(params); end
|
14
|
+
end
|
15
|
+
|
16
|
+
subject { TestGetLookup.new }
|
17
|
+
|
18
|
+
let(:operation_params) { { path: "/lookup", method: :get } }
|
19
|
+
|
20
|
+
let(:config) { double(debug?: true, logger: double(debug: nil)) }
|
21
|
+
|
22
|
+
before(:each) { allow(subject).to receive(:emit).with(anything, anything).and_return(true) }
|
23
|
+
|
24
|
+
before(:each) { subject.config = config }
|
25
|
+
|
26
|
+
specify { expect(subject).respond_to?(:get_lookup) }
|
27
|
+
|
28
|
+
describe "when the request is valid" do
|
29
|
+
before(:each) { allow(subject).to receive(:request).and_return(nil) }
|
30
|
+
|
31
|
+
let(:params) { { latitude: "52.3456", longitude: "-0.2567" } }
|
32
|
+
|
33
|
+
it "resolves with the correct request parameters" do
|
34
|
+
expected_request_params = operation_params.merge(
|
35
|
+
query_params: {
|
36
|
+
latitude: params[:latitude].to_f,
|
37
|
+
longitude: params[:longitude].to_f,
|
38
|
+
radius: anything,
|
39
|
+
}
|
40
|
+
)
|
41
|
+
|
42
|
+
expect(subject).to receive(:request).with(expected_request_params)
|
43
|
+
end
|
44
|
+
|
45
|
+
after(:each) { subject.get_lookup(params) }
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "when the request is invalid" do
|
49
|
+
describe "when there are no request options" do
|
50
|
+
it "raises an error" do
|
51
|
+
expect { subject.get_lookup("sertgh") }.to raise_error(StandardError)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "when the latitude is invalid" do
|
56
|
+
let(:params) { { latitude: "4567.123", longitude: "-0.2567" } }
|
57
|
+
|
58
|
+
it "raises an error" do
|
59
|
+
expect { subject.get_lookup(params) }.to raise_error(StandardError)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "when the latitude is incorrect" do
|
64
|
+
let(:params) { { latitude: "aaaaa", longitude: "-0.2567" } }
|
65
|
+
|
66
|
+
it "raises an error" do
|
67
|
+
expect { subject.get_lookup(params) }.to raise_error(StandardError)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "when the latitude is missing" do
|
72
|
+
let(:params) { { longitude: "-0.2567" } }
|
73
|
+
|
74
|
+
it "raises an error" do
|
75
|
+
expect { subject.get_lookup(params) }.to raise_error(StandardError)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "when the longitude is invalid" do
|
80
|
+
let(:params) { { latitude: "56.123", longitude: "-23456.2567" } }
|
81
|
+
|
82
|
+
it "raises an error" do
|
83
|
+
expect { subject.get_lookup(params) }.to raise_error(StandardError)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "when the longitude is incorrect" do
|
88
|
+
let(:params) { { latitude: "56.123", longitude: "ertyhgfd" } }
|
89
|
+
|
90
|
+
it "raises an error" do
|
91
|
+
expect { subject.get_lookup(params) }.to raise_error(StandardError)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "when the longitude is missing" do
|
96
|
+
let(:params) { { latitude: "56.123" } }
|
97
|
+
|
98
|
+
it "raises an error" do
|
99
|
+
expect { subject.get_lookup(params) }.to raise_error(StandardError)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|