Typically when writing a model in Rails you use a DSL to setup various aspects of derived objects, for example:
class Question < ActiveRecord::Base
has_one :category
validates_presence_of :category
end
In this case, "has_one" and "validates_presence_of" create associations and validation call backs on models instantiated from Question.
I want to add a new method called "parent" to be used when defining a class:
class Question
attr_accessor :category
parent :category
end
q = Question.new
q.category = 'a category'
puts q.parent
-> 'a category'
So when objects are instantiated from class, they should have the method "parent" defined.
How do I do this? My first thought was to use a module, but this isn't an instance method, or a class method.
I believe this is what you are looking for:
module QuestionParent
module ClassMethods
def inherited(descendant)
descendant.instance_variable_set(:#parent, parent.dup)
super
end
def parent(args=nil)
#parent ||= args
end
end
module InstanceMethods
def parent
self.send self.class.parent.to_sym
end
end
def self.included(receiver)
receiver.extend ClassMethods
receiver.send :include, InstanceMethods
end
end
class Question
include QuestionParent
attr_accessor :category
parent :category
end
Which produces:
q = Question.new
q.category = 'a category'
puts q.parent
a category
What this does is add a class method parent that will define the class variable #parent, when an instance calls the parent in the InstanceMethod the #parent symbol (here is category) is called.
Related
I'm trying to figure out a way to dynamically generate subclasses based on a parent class. In my specific case I'd want to have attr_accessor for every instance variable, initialized in my Parent class and inherited on the SubClasses.
My classes are three different models representing three different tables in a DB.
"Record" is my parent class where I want to store and write all of my code.
"Post" and "User" are the Subclasses inheriting.
My code
class Record
attr_reader :id
# attr_accessor
def initialize(**params)
#id = params[:id]
instance_variable_set("##{params.keys[0]}", params.values[0])
instance_variable_set("##{params.keys[1]}", params.values[1])
instance_variable_set(:#votes, params["votes"] || 0) if instance_of?(Post)
# p self.title
end
Want I want to achieve is setting attr_accessor as for example in my Subclass "Post" I want to call
post = Post.new(title: "New post", url: "some url")
puts post.title
I can access the title instance variable without raising a NoMethodError
Could someone guide me, or give me some hint?
Thanks
You're going about it backwards. A parent class should not have to know about or implement specific logic for its subclasses.
class Record
attr_reader :id
def initialize(**attributes)
attributes.each do |key, value|
send("#{key}=", value)
end
end
end
class Post < Record
attr_accessor :title
attr_accessor :votes
end
irb(main):066:0> Post.new(id: 1, votes: 10, title: "Hello World").title
=> "Hello World"
attr_accessor is just a metaprogramming convenience for defining methods so your accessor methods are inherited anyways. But if you're writing something like an Object Relational Manager you'll want to define your own macro method for defining attributes that lets you keep track of the attributes of a class:
module Attributes
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
#attributes ||= {}
end
end
# assigns the passed attributes to the instance
def initialize(**attributes)
attributes.each do |key, value|
send "#{key}=", value
end
end
# gets all the attributes of an instance
def attributes
self.class.attributes.keys.each_with_object(Hash.new) do |key, hash|
hash[key] = send(key)
end
end
module ClassMethods
# Inherits the attributes of the parent class
def inherited(subclass)
attributes.tap do |parent_attributes|
subclass.class_eval do
#attributes ||= {}.merge(parent_attributes)
end
end
end
# defines an attribute that is inherited
def attribute(name, type = nil, **kwargs)
#attributes[name] = { type: type }.merge(kwargs)
attr_accessor name
end
def attributes
#attributes
end
end
end
class Record
include Attributes
attribute :id, Integer
end
class Post < Record
attribute :title, String
attribute :votes, Integer
end
irb(main):101:0> Post.new(votes: 10, title: "Hello World").attributes
=> {:id=>nil, :title=>"Hello World", :votes=>10}
This stores the attribute definitions in a class instance variable which lets you attach "metadata" which opens up for features that you will want later such as typecasting, serialization and dirty tracking.
I was reading the jbuilder's README and saw these code:
class Person
# ... Class Definition ... #
def to_builder
Jbuilder.new do |person|
person.(self, :name, :age)
end
end
end
I tried to replicate it myself, and it asks for a call method, so:
class Thing
attr_accessor :name, :age
def call(*args)
puts args.inspect
end
end
Thing.new.(:name, :age) # => [:name, :age]
So why is there a self in the jbuilder call?
self here is just a parameter passed to the Jbuilder's call method.
Jbuilder needs the instance of person (which is self in the code) and the attribute names (:name and :age in the code) to produce the json data.
Example:
class Thing
attr_accessor :name, :age
def call(*args)
puts args.inspect
end
end
class Bar
def to_thing
Thing.new.(self, :name, :age)
end
end
Bar.new.to_thing
I'm fairly new to Ruby metaprogramming. I'm trying to write code which generates the
"dup" function for a class when it's created, using a list of fields which should be passed into the constructor. However, I can't figure out how to get access to the name of the class I'm creating, while I'm creating it.
So for example, if I had this code:
class Example
make_dup :name, :value
attr_accessor :name, :value
def initialize(name,value)
#name, #value = name, value
end
end
I'd want it to create the method:
def dup
Example.new(name,value)
end
I'm just getting stuck on how it would figure out to insert Example there.
Note that all classes have built-in dup and clone methods. You can customize what happens in them by adding an initialize_copy method, e.g.:
class Foo
attr_accessor :bar
def initialize_copy(orig)
super
#bar = #bar.dup
end
end
In case that isn't what you're truly looking for, you can access an object's class using its class method:
class Foo
def p_class
p self.class # Foo.new.p_class => Foo ; self is *a* `Foo'
end
def self.p_class
p self.class # Foo.p_class => Class ; self *is* `Foo'
end
end
def dup
self.class.new(name,value)
end
Maybe you can implement it this way:
module MyDup
def make_dup(*args)
define_method(:my_dup) do
obj = self.class.new(nil, nil)
args.each do |arg|
obj.send(arg.to_s + "=", self.send(arg))
end
obj
end
end
end
class Example
extend MyDup
make_dup :name, :value
attr_accessor :name, :value
def initialize(name,value)
#name, #value = name, value
end
end
e = Example.new("John", 30)
p e
d = e.my_dup
p d
Execution result as follows:
#<Example:0x000000022325d8 #name="John", #value=30>
#<Example:0x00000002232358 #name="John", #value=30>
Suppose I have a DataMapper scope for carnivores, like this:
class Animal
#...
def self.carnivores
all(:diet => 'meat')
end
#...
end
Can I reuse that scope in an association's scope?
class Zoo
#...
def self.with_carnivores
# Use `Animal.carnivores` scope to get zoos that have some?
all(:animals => ???)
end
#...
end
You can do this by going "in reverse" from the association.
class Zoo
#...
def self.with_carnivores
# Assuming `Animal belongs_to :zoo`
Animal.carnivores.zoo
end
#...
end
class Habitat
#...
def self.with_carnivores
# Assuming `Animal has_many :habitats`
Animal.carnivores.habitats
end
#...
end
class Product < ActiveRecord::Base
set_table_name 'produce'
end
module ActiveRecord
class Base
def self.set_table_name name
define_attr_method :table_name, name
end
def self.define_attr_method(name, value)
singleton_class.send :alias_method, "original_#{name}", name
singleton_class.class_eval do
define_method(name) do
value
end
end
end
end
I'd like to understand how set_table_name becomes defined in this example.
Why is singleton_class.send needed here?
And why is class_eval called on singleton_class instead of on self?
The reason for using "singleton_class" is because you do not want to modify the ActiveRecord::Base class, but the Product class.
More info about metaptogramming and singleton class here: http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html