Reputation: 12923
Suppose you have this code from the documentation of ActiveSupport::Concern, but you want to the included block of Foo to have something different depending on the module or class that including Foo.
In the specific problem I'm trying to solve, I have a set of validations for addresses, but the address fields will be named home_zip_code or work_zip_code, and I want the inclusion of the validation concern to know the prefix of the zip_code field.
require 'active_support/concern'
module Foo
extend ActiveSupport::Concern
included do
# have some_value be accessible
def self.method_injected_by_foo
...
end
end
end
module Bar
extend ActiveSupport::Concern
# set some_value that will used when Foo is included
include Foo
included do
self.method_injected_by_foo
end
end
class Host
include Bar # It works, now Bar takes care of its dependencies
end
I've placed this discussion here: http://forum.railsonmaui.com/t/how-make-a-concern-parameterized/173
The following 2 options work. I'm wondering which is preferable.
This is the concern that needs to be "parameterized":
module Addressable
extend ActiveSupport::Concern
included do
zip_field = "#{address_prefix}_zip_code".to_sym
zip_code_regexp = /^\d{5}(?:[-\s]\d{4})?$/
validates zip_field, format: zip_code_regexp, allow_blank: true
end
I found 2 ways to set the address_prefix before including the Addressable concern.
The class method needs to be defined before including the concern
cattr_accessor :address_prefix
self.address_prefix = "home"
include Addressable
or like this
def self.address_prefix
"home"
end
include Addressable
The trick here is to override self.append_features
and to add the method.
def self.append_features(base)
base.class_eval do
def self.address_prefix
"home"
end
end
super
end
or
def self.append_features(base)
base.cattr_accessor :address_prefix
base.address_prefix = "home"
super
end
cattr_accessor
way or defining the class method?self.append_features
the correct hook?class_eval
the right call to create the method, rather than class exec
? Or really doesn't matter if the code doesn't need access to instance variables. Module docs here.Upvotes: 9
Views: 3734
Reputation: 4657
The cattr_accessor
or class method way is a good one I've used before. cattr_accessor
is defining a method, so either is fine. One difference involves inheritance, I think cattr_accessor
variables are not inherited. Might want to look into that if that might be a concern.
But I'd actually at this point approach this a whole different way. Simplest for me would be to define a class method that can take a parameter, such as acts_as_addressable(address_prefix
. You can define this method in a module, Addressible
, where you can do this:
extend Addressible
acts_as_addressbile('home')
acts_as_addressible('work')
You can take a shortcut that a lot of gems take and actually force your module into ActiveRecord::Base
, preventing you from needing to use the extend
in each class, but I personally hate that style.
Depending on what else your included block is doing, maybe validates_zip_code
is a better name for the class method. For style, I'd recommend actually taking the full name of the field. Slightly more typing, much more readable:
module ZipCodeValidatable
ZIP_CODE_REGEX = /^\d{5}(?:[-\s]\d{4})?$/
def validates_zip_code(zip_field)
validates zip_field, format: ZIP_CODE_REGEX, allow_blank: true
end
end
class MyRecord << ActiveRecord::Base
extend ZipCodeValidatable
validates_zip_code("work_zip_code")
validates_zip_code("home_zip_code")
end
If validating the zip code is all you're doing, then you could use a custom validator. Looks to me like you can use the second section on ActiveModel::EachValidator
. I've never used these, but my read it it would look like:
class ZipCodeValidator < ActiveModel::EachValidator
ZIP_CODE_REGEX = /^\d{5}(?:[-\s]\d{4})?$/
def validate(record, attribute, value)
unless ZIP_CODE_REGEX =~ value
errors[attribute] << "Invalid zip code in field #{attribute}"
end
end
end
Upvotes: 0