ROR How to best handle nil in custom validations - ruby

I have this custom validations that throws an "undefined method `>' for nil:NilClass" when ever birthday is not set because birthday is nil.
validate :is_21_or_older
def is_21_or_older
if birthday > 21.years.ago.to_date
errors.add(:birthday, "Must 21 Or Older")
end
end
I already have validates_presence_of for birthday so is there a way to have is_21_or_older called only after validates_presence_of passes?

Rails runs all validators independently, in order to give you an array of all the errors at once. This is done to avoid the all too common scenario:
Please enter a password.
pass
The password you have entered is invalid: it does not contain a number.
1234
The password you have entered is invalid: it does not contain a letter.
a1234
The password you have entered is invalid: it is not at least six characters long.
ab1234
The password you have entered is invalid: you cannot use three or more consecutive characters in a sequence.
piss off
The password you have entered is invalid: it does not contain a number.
There are two things you can do, that I know of. Either include everything under your custom validator, in which case everything is under your control, or use the :unless => Proc.new { |x| x.birthday.nil? } modifier to explicitly restrict your validator from running under the circumstances it would break. I'd definitely suggest the first approach; the second is hacky.
def is_21_or_older
if birthday.blank?
errors.add(:birthday, "Must have birthday")
elsif birthday > 21.years.ago.to_date
errors.add(:birthday, "Must 21 Or Older")
end
end
Maybe an even better approach is to keep the presence validator, just exit your custom validator when it sees the other validator's condition is failed.
def is_21_or_older
return true if birthday.blank? # let the other validator handle it
if birthday > 21.years.ago.to_date
errors.add(:birthday, "Must 21 Or Older")
end
end

Related

I'm trying to convert a value to an integer what am I doing wrong here?

I'm just trying to make this code work and I keep getting:
<=': comparison of String with 21 failed (ArgumentError)
Please tell me what I'm doing wrong.
I'm learning and I've gone through every iteration of the code I can think of to try and make it work, I'm just not sure what I've done wrong.
puts "How old are you?"
old = gets.chomp
if old <= 21
return "You are not legally allowed to buy alcohol in the US"
else
return "You are legally allowed to buy alcohol in the US"
end
I believe you have to use to_i to convert the string into an integer.
The previous answer is correct but more verbosely, here is the line you need to change in your code:
old = gets.chomp.to_i
But you might also want to make sure user only enters an integer because calling .to_i on non-numeric characters will return 0.
You might want to look at Accept only numeric input
Try/improve as needed:
input = gets.chomp
if(val = Integer(input) rescue false)
val < 21 ? 'Not old enough' : 'The usual martini?'
else
'You did not provide an age (number)'
end
It checks if the input is an Integer, so it accounts for an input like foo.
Hth...

ruby date validation on wrong input

Im a newbie to rails and I need your help on this one.
On the code below, if the first input is wrong and the second one is correct, the expression evaluates the first input.
I would like that the user input is always evaluated.
require 'date'
puts "x is not a valid date"
until (x = gets.chomp) == ((x.match(/\d{4}-\d{1,2}-\d{1,2}$/).to_s &&Date.strptime(x, '%Y-%m-%d')rescue false) ==true)
In general, you should try to separate your data inout logic from data validation and later processing. That way, your methods concern themselves with only one thing each, which makes them much easier to reason about and thus more robust.
Here, we are introducing a valid_date_string? method which checks if the passed input object is a valid date string.
require 'date'
def valid_date_string?(input)
# First, we check that the given input is of the expected type.
# We do this here explicitly since we are validating a String. Most
# of the time, you would rely on duck typing instead.
return false unless input.is_a?(String)
# Then, we ensure roughly valid syntax (i.e. it looks like a date)
return false unless input =~ /\A\d+-\d+-\d+\z/
# Then, we "parse" it and split it into an array of year, month, day...
parts = input.split('-').map(&:to_i)
# ...and validate that this represents a valid date
Date.valid_date?(*parts)
end
In this solution, I took care not to rely on Exceptions for program flow since this is usually frowned upon as it can result in undetected bugs and has poor performance.
We can then use this method in some input loop to ask the user to input some value
def get_valid_date
loop do
input = gets.chomp
return input if valid_date_string?(input)
# Here, the user entered an invalid date. We can now do our
# error handling and try again.
puts "Invalid date given. Please try again..."
end
end
my_valid_date_string = get_valid_date
I would approach this problem as follows:
# date_test.rb
def date_input
puts "Please enter a date (format YYYY-MM-DD):"
d = gets.chomp
if d =~ /\d{4}-\d{1,2}-\d{1,2}$/
puts "You entered: #{d}"
else
puts "#{d} is not a valid date."
date_input
end
end
date_input
Explanation:
prompt to enter a date (specifying the expected format)
gets and chomp the user input
validate the format
if valid, puts the date
if not valid, puts the error and start again
When you run it:
$ ruby date_test.rb
Please enter a date (format YYYY-MM-DD):
invalid date
invalid date is not a valid date.
Please enter a date (format YYYY-MM-DD):
2017-10-13
You entered: 2017-10-13
$

