I have the following class:
class Message
extend ActiveModel::Naming
include ActiveModel::Conversion
attr_accessor :name, :emails, :custom_content
def initialize(attrs = {})
attrs.each do |k, v|
self.send "#{k}=", v
end
end
def persisted?
false
end
def email_list
self.emails.split(",").collect { |email| {:email => email.delete(" ")} }
end
end
I always want to capitalize the name attribute instead of having to always do it when I call the attribute in code.
Figured it out:
class Message
extend ActiveModel::Naming
include ActiveModel::Conversion
attr_accessor :name, :emails, :custom_content
def initialize(attrs = {})
attrs.each do |k, v|
self.send "#{k}=", v
end
end
def persisted?
false
end
#Fix is here
def name=(s)
#name = s.titleize
end
def email_list
self.emails.split(",").collect { |email| {:email => email.delete(" ")} }
end
end
Related
I'm building a small script where I'd need to implement a Mongoid Document like class, where I include my base module and then I can build a class which looks like:
class MyClass
include MyBaseModule
field :some_field, :attr => 'attributes'
end
This is my last try:
module Model
def initialize(keys = {})
puts ##keys
end
def method_missing sym, *args
if sym =~ /^(\w+)=$/
if ##keys.has_key?($1)
##keys[$1.to_sym] = args[0]
else
nil
end
else
if ##keys.has_key?($1)
##keys[sym.to_sym]
else
nil
end
end
end
def inspect
puts "#<#{self.class} #keys=#{##keys.each {|k,v| "#{k} => #{v}"}}>"
end
def self.included(base)
base.extend(ClassMethods)
end
def save
##keys.each do |k, v|
SimpleMongo::connection.collection.insert({k => v})
end
end
module ClassMethods
def field(name, args)
if ##keys.nil?
##keys = {}
end
##keys[name.to_sym] = default_value
end
end
end
Mongoid documents look like this:
class StoredFile
include Mongoid::Document
field :name, type: String
field :description, type: String
field :password, type: String
field :public, type: Boolean
field :shortlink, type: String
mount_uploader :stored_file, StoredFileUploader
before_save :gen_shortlink
before_save :hash_password
belongs_to :user
def gen_shortlink
self.shortlink = rand(36**10).to_s(36)
end
def public?
self.public
end
def hash_password
require 'bcrypt'
self.password = BCrypt::Password.create(self.password).to_s
end
def check_pass(password)
BCrypt::Password.new(self.password) == password
end
end
It doesn't works because the ##keys variable inside ClassMethods isn't available anywhere outside that module. What would be the easiest way to implement this? Thanks!
The easiest way to implement it would be to have a class variable getter.
module Model
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def keys
#keys ||= {}
end
def field(name, opts)
#keys ||= {}
#keys[name] = opts
end
end
def initialize(attributes)
# stuff
puts self.class.keys
end
end
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_reader attr_name
attr_reader attr_name + "_history"
class_eval %Q{
def #{attr_name}=(new_value)
##{attr_name}_history = [nil] if ##{attr_name}_history.nil?
##{attr_name}_history << ##{attr_name} = new_value
end
}
end
end
class Example
attr_accessor_with_history :foo
attr_accessor_with_history :bar
end
There is Class.attr_accessor_with_history method that provides the same
functionality as attr_accessor but also tracks every value the attribute has
ever had.
> a = Example.new; a.foo = 2; a.foo = "test"; a.foo_history
=> [nil, 2, "test"]
But,
> a = Example.new; a.foo_history
=> nil
and it should be [nil.
How can I define single initialize method for Example class where each
…_history value will be initialize as [nil]?
I think, your best bet is to define a custom reader for history (along with your custom writer).
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_reader attr_name
class_eval %Q{
def #{attr_name}_history
##{attr_name}_history || [nil] # give default value if not assigned
end
def #{attr_name}=(new_value)
##{attr_name}_history ||= [nil] # shortcut, compare to your line
##{attr_name}_history << ##{attr_name} = new_value
end
}
end
end
class Example
attr_accessor_with_history :foo
attr_accessor_with_history :bar
end
a = Example.new; a.foo = 2; a.foo = "test";
a.foo_history # => [nil, 2, "test"]
a = Example.new
a.foo_history # => [nil]
Edit:
Here's a slightly more verbose snippet, but it doesn't use class_eval (which is frowned upon, when used without necessity).
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_reader attr_name
define_method "#{attr_name}_history" do
instance_variable_get("##{attr_name}_history") || [nil]
end
define_method "#{attr_name}=" do |new_value|
v = instance_variable_get("##{attr_name}_history")
v ||= [nil]
v << new_value
instance_variable_set("##{attr_name}_history", v)
instance_variable_set("##{attr_name}", new_value)
end
end
end
Sloves in one class_eval
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_reader attr_name
attr_reader attr_name+"_history"
class_eval %Q{
def #{attr_name}=(val)
if ##{attr_name}_history
##{attr_name}_history << ##{attr_name}
else
##{attr_name}_history = [nil]
end
##{attr_name} = val
end
}
end
end
How do I add a class instance variable, the data for it and a attr_reader at runtime?
class Module
def additional_data member, data
self.class.send(:define_method, member) {
p "Added method #{member} to #{name}"
}
end
end
For example, given this class
class Test
additional_data :status, 55
end
So that now I can call:
p Test.status # => prints 55
How about this?
class Object
def self.additional_data(name, value)
ivar_name = "##{name}"
instance_variable_set(ivar_name, value)
self.class.send(:define_method, name) do
instance_variable_get(ivar_name)
end
self.class.send(:define_method, "#{name}=") do |new_value|
instance_variable_set(ivar_name, new_value)
end
end
end
class Foo
additional_data :bar, 'baz'
end
puts Foo.bar # => 'baz'
Foo.bar = 'quux'
puts Foo.bar # => 'quux'
It's pretty self-explanatory, but let me know if you have any questions.
Module#class_eval is what you want:
def add_status(cls)
cls.class_eval do
attr_reader :status
end
end
add_status(Test)
(Big edit, I got part of the way there…)
I've been hacking away and I've come up with this as a way to specify things that need to be done before attributes are read:
class Class
def attr_reader(*params)
if block_given?
params.each do |sym|
define_method(sym) do
yield
self.instance_variable_get("##{sym}")
end
end
else
params.each do |sym|
attr sym
end
end
end
end
class Test
attr_reader :normal
attr_reader(:jp,:nope) { changethings if #nope.nil? }
def initialize
#normal = "Normal"
#jp = "JP"
#done = false
end
def changethings
p "doing"
#jp = "Haha!"
#nope = "poop"
end
end
j = Test.new
p j.normal
p j.jp
But changethings isn't being recognised as a method — anyone got any ideas?
You need to evaluate the block in the context of the instance. yield by default will evaluate it in its native context.
class Class
def attr_reader(*params, &blk)
if block_given?
params.each do |sym|
define_method(sym) do
self.instance_eval(&blk)
self.instance_variable_get("##{sym}")
end
end
else
params.each do |sym|
attr sym
end
end
end
end
Here's another alternative approach you can look at. It's not as elegant as what you're trying to do using define_method but it's maybe worth looking at.
Add a new method lazy_attr_reader to Class
class Class
def lazy_attr_reader(*vars)
options = vars.last.is_a?(::Hash) ? vars.pop : {}
# get the name of the method that will populate the attribute from options
# default to 'get_things'
init_method = options[:via] || 'get_things'
vars.each do |var|
class_eval("def #{var}; #{init_method} if !defined? ##{var}; ##{var}; end")
end
end
end
Then use it like this:
class Test
lazy_attr_reader :name, :via => "name_loader"
def name_loader
#name = "Bob"
end
end
In action:
irb(main):145:0> t = Test.new
=> #<Test:0x2d6291c>
irb(main):146:0> t.name
=> "Bob"
IMHO changing the context of the block is pretty counter-intuitive, from a perspective of someone who would use such attr_reader on steroids.
Perhaps you should consider plain ol' "specify method name using optional arguments" approach:
def lazy_attr_reader(*args, params)
args.each do |e|
define_method(e) do
send(params[:init]) if params[:init] && !instance_variable_get("##{e}")
instance_variable_get("##{e}")
end
end
end
class Foo
lazy_attr_reader :foo, :bar, :init => :load
def load
#foo = 'foo'
#bar = 'bar'
end
end
f = Foo.new
puts f.bar
#=> bar
What's the shortest, one-liner way to list all methods defined with attr_accessor? I would like to make it so, if I have a class MyBaseClass, anything that extends that, I can get the attr_accessor's defined in the subclasses. Something like this:
class MyBaseClass < Hash
def attributes
# ??
end
end
class SubClass < MyBaseClass
attr_accessor :id, :title, :body
end
puts SubClass.new.attributes.inspect #=> [id, title, body]
What about to display just the attr_reader and attr_writer definitions?
Extract the attributes in to an array, assign them to a constant, then splat them in to attr_accessor.
class SubClass < MyBaseClass
ATTRS = [:id, :title, :body]
attr_accessor(*ATTRS)
end
Now you can access them via the constant:
puts SubClass.ATTRS #=> [:id, :title, :body]
There is no way (one-liner or otherwise) to list all methods defined by attr_accessor and only methods defined by attr_accessor without defining your own attr_accessor.
Here's a solution that overrides attr_accessor in MyBaseClass to remember which methods have been created using attr_accessor:
class MyBaseClass
def self.attr_accessor(*vars)
#attributes ||= []
#attributes.concat vars
super(*vars)
end
def self.attributes
#attributes
end
def attributes
self.class.attributes
end
end
class SubClass < MyBaseClass
attr_accessor :id, :title, :body
end
SubClass.new.attributes.inspect #=> [:id, :title, :body]
Heres an alternative using a mixin rather than inheritance:
module TrackAttributes
def attr_readers
self.class.instance_variable_get('#attr_readers')
end
def attr_writers
self.class.instance_variable_get('#attr_writers')
end
def attr_accessors
self.class.instance_variable_get('#attr_accessors')
end
def self.included(klass)
klass.send :define_singleton_method, :attr_reader, ->(*params) do
#attr_readers ||= []
#attr_readers.concat params
super(*params)
end
klass.send :define_singleton_method, :attr_writer, ->(*params) do
#attr_writers ||= []
#attr_writers.concat params
super(*params)
end
klass.send :define_singleton_method, :attr_accessor, ->(*params) do
#attr_accessors ||= []
#attr_accessors.concat params
super(*params)
end
end
end
class MyClass
include TrackAttributes
attr_accessor :id, :title, :body
end
MyClass.new.attr_accessors #=> [:id, :title, :body]
Following up on Christian's response, but modifying to use ActiveSupport::Concern...
module TrackAttributes
extend ActiveSupport::Concern
included do
define_singleton_method(:attr_reader) do |*params|
#attr_readers ||= []
#attr_readers.concat params
super(*params)
end
define_singleton_method(:attr_writer) do |*params|
#attr_writers ||= []
#attr_writers.concat params
super(*params)
end
define_singleton_method(:attr_accessor) do |*params|
#attr_accessors ||= []
#attr_accessors.concat params
super(*params)
end
end
def attr_readers
self.class.instance_variable_get('#attr_readers')
end
def attr_writers
self.class.instance_variable_get('#attr_writers')
end
def attr_accessors
self.class.instance_variable_get('#attr_accessors')
end
end
Class definitions
class MyBaseClass
attr_writer :an_attr_writer
attr_reader :an_attr_reader
def instance_m
end
def self.class_m
end
end
class SubClass < MyBaseClass
attr_accessor :id
def sub_instance_m
end
def self.class_sub_m
end
end
Call as class methods
p SubClass.instance_methods - Object.methods
p MyBaseClass.instance_methods - Object.methods
Call as instance methods
a = SubClass.new
b = MyBaseClass.new
p a.methods - Object.methods
p b.methods - Object.methods
Both will output the same
#=> [:id, :sub_instance_m, :id=, :an_attr_reader, :instance_m, :an_attr_writer=]
#=> [:an_attr_reader, :instance_m, :an_attr_writer=]
How to tell which are writer reader and accessor?
attr_accessor is both attr_writer and attr_reader
attr_reader outputs no = after the method name
attr_writer outputs an = after the method name
You can always use a regular expression to filter that output.
It could be filtered with the equal sign matching logic:
methods = (SubClass.instance_methods - SubClass.superclass.instance_methods).map(&:to_s)
methods.select { |method| !method.end_with?('=') && methods.include?(method + '=') }
#> ["id", "title", "body"]