google_distance_matrix 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 (34) hide show
  1. data/.gitignore +17 -0
  2. data/.rbenv-version +1 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +106 -0
  6. data/Rakefile +1 -0
  7. data/google_distance_matrix.gemspec +30 -0
  8. data/lib/google_distance_matrix.rb +38 -0
  9. data/lib/google_distance_matrix/client.rb +47 -0
  10. data/lib/google_distance_matrix/configuration.rb +68 -0
  11. data/lib/google_distance_matrix/errors.rb +88 -0
  12. data/lib/google_distance_matrix/log_subscriber.rb +14 -0
  13. data/lib/google_distance_matrix/logger.rb +32 -0
  14. data/lib/google_distance_matrix/matrix.rb +122 -0
  15. data/lib/google_distance_matrix/place.rb +101 -0
  16. data/lib/google_distance_matrix/places.rb +43 -0
  17. data/lib/google_distance_matrix/railtie.rb +9 -0
  18. data/lib/google_distance_matrix/route.rb +49 -0
  19. data/lib/google_distance_matrix/routes_finder.rb +149 -0
  20. data/lib/google_distance_matrix/url_builder.rb +63 -0
  21. data/lib/google_distance_matrix/version.rb +3 -0
  22. data/spec/lib/google_distance_matrix/client_spec.rb +67 -0
  23. data/spec/lib/google_distance_matrix/configuration_spec.rb +63 -0
  24. data/spec/lib/google_distance_matrix/logger_spec.rb +38 -0
  25. data/spec/lib/google_distance_matrix/matrix_spec.rb +169 -0
  26. data/spec/lib/google_distance_matrix/place_spec.rb +93 -0
  27. data/spec/lib/google_distance_matrix/places_spec.rb +77 -0
  28. data/spec/lib/google_distance_matrix/route_spec.rb +28 -0
  29. data/spec/lib/google_distance_matrix/routes_finder_spec.rb +190 -0
  30. data/spec/lib/google_distance_matrix/url_builder_spec.rb +105 -0
  31. data/spec/request_recordings/success +62 -0
  32. data/spec/request_recordings/zero_results +57 -0
  33. data/spec/spec_helper.rb +24 -0
  34. metadata +225 -0
