Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,7 @@ group :development, :test do

gem 'factory_bot_rails'
gem 'rspec-rails'
gem 'pry'
gem 'shoulda-matchers'
end

14 changes: 14 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ GEM
brakeman (7.1.0)
racc
builder (3.3.0)
coderay (1.1.3)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
crass (1.0.6)
Expand Down Expand Up @@ -139,6 +140,7 @@ GEM
net-pop
net-smtp
marcel (1.0.4)
method_source (1.1.0)
mini_mime (1.1.5)
minitest (5.25.5)
msgpack (1.8.0)
Expand All @@ -165,6 +167,8 @@ GEM
racc (~> 1.4)
nokogiri (1.18.9-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.9-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.9-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.9-x86_64-linux-gnu)
Expand All @@ -179,13 +183,17 @@ GEM
pg (1.6.1)
pg (1.6.1-aarch64-linux)
pg (1.6.1-aarch64-linux-musl)
pg (1.6.1-arm64-darwin)
pg (1.6.1-x86_64-darwin)
pg (1.6.1-x86_64-linux)
pg (1.6.1-x86_64-linux-musl)
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
prism (1.4.0)
pry (0.15.2)
coderay (~> 1.1)
method_source (~> 1.0)
psych (5.2.6)
date
stringio
Expand Down Expand Up @@ -285,6 +293,8 @@ GEM
rubocop-rails (>= 2.30)
ruby-progressbar (1.13.0)
securerandom (0.4.1)
shoulda-matchers (6.5.0)
activesupport (>= 5.2.0)
solid_cable (3.0.12)
actioncable (>= 7.2)
activejob (>= 7.2)
Expand Down Expand Up @@ -312,6 +322,7 @@ GEM
thor (1.4.0)
thruster (0.1.15)
thruster (0.1.15-aarch64-linux)
thruster (0.1.15-arm64-darwin)
thruster (0.1.15-x86_64-darwin)
thruster (0.1.15-x86_64-linux)
timeout (0.4.3)
Expand All @@ -334,6 +345,7 @@ PLATFORMS
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
arm64-darwin-24
x86_64-darwin-24
x86_64-linux
x86_64-linux-gnu
Expand All @@ -347,10 +359,12 @@ DEPENDENCIES
factory_bot_rails
kamal
pg (>= 1.1)
pry
puma (>= 5.0)
rails (~> 8.0.2, >= 8.0.2.1)
rspec-rails
rubocop-rails-omakase
shoulda-matchers
solid_cable
solid_cache
solid_queue
Expand Down
19 changes: 19 additions & 0 deletions app/controllers/routes_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class RoutesController < ApplicationController
def index
service = Routes::SearchService.new(routes_params)

if service.perform
render json: service.routes_to_json
else
render json: { errors: service.errors.messages }, status: :unprocessable_entity
end
end

private

def routes_params
params.permit(:carrier, :origin_iata, :destination_iata, :departure_from, :departure_to)
end
end


2 changes: 1 addition & 1 deletion app/models/permitted_route.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@

class PermittedRoute < ApplicationRecord
validates :carrier, :origin_iata, :destination_iata, presence: true
end
end
7 changes: 6 additions & 1 deletion app/models/segment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@
class Segment < ApplicationRecord
validates :airline, :segment_number, presence: true
validates :origin_iata, :destination_iata, presence: true, length: { is: 3 }
end

scope :by_airline, ->(code) { where(airline: code) }
scope :by_path, ->(origin, destination) { where(origin_iata: origin, destination_iata: destination) }
scope :departure_between, ->(from_time, to_time) { where(std: from_time..to_time) }
scope :arrival_before, ->(time) { where('sta < ?', time) }
end
53 changes: 53 additions & 0 deletions app/services/routes/search_segments_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
module Routes
class SearchSegmentsService
MIN_CONNECTION_TIME = 8.hours
MAX_CONNECTION_TIME = 48.hours

def initialize(carrier:, departure_from:, departure_to:, segment_paths:)
@carrier = carrier
@departure_from = departure_from
@departure_to = departure_to
@segment_paths = segment_paths
end

def perform
find_segments(
segment_paths: @segment_paths,
segment_departure_from: @departure_from,
segment_departure_to: @departure_to
)
end

def segments
@segments ||= []
end

private

def find_segments(segment_paths:, segment_departure_from:, segment_departure_to:, previous_segments: [])
segment_paths.each do |segment_path|
path_segments = Segment.by_airline(@carrier)
.by_path(segment_path[0], segment_path[1])
.departure_between(segment_departure_from, segment_departure_to)
.arrival_before(@departure_to)