Ruby validation of name, email, and phone number

After creating the loop to check that the phone number is 10 characters I believe the phone issue is now resolved. Now I'm working with checking the email address, name, and making sure it outputs correctly, and making sure 2 names are entered by the user. Having issues getting the email address to validate and output in the correct format.
NAME_PATTERN = /([\w\-\']{2,})([\s]+)([\w\-\']{2,})/
EMAIL_PATTERN = /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
PHONE_PATTERN = /^(?:\+?1\s*(?:[.-]\s*)?)?(?:\(\s*([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9])\s*\)|([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9]))\s*(?:[.-]\s*)?([2-9]1[02-9]|[2-9][02-9]1|[2-9][02-9]{2})\s*(?:[.-]\s*)?([0-9]{4})$/
def valid_name?(name)
!!name.match(NAME_PATTERN)
end
puts "Enter your first and last name (John Doe): "
while (name=gets)
names = name.split(" ", 2)
if valid_name?(name)
puts "Great, that looks like it could work."
break
else
puts "Please enter first and last name."
end
end
def valid_email?(email)
!!email.match(EMAIL_PATTERN)
end
puts "Enter your email address (joe#info.com): "
while (email = gets)
if valid_email?(email)
puts "Great, that looks like it could work."
break
else
puts "Invalid email address entered. Please try again. "
end
end
def valid_phone?(number)
!!number.match(PHONE_PATTERN)
end
puts "Enter your phone number including area code (numbers only): "
while (number=gets)
if valid_phone?(number)
puts "Great, that looks like it could work."
break
else
puts "Invalid phone number entered. Please try again."
end
end
puts names
puts email
puts number
I suspect you didn't intend to use the assignment operator here:
if (email = /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i)
Try this:
if email =~ /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
You have a similar error in the phone_number method:
if number = 10
I'm not sure what you intended here. Perhaps this?
if number.size == 10
You have more problems, however. Take a look at this loop:
loop do
if number.size == 10
break
else
puts "Invalid phone number entered. Please try again."
end
end
How will the user ever exit this loop if the number is invalid? The value of number never changes.
Here's a Ruby flavoured approach to what you're trying to do:
# Define constants for things that are special and get re-used.
EMAIL_PATTERN = /\A\S+#\S+\z/
# Methods that test something and return true or false often end with
# a question mark (?) to indicate this.
def valid_email?(email)
!!email.match(EMAIL_PATTERN)
end
# Try and keep your support methods separate from the main body of
# your program.
puts "Enter your email address (joe#info.com): "
# This sets up a loop that waits until you get a valid response.
while (email = gets)
email = gets
if valid_email?(email)
puts "Great, that looks like it could work"
break
else
# Note that the message is rendered here, not in the validation
# method, so there's no assumptions about how this method is used.
puts "Invalid email address entered. Please try again. "
end
end
If you try and structure your code this way you'll find it's a lot easier to keep things organized. This is one of the big challenges when learning programming so as not to get overwhelmed.
Regular expressions are great for validating things that conform to a very specific pattern, but try not to get overly confident in the pattern of everyday things. Even the humble IPv4 address comes in a multitude of forms.

Accessing variable name

I trying to some parameter validation outside of Rails.
def create_statement(action, ...)
valid_one_of(action, ['ADD', 'MOVE', 'DELETE'])
...
end
Validation Method:
def valid_one_of(input, valid_values)
return true if valid_values.include?(input)
raise "#{input} was not a valid value for #{input.var_name}"
end
Sample Call:
create_statement('Bob')
So the output would be: Bob was not valid value for action
The problem how do i get input.var_name ?
I for a workaround I could pass
valid_one_of(action, ['ADD', 'MOVE', 'DELETE'], 'action')
(and use 3rd parm for my output) but this doesn't seem feels bit redundant.
If its not possible to access variable name is there a more DRY coding style than this workaround?
I don't know of a way to get the name of a local variable, and even if you could, which name would you want? The name of the parameter as define on the method definition? the name of the variable in the caller?
I don't think your "work-around" is that horrible, but I could suggest a couple of slight variations that might read a little better.
Also I am not sure if your intent is to actually pass in the list of valid values at the call site, or if you might have, say a hash of valid values that you might use the passed in type to look up the valid values from.
def valid_one_of_type type, input:, valid_values:
return true if valid_values.include?(input)
raise "#{input} was not a valid value for #{type}"
end
my_action = "bob"
valid_one_of_type :action , input: my_action, valid_values: ['ADD', 'MOVE', 'DELETE']
def valid_one_of_type type, input, valid_values
return true if valid_values.include?(input)
raise "#{input} was not a valid value for #{type}"
end
my_action = "bob"
valid_one_of_type :action , my_action, ['ADD', 'MOVE', 'DELETE']
Another option would be to wrap your actions in a class and do the validations on creation:
class Action
VALID_ACTIONS = ['ADD', 'MOVE', 'DELETE']
def initialize action
unless VALID_ACTIONS.include? action
raise "#{action} is not a valid value for #{self.class.name}"
end
end
end
my_action = Action.new "bob"

How can I get this ruby code with sequel to work in sinatra?

I am trying to only allow a person to see the page if their name is in the database. I figured the best way to go about it was to loop through all of the entries and check if it matches, if it does then display it and stop looping. I keep getting a blank page, any help?
get '/' do
user = "john"
num = DB[:users].all
for person in num do
if person[:name].to_s == user then
File.read('index.html')
break
else
"you're not authorized"
end
end
end
If I were to remove the line that says break within the if statement, I get this error:
NoMethodError at /
undefined method `bytesize' for #<Hash:0x007fcf60970a68>
file: utils.rb location: bytesize line: 369
The problem is that a for loop evaluates to nil (unless you break and supply a value to break), so your block is returning nil, so there's nothing to render.
But the real problem is that for is the wrong solution here. What you're trying to do is check if the Array DB[:users].all contains a Hash whose :name member equals user. You can use a loop for that, but in addition to for being rare in idiomatic Ruby code (Enumerable#each is preferred) it makes the intent of your code harder to understand. Instead, you could use Enumerable#find (the Array class includes the methods in the Enumerable module) like so:
get '/' do
username = "john"
users = DB[:users].all
matching_user = users.find do |user|
user[:name] == user
end
if matching_user
return File.read('index.html')
end
"you're not authorized"
end
...but since you don't actually care about the matching user—you only care if a matching user exists—it would be clearer to use Enumerable#any?, which just returns true or false:
get '/' do
username = "john"
users = DB[:users].all
if users.any? {|user| user[:name] == user }
return File.read('index.html')
end
"you're not authorized"
end
Edit: As #user846250 points out, it would be better to let the database do the work of checking if any matching users exist. Something like this:
get '/' do
username = "john"
if DB[:users].where(:name => username).empty?
return "you're not authorized"
end
File.read('index.html')
end
This is preferable because instead of loading all of the records from the database into Ruby (which is what DB[:users].all will do)—when you don't actually care about the data in any of them—Sequel will just ask the database if there are any matching records and then return true or false.

Resources