Dynamic slugs on Rails 3

UPDATE: I gemified this code, and you can now find the ‘inferred_slug’ gem (rubygems, github).

Abstract

Slugs have typically been stored in databases. At least that is what I read about slugs. It turns out that I am starting an amazing Rails project, which is pretty complex, and I want to keep it simple, stupid. So, I am setting up some facts:

  • slugs are used for mainly two reasons:
    • secure accesses, repelling information fetching scripts that just increment an id.
    • prettify URI’s.
  • slugs are actually duplicated information:
    • duplicated information is never good. Even if you have amazing callbacks, like those that ActiveRecord provides.
      • you need to take care of keeping them updated. Forget a callback, and your slugs will be outdated. Inconsistent.
      • update the ‘no-brainer-logic’ that creates your slugs, and you need to ‘migrate’ your whole database, full of information. If you want to be consistent, of course. And I really expect you to be.

From my point of view, and when thought about this issue yesterday, it was a no-brainer that slugs need to be logic, not data. So, here is my take on the problem.

Please, note that for correctly running this example you need the  stringex gem, that adds some neat features to Ruby strings (among other nice candy).

Prettify URI’s

First of all, and the easiest part. Let’s prettify all URI’s. Let’s make it global for keeping the point (obviously you could decide on which models you want this).

Ok, this is a no brainer. If there is a name attribute on your model, it will be used to prettify your URI’s. It’s important to note that the returned string contains the  id at the beginning, followed by a dash, and whatever you want after that, in this case, the name. This will be explained later.

This way you can also reimplement  slug method on your model, if your model instead of  name contains a  title column, that you want to show on the slug.

Now, let’s write an initializer that loads this directly on all your  ActiveRecord::Base instances:

That’s it ! Now, you might be wondering why it works at all. You did not change any code on your controllers that call, for example, Model.find, and they are still able to find the record, when the passed parameter is 14-john-murray. Well, test on an irb session what returns '14-john-murray'.to_i before continuing.

Yes, it returns  14. This is the reason why our slugs are of the form id-whatever-you-want-here. "#{id}-whatever-you-want-here".to_i , where id  is an integer, will return that integer. We can still use a regular ActiveRecord find method !

We can still follow the links on our website; but ! Not everything is so cool. /users/14-john-murray, for instance. Try to point directly to /users/14-john-doe. It loads. It just fetches the id (14) and there are no checks about the rest of the slug. So it’s time to fix that. We made URI’s pretty, but we aren’t checking that 14-john-murray is our record with ID 14 , whose name should be  John Murray.

 Protect yourself !

The first brain inner war that comes here is where to place this logic, and how. Let’s state some requirements that were basic:

  • This point should not exist, really. This should be crystal clear, but just in case. Obey MVC. No hacks.
  • We need to keep  find method virgin. We’d like to still use it.
  • We don’t like to copy and paste code. We really do love kitties.

The controller looks like a nice place at a first sight. However you depend on your developer to remember that he/she needs to validate the slug somehow. Doesn’t look so nice now. The model cannot know anything about the controller. However, the model is given the whole id by the controller. There we go.

Let’s modify our simple slug initializer then:

Okay, great. We added two methods that we can use to search.  find_by_slug, and  find_by_slug!, following the usual Rails convention.

The idea is really easy. Let’s focus on  find_by_slug, since find_by_slug! contains the exact same logic from the point of view that we are interested in. First, let’s do a regular find call, that will return (or not) the record. Let’s suppose we got a record. If that record responds to the  :slug message, we just check that its slug is the same as the one that the model was asked for.

Wrapping up

So, we can do now things like:

  • Client.find_by_slug '1' => nil
  • Client.find_by_slug '1-john-doe' => Client(...)
  • Client.find_by_slug '1-alice-smith' => nil
  • Client.first.payments.find_by_slug '1' => nil
  • Client.first.payments.find_by_slug '1-niceproduct' => Payment(...)
  • Client.first.payments.find_by_slug '1-fakeproduct' => nil

We got lots of things for free (yeah, as in free beer):

  • Avoid data replication. It sounds so wrong, and feels even worse…
  • Avoid database headaches.
  • Save space in our database. Sounds stupid, right ?
  • Our logic controls the whole slug system. Change your slug logic, and magically all of them change.
  • Little impact in our project source code.

I hope this post was useful to you. Please take into account that is just an example. If someone asks for it, I can create an empty rails project showing how it works.

Happy coding !

realtime-validations rails gem

There is a new gem in the Rails world: realtime-validations.

It provides automatic realtime validations on your forms based on the validations set on the model.

The idea is to ask on every ‘blur’ event on every field of the form to the server whether this field is correct or not, creating a new model or finding the row if it contains an id.

This way you don’t need to replicate logic in your client that is already defined and implemented in your model at the server.

You can read more about realtime-validations here. You can also find the project on github.