path_segments.each do |path_segment|
current_segments = previous_segments + [path_segment]
if segment_paths.size == 1
segments << current_segments
else
next_departure_from = path_segment.sta + MIN_CONNECTION_TIME
next_departure_to = path_segment.sta + MAX_CONNECTION_TIME

find_segments(
segment_paths: segment_paths[1..-1],
segment_departure_from: next_departure_from,
segment_departure_to: next_departure_to,
previous_segments: current_segments
)
end
end
end
end
end
end
96 changes: 96 additions & 0 deletions app/services/routes/search_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
module Routes
class SearchService
include ActiveModel::API

attr_accessor :carrier, :origin_iata, :destination_iata, :departure_from, :departure_to

validates :carrier, presence: true
validates :origin_iata, presence: true, length: { is: 3 }
validates :destination_iata, presence: true, length: { is: 3 }
validates :departure_from, presence: true
validates :departure_to, presence: true

def perform
return false unless valid?

permitted_route =
PermittedRoute.find_by(
carrier: @carrier,
origin_iata: @origin_iata,
destination_iata: @destination_iata
)

return true if permitted_route.blank?

route_paths(permitted_route).each do |segment_paths|
route_segments_service =
SearchSegmentsService.new(
carrier: carrier,
departure_from: departure_from,
departure_to: departure_to,
segment_paths: segment_paths
)

route_segments_service.perform
routes.push(*route_segments_service.segments)
end

true
end

def routes_to_json
routes.map do |route_segments|
{
origin_iata: @origin_iata,
destination_iata: @destination_iata,
departure_time: route_segments.first.std.iso8601(3),
arrival_time: route_segments.last.sta.iso8601(3),
segments:
route_segments.map do |segment|
{
carrier: segment.airline,
segment_number: segment.segment_number,
origin_iata: segment.origin_iata,
destination_iata: segment.destination_iata,
std: segment.std.iso8601(3),
sta: segment.sta.iso8601(3)
}
end
}
end
end

def routes
@routes ||= []
end

def departure_from=(value)
@departure_from = Time.zone.parse(value.to_s)&.beginning_of_day
end

def departure_to=(value)
@departure_to = Time.zone.parse(value.to_s)&.end_of_day
end

private

def route_paths(permitted_route)
paths = []

if permitted_route.direct
paths << [[permitted_route.origin_iata, permitted_route.destination_iata]]
end

permitted_route.transfer_iata_codes.each do |code|
stops = code.scan(/.{3}/)
paths.push(
([permitted_route.origin_iata] + stops + [permitted_route.destination_iata]).each_cons(2).to_a
)
end

paths
end
end
end


3 changes: 1 addition & 2 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,5 @@
# Can be used by load balancers and uptime monitors to verify that the app is live.
get "up" => "rails/health#show", as: :rails_health_check

# Defines the root path route ("/")
# root "posts#index"
resources :routes, only: :index
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddCompositeIndexOnPermittedRoutes < ActiveRecord::Migration[8.0]
def change
add_index :permitted_routes, [:carrier, :origin_iata, :destination_iata]
end
end
5 changes: 5 additions & 0 deletions db/migrate/20250904163408_add_composite_index_on_segments.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddCompositeIndexOnSegments < ActiveRecord::Migration[8.0]
def change
add_index :segments, [:airline, :origin_iata, :destination_iata]
end
end
4 changes: 3 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions spec/factories/permitted_routes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FactoryBot.define do
factory :permitted_route do
carrier { "S7" }
origin_iata { "UUS" }
destination_iata { "DME" }
direct { true }
transfer_iata_codes { [] }
end
end
10 changes: 10 additions & 0 deletions spec/factories/segments.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FactoryBot.define do
factory :segment do
airline { "S" }
segment_number { generate(:segment_number) }
origin_iata { "UUS" }
destination_iata { "DME" }
std { 1.hour.from_now }
sta { 5.hours.from_now }
end
end
5 changes: 4 additions & 1 deletion spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
# directory. Alternatively, in the individual `*_spec.rb` files, manually
# require only the support files necessary.
#
# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }
Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }

# Ensures that the test database schema matches the current schema file.
# If there are pending migrations it will invoke `db:test:prepare` to
Expand Down Expand Up @@ -69,4 +69,7 @@
config.filter_rails_from_backtrace!
# arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name")

# Include FactoryBot methods
config.include FactoryBot::Syntax::Methods
end
Loading