There is a general way of adding class methods from Module via its included hook, and following extending base class with ClassMethods submodule. This way is described in book "Metaprogramming Ruby 2: Program Like the Ruby Pros". Here is an example from there:
module CheckedAttributes
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def attr_checked(attribute, &validation)
define_method "#{attribute}=" do |value|
raise 'Invalid attribute' unless validation.call(value)
instance_variable_set("##{attribute}", value)
end
define_method attribute do
instance_variable_get "##{attribute}"
end
end
end
end
class Person
include CheckedAttributes
attr_checked :age do |v|
v >= 18
end
end
But what is the reason of including the almost empty module first, and then extending its includer with one more module? Why not extend the class right the way with target module itself?
module CheckedAttributes
def attr_checked(attribute, &validation)
define_method "#{attribute}=" do |value|
raise 'Invalid attribute' unless validation.call(value)
instance_variable_set("##{attribute}", value)
end
define_method attribute do
instance_variable_get "##{attribute}"
end
end
end
class Person
extend CheckedAttributes
attr_checked :age do |v|
v >= 18
end
end
Is code above totally equal to initial example from this book? Or there are any pitfalls?
I have no idea where you took this code from, but this pattern involving ClassMethods is normally used in the cases when you want to alter both class and eigenclass to avoid the necessity to call both include Foo and extend Bar.
module Named
def self.included(base)
base.extend ClassMethods
end
def describe
"Person is: #{name}"
end
module ClassMethods
def name!
define_method "name=" do |value|
raise 'Invalid attribute' unless validation.call(value)
instance_variable_set("#name", value)
end
define_method "name" do
instance_variable_get "#name"
end
end
end
end
class Person
include Named
name!
end
p = Person.new
p.name = "Trump"
p.describe #⇒ "Person is: Trump"
In your example, it makes zero sense.
Related
I'm trying to achieve something that is for sure possible but I'm not able to put it into to find it from the docuemntation.
In a nutshell, I would like to define methods dynamically:
Initial point:
class Foo < Bar
def baz
RecordLoader.for(Baz).load(object.baz_id)
end
def qux
RecordLoader.for(Quz).load(object.qux_id)
end
end
class Bar
end
I would like to be able to change it to
class Foo < Bar
record_loader_for :baz
record_loader_for :qux
end
class Bar
def self.record_loader_for(attribute)
define_method attribute.to_s do
# What is missing here?
end
end
end
I'm trying to figure out how I can use the value of attribute to write something like
RecordLoader.for(attribute.to_s.classify.constantize). # <- attribute is local to the class
.load(object.send("#{attribute.to_s}_id")) # <- object is local to the instance
You can go with class_eval and generate your method into string:
def self.record_loader_for(attribute)
class_eval <<~RUBY, __FILE__ , __LINE__ + 1
def #{attribute}
RecordLoader.for(#{attribute.to_s.classify}).load(#{attribute}_id)
end
RUBY
end
but in fact, define_method should work too, ruby will save closure from the method call:
require 'active_support'
require 'active_support/core_ext'
require 'ostruct'
class RecordLoader
def self.for(cls)
new(cls)
end
def initialize(cls)
#cls = cls
end
def load(id)
puts "loading #{#cls} id #{id}"
end
end
class Baz; end
class Bar
def object
OpenStruct.new(baz_id: 123, qux_id:321)
end
def self.record_loader_for(attribute)
define_method attribute.to_s do
RecordLoader.for(attribute.to_s.classify.constantize).
load(object.send("#{attribute.to_s}_id"))
end
end
end
class Foo < Bar
record_loader_for :baz
record_loader_for :qux
end
Foo.new.baz
class_eval is slower to define method, but resulting method executes faster and does not keep references to original closure context, define_method is the opposite - defines faster, but method runs slower.
how do we find out in which class we are currently including a module? (coming from rails-background where we do the has_* style modules)
class Foo
has_likes
end
module HasLikes
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def has_likes(options = {})
end
end
module ContentMethods
def self.included(base)
base.extend ClassMethods
end
# ?????
# how do we get Foo here?
end
end
First of all, as #Jordan said in comments, it smells as a design flaw.
Secondary, inside ContentMethods declaration, it’s definitely impossible, since it is always included after been declared.
In general, one might do it afterwards with ObjectSpace#each_object:
module HasLikes
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def has_likes(options = {})
end
end
module ContentMethods
def self.included(base)
base.extend ClassMethods
end
end
end
class Foo
include ::HasLikes::ContentMethods
has_likes
end
ObjectSpace.each_object(Class) do |x|
p(x) if x <= ::HasLikes::ContentMethods
end
#⇒ Foo
But please do not do that at home or school.
I'm fairly new to rails so bear with me.
I want to strip whitespace from a selective group of input forms.
But I would like a DRY solution.
So I was thinking there might be a solution such as a helper method, or a custom callback. Or a combination such as before_validation strip_whitespace(:attribute, :attribute2, etc)
Any help is awesome! Thanks!
EDIT
I have this in my model file ...
include ApplicationHelper
strip_whitespace_from_attributes :employer_name
... and I have this in my ApplicationHelper ...
def strip_whitespace_from_attributes(*args)
args.each do |attribute|
attribute.gsub('\s*', '')
end
end
but now I'm getting the error message:
undefined method `strip_whitespace_from_attributes' for "the test":String
EDIT II -- SUCCESS
I added this StripWhitespace module file to the lib directory
module StripWhitespace
extend ActiveSupport::Concern
module ClassMethods
def strip_whitespace_from_attributes(*args)
args.each do |attribute|
define_method "#{attribute}=" do |value|
#debugger
value = value.gsub(/\s*/, "")
#debugger
super(value)
end
end
end
end
end
ActiveRecord::Base.send(:include, StripWhitespace)
and then added this to any model class this wants to strip whitespace ...
include StripWhitespace
strip_whitespace_from_attributes #add any attributes that need whitespace stripped
I would go with sth like this (not tested):
module Stripper # yeah!
extend ActiveSupport::Concern
module ClassMethods
def strip_attributes(*args)
mod = Module.new
args.each do |attribute|
define_method "#{attribute}=" do |value|
value = value.strip if value.respond_to? :strip
super(value)
end
end
end
include mod
end
end
end
class MyModel < ActiveRecord::Base
include Stripper
strip_attributes :foo, :bar
end
m = MyModel.new
m.foo = ' stripped '
m.foo #=> 'stripped'
If you can get your attributes in to a single array (perhaps there's a [:params] key you can use instead), you can do the following:
class FooController < ApplicationController
before_create strip_whitespace(params)
private
def strip_whitespace(*params)
params.map{ |attr| attr.strip }
end
end
I'm trying to understand some Ruby metaprogramming concepts.
I think I understand classes, objects, and metaclasses. Unfortunately, I'm very unclear on exactly what happens with included Modules with respect to their instance/'class' variables.
Here's a contrived question whose solution will answer my questions:
Suppose I'm writing my own crappy Rails "validates" method, but I want it to come from a mixed-in module, not a base class:
module MyMixin
# Somehow validates_wordiness_of() is defined/injected here.
def valid?
# Run through all of the fields enumerated in a class that uses
# "validate_wordiness_of" and make sure they .match(/\A\w+\z/)
end
end
class MyClass
include MyMixin
# Now I can call this method in my class definition and it will
# validate the word-ness of my string fields.
validate_wordiness_of :string_field1, :string_field2, :string_field3
# Insert rest of class here...
end
# This should work.
MyMixin.new.valid?
Ok, so how would you store that list of fields from the validate_wordiness_of invocation (in MyClass) in such a way that it can be used in the valid? method (from MyMixin)?
Or am I coming at this all wrong? Any info would be super appreciated!
So here are two alternative ways of doing it:
With "direct" access
module MyMixin
def self.included(base)
base.extend(ClassMethods)
end
def wordy?(value)
value.length > 2
end
module ClassMethods
def validates_wordiness_of(*attrs)
define_method(:valid?) do
attrs.all? do |attr|
wordy?(send(attr))
end
end
end
end
end
class MyClass
include MyMixin
validates_wordiness_of :foo, :bar
def foo
"a"
end
def bar
"asrtioenarst"
end
end
puts MyClass.new.valid?
The downside to this approach is that several consecutive calls to validates_wordiness_of will overwrite each other.
So you can't do this:
validates_wordiness_of :foo
validates_wordiness_of :bar
Saving validated attribute names in the class
You could also do this:
require 'set'
module MyMixin
def self.included(base)
base.extend(ClassMethods)
end
module Validation
def valid?
self.class.wordy_attributes.all? do |attr|
wordy?(self.send(attr))
end
end
def wordy?(value)
value.length > 2
end
end
module ClassMethods
def wordy_attributes
#wordy_attributes ||= Set.new
end
def validates_wordiness_of(*attrs)
include(Validation) unless validation_included?
wordy_attributes.merge(attrs)
end
def validation_included?
ancestors.include?(Validation)
end
end
end
class MyClass
include MyMixin
validates_wordiness_of :foo, :bar
def foo
"aastrarst"
end
def bar
"asrtioenarst"
end
end
MyClass.new.valid?
# => true
I chose to make the valid? method unavailable until you actually add a validation. This may be unwise. You could probably just have it return true if there are no validations.
This solution will quickly become unwieldy if you introduce other kinds of validations. In that case I would start wrapping validations in validator objects.
Is there a way to use arguments when including a ruby module? I have a module Assetable which is included across many classes. I want to be able to generate attr_accessor's on the fly.
module Assetable
extend ActiveSupport::Concern
included do
(argument).times do |i|
attr_accessor "asset_#{i}".to_sym
attr_accessible "asset_#{i}".to_sym
end
end
end
There is a trick: making a class that's inheriting from a module so that you could pass any arguments to the module like class.
class Assetable < Module
def initialize(num)
#num = num
end
def included(base)
num = #num
base.class_eval do
num.times do |i|
attr_accessor "asset_#{i}"
end
end
end
end
class A
include Assetable.new(3)
end
a = A.new
a.asset_0 = 123
a.asset_0 # => 123
The details are blogged at http://kinopyo.com/en/blog/ruby-include-module-with-arguments, hope you'll find it useful.
There is no way of passing arguments when including the module. The best next thing would be to define a class method that lets you create what you need afterwards:
module Assetable
extend ActiveSupport::Concern
module ClassMethods
def total_assets(number)
number.times do |i|
attr_accessor "asset_#{i}"
attr_accessible "asset_#{i}"
end
end
end
end
class C
include Assetable
total_assets 3
end
o = C.new
o.asset_2 = "Some value."
o.asset_2 #=> "Some value."
Also be careful when overriding the included method within a concern because it's also used by ActiveSupport::Concern. You should call super within the overriden method in order to ensure proper initialization.
You can generate and include an anonymous module without polluting global namespaces:
module Assetable
def self.[](argument)
Module.new do
extend ActiveSupport::Concern
included do
(argument).times do |i|
attr_accessor :"asset_#{i}"
attr_accessible :"asset_#{i}"
end
end
end
end
end
class Foo
include Assetable[5]
end
You can't pass arguments to a module. In fact, you can't pass arguments to anything except a message send.
So, you have to use a message send:
module Kernel
private def Assetable(num)
#__assetable_cache__ ||= []
#__assetable_cache__[num] ||= Module.new do
num.times do |i|
attr_accessor :"asset_#{i}"
attr_accessible :"asset_#{i}"
end
end
end
end
class Foo
include Assetable 3
end
Note: I didn't see why you would need ActiveSupport::Concern here at all, but it's easy to add back in.