Skip to content

Commit

Permalink
Feature/question class (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcioFPaludo authored Feb 12, 2024
1 parent b51aacc commit cadf671
Show file tree
Hide file tree
Showing 17 changed files with 457 additions and 31 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"simplecov-vscode.enabled": true
}
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,58 @@ puts message.to_s
# We hope you are well Alice!
```

#### Question

The Question class was created with the objective of asking questions to the user.
It receives a message that will be presented as a question to the user when a response method is called.
All messages received by the class are automatically placed in an instance of Message.
Here are some examples with the available answers:

1. Boolean Answer

```ruby
# Asking the user a yes/no question and processing the response.
question = Question.new("Do you like Ruby?")
puts question.bool_answer ? "You like Ruby!" : "You don't like Ruby?"
````

2. Float Answer

```ruby
# Asking the user for a floating-point number, such as a version number.
question = Question.new("What is the value of pi?")
version = question.float_answer
puts "The value of pi is #{version}"
```

3. Integer Answer

```ruby
# Prompting for an integer, for example, asking for a quantity.
question = Question.new("How many Ruby gems do you need?")
quantity = question.integer_answer
puts "You need #{quantity} gems."
```

4. Option Answer

```ruby
# Allowing the user to choose from a list of options.
question = Question.new("Choose your preferred Ruby web framework:")
options = ["Rails", "Sinatra", "Hanami"]
framework = question.option_answer(options)
puts "You have chosen #{framework}."
```

5. String Answer

```ruby
# Asking for a string input that matches a specific pattern, such as a name.
question = Question.new("What is your name?")
name = question.string_answer(regex: /^[A-Za-z ]+$/)
puts "Hello, #{name}!"
```

## Development

To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
Expand Down
2 changes: 2 additions & 0 deletions lib/mp_utils.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'utils/key'
require 'utils/array'
require 'utils/message'
require 'utils/question'
require 'resources/path_helper'
1 change: 1 addition & 0 deletions lib/resources/messages/question/bool_sufix.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(Yes/No/Y/N)
3 changes: 3 additions & 0 deletions lib/resources/messages/question/error/bool.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
You need to answer with "Yes or No" only.
These are the valid response options: Yes, No, Y, N
Note: You can use both lowercase and uppercase letters for the answers.
2 changes: 2 additions & 0 deletions lib/resources/messages/question/error/float.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
You need to provide a numeric value with decimal places.
Example: 3143.14
1 change: 1 addition & 0 deletions lib/resources/messages/question/error/index.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The index value needs to be within the above listing.
2 changes: 2 additions & 0 deletions lib/resources/messages/question/error/integer.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
You need to provide an integer numeric value.
Example: 123456
2 changes: 2 additions & 0 deletions lib/resources/messages/question/error/regex.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Message does not meet the requested standard.
Please provide a message within the expected standard.
2 changes: 1 addition & 1 deletion lib/resources/path_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def self.library_path
# {define} method. This method allows for easy retrieval of the custom path, facilitating flexible
# resource management.
#
# @return [String, nil] the custom resources path as defined by the environment variable, or nil if it hasn't been set.
# @return [String, nil] the custom path as defined by the environment variable, or nil if it hasn't been set.
def self.custom_path
ENV['SCRIPT_CUSTOM_RESOURCES']
end
Expand Down
22 changes: 22 additions & 0 deletions lib/utils/array.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

# Extension to Ruby's Array class to enhance functionality.
class Array
# Lists all elements in the array with their index.
#
# This method outputs each element of the array to the console, prefixed by its index (1-based).
# The index is right-justified based on the length of the array, ensuring a tidy, column-aligned output.
#
# Example output for a 3-element array:
# |1| Element 1
# |2| Element 2
# |3| Element 3
#
# @return [void]
def list_all_elements
index_size = count.to_s.length
each_index do |index|
puts "|#{(index + 1).to_s.rjust(index_size)}| #{self[index]}"
end
end
end
64 changes: 39 additions & 25 deletions lib/utils/message.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ class Message
# Initializes a new instance of the Message class with a given message template.
#
# @param message [String] The message template to be used.
# @param replaces [String] The replaces Hash used to change key finded in @message by custom values.
# @param replaces [Hash] Used to replace all occurrences of a key with its value.
def initialize(message, replaces: nil)
raise 'Messsage replaces content need be a Hash' if !replaces.nil? && !replaces.is_a?(Hash)
@replaces = replaces

