google_distance_matrix 0.0.1

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