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.
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