Learning Ruby

Reference, Mnemonic & Ramblings

DRY Up Example With Modules and Gem Creation

Almost all the code enabling voting for the Posts and Comments in the PostIt app is common. Its original implementation screamed Don't Repeat Yourself, or, in short DRY.

The same thing is true for the slug-generation and use functionality as well. The slug code is repeated for not two, but three models, namely Post, Category and User. DRYing up the slug code however represents one small challenge as compared to the voting code: slugs are generated from different attributes in each model, :title for Post, :username for User, and :name for Category.

This adds a degree of complexity to how the slug code can be DRYed, and that's why, this article uses slug as the example to explain the process. Voting DRYing up is not just similar, but much simpler.

  1. DRYing with Modules
  2. DRYing with Gems

1. 'DRY'ing up code with Modules

For the application to load modules from a common location (./lib), the following line needs to be added to ./config/application.rb under the Application class definition: config.autoload_paths += %W(#{config.root}/lib)

The above creates an array with application_root/lib path in a string format and adds it to the pre-existing array stored in PostitTemplate::Application.config.autoload_paths. This will ensure that all files stored in the ./lib folder will be automatically loaded at application startup.

Slugable.rb stored in this path will be populated by cutting the slug related code from any of the models that implements it (Post, Category, or User). In addition to that, the module file has a call to extend ActiveSupport::Concern. This enables some extra features like capability to add Class methods to the including class and the included hook to let some code to be executed only at the inclusion time.

Shown below is the above implemented for the slugging code, which is also repeated in the User, Post and Category models.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
module Slugable
  extend ActiveSupport::Concern

  included do
    class_attribute :slug_column
    before_save :generate_slug!
  end

  module ClassMethods
    def set_slug_column_to(column)
      self.slug_column = column
    end
  end

  def to_param
    self.slug
  end

  def generate_slug!
    the_slug = to_slug(self.send(self.class.slug_column))
    model = self.class.to_s

    count = 1
    record = self.class.find_by slug: the_slug
    while record and record != self
      the_slug = make_unique(the_slug, count)
      record = self.class.find_by slug: the_slug
      count += 1
    end

    self.slug = the_slug
  end

  def to_slug(str)                # str=" @#$@ My First @#2@%#@ Post!!  "
    str = str.strip               #  -->"@#$@ My First @#2@%#@ Post!!"
    str.gsub!(/[^A-Za-z0-9]/,'-') #  -->"-----My-First---2-----Post--"
    str.gsub!(/-+/,'-')           #  -->"-My-First-2-Post-"
    str.gsub!(/^-+/,'')           #  -->"My-First-2-Post-"
    str.gsub!(/-+$/,'')           #  -->"My-First-2-Post"
    str.downcase                  #  -->"my-first-2-post"
  end

  def make_unique(the_slug, count)
    arr = the_slug.split('-')
    if arr.last.to_i == 0
      the_slug = the_slug + '-' + count.to_s
    else
      the_slug = arr[0...-1].join('-') + '-' + count.to_s
    end
    the_slug
  end

end

What makes the Slugable module more complex is the need to handle varying attribute names for the model. This has to be specified by the model including this module. This is implemented using the class_attribute :slug_column and the class method set_slug_column_to(column). So, apart from the include Slugable call, the models have to call the set_slug_column_to and pass the appropriate :attribute_name to it.

Model should include this call
User set_slug_column_to :username
Post set_slug_column_to :title
Category set_slug_column_to :name

Back

2. 'DRY'ing up code with Gems

Once we have the Slugable module, it is fairly easy to create a gem from it. But why create a gem when we have DRY code already? The main reason is usability across multiple Rails projects. It will also enable other developers to use (a.k.a. 'consume') the gem if it is pushed to RubyGems.org.

Note: One prerequisite for creating a gem is having a gem called 'gemcutter' installed. If you don't already have it (check using gem list gemcutter), install it by issuing gem install gemcutter in your shell.

Since the gem is going to be a separate project, we need to create a separate folder to hold its code. The following steps highlight the process to create the slug gem.

A 'Slugable.gemspec' file needs to be in the root path of the gem folder and it should have the following code:

./slugable.gemspec
1
2
3
4
5
6
7
8
9
10
11
Gem::Specification.new do |s|
  s.name        = "sluggit-ppj"
  s.version     = "0.0.0"
  s.date        = "2014-10-20"
  s.summary     = "A slug generator gem"
  s.description = "A cool gem for slugifying models."
  s.author      = "Prasanna Joshi"
  s.email       = "author@myemail.com"
  s.files       = ["lib/slugable_ppj.rb"]
  s.homepage    = "http://www.github.com"
end

Some key settings:
  • s.name: specifies the name of the gem
  • s.files: specifies where the code for the gem resides
  • s.version: versioning mechanism for the gem

The './lib/slugable_ppj.rb' could be an exact replica of the module file created earlier.

This is all we need to create our gem. Issue
gem build slugable.gemspec

from the gem root folder path at the shell prompt. If successful in creating the gem, you should see the following output from the command:
./slugable.gemspec
1
2
3
4
5
  ~/sluggable-gem> gem build slugable.gemspec
    Successfully built RubyGem
    Name: sluggit-ppj
    Version: 0.0.0
    File: sluggit-ppj-0.0.0.gem
You can see that the 'sluggit-ppj-0.0.0.gem' file has been created in you gem root path.

Once created, we need to follow the steps below to actually use it now in our project:
  1. add the following lines in the Gemfile of your project to make it available: gem 'sluggit-ppj', path: '% local_root_path_of_the_gem %'
  2. run bundle install after modifying the Gemfile
  3. add require slugable_ppj before the features of the gem are called in your code (safest place to add it in is the file 'projec_root_path/config/application.rb' because this file gets loaded immediately after the application starts)

Once you have the gem working like you want to, you can push it to RubyGems.org using the 'gemcutter' command:
gem push sluggit-ppj-0.0.0.gem
The '.gem' file version should change based on your changes to the s.version setting in the '.gemspec' file.

Note: The gem can be disabled (at RubyGems.org) by issuing the gem yank sluggit-ppj -v '0.0.0'
command from the gem root path in the shell. As you can see, you can yank a particular version of the gem from RubyGems.org.

Back

comments powered by Disqus