Skip to content

Commit

Permalink
11 expose api (#27)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mikeheft authored Jul 7, 2024
1 parent c0ba622 commit 4834f53
Show file tree
Hide file tree
Showing 22 changed files with 1,194 additions and 11 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -337,6 +339,7 @@ DEPENDENCIES
faraday
figaro
geocoder
jsonapi-serializer
money-rails
pg (~> 1.1)
pry-rails
Expand Down
10 changes: 10 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions app/controllers/drivers/selectable_rides_controller.rb
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions app/controllers/drivers_controller.rb
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion app/models/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions app/serializers/address_serializer.rb
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions app/serializers/driver_serializer.rb
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions app/serializers/ride_pojo_serializer.rb
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,10 @@

en:
hello: "Hello world"
distance:
centi:
one: "foot"
other: "feet"
unit:
one: "mile"
other: "miles"
7 changes: 6 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion db/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions lib/rides/commands/get_commute_duration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 18 additions & 2 deletions lib/rides/commands/get_routes_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
88 changes: 88 additions & 0 deletions lib/rides/commands/rank_rides.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 4834f53

Please sign in to comment.