@to_replace = replaces
@message = message
end

Expand All @@ -42,29 +43,8 @@ def ==(other)
#
# @return [String] The formatted message with placeholders substituted with actual content.
def to_s
new_message = String.new(@message)
keys = Key.find_keys_in(@message)

if keys.count.positive?
keys.each do |key|
message = recover_message_with(key.value)
new_message.gsub!(key.to_s, message) if message != key.value
end
else
new_message = recover_message_with(new_message)
end

if !@replaces.nil?
@replaces.each do |key, value|
if key.is_a?(Key)
new_message.gsub!(key.to_regexp, value.to_s)
else
new_message.gsub!(/#{Regexp.escape(key.to_s)}/, value.to_s)
end
end
end

new_message
new_message = replace_message_keys(String.new(@message))
replace_all_to_replace_elements(new_message)
end
end

Expand All @@ -90,6 +70,40 @@ def library_path
File.join(Resources.library_path, 'messages')
end

# Replaces keys found in the original message with their corresponding values.
#
# @param message [String] The original message with keys to be replaced.
# @return [String] The message with keys replaced by their corresponding values.
def replace_message_keys(message)
keys = Key.find_keys_in(message)
return recover_message_with(message) if keys.empty?

keys.each do |key|
key_message = recover_message_with(key.value)
message.gsub!(key.to_s, key_message) if key_message != key.value
end

message
end

# Replaces all placeholders in the message with their corresponding values from the @to_replace hash.
#
# @param message [String] The message with placeholders to replace.
# @return [String] The message with all placeholders replaced with actual content.
def replace_all_to_replace_elements(message)
return message if @to_replace.nil?

@to_replace.each do |key, value|
if key.is_a?(Key)
message.gsub!(key.to_regexp, value.to_s)
else
message.gsub!(/#{Regexp.escape(key.to_s)}/, value.to_s)
end
end

message
end

# Attempts to recover and return the content of a message file identified by the file_name parameter.
# It first looks in the custom path (if defined) and then in the library path.
#
Expand Down
115 changes: 115 additions & 0 deletions lib/utils/question.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# frozen_string_literal: true

require_relative 'message'
require_relative 'array'

# The Question class facilitates the creation and management of interactive questions in the console.
# It provides methods to validate and return user input as various data types including boolean,
# float, integer, options (from a list), and string.
class Question
# Initializes a new instance of the Question class.
#
# @param message [String] The question message to be displayed to the user.
def initialize(message)
@message = Message.new(message)
end

# Prompts the user with a boolean question and returns the answer as true or false.
#
# @param error_message [String, nil] Custom error message for invalid inputs.
# @return [Boolean] True if the user answers affirmatively, otherwise false.
def bool_answer(error_message: nil)
error = error_message || File.join('question', 'error', 'bool')
bool_sufix = Message.new(File.join('question', 'bool_sufix'))
result = read_input("#{@message} #{bool_sufix}", error_message: error) do |input|
input.match?(/^((Y|y)((E|e)(S|s))*)|((N|n)(O|o)*)$/)
end

result.match?(/^(Y|y)((E|e)(S|s))*$/)
end

# Prompts the user for a floating-point number and returns the value.
#
# @param error_message [String, nil] Custom error message for invalid inputs.
# @return [Float] The user's input converted to a float.
def float_answer(error_message: nil)
error = error_message || File.join('question', 'error', 'float')
string_answer(regex: /^\d+\.\d+$/, error_message: error).to_f
end

# Prompts the user for an integer and returns the value.
#
# @param error_message [String, nil] Custom error message for invalid inputs.
# @return [Integer] The user's input converted to an integer.
def integer_answer(error_message: nil)
error = error_message || File.join('question', 'error', 'integer')
string_answer(regex: /^\d+$/, error_message: error).to_i
end

# Prompts the user to select an option from a given list and returns the selected option.
#
# @param options [Array] The list of options for the user to choose from.
# @param error_message [String, nil] Custom error message for invalid inputs.
# @raise [RuntimeError] If options is not an Array or is empty.
# @return [Object] The selected option from the list. If options count is 1 return the first element.
def option_answer(options, error_message: nil)
raise 'Options should be an Array' unless options.is_a?(Array)
raise 'Options should not be empty' if options.empty?
return options.first if options.count == 1

options.list_all_elements
index = read_index_input(count: options.count, error_message: error_message)
options[index]
end

# Prompts the user for a string that matches a given regular expression.
#
# @param regex [Regexp, nil] The regular expression the user's input must match.
# @param error_message [String, nil] Custom error message for invalid inputs.
# @return [String] The user's input if it matches the given regex.
def string_answer(regex: nil, error_message: nil)
error = error_message || File.join('question', 'error', 'regex')
read_input(error_message: error) do |input|
regex.nil? || input.match?(regex)
end
end

private

# Displays a message to the user and returns their input, if it satisfies the given condition.
#
# @param message [String] The message to display to the user.
# @param error_message [String] The error message to display for invalid inputs.
# @yieldparam input [String] The user's input to validate.
# @yieldreturn [Boolean] True if the input is valid, otherwise false.
# @return [String] The user's valid input.
def read_input(message = @message, error_message:)
loop do
puts message
input = $stdin.gets.chomp
return input if yield input

puts Message.new(error_message)
end
end

# Reads an index input from the user, ensuring it falls within a specified range.
#
# This method prompts the user to enter an index number. It validates the input to ensure
# it is an integer within the range of 0 to (count - 1). If the input is invalid, an error
# message is displayed, and the user is prompted again.
#
# @param count [Integer] The count of items, setting the upper limit of the valid index range.
# @param error_message [String, nil] Custom error message for invalid index inputs.
# Defaults to the path 'question/error/index' if nil.
# @return [Integer] The user's input adjusted to be zero-based (input - 1) and validated to be within the range.
def read_index_input(count:, error_message: nil)
loop do
index = integer_answer(error_message: error_message) - 1
return index if index >= 0 && index < count

error = error_message || File.join('question', 'error', 'index')
puts Message.new(error)
end
end
end
2 changes: 1 addition & 1 deletion lib/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module MPUtils
VERSION = '0.1.3'
VERSION = '0.2.1'
end
52 changes: 52 additions & 0 deletions spec/utils/array_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

require_relative '../../lib/utils/array'

RSpec.describe Array do
describe '#list_all_elements' do
context 'when the array is empty' do
it 'prints nothing' do
expect { [].list_all_elements }.to output('').to_stdout
end
end

context 'when the array has one element' do
it 'prints the element with its index' do
expect { ['Element'].list_all_elements }.to output("|1| Element\n").to_stdout
end
end

context 'when the array has multiple elements' do
it 'prints all elements with their indices, aligned properly' do
array = %w[First Second Third]
expected_output = <<~OUTPUT
|1| First
|2| Second
|3| Third
OUTPUT
expect { array.list_all_elements }.to output(expected_output).to_stdout
end
end

context 'with a larger set of elements affecting index alignment' do
it 'adjusts the index alignment based on the number of elements' do
array = (1..12).to_a
expected_output = <<~OUTPUT
| 1| 1
| 2| 2
| 3| 3
| 4| 4
| 5| 5
| 6| 6
| 7| 7
| 8| 8
| 9| 9
|10| 10
|11| 11
|12| 12
OUTPUT
expect { array.list_all_elements }.to output(expected_output).to_stdout
end
end
end
end
Loading

0 comments on commit cadf671

Please sign in to comment.