Rails regex validation failing in rspec - ruby-on-rails

I am trying to write and test a regex valiation which allows only for a sequence of paired integers, in the format
n,n n,n
where n is any integer not beginning with zero and pairs are space separated. There may be a single pair or the field may also be empty.
So with this data, it should give 2 errors
12,2 11,2 aa 111,11,11
error 1: the 'aa'
error 2: the triplet (111,11,11)
In my Rails model I have this
validates_format_of :sequence_excluded_region, :sequence_included_region,
with: /[0-9]*,[0-9] /, allow_blank: true
In my Rspec model test I have this
it 'is invalid with alphanumeric SEQUENCE_INCLUDED_REGION' do
expect(DesignSetting.create!(sequence_included_region: '12,2 11,2 aa 111,11,11')).to have(1).errors_on(:sequence_included_region)
end
The test fails, as the regex does not find the errors, or perhaps I am calling the test incorrectly.
Failures:
1) DesignSetting is invalid with alphanumeric SEQUENCE_INCLUDED_REGION
Failure/Error: expect(DesignSetting.create!(sequence_included_region: '12,2 11,2 aa 111,11,11')).to have(2).errors_on(:sequence_included_region)
expected 2 errors on :sequence_included_region, got 0
# ./spec/models/design_setting_spec.rb:5:in `block (2 levels) in <top (required)>'

