Ok, so I think I've got this.
I was looking into doing something with middleware, but then decided that's probably not the place for this type of functionality (since we need to access ActiveRecord).
So I ended up building a service object, known as a PathCheck
. The service looks like this:
class PathCheck
def initialize(model, request)
@model = model
@request = request
end
# Says if we are already where we need to be
# /:id/*slug
def at_proper_path?
@request.fullpath == proper_path
end
# Returns what the proper path is
def proper_path
Rails.application.routes.url_helpers.send(path_name, @model)
end
private
def path_name
return "edit_#{model_lowercase_name}_path" if @request.filtered_parameters["action"] == "edit"
"#{model_lowercase_name}_path"
end
def model_lowercase_name
@model.class.name.underscore
end
end
This is easy enough to implement into my controller:
def show
@post = Post.find params[:post_id] || params[:id]
check_path
end
private
def check_path
path_check = PathCheck.new @post, request
redirect_to path_check.proper_path if !path_check.at_proper_path?
end
My ||
in my find
method is because in order to maintain resourceful routes, I did something like...
resources :posts do
get '*id' => 'posts#show'
end
Which will make a routes like: /posts/:post_id/*id
on top of /posts/:id
This way, the numeric id is primarily used to look up the record, if available. This allows us to loosely match /posts/12345/not-the-right-slug
to be redirected to /posts/12345/the-right-slug
The service is written in a universal fashion, so I can use it in any resourceful controller. I have't found a way to break it yet, but I'm open to correction.
Resources
Railscast #398: Service Objects by Ryan Bates
This Helpful Tweet by Jared Fine