RailsCases33 Making a Plugin 创造插件

In the last episode we showed how to edit a datetime column with a text field, rather than with a series of dropdown menus. The date and time for a task were entered in a text field then parsed as a date and time before being stored in the database.

Editing a time in a textfield.

We did this by creating a virtual attribute in our Task model called due_at_string. This attribute has a getter method that displays the date in a format that is suitable for storing in the database and a setter method that parses the string to turn it back into a date.

ruby
def due_at_string
  due_at.to_s(:db)
end
  
def due_at_string=(due_at_str)
  self.due_at = Time.parse(due_at_str)
rescue ArgumentError
  @due_at_invalid = true
end

This approach works if there’s only one attribute we want to modify this way, but if there are several then we’ll quickly have a lot of duplication in the model as we create getter and setter methods for each attribute.

Instead of this we’re going to create a class method in our model, which we’ll call stringify_time. This method will dynamically generate getter and setter methods for any attribute we pass to it. As this is something we may well want to use in other applications we’ll develop this as a plugin.

Creating a Plugin

To start we’ll generate an empty plugin called stringify_time. To do this we run

ruby
script/generate plugin stringify_time

from our application’s directory. This will generate a number of files in a new stringify_time directory under /vendor/plugins.

The files that are created when generating a plugin.

We’ll look at the init.rb file first. This file is loaded when the plugin loads, so here we’ll require the file in the lib directory where we’ll develop the plugin’s functionality.

ruby
require 'stringify_time'

The stringify_time.rb file is where we’ll write the code that generates getter and setter methods similar to the due_at_string methods we used earlier. We’ll start by defining a module with astringify_time method.

ruby
module StringifyTime
  def stringify_time(*names)
    
  end
end

The stringify_time method takes a list of names as a parameter. We’ve used the “splat” mark to indicate that the method can take a number of arguments, each of which will be put into an array callednames.

The method will need to loop through the names array and create two methods for each name, a getter and a setter. Ruby makes this kind of metaprogramming easy; all we need to do to dynamically create a method in a class is to call define_method. The code to create the getter methods is:

ruby
names.each do |name|

  define_method "#{name}_string" do
    read_attribute(name).to_s(:db)
  end

end

This code loops through each name in the array and uses define_method to dynamically create a method whose name is the passed name with _string appended to it, so if we pass due_at we’ll get a newdue_at_string method. define_method takes a block, the code within the block becoming the body of the method. The due_at_string method we created earlier took the value of the model’s due_atattribute and returned it as a formatted string. We do the same here, but as our attribute is dynamic we have to use read_attribute to get the value.

With the getter defined we can now write the setter method.

ruby
define_method "#{name}_string=" do |time_str|
  begin
    write_attribute(name, Time.parse(time_str))
  rescue ArgumentError
    instance_variable_set("@#{name}_invalid", true)
  end
end

We use define_method again here. As we’re creating a setter method the method name ends in an equals sign and, as it needs to take a parameter, we define that as a block variable.

Our due_at_string= method parses the string passed to it and converts it to a Time value, then setsdue_at to that value. If the value cannot be parsed, the exception is caught and a instance variable called@due_at_invalid is set to true. In our dynamic setter we use write_attribute to set the dynamic attribute instead and if that fails call instance_variable_set to set the corresponding instance variable.

Putting the pieces above together, our StringifyTime module looks like this:

ruby
module StringifyTime
  def stringify_time(*names)
    names.each do |name|
      
      define_method "#{name}_string" do
        read_attribute(name).to_s(:db)
      end
      
      define_method "#{name}_string=" do |time_str|
        begin
          write_attribute(name, Time.parse(time_str))
        rescue ArgumentError
          instance_variable_set("@#{name}_invalid", true)
        end
      end
      
    end
  end
end

There is one more change we’ll have to make before our plugin will work. We’ll be calling stringify_timein model classes that inherit from ActiveRecord::Base, so we’ll have to extend ActiveRecord with our new module. Back in init.rb we can do that by adding

ruby
class ActiveRecord::Base
  extend StringifyTime
end

Note that we’re using extend rather than include as that makes the method in our module a class method rather than an instance method.

Now that we’ve defined our plugin we can use it in our Task model, replacing the getter and setter methods with a call to stringify_time.

ruby
class Task < ActiveRecord::Base
  belongs_to :project
  stringify_time :due_at
  
  def validate
    errors.add(:due_at, "is invalid") if @due_at_invalid
  end
  
end

Before we refresh the edit task page to see if our plugin works we’ll have to restart the web server. With that done we can refresh the page and see if it all works.

The edit page now works with our plugin.

It seems to be OK. The task’s time has been displayed correctly in the time field. Let’s now try entering an invalid value and seeing if that is handled correctly.

The validation is working too.

That works too, but the validation is only working because we’re using the correct instance variable name from the plugin to detect whether the model is valid or not.

ruby
def validate
  errors.add(:due_at, "is invalid") if @due_at_invalid
end

It seems a little ugly to be relying on an instance variable generated by a plugin so instead we’ll use a method.

ruby
def validate
  errors.add(:due_at, "is invalid") if due_at_invalid?
end

We’ll have to generate one more dynamic method in stringify_time.rb to create this method. Immediately below the code that creates the dynamic getter and setter methods we can add this

ruby
define_method "#{name}_is_invalid?" do
  return instance_variable_get("@#{name}_invalid")
end

to create the _invalid? method.

And that’s it. We have succesfully created our first Rails plugin. While it’s a fairly basic one, the principle is the same no matter how complex a plugin you need to create.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值