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?
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
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 (:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With