-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
2fb70a4
commit 500d69d
Showing
10 changed files
with
229 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,3 @@ | ||
# frozen_string_literal: true | ||
|
||
# app.rb | ||
require_relative 'config/environment' | ||
|
||
# Mount the controller | ||
map '/api' do | ||
run CoffeeShopController | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'sinatra/base' | ||
require 'json' | ||
require_relative '../services/coffee_shop_finder_service' | ||
|
||
class CoffeeShopController < Sinatra::Base | ||
configure do | ||
set :protection, except: [:host_authorization] | ||
set :show_exceptions, false | ||
set :raise_errors, true | ||
enable :logging | ||
end | ||
|
||
before do | ||
content_type :json | ||
end | ||
|
||
get '/closest_shops' do | ||
content_type 'text/plain' | ||
lat, lon = validate_coordinates!(params) | ||
|
||
shops_with_distances = coffee_shop_finder_service.closest_shops(lat, lon) | ||
format_response(shops_with_distances, lat, lon) | ||
rescue StandardError => e | ||
handle_error(e) | ||
end | ||
|
||
private | ||
|
||
# Format shops into "Name --> distance <-- (user-lat, user_lon)" strings | ||
def format_response(shops_with_distances, user_lat, user_lon) | ||
header = "Coffee shops nearest (#{user_lat}, #{user_lon}) by distance:\n\n" | ||
|
||
header + shops_with_distances.map do |shops_with_distance| | ||
shop = shops_with_distance[:shop] | ||
distance = shops_with_distance[:distance] | ||
|
||
"#{distance} <--> #{shop.name}" | ||
end.join("\n") | ||
end | ||
|
||
def validate_coordinates!(parameters) | ||
error!(400, 'Invalid coordinates') unless parameters[:lat] && parameters[:lon] | ||
error!(400, 'Coordinates must be numeric') unless numeric?(parameters[:lat]) && numeric?(parameters[:lon]) | ||
|
||
lat = parameters[:lat].to_f | ||
lon = parameters[:lon].to_f | ||
error!(400, 'Invalid coordinates') unless lat.between?(-90, 90) && lon.between?(-180, 180) | ||
|
||
[lat, lon] | ||
end | ||
|
||
def numeric?(str) | ||
return false unless str =~ /\A[-+]?[0-9]*\.?[0-9]+\Z/ | ||
|
||
Float(str) | ||
end | ||
|
||
# Handle errors with appropriate HTTP status codes | ||
def handle_error(error) | ||
status_code = case error.message | ||
when /Invalid CSV/ then 400 | ||
when /Failed to fetch CSV/ then 502 | ||
else 500 | ||
end | ||
|
||
status status_code | ||
{ error: error.message }.to_json | ||
end | ||
|
||
def error!(code, message) | ||
halt code, { error: message }.to_json | ||
end | ||
|
||
def coffee_shop_finder_service | ||
CoffeeShopFinderService.new | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
#!/usr/bin/env ruby | ||
# frozen_string_literal: true | ||
|
||
puts 'Good luck!' | ||
system('bundle exec rackup config.ru') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative 'app' | ||
|
||
map '/api' do | ||
run CoffeeShopController | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'rack/test' | ||
require_relative '../../app/controllers/coffee_shop_controller' | ||
require_relative '../../app/services/coffee_shop_finder_service' | ||
|
||
RSpec.describe CoffeeShopController do # rubocop:disable Metrics/BlockLength | ||
include Rack::Test::Methods | ||
|
||
def app | ||
@app ||= Rack::Builder.new do | ||
map '/api' do | ||
run CoffeeShopController | ||
end | ||
end | ||
end | ||
|
||
before(:all) do | ||
CoffeeShopController.set :environment, :test | ||
end | ||
|
||
let(:valid_lat) { 40.7128 } | ||
let(:valid_lon) { -74.0060 } | ||
let(:invalid_coord) { 200.0 } | ||
|
||
let(:mock_shops) do | ||
[ | ||
{ shop: double(name: 'Shop A', distance: 0.5), distance: 0.5 }, | ||
{ shop: double(name: 'Shop B', distance: 1.2), distance: 1.2 } | ||
] | ||
end | ||
|
||
before do | ||
allow_any_instance_of(CoffeeShopController) | ||
.to receive(:format_response) do |_instance, _shops_with_distances, lat, lon| | ||
"Coffee shops nearest (#{lat}, #{lon}) by distance:\n\n0.5 <--> Shop A\n1.2 <--> Shop B" | ||
end | ||
end | ||
|
||
describe 'GET /api/closest_shops' do # rubocop:disable Metrics/BlockLength | ||
context 'with valid coordinates' do | ||
before do | ||
allow(CoffeeShopFinderService).to receive_message_chain(:new, :closest_shops).and_return(mock_shops) | ||
end | ||
|
||
it 'returns a 200 OK' do | ||
get '/api/closest_shops', lat: valid_lat, lon: valid_lon | ||
expect(last_response.status).to eq(200) | ||
expect(last_response.content_type).to include('text/plain') | ||
expect(last_response.body).to include('0.5 <--> Shop A') | ||
expect(last_response.body).to include('1.2 <--> Shop B') | ||
end | ||
end | ||
|
||
context 'with invalid coordinates' do | ||
it 'returns 400 for missing lat' do | ||
get '/api/closest_shops', lon: valid_lon | ||
expect(last_response.status).to eq(400) | ||
expect(JSON.parse(last_response.body)).to include('error' => 'Invalid coordinates') | ||
end | ||
|
||
it 'returns 400 for non-numeric lat' do | ||
get '/api/closest_shops', lat: 'abc', lon: valid_lon | ||
expect(last_response.status).to eq(400) | ||
end | ||
|
||
it 'returns 400 for out-of-range lat' do | ||
get '/api/closest_shops', lat: invalid_coord, lon: valid_lon | ||
expect(last_response.status).to eq(400) | ||
end | ||
end | ||
|
||
context 'when service raises errors' do | ||
it 'returns 502 for CSV fetch failure' do | ||
allow(CoffeeShopFinderService).to receive_message_chain(:new, :closest_shops) | ||
.and_raise(StandardError.new('Failed to fetch CSV')) | ||
|
||
get '/api/closest_shops', lat: valid_lat, lon: valid_lon | ||
expect(last_response.status).to eq(502) | ||
end | ||
|
||
it 'returns 400 for invalid CSV data' do | ||
allow(CoffeeShopFinderService).to receive_message_chain(:new, :closest_shops) | ||
.and_raise(StandardError.new('Invalid CSV')) | ||
|
||
get '/api/closest_shops', lat: valid_lat, lon: valid_lon | ||
expect(last_response.status).to eq(400) | ||
end | ||
|
||
it 'returns 500 for unexpected errors' do | ||
allow(CoffeeShopFinderService).to receive_message_chain(:new, :closest_shops) | ||
.and_raise(StandardError.new('Unknown error')) | ||
|
||
get '/api/closest_shops', lat: valid_lat, lon: valid_lon | ||
expect(last_response.status).to eq(500) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters