From 4834f532b3f72f0104dcdf058f252592d17afd6e Mon Sep 17 00:00:00 2001 From: Mike Heft Date: Sat, 6 Jul 2024 19:24:32 -0600 Subject: [PATCH] 11 expose api (#27) * Generate drivers controller * Add jsonapi-serializer * Add spec showing driver return * Add routes for rides * Add rank command to rank rides based on provided formula * remove unneeded generator * Work on creating spec for ranked rides * Work on serialization * work on serialization * Add expectations for api return * Fix return specs * remove controllers for now * fix rubocop * fix rubocop errors * fix flaky spec --- Gemfile | 1 + Gemfile.lock | 3 + app/controllers/application_controller.rb | 10 + .../drivers/selectable_rides_controller.rb | 19 + app/controllers/drivers_controller.rb | 12 + app/models/driver.rb | 6 +- app/serializers/address_serializer.rb | 7 + app/serializers/driver_serializer.rb | 8 + app/serializers/ride_pojo_serializer.rb | 32 + config/environments/development.rb | 6 +- config/environments/test.rb | 2 +- config/locales/en.yml | 7 + config/routes.rb | 7 +- db/seeds.rb | 2 +- lib/rides/commands/get_commute_duration.rb | 4 +- lib/rides/commands/get_routes_data.rb | 20 +- lib/rides/commands/rank_rides.rb | 88 ++ spec/cassettes/ranked_rides.yml | 838 ++++++++++++++++++ .../rides/commands/get_routes_data_spec.rb | 8 +- spec/requests/drivers/drivers_spec.rb | 35 + .../requests/drivers/selectable_rides_spec.rb | 55 ++ spec/requests/drivers_spec.rb | 35 + 22 files changed, 1194 insertions(+), 11 deletions(-) create mode 100644 app/controllers/drivers/selectable_rides_controller.rb create mode 100644 app/controllers/drivers_controller.rb create mode 100644 app/serializers/address_serializer.rb create mode 100644 app/serializers/driver_serializer.rb create mode 100644 app/serializers/ride_pojo_serializer.rb create mode 100644 lib/rides/commands/rank_rides.rb create mode 100644 spec/cassettes/ranked_rides.yml create mode 100644 spec/requests/drivers/drivers_spec.rb create mode 100644 spec/requests/drivers/selectable_rides_spec.rb create mode 100644 spec/requests/drivers_spec.rb diff --git a/Gemfile b/Gemfile index 483d356..c3e3e02 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gem "faker" gem "faraday" gem "figaro" gem "geocoder" +gem "jsonapi-serializer" gem "money-rails" gem "pg", "~> 1.1" gem "puma", ">= 5.0" diff --git a/Gemfile.lock b/Gemfile.lock index f015af6..6cbda39 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -131,6 +131,8 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.7.2) + jsonapi-serializer (2.2.0) + activesupport (>= 4.2) language_server-protocol (3.17.0.3) loofah (2.22.0) crass (~> 1.0.2) @@ -337,6 +339,7 @@ DEPENDENCIES faraday figaro geocoder + jsonapi-serializer money-rails pg (~> 1.1) pry-rails diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 13c271f..aadf6ca 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,14 @@ # frozen_string_literal: true class ApplicationController < ActionController::API + private def pagination_params + params.permit(:limit, :offset) + end + private def limit + pagination_params[:limit] || 2 + end + + private def offset + pagination_params[:offset] || 0 + end end diff --git a/app/controllers/drivers/selectable_rides_controller.rb b/app/controllers/drivers/selectable_rides_controller.rb new file mode 100644 index 0000000..623b89a --- /dev/null +++ b/app/controllers/drivers/selectable_rides_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Drivers + class SelectableRidesController < ApplicationController + def index + rides = Rides::Commands::RankRides.call(driver:)[offset, limit] + opts = { include: %i[from_address to_address] } + render json: RidePojoSerializer.new(rides, opts) + end + + private def driver + @driver ||= Driver.find(ride_params[:driver_id]) + end + + private def ride_params + params.permit(:driver_id, :limit, :offset) + end + end +end diff --git a/app/controllers/drivers_controller.rb b/app/controllers/drivers_controller.rb new file mode 100644 index 0000000..a295377 --- /dev/null +++ b/app/controllers/drivers_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class DriversController < ApplicationController + def index + drivers = Driver.limit(limit).offset(offset) + render json: DriverSerializer.new(drivers, is_collection: true) + end + + private def pagination_params + params.permit(:limit, :offset) + end +end diff --git a/app/models/driver.rb b/app/models/driver.rb index 3b362ab..631a4b1 100644 --- a/app/models/driver.rb +++ b/app/models/driver.rb @@ -9,7 +9,11 @@ class Driver < ApplicationRecord inverse_of: :driver has_one :current_address, through: :current_driver_address, source: :address, dependent: :destroy - validates :first_name, :last_name, :current_address, presence: true + validates :first_name, :last_name, presence: true + + def full_name + [first_name, last_name].join(" ") + end def origin_place_id current_address.place_id diff --git a/app/serializers/address_serializer.rb b/app/serializers/address_serializer.rb new file mode 100644 index 0000000..0764751 --- /dev/null +++ b/app/serializers/address_serializer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddressSerializer + include JSONAPI::Serializer + set_type :address + attributes :id, :line_1, :line_2, :city, :state, :zip_code, :full_address +end diff --git a/app/serializers/driver_serializer.rb b/app/serializers/driver_serializer.rb new file mode 100644 index 0000000..f82a9aa --- /dev/null +++ b/app/serializers/driver_serializer.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class DriverSerializer + include JSONAPI::Serializer + set_type :driver + attributes :id, :full_name + has_one :current_address, serializer: AddressSerializer +end diff --git a/app/serializers/ride_pojo_serializer.rb b/app/serializers/ride_pojo_serializer.rb new file mode 100644 index 0000000..d5c226e --- /dev/null +++ b/app/serializers/ride_pojo_serializer.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class RidePojoSerializer + include JSONAPI::Serializer + extend ActionView::Helpers::DateHelper + extend ActionView::Helpers::NumberHelper + + set_id do |struct, _params| + struct.ride_id + end + + set_type "pre-ride" + + attribute :distance do |struct, _params| + number_to_human((struct.distance_meters / 1609.34).round(2), units: :distance) + end + + attribute :duration do |struct, _params| + distance_of_time_in_words(struct.duration.to_f) + end + + attribute :commute_duration do |struct, _params| + distance_of_time_in_words(struct.commute_duration.to_f) + end + + attribute :ride_earnings do |struct, _params| + struct.ride_amount.format + end + + belongs_to :from_address, serializer: AddressSerializer + belongs_to :to_address, serializer: AddressSerializer +end diff --git a/config/environments/development.rb b/config/environments/development.rb index 2bff298..1bd8ba1 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -8,11 +8,15 @@ # since you don't have to restart the web server when you make code changes. config.enable_reloading = true + # preserve response format with errors + config.debug_exception_response_format = :api + + # Do not eager load code on boot. config.eager_load = false # Show full error reports. - config.consider_all_requests_local = true + config.consider_all_requests_local = false # Enable server timing config.server_timing = true diff --git a/config/environments/test.rb b/config/environments/test.rb index adbb4a6..0aef472 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -24,7 +24,7 @@ } # Show full error reports and disable caching. - config.consider_all_requests_local = true + config.consider_all_requests_local = false config.action_controller.perform_caching = false config.cache_store = :null_store diff --git a/config/locales/en.yml b/config/locales/en.yml index 6c349ae..46784c1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -29,3 +29,10 @@ en: hello: "Hello world" + distance: + centi: + one: "foot" + other: "feet" + unit: + one: "mile" + other: "miles" diff --git a/config/routes.rb b/config/routes.rb index a125ef0..33ed0d6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,5 +6,10 @@ get "up" => "rails/health#show", as: :rails_health_check # Defines the root path route ("/") - # root "posts#index" + # root "drivers#index" + + resources :drivers, only: %i[index] do + # get "rides", on: :member + resources :selectable_rides, only: :index, module: :drivers + end end diff --git a/db/seeds.rb b/db/seeds.rb index e0c778a..0126b70 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -19,7 +19,7 @@ last_name = Faker::Name.last_name driver = Driver.build(first_name:,last_name:) address = Address.create(ADDRESSES.sample) - driver.create_current_driver_address(address:, current: true) + driver.driver_addresses.build(address_id: address.id, current: true) driver.save! end diff --git a/lib/rides/commands/get_commute_duration.rb b/lib/rides/commands/get_commute_duration.rb index f37ea78..4f55659 100644 --- a/lib/rides/commands/get_commute_duration.rb +++ b/lib/rides/commands/get_commute_duration.rb @@ -8,8 +8,8 @@ module Commands # 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) + converted_rides_for_commute = convert_rides(rides, driver) + GetRoutesData.call(rides: converted_rides_for_commute) end # Converts the Driver's current home address and diff --git a/lib/rides/commands/get_routes_data.rb b/lib/rides/commands/get_routes_data.rb index 1f86e1a..659147c 100644 --- a/lib/rides/commands/get_routes_data.rb +++ b/lib/rides/commands/get_routes_data.rb @@ -2,7 +2,11 @@ module Rides module Commands - # Makes a request to the Google API to obtain the route information + # Makes a request to the Google API to obtain the route information for all the + # selectable rides for a given driver. We use the Route Matrix to ensure we are making as few calls + # as possible. The Route Matrix returns all possible routes for the given origins/destinations. + # Even though we only care about one combo per route, it is still more efficient to do it in this manner. + # We then compare the indexes and get the ones that match, which gives us our original desired routes. class GetRoutesData < BaseCommand CACHE = Cache::Store DIRECTIONS_API_URL = "https://routes.googleapis.com/distanceMatrix/v2:computeRouteMatrix" @@ -29,7 +33,19 @@ def call(rides:) data = data.select { _1[:originIndex] == _1[:destinationIndex] } data = transform_keys!(data) - data.map.with_index { OpenStruct.new(ride: rides[_2], **_1) } + combine_routes_data!(data, rides) + end + + # The manner in which jsonapi-serializer serialzies pojos, + # in order to adhere to the json api spec, we need to define + # the id _and_ the object iteself. + private def combine_routes_data!(data, rides) + data.map.with_index do |d, idx| + ride = rides[idx] + OpenStruct.new(ride_id: ride.id, from_address_id: ride.from_address&.id, + from_address: ride.from_address, to_address: ride.to_address, + to_address_id: ride.to_address&.id, **d) + end end private def connection diff --git a/lib/rides/commands/rank_rides.rb b/lib/rides/commands/rank_rides.rb new file mode 100644 index 0000000..e125ef8 --- /dev/null +++ b/lib/rides/commands/rank_rides.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module Rides + module Commands + class RankRides < BaseCommand + def call(driver:) + rank!(driver) + end + + # How the Sorting Works + # 1. Calculate the Key: For each ride, calculate the earnings per hour using rank_key. + # 2. Negate the Key: By using -rank_key, we convert the highest earnings per hour to the smallest negative + # number, and the smallest earnings per hour to the largest negative number. + # 3. Sort in Ascending Order: Ruby's sort_by method will then sort the rides based on these negative values, + # effectively putting the rides with the highest earnings per hour first. + private def rank!(driver) + rides = combined_ride_data(driver) + return [] if rides.empty? + + rides.sort_by { |ride| -rank_key(ride) } + end + + # (ride earnings) / (commute duration + ride duration) + private def rank_key(ride) + earnings = ride.ride_amount.amount + ride_duration_hours = duration_to_hours(ride.duration) + commute_duration_hours = duration_to_hours(ride.commute_duration) + total_duration_hours = ride_duration_hours + commute_duration_hours + + # Earnings per hour + earnings / total_duration_hours + end + + private def duration_to_hours(duration) + duration.to_i / 3600.0 + end + + # Once we get the data, we want to combine them into one struct + # to more easily compute the ride earnings and assign a rank + private def combined_ride_data(driver) + commutes = commute_durations(driver) + rides = route_data(driver) + + if commutes.count != rides.count + raise RideCountMismatchError, + "The number of rides doesn't match the number of commute rides." \ + "Please check the ride(s) configuration and try again." + end + + combine_rides!(rides, commutes) + end + + # Combines the duration data for the driver to get to the start of the ride + # with that of the ride's data, e.g., duration, distance + private def combine_rides!(rides, commutes) + rides.map.with_index do |ride, idx| + commute = commutes.fetch(idx) + commute_duration = commute.duration + commute_distance = commute.distance_meters + ride_amount = ComputeAmount.call(ride:) + + ride.commute_duration = commute_duration + ride.commute_distance = commute_distance + ride.ride_amount = ride_amount + ride + end + end + + # Get the driving duration to the Ride#from_address, + # to get the duration of the commute to start the ride + private def commute_durations(driver) + rides = selectable_rides_near_driver(driver) + @commute_durations ||= GetCommuteDuration.call(rides:, driver:) + end + + # Get route data for the ride, the duration and the distance between + # the from and to addresses + private def route_data(driver) + rides = selectable_rides_near_driver(driver) + @route_data ||= GetRoutesData.call(rides:) + end + + private def selectable_rides_near_driver(driver) + @selectable_rides_near_driver ||= Ride.selectable.nearby_driver(driver) + end + end + end +end diff --git a/spec/cassettes/ranked_rides.yml b/spec/cassettes/ranked_rides.yml new file mode 100644 index 0000000..ad2bc59 --- /dev/null +++ b/spec/cassettes/ranked_rides.yml @@ -0,0 +1,838 @@ +--- +http_interactions: +- request: + method: get + uri: https://maps.googleapis.com/maps/api/geocode/json?address=1221%20E%20Elizabeth%20St,%20Fort%20Collins,%20CO,%2080524&key=&language=en&sensor=false + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sun, 07 Jul 2024 00:57:46 GMT + Pragma: + - no-cache + Expires: + - Fri, 01 Jan 1990 00:00:00 GMT + Cache-Control: + - no-cache, must-revalidate + Access-Control-Allow-Origin: + - "*" + Server: + - mafe + Content-Length: + - '3024' + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + Server-Timing: + - gfet4t7; dur=121 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + body: + encoding: UTF-8 + string: "{\n \"results\" : \n [\n {\n \"address_components\" + : \n [\n {\n \"long_name\" : \"1221\",\n + \ \"short_name\" : \"1221\",\n \"types\" : \n [\n + \ \"street_number\"\n ]\n },\n {\n + \ \"long_name\" : \"East Elizabeth Street\",\n \"short_name\" + : \"E Elizabeth St\",\n \"types\" : \n [\n \"route\"\n + \ ]\n },\n {\n \"long_name\" + : \"Fort Collins\",\n \"short_name\" : \"Fort Collins\",\n \"types\" + : \n [\n \"locality\",\n \"political\"\n + \ ]\n },\n {\n \"long_name\" + : \"Larimer County\",\n \"short_name\" : \"Larimer County\",\n + \ \"types\" : \n [\n \"administrative_area_level_2\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"Colorado\",\n \"short_name\" + : \"CO\",\n \"types\" : \n [\n \"administrative_area_level_1\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"United States\",\n \"short_name\" + : \"US\",\n \"types\" : \n [\n \"country\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"80524\",\n \"short_name\" : + \"80524\",\n \"types\" : \n [\n \"postal_code\"\n + \ ]\n },\n {\n \"long_name\" + : \"4066\",\n \"short_name\" : \"4066\",\n \"types\" + : \n [\n \"postal_code_suffix\"\n ]\n + \ }\n ],\n \"formatted_address\" : \"1221 E Elizabeth + St, Fort Collins, CO 80524, USA\",\n \"geometry\" : \n {\n + \ \"bounds\" : \n {\n \"northeast\" : \n + \ {\n \"lat\" : 40.5740995,\n \"lng\" + : -105.0547518\n },\n \"southwest\" : \n {\n + \ \"lat\" : 40.5739237,\n \"lng\" : -105.0555168\n + \ }\n },\n \"location\" : \n {\n + \ \"lat\" : 40.5740149,\n \"lng\" : -105.0551343\n + \ },\n \"location_type\" : \"ROOFTOP\",\n \"viewport\" + : \n {\n \"northeast\" : \n {\n \"lat\" + : 40.5753192302915,\n \"lng\" : -105.0537853197085\n },\n + \ \"southwest\" : \n {\n \"lat\" + : 40.5726212697085,\n \"lng\" : -105.0564832802915\n }\n + \ }\n },\n \"place_id\" : \"ChIJK-pLGN5KaYcR3uWeXBYE6ts\",\n + \ \"types\" : \n [\n \"premise\"\n ]\n }\n + \ ],\n \"status\" : \"OK\"\n}" + recorded_at: Sun, 07 Jul 2024 00:57:46 GMT +- request: + method: get + uri: https://maps.googleapis.com/maps/api/geocode/json?address=2121%20E%20Harmony%20Rd%20%23%20180,%20Fort%20Collins,%20CO,%2080528&key=&language=en&sensor=false + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sun, 07 Jul 2024 00:57:46 GMT + Pragma: + - no-cache + Expires: + - Fri, 01 Jan 1990 00:00:00 GMT + Cache-Control: + - no-cache, must-revalidate + Access-Control-Allow-Origin: + - "*" + Server: + - mafe + Content-Length: + - '2887' + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + Server-Timing: + - gfet4t7; dur=111 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + body: + encoding: UTF-8 + string: "{\n \"results\" : \n [\n {\n \"address_components\" + : \n [\n {\n \"long_name\" : \"# 180\",\n + \ \"short_name\" : \"# 180\",\n \"types\" : \n + \ [\n \"subpremise\"\n ]\n },\n + \ {\n \"long_name\" : \"2121\",\n \"short_name\" + : \"2121\",\n \"types\" : \n [\n \"street_number\"\n + \ ]\n },\n {\n \"long_name\" + : \"East Harmony Road\",\n \"short_name\" : \"E Harmony Rd\",\n + \ \"types\" : \n [\n \"route\"\n + \ ]\n },\n {\n \"long_name\" + : \"Fort Collins\",\n \"short_name\" : \"Fort Collins\",\n \"types\" + : \n [\n \"locality\",\n \"political\"\n + \ ]\n },\n {\n \"long_name\" + : \"Larimer County\",\n \"short_name\" : \"Larimer County\",\n + \ \"types\" : \n [\n \"administrative_area_level_2\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"Colorado\",\n \"short_name\" + : \"CO\",\n \"types\" : \n [\n \"administrative_area_level_1\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"United States\",\n \"short_name\" + : \"US\",\n \"types\" : \n [\n \"country\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"80528\",\n \"short_name\" : + \"80528\",\n \"types\" : \n [\n \"postal_code\"\n + \ ]\n },\n {\n \"long_name\" + : \"3401\",\n \"short_name\" : \"3401\",\n \"types\" + : \n [\n \"postal_code_suffix\"\n ]\n + \ }\n ],\n \"formatted_address\" : \"2121 E Harmony + Rd # 180, Fort Collins, CO 80528, USA\",\n \"geometry\" : \n {\n + \ \"location\" : \n {\n \"lat\" : 40.5221078,\n + \ \"lng\" : -105.0368968\n },\n \"location_type\" + : \"ROOFTOP\",\n \"viewport\" : \n {\n \"northeast\" + : \n {\n \"lat\" : 40.52349463029149,\n \"lng\" + : -105.0356535697085\n },\n \"southwest\" : \n + \ {\n \"lat\" : 40.5207966697085,\n \"lng\" + : -105.0383515302915\n }\n }\n },\n \"place_id\" + : \"ChIJS_rwErJMaYcRp8Q9z60tJjQ\",\n \"types\" : \n [\n \"subpremise\"\n + \ ]\n }\n ],\n \"status\" : \"OK\"\n}" + recorded_at: Sun, 07 Jul 2024 00:57:46 GMT +- request: + method: get + uri: https://maps.googleapis.com/maps/api/geocode/json?address=1024%20S%20Lemay%20Ave,%20Fort%20Collins,%20CO,%2080524&key=&language=en&sensor=false + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sun, 07 Jul 2024 00:57:46 GMT + Pragma: + - no-cache + Expires: + - Fri, 01 Jan 1990 00:00:00 GMT + Cache-Control: + - no-cache, must-revalidate + Access-Control-Allow-Origin: + - "*" + Server: + - mafe + Content-Length: + - '3015' + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + Server-Timing: + - gfet4t7; dur=109 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + body: + encoding: UTF-8 + string: "{\n \"results\" : \n [\n {\n \"address_components\" + : \n [\n {\n \"long_name\" : \"1024\",\n + \ \"short_name\" : \"1024\",\n \"types\" : \n [\n + \ \"street_number\"\n ]\n },\n {\n + \ \"long_name\" : \"South Lemay Avenue\",\n \"short_name\" + : \"S Lemay Ave\",\n \"types\" : \n [\n \"route\"\n + \ ]\n },\n {\n \"long_name\" + : \"Fort Collins\",\n \"short_name\" : \"Fort Collins\",\n \"types\" + : \n [\n \"locality\",\n \"political\"\n + \ ]\n },\n {\n \"long_name\" + : \"Larimer County\",\n \"short_name\" : \"Larimer County\",\n + \ \"types\" : \n [\n \"administrative_area_level_2\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"Colorado\",\n \"short_name\" + : \"CO\",\n \"types\" : \n [\n \"administrative_area_level_1\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"United States\",\n \"short_name\" + : \"US\",\n \"types\" : \n [\n \"country\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"80524\",\n \"short_name\" : + \"80524\",\n \"types\" : \n [\n \"postal_code\"\n + \ ]\n },\n {\n \"long_name\" + : \"3929\",\n \"short_name\" : \"3929\",\n \"types\" + : \n [\n \"postal_code_suffix\"\n ]\n + \ }\n ],\n \"formatted_address\" : \"1024 S Lemay + Ave, Fort Collins, CO 80524, USA\",\n \"geometry\" : \n {\n + \ \"bounds\" : \n {\n \"northeast\" : \n + \ {\n \"lat\" : 40.5728058,\n \"lng\" + : -105.0556003\n },\n \"southwest\" : \n {\n + \ \"lat\" : 40.5710308,\n \"lng\" : -105.0576701\n + \ }\n },\n \"location\" : \n {\n + \ \"lat\" : 40.5716484,\n \"lng\" : -105.0566547\n + \ },\n \"location_type\" : \"ROOFTOP\",\n \"viewport\" + : \n {\n \"northeast\" : \n {\n \"lat\" + : 40.5732687302915,\n \"lng\" : -105.0552862197085\n },\n + \ \"southwest\" : \n {\n \"lat\" + : 40.5705707697085,\n \"lng\" : -105.0579841802915\n }\n + \ }\n },\n \"place_id\" : \"ChIJRbgIZeBKaYcRRG_TIgGYoIw\",\n + \ \"types\" : \n [\n \"premise\"\n ]\n }\n + \ ],\n \"status\" : \"OK\"\n}" + recorded_at: Sun, 07 Jul 2024 00:57:46 GMT +- request: + method: get + uri: https://maps.googleapis.com/maps/api/geocode/json?address=1106%20E%20Prospect%20Rd,%20Fort%20Collins,%20CO,%2080525&key=&language=en&sensor=false + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sun, 07 Jul 2024 00:57:47 GMT + Pragma: + - no-cache + Expires: + - Fri, 01 Jan 1990 00:00:00 GMT + Cache-Control: + - no-cache, must-revalidate + Access-Control-Allow-Origin: + - "*" + Server: + - mafe + Content-Length: + - '3284' + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + Server-Timing: + - gfet4t7; dur=95 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + body: + encoding: UTF-8 + string: "{\n \"results\" : \n [\n {\n \"address_components\" + : \n [\n {\n \"long_name\" : \"1106\",\n + \ \"short_name\" : \"1106\",\n \"types\" : \n [\n + \ \"street_number\"\n ]\n },\n {\n + \ \"long_name\" : \"East Prospect Road\",\n \"short_name\" + : \"E Prospect Rd\",\n \"types\" : \n [\n \"route\"\n + \ ]\n },\n {\n \"long_name\" + : \"Highlander Heights\",\n \"short_name\" : \"Highlander Heights\",\n + \ \"types\" : \n [\n \"neighborhood\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"Fort Collins\",\n \"short_name\" + : \"Fort Collins\",\n \"types\" : \n [\n \"locality\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"Larimer County\",\n \"short_name\" + : \"Larimer County\",\n \"types\" : \n [\n \"administrative_area_level_2\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"Colorado\",\n \"short_name\" + : \"CO\",\n \"types\" : \n [\n \"administrative_area_level_1\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"United States\",\n \"short_name\" + : \"US\",\n \"types\" : \n [\n \"country\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"80525\",\n \"short_name\" : + \"80525\",\n \"types\" : \n [\n \"postal_code\"\n + \ ]\n },\n {\n \"long_name\" + : \"5304\",\n \"short_name\" : \"5304\",\n \"types\" + : \n [\n \"postal_code_suffix\"\n ]\n + \ }\n ],\n \"formatted_address\" : \"1106 E Prospect + Rd, Fort Collins, CO 80525, USA\",\n \"geometry\" : \n {\n + \ \"bounds\" : \n {\n \"northeast\" : \n + \ {\n \"lat\" : 40.56795959999999,\n \"lng\" + : -105.0571725\n },\n \"southwest\" : \n {\n + \ \"lat\" : 40.5676192,\n \"lng\" : -105.0576284\n + \ }\n },\n \"location\" : \n {\n + \ \"lat\" : 40.5677337,\n \"lng\" : -105.0574767\n + \ },\n \"location_type\" : \"ROOFTOP\",\n \"viewport\" + : \n {\n \"northeast\" : \n {\n \"lat\" + : 40.56913788029149,\n \"lng\" : -105.0560383697085\n },\n + \ \"southwest\" : \n {\n \"lat\" + : 40.56643991970849,\n \"lng\" : -105.0587363302915\n }\n + \ }\n },\n \"place_id\" : \"ChIJ4U-oox9LaYcRrCvlPHIAuKI\",\n + \ \"types\" : \n [\n \"premise\"\n ]\n }\n + \ ],\n \"status\" : \"OK\"\n}" + recorded_at: Sun, 07 Jul 2024 00:57:46 GMT +- request: + method: get + uri: https://maps.googleapis.com/maps/api/geocode/json?address=1939%20Wilmington%20Dr,%20Fort%20Collins,%20CO,%2080528&key=&language=en&sensor=false + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sun, 07 Jul 2024 00:57:47 GMT + Pragma: + - no-cache + Expires: + - Fri, 01 Jan 1990 00:00:00 GMT + Cache-Control: + - no-cache, must-revalidate + Access-Control-Allow-Origin: + - "*" + Server: + - mafe + Content-Length: + - '3016' + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + Server-Timing: + - gfet4t7; dur=104 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + body: + encoding: UTF-8 + string: "{\n \"results\" : \n [\n {\n \"address_components\" + : \n [\n {\n \"long_name\" : \"1939\",\n + \ \"short_name\" : \"1939\",\n \"types\" : \n [\n + \ \"street_number\"\n ]\n },\n {\n + \ \"long_name\" : \"Wilmington Drive\",\n \"short_name\" + : \"Wilmington Dr\",\n \"types\" : \n [\n \"route\"\n + \ ]\n },\n {\n \"long_name\" + : \"Fort Collins\",\n \"short_name\" : \"Fort Collins\",\n \"types\" + : \n [\n \"locality\",\n \"political\"\n + \ ]\n },\n {\n \"long_name\" + : \"Larimer County\",\n \"short_name\" : \"Larimer County\",\n + \ \"types\" : \n [\n \"administrative_area_level_2\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"Colorado\",\n \"short_name\" + : \"CO\",\n \"types\" : \n [\n \"administrative_area_level_1\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"United States\",\n \"short_name\" + : \"US\",\n \"types\" : \n [\n \"country\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"80528\",\n \"short_name\" : + \"80528\",\n \"types\" : \n [\n \"postal_code\"\n + \ ]\n },\n {\n \"long_name\" + : \"6104\",\n \"short_name\" : \"6104\",\n \"types\" + : \n [\n \"postal_code_suffix\"\n ]\n + \ }\n ],\n \"formatted_address\" : \"1939 Wilmington + Dr, Fort Collins, CO 80528, USA\",\n \"geometry\" : \n {\n + \ \"bounds\" : \n {\n \"northeast\" : \n + \ {\n \"lat\" : 40.5196871,\n \"lng\" + : -105.0418817\n },\n \"southwest\" : \n {\n + \ \"lat\" : 40.519378,\n \"lng\" : -105.0422273\n + \ }\n },\n \"location\" : \n {\n + \ \"lat\" : 40.5195298,\n \"lng\" : -105.0420545\n + \ },\n \"location_type\" : \"ROOFTOP\",\n \"viewport\" + : \n {\n \"northeast\" : \n {\n \"lat\" + : 40.5208085302915,\n \"lng\" : -105.0407055197085\n },\n + \ \"southwest\" : \n {\n \"lat\" + : 40.5181105697085,\n \"lng\" : -105.0434034802915\n }\n + \ }\n },\n \"place_id\" : \"ChIJvWJU9rBMaYcRtZ5wHymbrUU\",\n + \ \"types\" : \n [\n \"premise\"\n ]\n }\n + \ ],\n \"status\" : \"OK\"\n}" + recorded_at: Sun, 07 Jul 2024 00:57:47 GMT +- request: + method: get + uri: https://maps.googleapis.com/maps/api/geocode/json?address=1107%20S%20Lemay%20Ave,%20Suite%20240,%20Fort%20Collins,%20CO,%2080524&key=&language=en&sensor=false + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sun, 07 Jul 2024 00:57:47 GMT + Pragma: + - no-cache + Expires: + - Fri, 01 Jan 1990 00:00:00 GMT + Cache-Control: + - no-cache, must-revalidate + Access-Control-Allow-Origin: + - "*" + Server: + - mafe + Content-Length: + - '2929' + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + Server-Timing: + - gfet4t7; dur=105 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + body: + encoding: UTF-8 + string: "{\n \"results\" : \n [\n {\n \"address_components\" + : \n [\n {\n \"long_name\" : \"240\",\n \"short_name\" + : \"240\",\n \"types\" : \n [\n \"subpremise\"\n + \ ]\n },\n {\n \"long_name\" + : \"1107\",\n \"short_name\" : \"1107\",\n \"types\" + : \n [\n \"street_number\"\n ]\n + \ },\n {\n \"long_name\" : \"South Lemay + Avenue\",\n \"short_name\" : \"S Lemay Ave\",\n \"types\" + : \n [\n \"route\"\n ]\n },\n + \ {\n \"long_name\" : \"University Acres\",\n \"short_name\" + : \"University Acres\",\n \"types\" : \n [\n \"neighborhood\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"Fort Collins\",\n \"short_name\" + : \"Fort Collins\",\n \"types\" : \n [\n \"locality\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"Larimer County\",\n \"short_name\" + : \"Larimer County\",\n \"types\" : \n [\n \"administrative_area_level_2\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"Colorado\",\n \"short_name\" + : \"CO\",\n \"types\" : \n [\n \"administrative_area_level_1\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"United States\",\n \"short_name\" + : \"US\",\n \"types\" : \n [\n \"country\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"80524\",\n \"short_name\" : + \"80524\",\n \"types\" : \n [\n \"postal_code\"\n + \ ]\n }\n ],\n \"formatted_address\" + : \"1107 S Lemay Ave #240, Fort Collins, CO 80524, USA\",\n \"geometry\" + : \n {\n \"location\" : \n {\n \"lat\" + : 40.5720018,\n \"lng\" : -105.0584233\n },\n \"location_type\" + : \"ROOFTOP\",\n \"viewport\" : \n {\n \"northeast\" + : \n {\n \"lat\" : 40.5732694302915,\n \"lng\" + : -105.0570756197085\n },\n \"southwest\" : \n + \ {\n \"lat\" : 40.5705714697085,\n \"lng\" + : -105.0597735802915\n }\n }\n },\n \"place_id\" + : \"ChIJ2QtPVOBKaYcR33T96L5lGSA\",\n \"types\" : \n [\n \"subpremise\"\n + \ ]\n }\n ],\n \"status\" : \"OK\"\n}" + recorded_at: Sun, 07 Jul 2024 00:57:47 GMT +- request: + method: get + uri: https://maps.googleapis.com/maps/api/geocode/json?address=4601%20Corbett%20Dr,%20Fort%20Collins,%20CO,%2080528&key=&language=en&sensor=false + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Date: + - Sun, 07 Jul 2024 00:57:47 GMT + Pragma: + - no-cache + Expires: + - Fri, 01 Jan 1990 00:00:00 GMT + Cache-Control: + - no-cache, must-revalidate + Access-Control-Allow-Origin: + - "*" + Server: + - mafe + Content-Length: + - '3008' + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + Server-Timing: + - gfet4t7; dur=101 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + body: + encoding: UTF-8 + string: "{\n \"results\" : \n [\n {\n \"address_components\" + : \n [\n {\n \"long_name\" : \"4601\",\n + \ \"short_name\" : \"4601\",\n \"types\" : \n [\n + \ \"street_number\"\n ]\n },\n {\n + \ \"long_name\" : \"Corbett Drive\",\n \"short_name\" + : \"Corbett Dr\",\n \"types\" : \n [\n \"route\"\n + \ ]\n },\n {\n \"long_name\" + : \"Fort Collins\",\n \"short_name\" : \"Fort Collins\",\n \"types\" + : \n [\n \"locality\",\n \"political\"\n + \ ]\n },\n {\n \"long_name\" + : \"Larimer County\",\n \"short_name\" : \"Larimer County\",\n + \ \"types\" : \n [\n \"administrative_area_level_2\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"Colorado\",\n \"short_name\" + : \"CO\",\n \"types\" : \n [\n \"administrative_area_level_1\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"United States\",\n \"short_name\" + : \"US\",\n \"types\" : \n [\n \"country\",\n + \ \"political\"\n ]\n },\n {\n + \ \"long_name\" : \"80528\",\n \"short_name\" : + \"80528\",\n \"types\" : \n [\n \"postal_code\"\n + \ ]\n },\n {\n \"long_name\" + : \"9579\",\n \"short_name\" : \"9579\",\n \"types\" + : \n [\n \"postal_code_suffix\"\n ]\n + \ }\n ],\n \"formatted_address\" : \"4601 Corbett + Dr, Fort Collins, CO 80528, USA\",\n \"geometry\" : \n {\n + \ \"bounds\" : \n {\n \"northeast\" : \n + \ {\n \"lat\" : 40.5224051,\n \"lng\" + : -105.0277394\n },\n \"southwest\" : \n {\n + \ \"lat\" : 40.5216682,\n \"lng\" : -105.0288448\n + \ }\n },\n \"location\" : \n {\n + \ \"lat\" : 40.5220128,\n \"lng\" : -105.0284598\n + \ },\n \"location_type\" : \"ROOFTOP\",\n \"viewport\" + : \n {\n \"northeast\" : \n {\n \"lat\" + : 40.5233856302915,\n \"lng\" : -105.0268814697085\n },\n + \ \"southwest\" : \n {\n \"lat\" + : 40.5206876697085,\n \"lng\" : -105.0295794302915\n }\n + \ }\n },\n \"place_id\" : \"ChIJWxLXuU2zbocRgJWHLDl5uNU\",\n + \ \"types\" : \n [\n \"premise\"\n ]\n }\n + \ ],\n \"status\" : \"OK\"\n}" + recorded_at: Sun, 07 Jul 2024 00:57:47 GMT +- request: + method: post + uri: https://routes.googleapis.com/distanceMatrix/v2:computeRouteMatrix + body: + encoding: UTF-8 + string: '{"origins":[{"waypoint":{"placeId":"ChIJK-pLGN5KaYcR3uWeXBYE6ts"}},{"waypoint":{"placeId":"ChIJK-pLGN5KaYcR3uWeXBYE6ts"}},{"waypoint":{"placeId":"ChIJK-pLGN5KaYcR3uWeXBYE6ts"}},{"waypoint":{"placeId":"ChIJK-pLGN5KaYcR3uWeXBYE6ts"}},{"waypoint":{"placeId":"ChIJK-pLGN5KaYcR3uWeXBYE6ts"}},{"waypoint":{"placeId":"ChIJK-pLGN5KaYcR3uWeXBYE6ts"}}],"destinations":[{"waypoint":{"placeId":"ChIJS_rwErJMaYcRp8Q9z60tJjQ"}},{"waypoint":{"placeId":"ChIJRbgIZeBKaYcRRG_TIgGYoIw"}},{"waypoint":{"placeId":"ChIJ4U-oox9LaYcRrCvlPHIAuKI"}},{"waypoint":{"placeId":"ChIJvWJU9rBMaYcRtZ5wHymbrUU"}},{"waypoint":{"placeId":"ChIJ2QtPVOBKaYcR33T96L5lGSA"}},{"waypoint":{"placeId":"ChIJWxLXuU2zbocRgJWHLDl5uNU"}}],"routingPreference":"TRAFFIC_AWARE","travelMode":"DRIVE"}' + headers: + X-Goog-Fieldmask: + - originIndex,destinationIndex,status,condition,distanceMeters,duration + X-Goog-Api-Key: + - "" + Content-Type: + - application/json + User-Agent: + - Faraday v2.9.0 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Vary: + - Origin + - Referer + - X-Origin + Date: + - Sun, 07 Jul 2024 00:57:47 GMT + Server: + - scaffolding on HTTPServer2 + Cache-Control: + - private + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: "[{\n \"originIndex\": 1,\n \"destinationIndex\": 1,\n \"status\": + {},\n \"distanceMeters\": 205,\n \"duration\": \"55s\",\n \"condition\": + \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 3,\n \"destinationIndex\": + 1,\n \"status\": {},\n \"distanceMeters\": 205,\n \"duration\": \"55s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 5,\n \"destinationIndex\": + 1,\n \"status\": {},\n \"distanceMeters\": 205,\n \"duration\": \"55s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 2,\n \"destinationIndex\": + 1,\n \"status\": {},\n \"distanceMeters\": 205,\n \"duration\": \"55s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 0,\n \"destinationIndex\": + 1,\n \"status\": {},\n \"distanceMeters\": 205,\n \"duration\": \"55s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 0,\n \"destinationIndex\": + 4,\n \"status\": {},\n \"distanceMeters\": 710,\n \"duration\": \"178s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 4,\n \"destinationIndex\": + 1,\n \"status\": {},\n \"distanceMeters\": 205,\n \"duration\": \"55s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 3,\n \"destinationIndex\": + 4,\n \"status\": {},\n \"distanceMeters\": 710,\n \"duration\": \"178s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 3,\n \"destinationIndex\": + 2,\n \"status\": {},\n \"distanceMeters\": 1290,\n \"duration\": \"243s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 4,\n \"destinationIndex\": + 2,\n \"status\": {},\n \"distanceMeters\": 1290,\n \"duration\": \"243s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 5,\n \"destinationIndex\": + 4,\n \"status\": {},\n \"distanceMeters\": 710,\n \"duration\": \"178s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 5,\n \"destinationIndex\": + 5,\n \"status\": {},\n \"distanceMeters\": 8728,\n \"duration\": \"691s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 0,\n \"destinationIndex\": + 2,\n \"status\": {},\n \"distanceMeters\": 1290,\n \"duration\": \"243s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 2,\n \"destinationIndex\": + 2,\n \"status\": {},\n \"distanceMeters\": 1290,\n \"duration\": \"243s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 1,\n \"destinationIndex\": + 4,\n \"status\": {},\n \"distanceMeters\": 710,\n \"duration\": \"178s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 5,\n \"destinationIndex\": + 2,\n \"status\": {},\n \"distanceMeters\": 1290,\n \"duration\": \"243s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 0,\n \"destinationIndex\": + 5,\n \"status\": {},\n \"distanceMeters\": 8728,\n \"duration\": \"691s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 1,\n \"destinationIndex\": + 2,\n \"status\": {},\n \"distanceMeters\": 1290,\n \"duration\": \"243s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 3,\n \"destinationIndex\": + 5,\n \"status\": {},\n \"distanceMeters\": 8728,\n \"duration\": \"691s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 2,\n \"destinationIndex\": + 0,\n \"status\": {},\n \"distanceMeters\": 8071,\n \"duration\": \"680s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 2,\n \"destinationIndex\": + 4,\n \"status\": {},\n \"distanceMeters\": 710,\n \"duration\": \"178s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 4,\n \"destinationIndex\": + 4,\n \"status\": {},\n \"distanceMeters\": 710,\n \"duration\": \"178s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 2,\n \"destinationIndex\": + 5,\n \"status\": {},\n \"distanceMeters\": 8728,\n \"duration\": \"691s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 4,\n \"destinationIndex\": + 0,\n \"status\": {},\n \"distanceMeters\": 8071,\n \"duration\": \"680s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 1,\n \"destinationIndex\": + 3,\n \"status\": {},\n \"distanceMeters\": 8122,\n \"duration\": \"701s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 5,\n \"destinationIndex\": + 0,\n \"status\": {},\n \"distanceMeters\": 8071,\n \"duration\": \"680s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 0,\n \"destinationIndex\": + 3,\n \"status\": {},\n \"distanceMeters\": 8122,\n \"duration\": \"701s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 3,\n \"destinationIndex\": + 3,\n \"status\": {},\n \"distanceMeters\": 8122,\n \"duration\": \"701s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 4,\n \"destinationIndex\": + 5,\n \"status\": {},\n \"distanceMeters\": 8728,\n \"duration\": \"691s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 5,\n \"destinationIndex\": + 3,\n \"status\": {},\n \"distanceMeters\": 8122,\n \"duration\": \"701s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 3,\n \"destinationIndex\": + 0,\n \"status\": {},\n \"distanceMeters\": 8071,\n \"duration\": \"680s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 1,\n \"destinationIndex\": + 0,\n \"status\": {},\n \"distanceMeters\": 8071,\n \"duration\": \"680s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 0,\n \"destinationIndex\": + 0,\n \"status\": {},\n \"distanceMeters\": 8071,\n \"duration\": \"680s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 4,\n \"destinationIndex\": + 3,\n \"status\": {},\n \"distanceMeters\": 8122,\n \"duration\": \"701s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 1,\n \"destinationIndex\": + 5,\n \"status\": {},\n \"distanceMeters\": 8728,\n \"duration\": \"691s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 2,\n \"destinationIndex\": + 3,\n \"status\": {},\n \"distanceMeters\": 8122,\n \"duration\": \"701s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n]" + recorded_at: Sun, 07 Jul 2024 00:57:47 GMT +- request: + method: post + uri: https://routes.googleapis.com/distanceMatrix/v2:computeRouteMatrix + body: + encoding: UTF-8 + string: '{"origins":[{"waypoint":{"placeId":"ChIJS_rwErJMaYcRp8Q9z60tJjQ"}},{"waypoint":{"placeId":"ChIJRbgIZeBKaYcRRG_TIgGYoIw"}},{"waypoint":{"placeId":"ChIJ4U-oox9LaYcRrCvlPHIAuKI"}},{"waypoint":{"placeId":"ChIJvWJU9rBMaYcRtZ5wHymbrUU"}},{"waypoint":{"placeId":"ChIJ2QtPVOBKaYcR33T96L5lGSA"}},{"waypoint":{"placeId":"ChIJWxLXuU2zbocRgJWHLDl5uNU"}}],"destinations":[{"waypoint":{"placeId":"ChIJvWJU9rBMaYcRtZ5wHymbrUU"}},{"waypoint":{"placeId":"ChIJWxLXuU2zbocRgJWHLDl5uNU"}},{"waypoint":{"placeId":"ChIJWxLXuU2zbocRgJWHLDl5uNU"}},{"waypoint":{"placeId":"ChIJ4U-oox9LaYcRrCvlPHIAuKI"}},{"waypoint":{"placeId":"ChIJWxLXuU2zbocRgJWHLDl5uNU"}},{"waypoint":{"placeId":"ChIJRbgIZeBKaYcRRG_TIgGYoIw"}}],"routingPreference":"TRAFFIC_AWARE","travelMode":"DRIVE"}' + headers: + X-Goog-Fieldmask: + - originIndex,destinationIndex,status,condition,distanceMeters,duration + X-Goog-Api-Key: + - "" + Content-Type: + - application/json + User-Agent: + - Faraday v2.9.0 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Vary: + - Origin + - Referer + - X-Origin + Date: + - Sun, 07 Jul 2024 00:57:48 GMT + Server: + - scaffolding on HTTPServer2 + Cache-Control: + - private + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: "[{\n \"originIndex\": 5,\n \"destinationIndex\": 1,\n \"status\": + {},\n \"duration\": \"0s\",\n \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n + \ \"originIndex\": 5,\n \"destinationIndex\": 4,\n \"status\": {},\n \"duration\": + \"0s\",\n \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": + 3,\n \"destinationIndex\": 0,\n \"status\": {},\n \"duration\": \"0s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 2,\n \"destinationIndex\": + 3,\n \"status\": {},\n \"duration\": \"0s\",\n \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n + \ \"originIndex\": 5,\n \"destinationIndex\": 2,\n \"status\": {},\n \"duration\": + \"0s\",\n \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": + 1,\n \"destinationIndex\": 5,\n \"status\": {},\n \"duration\": \"0s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 4,\n \"destinationIndex\": + 5,\n \"status\": {},\n \"distanceMeters\": 427,\n \"duration\": \"117s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 3,\n \"destinationIndex\": + 4,\n \"status\": {},\n \"distanceMeters\": 1881,\n \"duration\": \"253s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 3,\n \"destinationIndex\": + 1,\n \"status\": {},\n \"distanceMeters\": 1881,\n \"duration\": \"253s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 5,\n \"destinationIndex\": + 0,\n \"status\": {},\n \"distanceMeters\": 1969,\n \"duration\": \"294s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 0,\n \"destinationIndex\": + 0,\n \"status\": {},\n \"distanceMeters\": 689,\n \"duration\": \"202s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 4,\n \"destinationIndex\": + 3,\n \"status\": {},\n \"distanceMeters\": 1067,\n \"duration\": \"230s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 3,\n \"destinationIndex\": + 2,\n \"status\": {},\n \"distanceMeters\": 1881,\n \"duration\": \"253s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 0,\n \"destinationIndex\": + 2,\n \"status\": {},\n \"distanceMeters\": 1301,\n \"duration\": \"184s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 1,\n \"destinationIndex\": + 3,\n \"status\": {},\n \"distanceMeters\": 1007,\n \"duration\": \"153s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 5,\n \"destinationIndex\": + 5,\n \"status\": {},\n \"distanceMeters\": 8503,\n \"duration\": \"674s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 2,\n \"destinationIndex\": + 5,\n \"status\": {},\n \"distanceMeters\": 1007,\n \"duration\": \"143s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 0,\n \"destinationIndex\": + 1,\n \"status\": {},\n \"distanceMeters\": 1301,\n \"duration\": \"184s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 1,\n \"destinationIndex\": + 4,\n \"status\": {},\n \"distanceMeters\": 8445,\n \"duration\": \"651s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 2,\n \"destinationIndex\": + 2,\n \"status\": {},\n \"distanceMeters\": 7923,\n \"duration\": \"635s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 0,\n \"destinationIndex\": + 4,\n \"status\": {},\n \"distanceMeters\": 1301,\n \"duration\": \"184s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 4,\n \"destinationIndex\": + 2,\n \"status\": {},\n \"distanceMeters\": 8505,\n \"duration\": \"686s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 4,\n \"destinationIndex\": + 1,\n \"status\": {},\n \"distanceMeters\": 8505,\n \"duration\": \"686s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 1,\n \"destinationIndex\": + 1,\n \"status\": {},\n \"distanceMeters\": 8445,\n \"duration\": \"651s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 2,\n \"destinationIndex\": + 1,\n \"status\": {},\n \"distanceMeters\": 7923,\n \"duration\": \"635s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 3,\n \"destinationIndex\": + 3,\n \"status\": {},\n \"distanceMeters\": 7124,\n \"duration\": \"621s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 2,\n \"destinationIndex\": + 4,\n \"status\": {},\n \"distanceMeters\": 7923,\n \"duration\": \"635s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 1,\n \"destinationIndex\": + 2,\n \"status\": {},\n \"distanceMeters\": 8445,\n \"duration\": \"651s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 3,\n \"destinationIndex\": + 5,\n \"status\": {},\n \"distanceMeters\": 7888,\n \"duration\": \"674s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 5,\n \"destinationIndex\": + 3,\n \"status\": {},\n \"distanceMeters\": 7738,\n \"duration\": \"624s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 1,\n \"destinationIndex\": + 0,\n \"status\": {},\n \"distanceMeters\": 7839,\n \"duration\": \"662s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 0,\n \"destinationIndex\": + 5,\n \"status\": {},\n \"distanceMeters\": 7781,\n \"duration\": \"644s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 2,\n \"destinationIndex\": + 0,\n \"status\": {},\n \"distanceMeters\": 7317,\n \"duration\": \"640s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 4,\n \"destinationIndex\": + 0,\n \"status\": {},\n \"distanceMeters\": 7899,\n \"duration\": \"698s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 0,\n \"destinationIndex\": + 3,\n \"status\": {},\n \"distanceMeters\": 7017,\n \"duration\": \"595s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n,\r\n{\n \"originIndex\": 4,\n \"destinationIndex\": + 4,\n \"status\": {},\n \"distanceMeters\": 8505,\n \"duration\": \"686s\",\n + \ \"condition\": \"ROUTE_EXISTS\"\n}\n]" + recorded_at: Sun, 07 Jul 2024 00:57:48 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/lib/rides/commands/get_routes_data_spec.rb b/spec/lib/rides/commands/get_routes_data_spec.rb index 4982dc7..d3bf600 100644 --- a/spec/lib/rides/commands/get_routes_data_spec.rb +++ b/spec/lib/rides/commands/get_routes_data_spec.rb @@ -3,8 +3,12 @@ RSpec.describe Rides::Commands::GetRoutesData do let(:rides) do [ - double(:ride, origin_place_id: "origin1", destination_place_id: "dest1"), - double(:ride, origin_place_id: "origin2", destination_place_id: "dest2") + double(:ride, id: 1, origin_place_id: "origin1", destination_place_id: "dest1", from_address_id: 1, + from_address: double(:address, id: 1), to_address_id: 1, + to_address: double(:address, id: 1)), + double(:ride, id: 2, origin_place_id: "origin2", destination_place_id: "dest2", from_address_id: 2, + from_address: double(:address, id: 2), to_address_id: 1, + to_address: double(:address, id: 1)) ] end diff --git a/spec/requests/drivers/drivers_spec.rb b/spec/requests/drivers/drivers_spec.rb new file mode 100644 index 0000000..90a8e06 --- /dev/null +++ b/spec/requests/drivers/drivers_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Drivers::Drivers", :skip_geocode, type: :request do + let!(:drivers) { create_list(:driver, 10) } + describe "GET /index" do + it "returns list of drivers" do + get "/drivers?limit=10" + expect(response.status).to eq(200) + result = JSON.parse(response.body, symbolize_names: true) + + expect(result[:data].count).to eq(10) + obj = result.dig(:data, 0) + expect(obj[:type]).to eq("driver") + expect(obj.dig(:relationships, :current_address, :data, :type)).to eq("address") + end + + it "can paginate list of drivers" do + get "/drivers?limit=2&offset=0" + expect(response.status).to eq(200) + result = JSON.parse(response.body, symbolize_names: true) + first_result = result[:data] + + expect(first_result.count).to eq(2) + + get "/drivers?limit=2&offset=2" + expect(response.status).to eq(200) + result = JSON.parse(response.body, symbolize_names: true) + second_result = result[:data] + + expect(first_result.map { _1[:id] }).to_not eq(second_result.map { _1[:id] }) + end + end +end diff --git a/spec/requests/drivers/selectable_rides_spec.rb b/spec/requests/drivers/selectable_rides_spec.rb new file mode 100644 index 0000000..260d22d --- /dev/null +++ b/spec/requests/drivers/selectable_rides_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Drivers::Rides", type: :request do + describe "GET /drivers/:driver_id/rides" do + it "returns ranked rides" do + VCR.use_cassette("ranked_rides") do + attrs = [ + { line_1: "1221 E Elizabeth St", line_2: nil, city: "Fort Collins", state: "CO", zip_code: "80524", + latitude: 40.5740149, longitude: -105.0551343, place_id: "ChIJK-pLGN5KaYcR3uWeXBYE6ts", id: nil }, + { line_1: "2121 E Harmony Rd # 180", line_2: nil, city: "Fort Collins", state: "CO", zip_code: "80528", + latitude: 40.5221078, longitude: -105.0368968, place_id: "ChIJS_rwErJMaYcRp8Q9z60tJjQ", id: nil }, + { line_1: "1024 S Lemay Ave", line_2: nil, city: "Fort Collins", state: "CO", zip_code: "80524", + latitude: 40.5716484, longitude: -105.0566547, place_id: "ChIJRbgIZeBKaYcRRG_TIgGYoIw", id: nil }, + { line_1: "1106 E Prospect Rd", line_2: nil, city: "Fort Collins", state: "CO", zip_code: "80525", + latitude: 40.5677337, longitude: -105.0574767, place_id: "ChIJ4U-oox9LaYcRrCvlPHIAuKI", id: nil }, + { line_1: "1939 Wilmington Dr", line_2: nil, city: "Fort Collins", state: "CO", zip_code: "80528", + latitude: 40.5195298, longitude: -105.0420545, place_id: "ChIJvWJU9rBMaYcRtZ5wHymbrUU", id: nil }, + { line_1: "1107 S Lemay Ave", line_2: "Suite 240", city: "Fort Collins", state: "CO", zip_code: "80524", + latitude: 40.5720018, longitude: -105.0584233, place_id: "ChIJ2QtPVOBKaYcR33T96L5lGSA", id: nil }, + { line_1: "4601 Corbett Dr", line_2: nil, city: "Fort Collins", state: "CO", zip_code: "80528", + latitude: 40.5220128, longitude: -105.0284598, place_id: "ChIJWxLXuU2zbocRgJWHLDl5uNU", id: nil } + ] + addresses = attrs.map { create(:address, **_1) } + driver = create(:driver, current_address: addresses[0]) + addresses.each do |from_address| + to_address = Address.where.not(id: [from_address.id, + driver.current_address_id]).order("RANDOM()").limit(1).first + create(:ride, from_address:, to_address:) + end + + get "/drivers/#{driver.id}/selectable_rides" + expect(response.status).to eq(200) + + result = JSON.parse(response.body, symbolize_names: true) + data = result.dig(:data, 0) + + expected_keys = %i[distance duration commute_duration ride_earnings] + attributes = data[:attributes] + expect(attributes[:ride_earnings]).to eq("$12.00") + expect(attributes[:duration].end_with?("minutes")).to be_truthy + actual_keys = attributes.keys + expect(expected_keys).to eq(actual_keys) + + expected_relationships = %i[from_address to_address] + actual_relationships = data[:relationships].keys + expect(expected_relationships).to eq(actual_relationships) + + included = result[:included] + expect(included.all? { _1[:type] == "address" }).to be_truthy + end + end + end +end diff --git a/spec/requests/drivers_spec.rb b/spec/requests/drivers_spec.rb new file mode 100644 index 0000000..852ec0f --- /dev/null +++ b/spec/requests/drivers_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Drivers", :skip_geocode, type: :request do + let!(:drivers) { create_list(:driver, 10) } + describe "GET /index" do + it "returns list of drivers" do + get "/drivers?limit=10" + expect(response.status).to eq(200) + result = JSON.parse(response.body, symbolize_names: true) + + expect(result[:data].count).to eq(10) + obj = result.dig(:data, 0) + expect(obj[:type]).to eq("driver") + expect(obj.dig(:relationships, :current_address, :data, :type)).to eq("address") + end + + it "can paginate list of drivers" do + get "/drivers?limit=2&offset=0" + expect(response.status).to eq(200) + result = JSON.parse(response.body, symbolize_names: true) + first_result = result[:data] + + expect(first_result.count).to eq(2) + + get "/drivers?limit=2&offset=2" + expect(response.status).to eq(200) + result = JSON.parse(response.body, symbolize_names: true) + second_result = result[:data] + + expect(first_result.map { _1[:id] }).to_not eq(second_result.map { _1[:id] }) + end + end +end