Skip to content

Commit

Permalink
Add Coffee Shops Finder service
Browse files Browse the repository at this point in the history
  • Loading branch information
popescualexandru9 committed Jan 28, 2025
1 parent 9231f80 commit 2fb70a4
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 0 deletions.
46 changes: 46 additions & 0 deletions app/services/coffee_shop_finder_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

require 'csv'
require 'httparty'

class CoffeeShopFinderService
def closest_shops(user_lat, user_lon, limit = 3)
coffee_shops = fetch_and_parse_coffee_shops

# Used for faster distance retrieval
distances = Hash.new { |h, shop| h[shop] = shop.distance_to(user_lat, user_lon) }
closest_shops = coffee_shops.min_by(limit) { |shop| distances[shop] }

closest_shops.map { |shop| { shop: shop, distance: distances[shop] } }
end

private

def fetch_and_parse_coffee_shops
response = fetch_csv
parse_coffee_shops(response)
end

def parse_coffee_shops(response)
CSV.parse(response.body, headers: true).map do |row|
validate_csv_row!(row)
CoffeeShop.new(row['Name'], row['X Coordinate'], row['Y Coordinate'])
end
rescue CSV::MalformedCSVError => e
raise "Malformed CSV: #{e.message}"
end

def fetch_csv
url = ENV['CSV_URL'] || APP_CONFIG[:csv_url]
response = HTTParty.get(url)
raise "Failed to fetch CSV: #{response.code}" unless response.success?

response
end

# Validate CSV row structure
def validate_csv_row!(row)
missing = %w[Name X Coordinate Y Coordinate].reject { |h| row[h] }
raise "Invalid CSV headers: #{missing.join(', ')}" if missing.any?
end
end
74 changes: 74 additions & 0 deletions spec/services/coffee_shop_finder_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

require_relative '../../app/services/coffee_shop_finder_service'
require_relative '../../app/models/coffee_shop'

RSpec.describe CoffeeShopFinderService do # rubocop:disable RSpec/BlockLength
let(:finder) { CoffeeShopFinderService.new }

context '#closest_shops' do # rubocop:disable RSpec/BlockLength
subject { finder.closest_shops(user_lat, user_lon) }

let(:user_lat) { 40.7128 }
let(:user_lon) { -74.0060 }

let(:shop_same) { CoffeeShop.new('Same Location', user_lat, user_lon) } # 0.0 distance
let(:shop_near) { CoffeeShop.new('Starbucks Seattle', 47.5869, -122.316) }
let(:shop_mid) { CoffeeShop.new('Starbucks Moscow', 55.752047, 37.595242) }
let(:shop_far) { CoffeeShop.new('Starbucks Sydney', -33.871843, 151.206767) }
let(:all_shops) { [shop_far, shop_near, shop_mid, shop_same] }

before do
allow_any_instance_of(CoffeeShopFinderService)
.to receive(:fetch_and_parse_coffee_shops)
.and_return(all_shops)
end

describe 'limit functionality' do
it 'returns the specified number of shops' do
results = finder.closest_shops(user_lat, user_lon, 2)
expect(results.size).to eq(2)
end

it 'returns all shops if limit is greater than the number of shops' do
results = finder.closest_shops(user_lat, user_lon, 20)
expect(results.size).to eq(4)
end
end

describe 'normal functionality' do
it 'returns shops in ascending order' do
expect(subject).to eq(
[
{ shop: shop_same, distance: 0.0 },
{ shop: shop_near, distance: 48.7966 },
{ shop: shop_mid, distance: 112.61 }
]
)
end
end

describe 'edge cases' do
context 'with empty shop list' do
it 'returns empty array' do
allow_any_instance_of(CoffeeShopFinderService)
.to receive(:fetch_and_parse_coffee_shops).and_return([])

expect(subject).to be_empty
end
end

context 'with identical locations' do
let(:dupe_shop) { CoffeeShop.new('Dupe', user_lat, user_lon) }

it 'returns all matching shops' do
allow_any_instance_of(CoffeeShopFinderService)
.to receive(:fetch_and_parse_coffee_shops).and_return([shop_same, dupe_shop])

expect(subject.size).to eq(2)
expect(subject.map { |r| r[:distance] }).to all(eq(0.0))
end
end
end
end
end

0 comments on commit 2fb70a4

Please sign in to comment.