@@ -0,0 +1,63 @@
1
+ module GoogleDistanceMatrix
2
+ class UrlBuilder
3
+ BASE_URL = "maps.googleapis.com/maps/api/distancematrix/json"
4
+ DELIMITER = CGI.escape("|")
5
+ MAX_URL_SIZE = 2048
6
+
7
+ attr_reader :matrix
8
+ delegate :configuration, to: :matrix
9
+
10
+ def initialize(matrix)
11
+ @matrix = matrix
12
+
13
+ fail InvalidMatrix.new matrix if matrix.invalid?
14
+ end
15
+
16
+ def url
17
+ @url ||= build_url
18
+ end
19
+
20
+
21
+ private
22
+
23
+ def build_url
24
+ url = [protocol, BASE_URL, "?", get_params_string].join
25
+
26
+ if sign_url?
27
+ url = GoogleBusinessApiUrlSigner.add_signature(url, configuration.google_business_api_private_key)
28
+ end
29
+
30
+ if url.length > MAX_URL_SIZE
31
+ fail MatrixUrlTooLong.new url, MAX_URL_SIZE
32
+ end
33
+
34
+ url
35
+ end
36
+
37
+ def sign_url?
38
+ configuration.google_business_api_client_id.present? and
39
+ configuration.google_business_api_private_key.present?
40
+ end
41
+
42
+ def get_params_string
43
+ params.to_a.map { |key_value| key_value.join("=") }.join("&")
44
+ end
45
+
46
+ def params
47
+ places_to_param_config = {lat_lng_scale: configuration.lat_lng_scale}
48
+
49
+ configuration.to_param.merge(
50
+ origins: matrix.origins.map { |o| escape o.to_param(places_to_param_config) }.join(DELIMITER),
51
+ destinations: matrix.destinations.map { |d| escape d.to_param(places_to_param_config) }.join(DELIMITER),
52
+ )
53
+ end
54
+
55
+ def protocol
56
+ configuration.protocol + "://"
57
+ end
58
+
59
+ def escape(string)
60
+ CGI.escape string
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,3 @@
1
+ module GoogleDistanceMatrix
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,67 @@
1
+ require "spec_helper"
2
+
3
+ describe GoogleDistanceMatrix::Client, :request_recordings do
4
+ let(:origin_1) { GoogleDistanceMatrix::Place.new address: "Karl Johans gate, Oslo" }
5
+ let(:destination_1) { GoogleDistanceMatrix::Place.new address: "Drammensveien 1, Oslo" }
6
+ let(:matrix) { GoogleDistanceMatrix::Matrix.new(origins: [origin_1], destinations: [destination_1]) }
7
+
8
+ let(:url_builder) { GoogleDistanceMatrix::UrlBuilder.new matrix }
9
+ let(:url) { url_builder.url }
10
+
11
+ subject { GoogleDistanceMatrix::Client.new }
12
+
13
+ describe "success" do
14
+ before { stub_request(:get, url).to_return body: recorded_request_for(:success) }
15
+
16
+ it "makes the request" do
17
+ expect(subject.get(url_builder.url).body).to eq recorded_request_for(:success).read
18
+ end
19
+ end
20
+
21
+ describe "client errors" do
22
+ describe "server issues 4xx client error" do
23
+ it "wraps the error http response" do
24
+ stub_request(:get, url).to_return status: [400, "Client error"]
25
+ expect { subject.get(url_builder.url) }.to raise_error GoogleDistanceMatrix::ClientError
26
+ end
27
+
28
+ it "wraps uri too long error" do
29
+ stub_request(:get, url).to_return status: [414, "Client error"]
30
+ expect { subject.get(url_builder.url) }.to raise_error GoogleDistanceMatrix::MatrixUrlTooLong
31
+ end
32
+ end
33
+
34
+ described_class::CLIENT_ERRORS.each do |error|
35
+ it "wraps '#{error}' client error" do
36
+ stub_request(:get, url).to_return body: JSON.generate({status: error})
37
+ expect { subject.get(url_builder.url) }.to raise_error GoogleDistanceMatrix::ClientError
38
+ end
39
+ end
40
+ end
41
+
42
+ describe "request errors" do
43
+ describe "server error" do
44
+ before { stub_request(:get, url).to_return status: [500, "Internal Server Error"] }
45
+
46
+ it "wraps the error http response" do
47
+ expect { subject.get(url_builder.url) }.to raise_error GoogleDistanceMatrix::RequestError
48
+ end
49
+ end
50
+
51
+ describe "timeout" do
52
+ before { stub_request(:get, url).to_timeout }
53
+
54
+ it "wraps the error from Net::HTTP" do
55
+ expect { subject.get(url_builder.url).body }.to raise_error GoogleDistanceMatrix::RequestError
56
+ end
57
+ end
58
+
59
+ describe "server error" do
60
+ before { stub_request(:get, url).to_return status: [999, "Unknown"] }
61
+
62
+ it "wraps the error http response" do
63
+ expect { subject.get(url_builder.url) }.to raise_error GoogleDistanceMatrix::RequestError
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,63 @@
1
+ require "spec_helper"
2
+
3
+ describe GoogleDistanceMatrix::Configuration do
4
+ subject { described_class.new }
5
+
6
+ describe "Validations" do
7
+ it { should ensure_inclusion_of(:sensor).in_array([true, false]) }
8
+
9
+ it { should ensure_inclusion_of(:mode).in_array(["driving", "walking", "bicycling"]) }
10
+ it { should allow_value(nil).for(:mode) }
11
+
12
+ it { should ensure_inclusion_of(:avoid).in_array(["tolls", "highways"]) }
13
+ it { should allow_value(nil).for(:avoid) }
14
+
15
+ it { should ensure_inclusion_of(:units).in_array(["metric", "imperial"]) }
16
+ it { should allow_value(nil).for(:units) }
17
+
18
+ it { should ensure_inclusion_of(:protocol).in_array(["http", "https"]) }
19
+ end
20
+
21
+
22
+ describe "defaults" do
23
+ its(:sensor) { should be_false }
24
+ its(:mode) { should eq "driving" }
25
+ its(:avoid) { should be_nil }
26
+ its(:units) { should eq "metric" }
27
+ its(:lat_lng_scale) { should eq 5 }
28
+ its(:protocol) { should eq "http" }
29
+
30
+ its(:google_business_api_client_id) { should be_nil }
31
+ its(:google_business_api_private_key) { should be_nil }
32
+
33
+ its(:logger) { should be_nil }
34
+ end
35
+
36
+
37
+ describe "#to_param" do
38
+ described_class::ATTRIBUTES.each do |attr|
39
+ it "includes #{attr}" do
40
+ subject[attr] = "foo"
41
+ expect(subject.to_param[attr]).to eq subject.public_send(attr)
42
+ end
43
+
44
+ it "does not include #{attr} when it is blank" do
45
+ subject[attr] = nil
46
+ expect(subject.to_param.with_indifferent_access).to_not have_key attr
47
+ end
48
+ end
49
+
50
+ described_class::API_DEFAULTS.each_pair do |attr, default_value|
51
+ it "does not include #{attr} when it equals what is default for API" do
52
+ subject[attr] = default_value
53
+
54
+ expect(subject.to_param.with_indifferent_access).to_not have_key attr
55
+ end
56
+ end
57
+
58
+ it "includes client if google_business_api_client_id has been set" do
59
+ subject.google_business_api_client_id = "123"
60
+ expect(subject.to_param['client']).to eq "123"
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,38 @@
1
+ require "spec_helper"
2
+
3
+ describe GoogleDistanceMatrix::Logger do
4
+ context "without a logger backend" do
5
+ subject { described_class.new }
6
+
7
+ described_class::LEVELS.each do |level|
8
+ it "logging #{level} does not fail" do
9
+ subject.public_send level, "log msg"
10
+ end
11
+ end
12
+ end
13
+
14
+ context "with a logger backend" do
15
+ let(:backend) { mock }
16
+
17
+ subject { described_class.new backend }
18
+
19
+ described_class::LEVELS.each do |level|
20
+ describe level do
21
+ it "sends log message to the backend" do
22
+ backend.should_receive(level).with("[google_distance_matrix] log msg")
23
+ subject.public_send level, "log msg"
24
+ end
25
+
26
+ it "supports sending in a tag" do
27
+ backend.should_receive(level).with("[google_distance_matrix] [client] log msg")
28
+ subject.public_send level, "log msg", tag: :client
29
+ end
30
+
31
+ it "supports sending in multiple tags" do
32
+ backend.should_receive(level).with("[google_distance_matrix] [client] [request] log msg")
33
+ subject.public_send level, "log msg", tag: ['client', 'request']
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,169 @@
1
+ require "spec_helper"
2
+
3
+ describe GoogleDistanceMatrix::Matrix do
4
+ let(:origin_1) { GoogleDistanceMatrix::Place.new address: "Karl Johans gate, Oslo" }
5
+ let(:origin_2) { GoogleDistanceMatrix::Place.new address: "Askerveien 1, Asker" }
6
+
7
+ let(:destination_1) { GoogleDistanceMatrix::Place.new address: "Drammensveien 1, Oslo" }
8
+ let(:destination_2) { GoogleDistanceMatrix::Place.new address: "Skjellestadhagen, Heggedal" }
9
+
10
+ let(:url_builder) { GoogleDistanceMatrix::UrlBuilder.new subject }
11
+ let(:url) { url_builder.url }
12
+
13
+ subject do
14
+ described_class.new(
15
+ origins: [origin_1, origin_2],
16
+ destinations: [destination_1, destination_2]
17
+ )
18
+ end
19
+
20
+ describe "#initialize" do
21
+ it "takes a list of origins" do
22
+ matrix = described_class.new origins: [origin_1, origin_2]
23
+ expect(matrix.origins).to include origin_1, origin_2
24
+ end
25
+
26
+ it "takes a list of destinations" do
27
+ matrix = described_class.new destinations: [destination_1, destination_2]
28
+ expect(matrix.destinations).to include destination_1, destination_2
29
+ end
30
+
31
+ it "has a default configuration" do
32
+ expect(subject.configuration).to be_present
33
+ end
34
+ end
35
+
36
+ describe "#configuration" do
37
+ it "is by default set from default_configuration" do
38
+ config = mock
39
+ config.stub(:dup).and_return config
40
+ GoogleDistanceMatrix.should_receive(:default_configuration).and_return config
41
+
42
+ expect(described_class.new.configuration).to eq config
43
+ end
44
+
45
+ it "has it's own configuration" do
46
+ expect {
47
+ subject.configure { |c| c.sensor = !GoogleDistanceMatrix.default_configuration.sensor }
48
+ }.to_not change(GoogleDistanceMatrix.default_configuration, :sensor)
49
+ end
50
+
51
+ it "has a configurable configuration :-)" do
52
+ expect {
53
+ subject.configure { |c| c.sensor = !GoogleDistanceMatrix.default_configuration.sensor }
54
+ }.to change(subject.configuration, :sensor).to !GoogleDistanceMatrix.default_configuration.sensor
55
+ end
56
+ end
57
+
58
+ %w[origins destinations].each do |attr|
59
+ let(:place) { GoogleDistanceMatrix::Place.new address: "My street" }
60
+
61
+ describe "##{attr}" do
62
+ it "can receive places" do
63
+ subject.public_send(attr) << place
64
+ expect(subject.public_send(attr)).to include place
65
+ end
66
+
67
+ it "does not same place twice" do
68
+ expect {
69
+ 2.times { subject.public_send(attr) << place }
70
+ }.to change(subject.public_send(attr), :length).by 1
71
+ end
72
+ end
73
+ end
74
+
75
+ %w[
76
+ route_for
77
+ route_for!
78
+ routes_for
79
+ routes_for!
80
+ shortest_route_by_duration_to
81
+ shortest_route_by_duration_to!
82
+ shortest_route_by_distance_to
83
+ shortest_route_by_distance_to!
84
+ ].each do |method|
85
+ it "delegates #{method} to routes_finder" do
86
+ finder = mock
87
+ result = mock
88
+
89
+ subject.stub(:routes_finder).and_return finder
90
+
91
+ finder.should_receive(method).and_return result
92
+ expect(subject.public_send(method)).to eq result
93
+ end
94
+ end
95
+
96
+ describe "#data", :request_recordings do
97
+ context "success" do
98
+ let!(:api_request_stub) { stub_request(:get, url).to_return body: recorded_request_for(:success) }
99
+
100
+ it "loads from Google's API" do
101
+ subject.data
102
+ api_request_stub.should have_been_requested
103
+ end
104
+
105
+ it "does not load twice" do
106
+ 2.times { subject.data }
107
+ api_request_stub.should have_been_requested
108
+ end
109
+
110
+ it "contains one row" do
111
+ expect(subject.data.length).to eq 2
112
+ end
113
+
114
+ it "contains two columns each row" do
115
+ expect(subject.data[0].length).to eq 2
116
+ expect(subject.data[1].length).to eq 2
117
+ end
118
+
119
+ it "assigns correct origin on routes in the data" do
120
+ expect(subject.data[0][0].origin).to eq origin_1
121
+ expect(subject.data[0][1].origin).to eq origin_1
122
+
123
+ expect(subject.data[1][0].origin).to eq origin_2
124
+ expect(subject.data[1][1].origin).to eq origin_2
125
+ end
126
+
127
+ it "assigns correct destination on routes in the data" do
128
+ expect(subject.data[0][0].destination).to eq destination_1
129
+ expect(subject.data[0][1].destination).to eq destination_2
130
+
131
+ expect(subject.data[1][0].destination).to eq destination_1
132
+ expect(subject.data[1][1].destination).to eq destination_2
133
+ end
134
+ end
135
+
136
+ context "some elements is not OK" do
137
+ let!(:api_request_stub) { stub_request(:get, url).to_return body: recorded_request_for(:zero_results) }
138
+
139
+ it "loads from Google's API" do
140
+ subject.data
141
+ api_request_stub.should have_been_requested
142
+ end
143
+
144
+ it "as loaded route with errors correctly" do
145
+ route = subject.data[0][1]
146
+
147
+ expect(route.status).to eq "zero_results"
148
+ expect(route.duration_in_seconds).to be_nil
149
+ end
150
+ end
151
+ end
152
+
153
+ describe "#reload" do
154
+ before do
155
+ subject.stub(:load_matrix).and_return { ['loaded'] }
156
+ subject.data.clear
157
+ end
158
+
159
+ it "reloads matrix' data from the API" do
160
+ expect {
161
+ subject.reload
162
+ }.to change(subject, :data).from([]).to ['loaded']
163
+ end
164
+
165
+ it "is chainable" do
166
+ expect(subject.reload.data).to eq ['loaded']
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,93 @@
1
+ require "spec_helper"
2
+
3
+ describe GoogleDistanceMatrix::Place do
4
+ let(:address) { "Karl Johans gate, Oslo" }
5
+ let(:lat) { 1.4 }
6
+ let(:lng) { 2.2 }
7
+
8
+ describe "#initialize" do
9
+ it "builds with an address" do
10
+ place = described_class.new(address: address)
11
+ expect(place.address).to eq address
12
+ end
13
+
14
+ it "builds with lat lng" do
15
+ place = described_class.new(lat: lat, lng: lng)
16
+ expect(place.lat).to eq lat
17
+ expect(place.lng).to eq lng
18
+ end
19
+
20
+ it "builds with an object responding to lat and lng" do
21
+ point = mock lat: 1, lng: 2
22
+ place = described_class.new(point)
23
+
24
+ expect(place.lat).to eq point.lat
25
+ expect(place.lng).to eq point.lng
26
+ end
27
+
28
+
29
+ it "keeps a record of the object it built itself from" do
30
+ point = mock lat: 1, lng: 2
31
+ place = described_class.new(point)
32
+
33
+ expect(place.extracted_attributes_from).to eq point
34
+ end
35
+ it "builds with an object responding to address" do
36
+ object = mock address: address
37
+ place = described_class.new(object)
38
+
39
+ expect(place.address).to eq object.address
40
+ end
41
+
42
+ it "builds with an object responding to lat, lng and address" do
43
+ object = mock lat: 1, lng:2, address: address
44
+ place = described_class.new(object)
45
+
46
+ expect(place.lat).to eq object.lat
47
+ expect(place.lng).to eq object.lng
48
+ expect(place.address).to be_nil
49
+ end
50
+
51
+ it "fails if no valid attributes given" do
52
+ expect { described_class.new }.to raise_error ArgumentError
53
+ expect { described_class.new(lat: lat) }.to raise_error ArgumentError
54
+ expect { described_class.new(lng: lng) }.to raise_error ArgumentError
55
+ end
56
+
57
+ it "fails if both address, lat ang lng is given" do
58
+ expect { described_class.new(address: address, lat: lat, lng: lng) }.to raise_error ArgumentError
59
+ end
60
+ end
61
+
62
+ describe "#to_param" do
63
+ context "with address" do
64
+ subject { described_class.new address: address }
65
+
66
+ its(:to_param) { should eq address }
67
+ end
68
+
69
+ context "with lat lng" do
70
+ subject { described_class.new lng: lng, lat: lat }
71
+
72
+ its(:to_param) { should eq "#{lat},#{lng}" }
73
+ end
74
+ end
75
+
76
+ describe "#equal?" do
77
+ it "is considered equal when address is the same" do
78
+ expect(described_class.new(address: address)).to be_eql described_class.new(address: address)
79
+ end
80
+
81
+ it "is considered equal when lat and lng are the same" do
82
+ expect(described_class.new(lat: lat, lng: lng)).to be_eql described_class.new(lat: lat, lng: lng)
83
+ end
84
+
85
+ it "is not considered equal when address differs" do
86
+ expect(described_class.new(address: address)).to_not be_eql described_class.new(address: address + ", Norway")
87
+ end
88
+
89
+ it "is not considered equal when lat or lng differs" do
90
+ expect(described_class.new(lat: lat, lng: lng)).to_not be_eql described_class.new(lat: lat, lng: lng + 1)
91
+ end
92
+ end
93
+ end