7

I want to ensure users can't create usernames that clash with my existing routes. I would also like the ability to deny future routes I may define. I am thinking of accomplishing this like so:

In the model:

class User < ActiveRecord::Base
  @@invalid_usernames = %w()

  cattr_accessor :invalid_usernames

  validates :username, :exclusion { :in => @@invalid_usernames }
end

In some initializer:

User.invalid_usernames += Rails.application.routes.routes.map(&:path).join("\n").scan(/\s\/(\w+)/).flatten.compact.uniq

Is this "Rails way"? Is there a better way?

poochenza
  • 485
  • 4
  • 11
  • @iWasRobbed Sorry for the confusion, I removed my other validations. This is purely to avoid clashes with *non-username routes*. For clashes with other usernames I already have a uniqueness validation plus db-level constraints. Edit: incidentally, if you *did* need to do it dynamically, you can always use `:in => lambda { ... }` (which is actually what I should be using above regardless, I believe) – poochenza Jan 03 '12 at 12:21

1 Answers1

6

Here's my own answer, tested and working with Rails 3.1.3 and Ruby 1.9.3

app/models/user.rb

class User < ActiveRecord::Base
  class_attribute :invalid_usernames
  self.invalid_usernames = Set.new %w()

  validates :username, presence:   true,
                       uniqueness: { case_sensitive: false },
                       exclusion:  { in: lambda { self.invalid_usernames }}
end

config/application.rb

[:after_initialize, :to_prepare].each do |hook|
  config.send(hook) do
    User.invalid_usernames += Rails.application.routes.routes.map(&:path).join("\n").scan(/\s\/(\w+)/).flatten.compact.uniq
  end
end

Notes

I initially tried setting User.invalid_usernames during after_initialize but found it needs to be set during to_prepare (i.e. before each request in development mode, and before the first request in production mode) since the models are reloaded in development before each request and the original setting is lost.

I am however also setting User.invalid_usernames during after_initialize because the routes don't seem to be available during to_prepare when running in the test environment. Another workaround I tried for this, which does work, is to force load the routes during to_prepare:

config.to_prepare do
  Rails.application.reload_routes!
  User.invalid_usernames += Rails.application.routes.routes.map(&:path).join("\n").scan(/\s\/(\w+)/).flatten.compact.uniq
end

I like this because it's DRY and easy to read. But I'm wary of reloading the routes on every request, even if it is only in development mode. I'd rather use something a little harder to read if it means I fully understand the impact. Open to criticisms!

I also ditched cattr_accessor for class_attribute when I found out the former applies to the entire class hierarchy (i.e. changing its value on a subclass would affect the superclass)

I also chose to use a Set for User.invalid_usernames instead of an array as there's no need to store and compare against dupes and it was a transparent change.

I also changed to Ruby 1.9 hash syntax (:

poochenza
  • 485
  • 4
  • 11
  • You could also filter the routes based on `.verb` if you want to be looser (check for `''` and `'GET'` though). You might want to do something with wildcard routes (`/*page.html` and such). And an [`after_initialize`](http://stackoverflow.com/a/8707687/479863) hook to make sure you haven't added a route that is taken as a username would be a good idea too. – mu is too short Jan 04 '12 at 06:40
  • And you're allowed to accept your own answers (there's even a [badge for it](http://stackoverflow.com/badges/14/self-learner)) and this is certainly a good candidate for accepting your own answer. – mu is too short Jan 04 '12 at 06:42