firstclasspostcodes 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/.dependabot/config.yml +12 -0
  3. data/.github/workflows/gem.yml +53 -0
  4. data/.gitignore +9 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +78 -0
  7. data/Gemfile +25 -0
  8. data/LICENSE +21 -0
  9. data/Rakefile +11 -0
  10. data/firstclasspostcodes.gemspec +54 -0
  11. data/lib/firstclasspostcodes.rb +33 -0
  12. data/lib/firstclasspostcodes/client.rb +81 -0
  13. data/lib/firstclasspostcodes/configuration.rb +156 -0
  14. data/lib/firstclasspostcodes/events.rb +27 -0
  15. data/lib/firstclasspostcodes/operations.rb +4 -0
  16. data/lib/firstclasspostcodes/operations/get_lookup.rb +52 -0
  17. data/lib/firstclasspostcodes/operations/get_postcode.rb +40 -0
  18. data/lib/firstclasspostcodes/operations/methods/format_address.rb +38 -0
  19. data/lib/firstclasspostcodes/operations/methods/list_addresses.rb +29 -0
  20. data/lib/firstclasspostcodes/response_error.rb +29 -0
  21. data/lib/firstclasspostcodes/version.rb +5 -0
  22. data/spec/firstclasspostcodes/client_spec.rb +94 -0
  23. data/spec/firstclasspostcodes/events_spec.rb +41 -0
  24. data/spec/firstclasspostcodes/operations/get_lookup_spec.rb +103 -0
  25. data/spec/firstclasspostcodes/operations/get_postcode_spec.rb +58 -0
  26. data/spec/firstclasspostcodes/operations/methods/format_address_spec.rb +106 -0
  27. data/spec/firstclasspostcodes/operations/methods/list_addresses_spec.rb +75 -0
  28. data/spec/firstclasspostcodes/response_error_spec.rb +43 -0
  29. data/spec/firstclasspostcodes/version_spec.rb +9 -0
  30. data/spec/firstclasspostcodes_spec.rb +108 -0
  31. data/spec/spec_helper.rb +42 -0
  32. data/spec/support/events_examples.rb +11 -0
  33. 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,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "operations/get_postcode"
4
+ require_relative "operations/get_lookup"
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Firstclasspostcodes
4
+ VERSION = "0.0.1"
5
+ 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