Regex
Your regex matches a single pair followed by a space anywhere in the string.
'12,2 11,2 aa 111,11,11 13,3'.scan /[0-9]*,[0-9] /
=> ["12,2 ", "11,2 "]
So any string with one valid pair followed by a space will be valid. Also a single pair would fail 3,4 as there is no space.
A regex that would validate the entire string:
positive_int = /[1-9][0-9]*/
pair = /#{positive_int},#{positive_int}/
re_validate = /
\A # Start of string
#{pair} # Must have one number pair.
(?:\s#{pair})* # Can be followed by any number of pairs with a space delimiter
\z # End of string (no newline)
/x
Validators
I don't use rails much but it seems like you are expecting too much from a simple regex validator for it to parse out the individual error components from a string for you.
If you split the variable up by space and then validated each element of the array you could get that detail for each field.
'12,2 11,2 aa 111,11,11 13,3'.split(' ').reject{|f| f =~ /^[1-9][0-9]*,[1-9][0-9]*$/ }
You can put something like that into a custom validator class using validates_with which you can then have direct control of your errors with...
class RegionValidator < ActiveModel::Validator
def validate(record)
record.sequence_included_region.split(' ').reject{|f| f =~ /^[1-9][0-9]*,[1-9][0-9]*$/ }.each do |err|
record.errors[sequence_included_region] << "bad region field [#{err}]"
end
end
end

(?<=\s|^)\d+,\d+(?=\s|$)
Try this.Replace with empty string.The left string split by are your errors.
See demo.
http://regex101.com/r/rQ6mK9/22

Related

ActiveRecord Validation of a string but being able to treat part of the string as an integer

I'm using Ruby 3.0.2 and trying to validate using ActiveRecord.
I want to validate user input that is a string but want treat the parts of the input as a numbers so that I can apply some logic to it.
The input needs to look something like this
21-10
I will use regex to ensure that there is a '-' between the two numbers, but I want to be able to validate the numbers to ensure that:
One of the two sets of numbers is 21
Both numbers cannot be larger than 21 unless the difference between them is two apart from each other
My challenge is being able to treat parts of a string as numbers with validation.
An example of input that would be valid/invalid:
11-21 // valid
10-23 // invalid because 23 > 21
11-11 // invalid because neither numbers are 21
22-24 // valid because 22 & 24 within 2 of each other
Any help appreciated!
That can be done with a custom validation method like this:
# in the model
validate :range_string_has_validate_pattern
private
def range_string_has validate_pattern
parts = range_string.split('-') # assuming the attribute is named `range_string`
if parts.size != 2
errors.add(:range_string, "does not contain two parts separated by a '-'")
return
end
begin
parts[0] = Integer(parts[0])
parts[1] = Integer(parts[1])
rescue
errors.add(:range_string, "does not contain two numbers")
return
end
return if parts[0] == 21 || parts[1] == 21
return if (parts[0] - parts[1]).abs == 2
errors.add(:range_string, "does not match requirements")
end
I would go with pattern matching (yay for modern ruby) and a custom validator method
validates :your_attribute, format: { with: /\A\d+-\d+\z/ }
validate :string_format
def string_format
# do not attempt if the format is wrong
return if errors.where(:your_attribute).present?
case your_attribute.split('-').map(&:to_i)
in [..20, ..20]
error.add(:your_attribute, 'Both numbers are less than 21')
in [..20, 22..] | [22.., ..20]
error.add(:your_attribute, 'One is less the other is more')
in [22.. => x, 22.. => y] if y - x > 2 or x - y > 2
error.add(:your_attribute, 'Difference is too big')
else
return
end
end

Rails - Usernames that cannot start or end with characters

Originally asked this question about Regex for Usernames: Usernames that cannot start or end with characters
How can I achieve this with correct syntax for Ruby on Rails?
Here's my current User.rb validation:
validates_format_of :username, with: /\A[\w\._]{3,28}\z/i
This validation allows underscores and periods, but the goal is to not allow them at the start or end of a username.
I'm trying to achieve these rules with Rails regex:
Can contain lowercase and uppercase letters and numbers
Can contain underscores and periods
Cannot contain 2 underscores in a row
Cannot contain 2 periods in a row
Cannot begin or end with an underscore or period
Cannot contain letters with accents
Must be between 3 and 28 letters in length
Valid:
Spicy_Pizza
97Indigos
Infinity.Beyond
Invalid:
_yahoo
powerup.
un__real
no..way
You may use
/\A(?=.{3,28}\z)[a-zA-Z0-9]+(?:[._][a-zA-Z0-9]+)*\z/
See the Rubular demo.
Details
\A - start of string
(?=.{3,28}\z) - 3 to 28 chars other than line break chars up to the end of the string are allowed/required
[a-zA-Z0-9]+ - one or more ASCII letters / digits
(?:[._][a-zA-Z0-9]+)* - 0+ sequences of:
[._] - a . or _
[a-zA-Z0-9]+ - one or more ASCII letters / digits
\z - end of string.
Although totally possible, as Wiktor's answer shows, my recommendation would be to not define this in a single regular expression, since:
The solution is quite confusing to understand, unless you know regular expressions quite well.
Similarly, the solution is quite difficult to update with new requirements, unless you understand regular expressions quite well.
By performing this entire check in one go, if a validation fails then you'll inevitably end up with one generic error message, e.g. "Invalid Format", which does not explain why it's invalid. The exercise is then left to the user to re-read the nontrivial format rules and understand why.
Instead, I would recommend defining a custom validation class, which can perform each of these checks separately (via easy to understand methods), and add a different error message upon each check failing.
Something along the lines of:
# app/models/user.rb
class User < ApplicationRecord
validates :username, presence: true, username: true
end
# app/validators/username_validator.rb
class UsernameValidator < ActiveModel::EachValidator
def validate(record, attribute, value)
validate_length(record, attribute, value)
validate_allowed_chars(record, attribute, value)
validate_sequential_chars(record, attribute, value)
validate_first_and_last_chars(record, attribute, value)
end
private
def validate_length(record, attribute, value)
unless value.length >= 3 && value.length <= 28
record.errors[attribute] << "must be between 3 and 28 characters long"
end
end
def validate_allowed_chars(record, attribute, value)
unless value =~ /\A[._a-zA-Z0-9]*\z/
record.errors[attribute] << "must only contain periods, underscores, a-z, A-Z or 0-9"
end
end
def validate_sequential_chars(record, attribute, value)
if value =~ /[._]{2}/
record.errors[attribute] << "cannot contain two consecutive periods or underscores"
end
end
def validate_first_and_last_chars(record, attribute, value)
if value =~ /\A[._]/ || value =~ /[._]\z/
record.errors[attribute] << "cannot start/end with a period or underscore"
end
end
end
So for instance, you asked above: "What if I needed to extend this to allow lowercase letters only?" I think it's now quite obvious how the code could be updated to accommodate such behaviour, but to be clear - all you'd need to do is:
def validate_allowed_chars(record, attribute, value)
unless value =~ /\A[._a-z0-9]*\z/
record.errors[attribute] << "must only contain periods, underscores, a-z or 0-9"
end
end
You could also now, quite easily, write tests for these validation checks, and assert that the correct validation is being performed by verifying against the contents of the error message; something that is not possible when all validation failures result in the same error,
Another benefit to this approach is that the code can easily be shared (perhaps with some slight behavioural differences). You could perform the same validation on multiple attributes, or multiple models, perhaps with different allowed lengths or formats.

rails: validates_format_of use regex to limit line of numbers

I'm try to use regex to limit line of numbers, I have tried this, does not work like what I thought.
I have a Product model has only info attribute, and it has one validation
validates_format_of :info, with: /(.*\n){,3}
in rails console
> product = Product.new
> product.info = <<-INFO
"> line one
"> line two
"> line three
"> line four
"> INFO
> product.valid?
=> true
I thought this should return false, as info has more than 3 line of numbers
You can use custom validation to a check number of lines in a string. In my opinion it provides better readability than using a regexp.
validate :info_cannot_have_many_lines
def info_cannot_have_many_lines
error << 'Info can not have more than 3 lines' if info.lines.count > 3
end
If you want to stick with a Regexp:
/\A(.*(\r\n|\r|\n)){,3}\z/
You need to use the anchors: \A (beginning of the string) and \z (end of the string)
Edit: I guess it is better to use (\r\n|\r|\n) instead of just \n, so it matches for other OS too.

How to test a model validation with a regex in RSpec?

After a bit of hacking I have come up with the following tests to make sure the regex pattern on my model validator is working correctly. I am wondering if there is a better way to test these conditions instead of building a bad string. I want to account for any and all characters outside the approved regex pattern. Different columns may have different validators too.
Model
validates :provider_unique_id,
presence: true,
length: { maximum: 50 },
format: { with: /\A[A-Za-z0-9]+\z/ }
Spec
describe 'provider unique id' do
let(:bad_string) { (0..255).map(&:chr).select { |x| x != /\A[A-Za-z0-9]+\z/ }.sample(20).join }
it 'should exist' do
shop.provider_unique_id = nil
expect(shop.valid?).to be_falsey
end
it 'passes regex rules' do
shop.provider_unique_id = bad_string
expect(shop.valid?).to be_falsey
end
end
Here's what I'd write if I were being extremely thorough. Imagine test-driving the validations, adding one test at a time and adding to the validations to make it pass.
describe '#provider_unique_id' do
%w(a z).each do |letter|
it "can be letter #{letter}" do
expect_to_be_valid letter
end
end
# after making this pass, I'd change the regex to use the i flag so I wouldn't need to test for Z
it "can be uppercase" do
expect_to_be_valid 'A'
end
[0, 9].each do |digit|
it "can be digit #{digit}" do
expect_to_be_valid digit
end
end
it "can be more than one character" do
expect_to_be_valid '00'
end
it "isn't nil" do
expect_to_be_invalid nil
end
it "isn't blank" do
expect_to_be_invalid ""
end
it "can be 50 characters long" do
expect_to_be_valid('0' * 50)
end
it "can't be longer than 50 characters" do
expect_to_be_invalid('0' * 51)
end
# I chose _ as a non-alphanumeric since it's the only non-alphanumeric word character.
# That is, it's as close to a valid character as it can be without be valid.
it "can't contain a non-alphanumeric character" do
expect_to_be_invalid '_'
end
# this example forces you to add \A
it "can't begin with a non-alphanumeric character" do
expect_to_be_invalid '_0'
end
# this example forces you to add \z
it "can't end with a non-alphanumeric character" do
expect_to_be_invalid '0_'
end
def expect_to_be_valid(provider_unique_id)
shop.provider_unique_id = provider_unique_id
expect(shop).to be_valid
end
def expect_to_be_invalid(provider_unique_id)
shop.provider_unique_id = provider_unique_id
expect(shop).to_not be_valid
end
end
I wouldn't randomly generate a bad string, because it wouldn't force you to write any additional code. I think the tests with _ are sufficient. Note that there are many more characters than ASCII 0-255, and it would be impractical to test them all.
You could imagine boundary-checking the ranges in the regex (a-z, A-Z, 0-9) by testing characters that come immediately before and after each range, but it's unlikely that someone would write code that would incorrectly include those characters, so I wouldn't go that far.

What is wrong with this Regex in Ruby on Rails?

I've written a regex to help validate a String for game character names. It's somehow passing seemingly invalid strings and not passing seemingly valid strings.
Requirements:
Starts with a capital letter
Has any number of alphanumeric characters after that (this includes spaces)
This is the rails code that does the validation in the Character Model:
validates :name, format: { with: %r{[A-Z][a-zA-Z0-9\s]*} }
Here's the unit test I'm using
test "character name should be properly formatted and does not contain any special characters" do
character = get_valid_character
assert character.valid?
character.name = "aBcd"
assert character.invalid?, "#{character.name} should be invalid"
character.name = "Number 1"
assert character.valid?, "#{character.name} should be valid"
character.name = "McDonalds"
assert character.valid?, "#{character.name} should be valid"
character.name = "Abcd."
assert character.invalid?, "#{character.name} should be invalid"
character.name = "Abcd%"
assert character.invalid?, "#{character.name} should be invalid"
end
The problems:
The regex passes "aBcd", "Abcd.", and "Abcd%" when it shouldn't. Now, I know this works because I tested this out in Python and it works just as you would expect.
What gives?
Thank you for your help!
Regular expressions look for matches anywhere in the given string unless told otherwise.
So the test string 'aBcd' is invalid, but it contains a valid substring: 'Bcd'. Same with 'Abcd%', where the valid substring is 'Abcd'.
If you want to match the entire string, use this as your regex:
# \A matches string beginning, \z matches string end
%r{\A[A-Z][a-zA-Z0-9\s]*\z}
PS: Some people will say to match the beginning of a string with ^ and the end with $. In Ruby, those symbols match the beginning and end of a line, not a string. So "ABCD\n%" would still match if you used ^ and $, but won't match if you use \A and \z. See the Rails security guide for more on this.
If you only want to match the capital letter at the beginning of the string, you need to put in the "start of line" marker ^ so it would look like:
validates :name, format: { with: %r{^[A-Z][a-zA-Z0-9\s]*} }
Check out Rubular to play around with your regex

Resources