From 13afe1668d35e65e3f23171161b055dacf1ec3af Mon Sep 17 00:00:00 2001 From: Mike Heft Date: Wed, 3 Jul 2024 20:45:00 -0600 Subject: [PATCH] Add functionality to get route duration for commuting the rides from address --- app/models/driver.rb | 6 +- lib/rides/commands/get_commute_duration.rb | 25 +++++++ lib/rides/commands/get_routes_data.rb | 66 +++++++++++++++++++ spec/factories/drivers.rb | 2 + .../commands/get_commute_duration_spec.rb | 23 +++++++ .../rides/commands/get_routes_data_spec.rb | 22 +++++++ spec/rails_helper.rb | 7 +- 7 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 lib/rides/commands/get_commute_duration.rb create mode 100644 lib/rides/commands/get_routes_data.rb create mode 100644 spec/lib/rides/commands/get_commute_duration_spec.rb create mode 100644 spec/lib/rides/commands/get_routes_data_spec.rb diff --git a/app/models/driver.rb b/app/models/driver.rb index 9c00472..145f47a 100644 --- a/app/models/driver.rb +++ b/app/models/driver.rb @@ -7,7 +7,11 @@ class Driver < ApplicationRecord class_name: "DriverAddress", dependent: :destroy, inverse_of: :driver - has_one :current_address, through: :current_driver_address, source: :driver, dependent: :destroy + has_one :current_address, through: :current_driver_address, source: :address, dependent: :destroy validates :first_name, :last_name, presence: true + + def origin_place_id + current_address.place_id + end end diff --git a/lib/rides/commands/get_commute_duration.rb b/lib/rides/commands/get_commute_duration.rb new file mode 100644 index 0000000..f37ea78 --- /dev/null +++ b/lib/rides/commands/get_commute_duration.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Rides + module Commands + # Computes the duration of the commute for the ride. + # This is used in the ranking for the rides that the driver will + # choose from. + # Returns a list of objects where the origin is the driver's current address(home) + class GetCommuteDuration < BaseCommand + def call(rides:, driver:) + commute_rides = convert_rides(rides, driver) + GetRoutesData.call(rides: commute_rides) + end + + # Converts the Driver's current home address and + # the Ride#from_address into structs that can be used to + # obtain the route information + private def convert_rides(rides, driver) + rides.each_with_object([]) do |ride, acc| + acc << OpenStruct.new(origin_place_id: driver.origin_place_id, destination_place_id: ride.origin_place_id) + end + end + end + end +end diff --git a/lib/rides/commands/get_routes_data.rb b/lib/rides/commands/get_routes_data.rb new file mode 100644 index 0000000..452e105 --- /dev/null +++ b/lib/rides/commands/get_routes_data.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module Rides + module Commands + # Makes a request to the Google API to obtain the route information + class GetRoutesData < BaseCommand + DIRECTIONS_API_URL = "https://routes.googleapis.com/distanceMatrix/v2:computeRouteMatrix" + DEFAULT_HEADERS = { + "X-Goog-FieldMask" => "originIndex,destinationIndex,status,condition,distanceMeters,duration", + "X-goog-api-key" => ENV["GOOGLE_API_KEY"], + "Content-Type" => "application/json" + }.freeze + + def call(rides:) + data = get_direction_data_for_ride(rides) + results(data, rides) + end + + # Returns a list of objects, with attributes of + # @param[:distance_in_meters] = Integer + # @param[:duration] = String, e.g., "577s" + # Duration is in seconds + private def results(data, rides) + # The response keeps the array positioning on the return. Since we're getting a matrix + # of routes back, we only want the ones where we explicitly have a 'Ride'. This means that + # we want the computations where the indicies match. + data = data.select { _1[:originIndex] == _1[:destinationIndex] } + data = transform_keys!(data) + + data.map.with_index { OpenStruct.new(ride: rides[_2], **_1) } + end + + private def connection + @connection ||= Client::Request.connection( + url: DIRECTIONS_API_URL, + headers: DEFAULT_HEADERS + ) + end + + private def get_direction_data_for_ride(rides) + body = build_request_body(rides) + + response = connection.post( + DIRECTIONS_API_URL, + body.merge(routingPreference: "TRAFFIC_AWARE", travelMode: "DRIVE") + ) + + JSON.parse(response.body, symbolize_names: true) + end + + private def build_request_body(rides) + rides.each_with_object({}) do |ride, acc| + acc[:origins] ||= [] + acc[:destinations] ||= [] + + acc[:origins] << { waypoint: { placeId: ride.origin_place_id } } + acc[:destinations] << { waypoint: { placeId: ride.destination_place_id } } + end + end + + private def transform_keys!(data) + data.map { |d| d.transform_keys { |k| k.to_s.underscore.to_sym } } + end + end + end +end diff --git a/spec/factories/drivers.rb b/spec/factories/drivers.rb index 34d7220..3cb805c 100644 --- a/spec/factories/drivers.rb +++ b/spec/factories/drivers.rb @@ -4,5 +4,7 @@ factory :driver do first_name { Faker::Name.first_name } last_name { Faker::Name.last_name } + + association :current_address, factory: :address end end diff --git a/spec/lib/rides/commands/get_commute_duration_spec.rb b/spec/lib/rides/commands/get_commute_duration_spec.rb new file mode 100644 index 0000000..5e26dfb --- /dev/null +++ b/spec/lib/rides/commands/get_commute_duration_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +RSpec.describe Rides::Commands::GetCommuteDuration do + it "gets data for an address" do + VCR.use_cassette("first_commute") do + from_address = create( + :address, :with_out_place_id, line_1: "4705 Weitzel Street", city: "Timnath", state: "CO", + zip_code: "80547" + ) + to_address = create( + :address, :with_out_place_id, line_1: "151 N College Ave", city: "Fort Collins", state: "CO", + zip_code: "80524" + ) + create_list(:ride, 2, from_address:, to_address:) + driver = create(:driver, current_address: from_address) + rides = Ride.selectable + data = described_class.call(rides:, driver:) + + expect(data.length).to eq(2) + expect(data.all? { _1.duration == "577s" }) + end + end +end diff --git a/spec/lib/rides/commands/get_routes_data_spec.rb b/spec/lib/rides/commands/get_routes_data_spec.rb new file mode 100644 index 0000000..aba66cf --- /dev/null +++ b/spec/lib/rides/commands/get_routes_data_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +RSpec.describe Rides::Commands::GetRoutesData do + it "gets data for an address" do + VCR.use_cassette("first_ride_directions") do + from_address = create( + :address, :with_out_place_id, line_1: "711 Oval Drive", city: "Fort Collins", state: "CO", + zip_code: "80521" + ) + to_address = create( + :address, :with_out_place_id, line_1: "151 N College Ave", city: "Fort Collins", state: "CO", + zip_code: "80524" + ) + create_list(:ride, 2, from_address:, to_address:) + rides = Ride.selectable + data = described_class.call(rides:) + + expect(data.length).to eq(2) + expect(data.all? { _1.distance_in_meters == 3105 && _1.duration == "577s" }) + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 31cf0eb..0a46939 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -13,9 +13,10 @@ VCR.configure do |config| config.cassette_library_dir = "spec/cassettes" config.hook_into :webmock - config.default_cassette_options = { - match_requests_on: %i[method uri] - } + config.debug_logger = File.open("log/vcr_debug.log", "w") + # config.default_cassette_options = { + # match_requests_on: %i[method uri] + # } end # Add additional requires below this line. Rails is not loaded until this point!