-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Coding challenge #23
base: master
Are you sure you want to change the base?
Coding challenge #23
Changes from all commits
2fc361a
afb5fd4
cef208f
0c5ee0c
1c8cbab
7493561
46d6b3a
53c6ca2
0178864
4fc1893
a4fa48d
b7bb3c4
41ea710
d36cbf0
62009e6
751f9b0
4549062
d086581
b379bab
e6c0236
3aaae73
88d9a42
b9819b1
e30c241
02c8b97
00e2ede
2ceca3c
b9beda3
7016e70
742d9ed
d290b3f
0caee5b
c406cad
482136f
033654a
89e854c
abb0fb0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Ignore RSpec slow examples file | ||
spec/examples.txt | ||
/.byebug_history |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
--require spec_helper |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
AllCops: | ||
NewCops: enable | ||
Exclude: | ||
- 'coffee_place' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
ruby-3.0 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# frozen_string_literal: true | ||
|
||
source 'https://rubygems.org' | ||
|
||
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } | ||
|
||
# gem "rails" | ||
group :development, :test do | ||
gem 'byebug', '~> 11.1' | ||
gem 'rspec', '~> 3.10' | ||
gem 'rubocop', '~> 1.20', require: false | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
GEM | ||
remote: https://rubygems.org/ | ||
specs: | ||
ast (2.4.2) | ||
byebug (11.1.3) | ||
diff-lcs (1.4.4) | ||
parallel (1.20.1) | ||
parser (3.0.2.0) | ||
ast (~> 2.4.1) | ||
rainbow (3.0.0) | ||
regexp_parser (2.1.1) | ||
rexml (3.2.5) | ||
rspec (3.10.0) | ||
rspec-core (~> 3.10.0) | ||
rspec-expectations (~> 3.10.0) | ||
rspec-mocks (~> 3.10.0) | ||
rspec-core (3.10.1) | ||
rspec-support (~> 3.10.0) | ||
rspec-expectations (3.10.1) | ||
diff-lcs (>= 1.2.0, < 2.0) | ||
rspec-support (~> 3.10.0) | ||
rspec-mocks (3.10.2) | ||
diff-lcs (>= 1.2.0, < 2.0) | ||
rspec-support (~> 3.10.0) | ||
rspec-support (3.10.2) | ||
rubocop (1.20.0) | ||
parallel (~> 1.10) | ||
parser (>= 3.0.0.0) | ||
rainbow (>= 2.2.2, < 4.0) | ||
regexp_parser (>= 1.8, < 3.0) | ||
rexml | ||
rubocop-ast (>= 1.9.1, < 2.0) | ||
ruby-progressbar (~> 1.7) | ||
unicode-display_width (>= 1.4.0, < 3.0) | ||
rubocop-ast (1.11.0) | ||
parser (>= 3.0.1.1) | ||
ruby-progressbar (1.11.0) | ||
unicode-display_width (2.0.0) | ||
|
||
PLATFORMS | ||
x86_64-linux | ||
|
||
DEPENDENCIES | ||
byebug (~> 11.1) | ||
rspec (~> 3.10) | ||
rubocop (~> 1.20) | ||
|
||
BUNDLED WITH | ||
2.2.25 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
The MIT License (MIT) | ||
==================== | ||
|
||
* Copyright © 2021 Spataru Madalin Daniel | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining | ||
a copy of this software and associated documentation files (the | ||
"Software"), to deal in the Software without restriction, including | ||
without limitation the rights to use, copy, modify, merge, publish, | ||
distribute, sublicense, and/or sell copies of the Software, and to | ||
permit persons to whom the Software is furnished to do so, subject to | ||
the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be | ||
included in all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | ||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
#! /usr/bin/env ruby | ||
|
||
require_relative './lib/coffee_place/cli.rb' | ||
|
||
cli = CoffeePlace::CLI.new | ||
cli.run(ARGV) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# frozen_string_literal: true | ||
|
||
module CoffeePlace | ||
VERSION = '0.1' | ||
BANNER = 'Usage: ./coffee_place <user x coordinate> <user y coordinate> <shop data url or file>' | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative '../coffee_place' | ||
require_relative './location' | ||
require_relative './cli_parser' | ||
require_relative './importer' | ||
require_relative './search' | ||
|
||
module CoffeePlace | ||
# Parses and runs CLI application using supplied args. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why did you feel the need to explain what the classes do through additional comments? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tldr; Rubocop is being naggy as always! 🤖 😛 Rubocop has a style cop that emits warnings when you haven't added a class comment description. Basically, some classes need no further explanation - their name is sufficient to understand their purpose. This rubocop linter forces you to consistently add documentation at the top of any class, I'm pretty sure the rubocop config for rails disables this, as convention over configuration makes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I apologise for writing you a short novel in response to your comment ... I don't know how to stop myself from rambling sometimes 😛 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I usually disable that cop, I find it naggy too, but recently the thought occured to me that it can be much more useful in more complex projects, as it would help new developers understand the project implementation-wise more easily. Out of curiosity - have you used this documentation cop in production? If so, has it helped the team understand the project better? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We usually used the rails-rubocop config, which I think has this cop turned off. What helped us understand new projects best were design documents and mockups, Maybe including links to helpful documents in the project's own ReadMe would help Comments at the top of more complicated methods, or specs in which these methods are |
||
class CLI | ||
attr_accessor :cli_opts, :parsed_args | ||
|
||
def initialize(parser: CLIParser.new, | ||
importer: Importer.new) | ||
@parser = parser | ||
@importer = importer | ||
end | ||
|
||
def run(args) | ||
cli_opts = parse_cli_options!(args) | ||
|
||
show_help_and_exit! if cli_opts.print_help? | ||
show_version_and_exit! if cli_opts.show_version? | ||
Comment on lines
+23
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm curious - why did you write up the methods with a bang? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like using a bang when naming methods that have important side effects. In In Ruby's standard library, a bang usually means that a method mutates data in place, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I imagined that's the reason. Thank you for answering. :) |
||
|
||
user_location = validate_user_location!(cli_opts) | ||
locations = import_locations_from!(cli_opts) | ||
|
||
search = Search.new(locations) | ||
closest_locations = search.find_closest_to(user_location, cli_opts.num_results) | ||
|
||
display_closest_locations(closest_locations) | ||
end | ||
|
||
private | ||
|
||
def parse_cli_options!(args) | ||
result = @parser.parse(args) | ||
show_help_and_exit!(exit_status: 1) unless result.success? | ||
|
||
cli_opts = result.value | ||
|
||
# We must have [lat, lon, filename] as args | ||
unless cli_opts.num_args == 3 | ||
puts 'Incorrect number of CLI arguments passed in' | ||
show_help_and_exit!(exit_status: 1) | ||
end | ||
|
||
cli_opts | ||
end | ||
|
||
def validate_user_location!(cli_opts) | ||
lat, lon = cli_opts.remaining | ||
location = Location.new(lat: lat, lon: lon, name: 'Current user') | ||
|
||
if location.valid? | ||
location | ||
else | ||
print_errors_and_exit!(location.errors) | ||
end | ||
end | ||
|
||
def import_locations_from!(cli_opts) | ||
source_name = cli_opts.source_name | ||
locations_result = @importer.import_from(source_name) | ||
|
||
if locations_result.success? | ||
locations_result.value | ||
else | ||
puts "Failed to import locations from: #{source_name.inspect}" | ||
print_errors_and_exit!(locations_result.error) | ||
end | ||
end | ||
|
||
def display_closest_locations(locations) | ||
locations.each do |distance, location| | ||
puts "#{location.name},#{distance.round(4)}" | ||
end | ||
end | ||
|
||
def show_help_and_exit!(exit_status: 0) | ||
@parser.show_help | ||
exit exit_status | ||
end | ||
|
||
def show_version_and_exit!(exit_status: 0) | ||
puts CoffeePlace::VERSION | ||
exit exit_status | ||
end | ||
|
||
def print_errors_and_exit!(errors) | ||
Array(errors).each do |error| | ||
puts error | ||
end | ||
|
||
exit 1 | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# frozen_string_literal: true | ||
|
||
module CoffeePlace | ||
# Keeps parsed CLI options, also defining defaults. | ||
class CLIOptions | ||
attr_accessor :version, :help, :remaining, :num_results | ||
|
||
def initialize(version: false, help: false, remaining: []) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting use of flags. Generally, are there any issues that could arise from the use of boolean params? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accidentally passing in
When I use these sort of params internally in my code, I try to make sure that I always send a boolean, When using user submitted fields, some sort of type casting is useful. I would use something like: module BooleanHelper
module_function
def cast(value)
ActiveModel::Type::Boolean.new.cast(value)
end
end There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This are all valid points, but there is also a code smell that arises from using bools in your methods, you can read more about it in this Martin Fowler article, dating way back in 2011 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One more article recommendation from me: Clean code: The curse of a boolean parameter. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that using boolean arguments for control flow leads to needless complexity. |
||
@version = version | ||
@help = help | ||
@remaining = remaining | ||
end | ||
|
||
def show_version? | ||
@version | ||
end | ||
|
||
def print_help? | ||
@help | ||
end | ||
|
||
def source_name | ||
@remaining.last | ||
end | ||
|
||
def <<(arg_value) | ||
@remaining << arg_value | ||
end | ||
|
||
# Counts free standing CLI arguments, not taking flags into account | ||
def num_args | ||
@remaining.size | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'optparse' | ||
require_relative '../coffee_place' | ||
require_relative './result' | ||
require_relative './cli_options' | ||
|
||
module CoffeePlace | ||
# Parses CLI arguments into a `CLIOptions` instance. | ||
class CLIParser | ||
attr_accessor :cli_opts, :parsed_args | ||
|
||
def parse(args) | ||
# Make sure we are not modifying `args` directly | ||
args = args.clone | ||
|
||
@cli_opts = CLIOptions.new | ||
@opt_parser = build_args_parser | ||
@parsed_args = extract_free_standing_arguments(args) | ||
|
||
Result.success(@cli_opts) | ||
end | ||
|
||
def show_help | ||
puts @opt_parser | ||
end | ||
|
||
private | ||
|
||
# Since we are declaratively defining independent flags, method length isn't a concern | ||
# | ||
# rubocop:disable Metrics/MethodLength | ||
def build_args_parser | ||
OptionParser.new do |opts| | ||
opts.banner = CoffeePlace::BANNER | ||
|
||
opts.on('-h', '--help', 'Print this help message') do | ||
@cli_opts.help = true | ||
end | ||
|
||
opts.on('-v', '--version', 'Print version and exit') do | ||
@cli_opts.version = true | ||
end | ||
|
||
opts.on('-n[NUM]', '--num=[NUM]', 'Number of search results to return - default is 3') do |num| | ||
@cli_opts.num_results = num.to_i | ||
end | ||
end | ||
end | ||
# rubocop:enable Metrics/MethodLength | ||
|
||
# Sadly, ruby's OptParser does not like arguments that start with a minus sign, | ||
# as it interprets them as CLI command switches, e.g -122.4 raises OptionParser::InvalidOption | ||
# | ||
# This method catches `InvalidOption`, and gathers arguments that `optparser` can't parse. | ||
# | ||
# https://stackoverflow.com/questions/3642331/can-optionparser-skip-unknown-options-to-be-processed-later-in-a-ruby-program | ||
def extract_free_standing_arguments(args) | ||
@opt_parser.order!(args) do |unrecognized_opt| | ||
@cli_opts << unrecognized_opt | ||
end | ||
rescue OptionParser::InvalidOption => e | ||
# Push back unrecognized argument onto args so we can shift it afterwards | ||
e.recover(args) | ||
@cli_opts << args.shift | ||
|
||
retry | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If that ain't the truth, I don't know what is! 😹