Four Key Reasons to Learn Markdown
Back-End Leveling UpWriting documentation is fun—really, really fun. I know some engineers may disagree with me, but as a technical writer, creating quality documentation that will...
Metaprogramming is your secret identical twin that likes doing all of the things you don’t. Need to take out the trash? Just tell your twin. Need to program in Java? Send your twin an email.
Metaprogramming, defined as writing code that writes code by Why The Lucky Stiff, makes scaffolding, associations, validations, and the many magical parts of Rails possible. Implementing metaprogramming techniques can drastically eliminate duplicate code, making your applications far easier to maintain and build. It also lets your code do the work – not you.
CampusSync.com, a client project of ours, is a collaboration site for college students. It has several administration areas that are almost identical, but not a good fit for Rail’s standard scaffolding. The solution to eliminating duplicate code: roll our own metaprogramming solution.
h3. The problem
Every school in CampusSync has on-campus events, organizations, shared files, comments, and more. The CampusSync staff needs to review this content. We need to be able to search and view items, sort, edit, etc. So how can we go about this without creating separate views and controller actions for each type of reviewable class?
class CommentsController < ActionController school_item_scaffold :comment end
Isn’t the above code pretty? One line of code adds all the functionality we need.
1. Write Your Tests
Let’s write a simple test to check our FilesController#list
action. The FilesController lets administrator review uploaded files.
def test_list get :list, :school => @school.id assert_response :success assert assigns(:file_records).any? end
2. Create a module with the custom scaffolding behavior.
I created a file called “acts_as_school_item.rb” and placed it in the “/lib” folder of the CampusSync application.
3. Load the file when starting the Rails application by placing the lines below in “/config/environment.rb”.
require 'acts_as_school_item' ActionController::Base.send(:include,ActionController::Acts::SchoolItem )
3. Setup the basic module structure.
Below is the typical structure of a metaprogramming module.
module ActionController module Acts module SchoolItem def self.included(base) base.extend(ClassMethods) end module ClassMethods # The method that attaches this behavior to the controller. def school_item_scaffold(model,options = {}) module_eval <<-CODE # we'll define our scaffolded actions in here CODE end # school_item_scaffold end # Class Methods end # SchoolItem end # Acts end # ActionController
4. Add Our Scaffolding Functionality
We’ll create the shared views in app/views/admin/school_items/
. For now, let’s just add a non-metaized #list
action:
def list @school = School.find(params[:school]) @file_records = @school.file_records paginate_file_records render :action => 'list' end alias index list
And here it is after meta-izing the #list
action:
module ActionController module Acts module SchoolItem def self.included(base) base.extend(ClassMethods) end module ClassMethods # Adds custom scaffolding for viewing a school's associated +model+ objects. # # Options: # - klass: The class of the model object # - friendly_name: A human name to use when referring to the +model+ objects in the views. # - pluralized_name: Defines how we retrieve the records from the school and the instance variable # that contains the items in the +list+ action. # - per_page: Number of records to display per-page in the list view. def school_item_scaffold(model,options = {}) klass = model.constantize write_inheritable_attribute(:school_item_options, { :klass => klass, :friendly_name => klass.to_s, :pluralized_name => klass.to_s.tableize, :per_page => 50 }.merge(options)) class_inheritable_reader :school_item_options module_eval <<-CODE def list @school = School.find(params[:school]) @#{school_item_options[:pluralized_name]} = @school.#{school_item_options[:pluralized_name]} paginate_#{school_item_options[:pluralized_name]} render :template => 'admin/school_items/list' end alias index list private def paginate_#{school_item_options[:pluralized_name]} @pages,@#{school_item_options[:pluralized_name]} = paginate_collection(@#{school_item_options[:pluralized_name]}, :per_page => #{school_item_options[:per_page]}, :page => params[:page]) end CODE end # school_item_scaffold end # ClassMethods end # SchoolItem end # Acts end # ActionController
When #school_item_scaffold
is called inside of a controller, we place all of the options in the school_item_options
inheritance attribute. We can access this attribute from inside the ClassMethods
module and from our views (<%= controller.school_item_options %>
).
After we set this inheritance attribute, we then build the actions. In this case, we are just creating a #list
action and a private method to paginate the results. And that’s it!
We can now add our customized scaffolding to any controller:
class Admin::EventsController < AbstractAdminController school_item_scaffold :event end
When CampusSync adds more reviewable content, we won’t have to create any duplicate code, and we have less code to maintain.
Writing documentation is fun—really, really fun. I know some engineers may disagree with me, but as a technical writer, creating quality documentation that will...
Humanity has come a long way in its technological journey. We have reached the cusp of an age in which the concepts we have...
Go 1.18 has finally landed, and with it comes its own flavor of generics. In a previous post, we went over the accepted proposal and dove...