Kobius
Kobius

Reputation: 714

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:

Valid:

Spicy_Pizza
97Indigos
Infinity.Beyond

Invalid:

_yahoo
powerup.
un__real
no..way

Upvotes: 3

Views: 1090

Answers (2)

Tom Lord
Tom Lord

Reputation: 28305

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.

Upvotes: 3

Wiktor Stribiżew
Wiktor Stribiżew

Reputation: 626794

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.

Upvotes: 4

Related Questions