From 9231f8006eb8c9e7dd378cd4b3936e33b1917c04 Mon Sep 17 00:00:00 2001 From: Alexandru Popescu Date: Tue, 28 Jan 2025 15:54:47 +0200 Subject: [PATCH 1/5] Add initial configuration and CoffeeShop model --- Gemfile | 5 +++++ Gemfile.lock | 39 +++++++++++++++++++++++++++++++++ app.rb | 9 ++++++++ app/models/coffee_shop.rb | 36 ++++++++++++++++++++++++++++++ config/environment.rb | 9 ++++++++ config/settings.yml | 1 + spec/models/coffee_shop_spec.rb | 37 +++++++++++++++++++++++++++++++ spec/spec_helper.rb | 1 + 8 files changed, 137 insertions(+) create mode 100644 app.rb create mode 100644 app/models/coffee_shop.rb create mode 100644 config/environment.rb create mode 100644 config/settings.yml create mode 100644 spec/models/coffee_shop_spec.rb diff --git a/Gemfile b/Gemfile index a5a0ff9..2569a85 100644 --- a/Gemfile +++ b/Gemfile @@ -20,3 +20,8 @@ end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] + +gem 'csv' +gem 'httparty' +gem 'pry' +gem 'sinatra' diff --git a/Gemfile.lock b/Gemfile.lock index 9ef1eb1..97cc317 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,18 +2,44 @@ GEM remote: https://rubygems.org/ specs: ast (2.4.2) + base64 (0.2.0) + bigdecimal (3.1.9) byebug (11.1.3) + coderay (1.1.3) + csv (3.3.2) diff-lcs (1.5.1) + httparty (0.22.0) + csv + mini_mime (>= 1.0.0) + multi_xml (>= 0.5.2) json (2.9.1) language_server-protocol (3.17.0.3) + logger (1.6.5) + method_source (1.1.0) + mini_mime (1.1.5) + multi_xml (0.7.1) + bigdecimal (~> 3.1) + mustermann (3.0.3) + ruby2_keywords (~> 0.0.1) nio4r (2.7.4) parallel (1.26.3) parser (3.3.7.0) ast (~> 2.4.1) racc + pry (0.15.2) + coderay (~> 1.1) + method_source (~> 1.0) puma (6.5.0) nio4r (~> 2.0) racc (1.8.1) + rack (3.1.8) + rack-protection (4.1.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-session (2.1.0) + base64 (>= 0.1.0) + rack (>= 3.0.0) rainbow (3.1.1) regexp_parser (2.10.0) rspec (3.13.0) @@ -42,6 +68,15 @@ GEM rubocop-ast (1.37.0) parser (>= 3.3.1.0) ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + sinatra (4.1.1) + logger (>= 1.6.0) + mustermann (~> 3.0) + rack (>= 3.0.0, < 4) + rack-protection (= 4.1.1) + rack-session (>= 2.0.0, < 3) + tilt (~> 2.0) + tilt (2.6.0) unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) @@ -52,9 +87,13 @@ PLATFORMS DEPENDENCIES byebug + csv + httparty + pry puma (~> 6.5) rspec (~> 3.10) rubocop + sinatra tzinfo-data RUBY VERSION diff --git a/app.rb b/app.rb new file mode 100644 index 0000000..080cbd1 --- /dev/null +++ b/app.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# app.rb +require_relative 'config/environment' + +# Mount the controller +map '/api' do + run CoffeeShopController +end diff --git a/app/models/coffee_shop.rb b/app/models/coffee_shop.rb new file mode 100644 index 0000000..300bdc1 --- /dev/null +++ b/app/models/coffee_shop.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class CoffeeShop + class InvalidCoordinatesError < StandardError; end + + attr_reader :name, :latitude, :longitude + + def initialize(name, latitude, longitude) + @name = validate_name(name) + @latitude = validate_coordinate(latitude.to_f, -90..90, 'Latitude') + @longitude = validate_coordinate(longitude.to_f, -180..180, 'Longitude') + end + + def distance_to(user_lat, user_lon) + user_lat = validate_coordinate(user_lat.to_f, -90..90, 'User Latitude') + user_lon = validate_coordinate(user_lon.to_f, -180..180, 'User Longitude') + + Math.sqrt(((user_lat - latitude)**2) + ((user_lon - longitude)**2)).round(4) + end + + private + + def validate_name(name) + name = name.to_s.strip + raise ArgumentError, 'Name cannot be empty' if name.empty? + + name + end + + def validate_coordinate(coord, range, name) + raise InvalidCoordinatesError, "#{name} must be a number" unless coord.is_a?(Numeric) + raise InvalidCoordinatesError, "#{name} out of range" unless range.include?(coord) + + coord + end +end diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..ebc4e06 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'yaml' +require 'logger' + +APP_CONFIG = YAML.load_file(File.join(__dir__, 'settings.yml')).transform_keys(&:to_sym) +APP_LOGGER = Logger.new($stdout) + +Dir[File.join(__dir__, '../app/**/*.rb')].each { |file| require file } diff --git a/config/settings.yml b/config/settings.yml new file mode 100644 index 0000000..92a0ebb --- /dev/null +++ b/config/settings.yml @@ -0,0 +1 @@ +csv_url: 'https://raw.githubusercontent.com/Agilefreaks/test_oop/master/coffee_shops.csv' \ No newline at end of file diff --git a/spec/models/coffee_shop_spec.rb b/spec/models/coffee_shop_spec.rb new file mode 100644 index 0000000..6748200 --- /dev/null +++ b/spec/models/coffee_shop_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require_relative '../../app/models/coffee_shop' + +RSpec.describe CoffeeShop do + describe '#initialize' do + it 'creates a valid coffee shop' do + shop = CoffeeShop.new('Test Shop', 45.0, -122.0) + + expect(shop.name).to eq('Test Shop') + expect(shop.latitude).to eq(45.0) + expect(shop.longitude).to eq(-122.0) + end + + it 'raises error for invalid coordinates' do + expect { CoffeeShop.new('Test', 100.0, 0) }.to raise_error(CoffeeShop::InvalidCoordinatesError) + expect { CoffeeShop.new('Test', 0, 200.0) }.to raise_error(CoffeeShop::InvalidCoordinatesError) + end + + it 'raises error for empty name' do + expect { CoffeeShop.new('', 0, 0) }.to raise_error(ArgumentError) + end + end + + describe '#distance_to' do + let(:shop) { CoffeeShop.new('Test', 0.0, 0.0) } + + it 'calculates distance correctly' do + expect(shop.distance_to(3.0, 4.0)).to eq(5.0) + expect(shop.distance_to(1.0, 1.0)).to eq(1.4142) + end + + it 'raises error for invalid user coordinates' do + expect { shop.distance_to(95.0, 0) }.to raise_error(CoffeeShop::InvalidCoordinatesError) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6a8a99f..1f0af8b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -17,6 +17,7 @@ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration require 'byebug' +require 'pry' RSpec.configure do |config| # rspec-expectations config goes here. You can use an alternate From 2fb70a434f327000844a1a2c1ef2179b24c4a58b Mon Sep 17 00:00:00 2001 From: Alexandru Popescu Date: Tue, 28 Jan 2025 15:55:51 +0200 Subject: [PATCH 2/5] Add Coffee Shops Finder service --- app/services/coffee_shop_finder_service.rb | 46 ++++++++++++ .../coffee_shop_finder_service_spec.rb | 74 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 app/services/coffee_shop_finder_service.rb create mode 100644 spec/services/coffee_shop_finder_service_spec.rb diff --git a/app/services/coffee_shop_finder_service.rb b/app/services/coffee_shop_finder_service.rb new file mode 100644 index 0000000..b4d9332 --- /dev/null +++ b/app/services/coffee_shop_finder_service.rb @@ -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 diff --git a/spec/services/coffee_shop_finder_service_spec.rb b/spec/services/coffee_shop_finder_service_spec.rb new file mode 100644 index 0000000..3f44a36 --- /dev/null +++ b/spec/services/coffee_shop_finder_service_spec.rb @@ -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 From 500d69dacfe6ab19de44029a53bc3ff682c5cd04 Mon Sep 17 00:00:00 2001 From: Alexandru Popescu Date: Tue, 28 Jan 2025 18:03:06 +0200 Subject: [PATCH 3/5] Add controller and test endpoint --- Gemfile | 3 + Gemfile.lock | 7 ++ README.md | 37 ++++--- app.rb | 6 -- app/controllers/coffee_shop_controller.rb | 79 +++++++++++++++ app/services/coffee_shop_finder_service.rb | 10 +- bin/{ruby-interview => start} | 2 +- config.ru | 7 ++ .../controller/coffee_shop_controller_spec.rb | 99 +++++++++++++++++++ spec/spec_helper.rb | 2 + 10 files changed, 229 insertions(+), 23 deletions(-) create mode 100644 app/controllers/coffee_shop_controller.rb rename bin/{ruby-interview => start} (56%) create mode 100644 config.ru create mode 100644 spec/controller/coffee_shop_controller_spec.rb diff --git a/Gemfile b/Gemfile index 2569a85..1313126 100644 --- a/Gemfile +++ b/Gemfile @@ -24,4 +24,7 @@ gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] gem 'csv' gem 'httparty' gem 'pry' +gem 'rack' +gem 'rack-test' +gem 'rackup' gem 'sinatra' diff --git a/Gemfile.lock b/Gemfile.lock index 97cc317..8988c22 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -40,6 +40,10 @@ GEM rack-session (2.1.0) base64 (>= 0.1.0) rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) rainbow (3.1.1) regexp_parser (2.10.0) rspec (3.13.0) @@ -91,6 +95,9 @@ DEPENDENCIES httparty pry puma (~> 6.5) + rack + rack-test + rackup rspec (~> 3.10) rubocop sinatra diff --git a/README.md b/README.md index 8f4adba..f62e8ec 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,6 @@ This repo will serve as a starting point for your code challenge. Feel free to change anything in order to complete it: Change framework, other tests, new gems etc. -## Get this repo - -- Fork this repo -- Clone your fork - -## Prerequisites -- Have RVM installed: https://letmegooglethat.com/?q=install+rvm+on+ubuntu ## Local setup 1. Install ruby: `$ rvm install 3.4.1` @@ -16,18 +9,36 @@ This repo will serve as a starting point for your code challenge. Feel free to c 3. Install bundler: `$ gem install bundler` 4. Install the dependencies with bundler: `$ bundle install` -## Run sample CLI command -`$ bin/ruby-interview` +## Run CLI command to start the server +`$ bin/start` ## Run tests `$ bundle exec rspec` ## Tools -- Write HTTP APIs [rails](https://rubyonrails.org/) or [roda](https://roda.jeremyevans.net/documentation.html) or others -- Write CLI tools [thor](http://whatisthor.com/) or [tty](https://ttytoolkit.org/) or others (including [rake](https://github.com/ruby/rake)) -- Test your code with [rspec](https://rspec.info/) --- +## Use +# Start the server: `$ bin/start` +# Choose a provider to call the endpoind, I choose Thunder Client in VS Code. (Postman, Insomnia) +`http://localhost:9292/api/closest_shops?lat=47.6&lon=-122.4` +# Should see a reponse similar to: + + +# Also can test from terminal +'curl "http://localhost:9292/api/closest_shops?lat=47.6&lon=-122.4"' +'Coffee shops nearest (47.6, -122.4) by distance: + +0.0645 <--> Starbucks Seattle2 +0.0861 <--> Starbucks Seattle +10.0793 <--> Starbucks SF' + + +## Disclaimer + +# Authentication is not taken into account +# I did not deploy it anywhere -Good luck! +# I tought about setting up a DB, querying the endpoint to seed it and then seeding it periodically. +# But taking into the consideration the size of the csv file, the minimal number of requests I choose to prioritize always having the latest data and calling the endpoint through my CoffeeShopFinderServiceon each request diff --git a/app.rb b/app.rb index 080cbd1..3776579 100644 --- a/app.rb +++ b/app.rb @@ -1,9 +1,3 @@ # frozen_string_literal: true -# app.rb require_relative 'config/environment' - -# Mount the controller -map '/api' do - run CoffeeShopController -end diff --git a/app/controllers/coffee_shop_controller.rb b/app/controllers/coffee_shop_controller.rb new file mode 100644 index 0000000..fb28f30 --- /dev/null +++ b/app/controllers/coffee_shop_controller.rb @@ -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 diff --git a/app/services/coffee_shop_finder_service.rb b/app/services/coffee_shop_finder_service.rb index b4d9332..52dc2f2 100644 --- a/app/services/coffee_shop_finder_service.rb +++ b/app/services/coffee_shop_finder_service.rb @@ -22,9 +22,9 @@ def fetch_and_parse_coffee_shops end def parse_coffee_shops(response) - CSV.parse(response.body, headers: true).map do |row| + CSV.parse(response.body, headers: headers).map do |row| validate_csv_row!(row) - CoffeeShop.new(row['Name'], row['X Coordinate'], row['Y Coordinate']) + CoffeeShop.new(row['Name'], row['Lat Coordinate'], row['Lon Coordinate']) end rescue CSV::MalformedCSVError => e raise "Malformed CSV: #{e.message}" @@ -40,7 +40,11 @@ def fetch_csv # Validate CSV row structure def validate_csv_row!(row) - missing = %w[Name X Coordinate Y Coordinate].reject { |h| row[h] } + missing = headers.reject { |h| row[h] } raise "Invalid CSV headers: #{missing.join(', ')}" if missing.any? end + + def headers + ['Name', 'Lat Coordinate', 'Lon Coordinate'] + end end diff --git a/bin/ruby-interview b/bin/start similarity index 56% rename from bin/ruby-interview rename to bin/start index 57f8e8c..c939cab 100755 --- a/bin/ruby-interview +++ b/bin/start @@ -1,4 +1,4 @@ #!/usr/bin/env ruby # frozen_string_literal: true -puts 'Good luck!' \ No newline at end of file +system('bundle exec rackup config.ru') diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..28c5b99 --- /dev/null +++ b/config.ru @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require_relative 'app' + +map '/api' do + run CoffeeShopController +end diff --git a/spec/controller/coffee_shop_controller_spec.rb b/spec/controller/coffee_shop_controller_spec.rb new file mode 100644 index 0000000..7b658c4 --- /dev/null +++ b/spec/controller/coffee_shop_controller_spec.rb @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1f0af8b..8800722 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -18,6 +18,7 @@ require 'byebug' require 'pry' +require 'rack/test' RSpec.configure do |config| # rspec-expectations config goes here. You can use an alternate @@ -95,4 +96,5 @@ # test failures related to randomization by passing the same `--seed` value # as the one that triggered the failure. Kernel.srand config.seed + config.include Rack::Test::Methods end From 6d5e674c6c85d4b2b6ee44efc88efc2b64971f67 Mon Sep 17 00:00:00 2001 From: Alexandru Popescu Date: Tue, 28 Jan 2025 18:23:25 +0200 Subject: [PATCH 4/5] Update README.md --- README.md | 55 +++++++++++++++++++++++-------------------------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index f62e8ec..f48031e 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,35 @@ -# Backend Interview Starting Point - -This repo will serve as a starting point for your code challenge. Feel free to change anything in order to complete it: Change framework, other tests, new gems etc. - - -## Local setup -1. Install ruby: `$ rvm install 3.4.1` -2. `$ cd .` or `$ cd ` to auto-create the rvm gemset -3. Install bundler: `$ gem install bundler` -4. Install the dependencies with bundler: `$ bundle install` - -## Run CLI command to start the server +
+ Local setup + 1. Install ruby: `$ rvm install 3.4.1` + 2. `$ cd .` or `$ cd ` to auto-create the rvm gemset + 3. Install bundler: `$ gem install bundler` + 4. Install the dependencies with bundler: `$ bundle install` +
+ +# Usage +#### Run CLI command to start the server `$ bin/start` -## Run tests -`$ bundle exec rspec` - -## Tools - - ---- -## Use -# Start the server: `$ bin/start` -# Choose a provider to call the endpoind, I choose Thunder Client in VS Code. (Postman, Insomnia) +#### Choose a provider to call the endpoind, I choose [Thunder Client](https://www.thunderclient.com/) inside VS Code. (Postman, Insomnia) `http://localhost:9292/api/closest_shops?lat=47.6&lon=-122.4` -# Should see a reponse similar to: +#### Should see a reponse similar to: +![image](https://github.com/user-attachments/assets/7b3f7d83-4b94-4e7a-b2c0-ad2452a09f84) -# Also can test from terminal -'curl "http://localhost:9292/api/closest_shops?lat=47.6&lon=-122.4"' -'Coffee shops nearest (47.6, -122.4) by distance: +#### It can be also tested using the terminal +```curl "http://localhost:9292/api/closest_shops?lat=47.6&lon=-122.4"``` +``` +Coffee shops nearest (47.6, -122.4) by distance: 0.0645 <--> Starbucks Seattle2 0.0861 <--> Starbucks Seattle 10.0793 <--> Starbucks SF' +``` +## Run tests +`$ bundle exec rspec` -## Disclaimer - -# Authentication is not taken into account -# I did not deploy it anywhere +### Disclaimers -# I tought about setting up a DB, querying the endpoint to seed it and then seeding it periodically. -# But taking into the consideration the size of the csv file, the minimal number of requests I choose to prioritize always having the latest data and calling the endpoint through my CoffeeShopFinderServiceon each request +- I thought about setting up a DB, querying the endpoint to seed it and then seeding it periodically. +- But taking into the consideration the size of the csv file, the minimal number of requests I choose to prioritize always having the latest data and calling the endpoint through my CoffeeShopFinderService on each request." From f64934a2d132ffbb072c9fa41edd92fee5e8d3dc Mon Sep 17 00:00:00 2001 From: Alexandru Popescu Date: Tue, 28 Jan 2025 19:13:27 +0200 Subject: [PATCH 5/5] Update README.md --- README.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f48031e..0f15221 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,26 @@
Local setup - 1. Install ruby: `$ rvm install 3.4.1` - 2. `$ cd .` or `$ cd ` to auto-create the rvm gemset - 3. Install bundler: `$ gem install bundler` - 4. Install the dependencies with bundler: `$ bundle install` + + 1. Install ruby: `$ rvm install 3.4.1` + 2. `$ cd .` or `$ cd ` to auto-create the rvm gemset + 3. Install bundler: `$ gem install bundler` + 4. Install the dependencies with bundler: `$ bundle install`
+### Code Structure and Purpose + +- **Controller to Service**: When a request is received, the controller delegates the task to the appropriate service. **Sinatra** is used to handle HTTP requests and responses in a lightweight manner. +- **Service to External API**: The `CoffeeShopFinderService` interacts with an external API to retrieve data about coffee shops. It processes this data to find the closest coffee shops based on the provided coordinates. +- **Response Handling**: After processing the request, the service returns the result to the controller, which then formats the response and sends it back to the client. + +### Testing + +- **Unit Tests**: The project is thoroughly tested using the `rspec` gem. +- Run tests ➡️ `$ bundle exec rspec` + + + + # Usage #### Run CLI command to start the server `$ bin/start` @@ -26,9 +41,6 @@ Coffee shops nearest (47.6, -122.4) by distance: 10.0793 <--> Starbucks SF' ``` -## Run tests -`$ bundle exec rspec` - ### Disclaimers - I thought about setting up a DB, querying the endpoint to seed it and then seeding it periodically.