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