how can I do this:
class MyClass
tile 'some title'
collection do
node1 'node1'
node2 'node2'
another_collection do
node1 'node1'
node2 'node2'
end
end
end_node 'some text'
end
and produce following:
MyClass.build #=>{:title=>'some title',:collection=>{:node1=>'node1',:node2=>'node2',:another_collection=>{:node1=>'node1',:node2=>'node2'}},:end_node=>'some text'}
What i was trying is to make simple DSL and build hash tree. I'm sure that can be done with method_missing and instance_eval, but i don't now how to build that logic.
Thanks for help
In your method_missing, you should check if a block is given, and, if so, recursively call the main method with it:
class HashBuilder
def self.build &block
hb = HashBuilder.new
hb.instance_eval(&block)
hb.hash
end
attr_reader :hash
def initialize
#hash = {}
end
def method_missing meth, *args, &block
#hash[meth] = block ? HashBuilder.build(&block) : args.first
end
end
p HashBuilder.build{
a :b
c :d
e do
f :g
end
}
#=> {:a=>:b, :c=>:d, :e=>{:f=>:g}}
Related
I have a class that was built for subclassing.
class A
def initialize(name)
end
def some
# to define in subclass
end
end
# usage
p A.new('foo').some
#=> nil
In my use case, I don't want to create a subclass since I need just one instance. Therefore, I'll change the initialize method to support the following usage.
p A.new('foo') { 'YEAH' }.some
#=> YEAH
How could I support the usage above?
BTW: I found the following solutions for a Ruby 1.8.7 project, but they look awkward to me.
class A
def singleton_class
class << self; self; end
end
def initialize(name, &block)
#name = name
self.singleton_class.send(:define_method, :some) { block.call } if block_given?
end
def some
# to define in subclass
end
end
You can store the block argument in an instance variable and call it later on:
class A
def initialize(name, &block)
#name = name
#block = block
end
def some
#block.call
end
end
A.new('foo') { 'YEAH' }.some
#=> "YEAH"
You can also pass arguments into the block:
class A
# ...
def some
#block.call(#name)
end
end
A.new('foo') { |s| s.upcase }.some
#=> "FOO"
Or instance_exec the block in the context of the receiver:
class A
# ...
def some
instance_exec(&#block)
end
end
Which allows you to bypass encapsulation:
A.new('foo') { #name.upcase }.some
#=> "FOO"
I am using the following code to enforce context of DSL nested constructs. What are the other ways of achieving the same functionality?
def a &block
p "a"
def b &block
p "b"
def c &block
p "c"
instance_eval &block
end
instance_eval &block
undef :c
end
instance_eval &block
undef :b
end
# Works
a do
b do
c do
end
end
end
# Doesn't Work
b do
end
c do
end
Source
You asked about other ways, not the best way. So here's some examples :
Example A
class A
def initialize
p "a"
end
def b &block
B.new.instance_eval &block
end
end
class B
def initialize
p "b"
end
def c &block
C.new.instance_eval &block
end
end
class C
def initialize
p "c"
end
end
def a &block
A.new.instance_eval &block
end
Example B
A bit shorter :
def a &block
p "a"
A.new.instance_eval &block
end
class A
def b &block
p "b"
B.new.instance_eval &block
end
class B
def c &block
p "c"
C.new.instance_eval &block
end
class C
end
end
end
Example C
If you don't plan to have a d method for an A::B::C object :
def a &block
p "a"
A.new.instance_eval &block
end
class A
def b &block
p "b"
B.new.instance_eval &block
end
class B
def c &block
p "c"
instance_eval &block
end
end
end
Example D
This was a fun one :
def new_class_and_method(klass_name, next_klass=Object)
dynamic_klass = Class.new do
define_method(next_klass.name.downcase){|&block| p next_klass.name.downcase; next_klass.new.instance_eval &block}
end
Object.const_set(klass_name, dynamic_klass)
end
new_class_and_method("A", new_class_and_method("B", new_class_and_method("C")))
def a &block
p "a"
A.new.instance_eval &block
end
Example E
I dare say this doesn't look half bad:
def new_method_and_class(x)
define_method(x) do |&block|
p x
self.class.const_get(x.capitalize).new.instance_eval &block
end
self.const_set(x.capitalize, Class.new)
end
["a", "b", "c"].inject(Object){|klass,x| klass.instance_eval{new_method_and_class(x)} }
Example F
A bit more robust :
def new_method_and_class(x, parent_klass = Object)
parent_klass.class_eval do
define_method(x) do |&block|
p x
parent_klass.const_get(x.capitalize).new.instance_eval &block if block
end
end
parent_klass.const_set(x.capitalize, Class.new)
end
["a", "b", "c"].inject(Object){|klass,x| new_method_and_class(x,klass) }
Explanation
Example B
In example B, we first define :
an a() method
an A class
both are defined in main, because we want a() to be available directly.
a() method doesn't do much expect printing "a" and passing a block to an instance of A.
Then comes b() method. We don't want it to be available from main, so we define it inside A class. We want to continue with the nested methods, so we define a B class, which is also defined inside A. The B class is actually a A::B class. The A::B#b() method also prints "b", and passes a block to an instance of B.
We continue with A::B::C inside of A::B, just like we did with A::B and A.
Example F
Example F is basically like Example B, but written dynamically.
In example B, we defined an x method and an X class in every step, with the exact same structure. It should be possible to avoid code repetition with a method called new_method_and_class(x) which uses define_method, const_set and Class.new :
new_method_and_class("a") # <- Object#a() and A are now defined
a do
puts self.inspect
end
#=> "a"
# <A:0x00000000e58bc0>
Now, we want to define a b() method and a B class, but they shouldn't be in main. new_method_and_class("b") wouldn't do. So we pass an extra parameter, called parent_klass, which defaults to Object :
parent_klass = new_method_and_class("a")
new_method_and_class("b", parent_klass)
a do
b do
puts self.inspect
end
end
# => "a"
# "b"
# <A::B:0x00000000daf368>
b do
puts "Not defined"
end
# => in `<main>': undefined method `b' for main:Object (NoMethodError)
To define the c method, we just add another line :
parent_klass = new_method_and_class("a")
parent_klass = new_method_and_class("b", parent_klass)
parent_klass = new_method_and_class("c", parent_klass)
And so on and so on.
To avoid code repetition, we can use inject with the parent_klass as accumulator value :
["a", "b", "c"].inject(Object){|klass,x| new_method_and_class(x,klass) }
Bonus - Example G
Here's a modified code from Example F which works with a basic tree structure.
# http://stackoverflow.com/questions/40641273/ruby-dsl-nested-constructs/40641743#40641743
def new_method_and_class(x, parent_klass = Object)
parent_klass.class_eval do
define_method(x) do |&block|
p x.to_s
parent_klass.const_get(x.capitalize).new.instance_eval &block if block
end
end
parent_klass.const_set(x.capitalize, Class.new)
end
def create_dsl(branch,parent_klass = Object)
case branch
when Symbol, String
new_method_and_class(branch,parent_klass)
when Array
branch.each do |child|
create_dsl(child, parent_klass)
end
when Hash
branch.each do |key, value|
create_dsl(value, new_method_and_class(key,parent_klass))
end
end
end
methods_tree = {
:a => {
:b => [
:c,
:d
],
:e => :f,
:g => nil
}
}
create_dsl(methods_tree)
a do
b do
c do
puts self.inspect
end
d do
end
end
e do
f do
end
end
g do
puts self.inspect
end
end
# =>
# "a"
# "b"
# "c"
# #<A::B::C:0x0000000243dfa8>
# "d"
# "e"
# "f"
# "g"
# #<A::G:0x0000000243d918>
Always working with Rails but now that I am coding completely without framework I am missing a lot of methods that I am not able to write myself. So I need a little hint here.
So I've got this code
class Foo
initialize(k)
(1..k).each do |b|
Boo.new(b)
end
end
class Boo
initialize(a)
#a = a
end
end
Foo.new(10)
#and now I want to access one of the Boo's with a certain a (in Rails Boo.find_by_a(2) e.g.)
How do I write a method that uses a certain value "a" to get the specific Boo back with #a = a?
Update:
Okay I added this methot to Boo
def self.all
ObjectSpace.each_object(self).to_a
end
And now I get the right object by:
boo = Boo.all.select{ |b| b.a == 2 }.first
But if I want this to be a method, where would i put it?
DO NOT RELY ON ObjectSpace for this functionality when the GC collects you will lose these objects because Ruby sees them as unneeded.
This is the general concept of what you are intending to do
class Foo
attr_reader :boos
def initialize(k)
#boos = (1..k).map {|b| Boo.new(b) }
end
def find_boo_with(x)
#boos.find {|b| b.a == x }
end
end
class Boo
attr_reader :a
def initialize(a)
#a = a
end
end
Then you can do this
f = Foo.new(10)
f.boos
#=> [#<Boo:0x256f428 #a=1>,
#<Boo:0x256f3b0 #a=2>,
#<Boo:0x256f368 #a=3>,
#<Boo:0x256f338 #a=4>,
#<Boo:0x256f2f0 #a=5>,
#<Boo:0x256f2d8 #a=6>,
#<Boo:0x256f2a8 #a=7>,
#<Boo:0x256f290 #a=8>,
#<Boo:0x256f278 #a=9>,
#<Boo:0x256f260 #a=10>]
f.find_boo_with(9)
#=> #<Boo:0x256f278 #a=9>
I hope this helps but you really should look into something like RubyMonk, or TryRuby to get a better understanding of how PORO (Plain Old Ruby Objects) work.
You could also do it this way if you really wanted your find methodology just know that no Boo in this case will ever be collected as they will all be stored in a class instance variable called #all_boos which could casue memory issues depending on what a Boo is and how many of them you are creating.
class Boo
class << self
def all_boos
#all_boos ||= []
end
def find(x)
#all_boos.find {|boo| boo.a == x }
end
alias_method :all, :all_boos
end
attr_reader :a
def initialize(a)
#a = a
self.class.all_boos << self
end
end
Then you can do
10.times {|i| Boo.new(i) }
Boo.all
#=> [#<Boo:0x256f428 #a=1>,
#<Boo:0x256f3b0 #a=2>,
#<Boo:0x256f368 #a=3>,
#<Boo:0x256f338 #a=4>,
#<Boo:0x256f2f0 #a=5>,
#<Boo:0x256f2d8 #a=6>,
#<Boo:0x256f2a8 #a=7>,
#<Boo:0x256f290 #a=8>,
#<Boo:0x256f278 #a=9>,
#<Boo:0x256f260 #a=10>]
Boo.find(9)
#=> #<Boo:0x256f278 #a=9>
If I did understand what you need then I think that you can use ObjectSpace to achieve this. This is the sample code that illustrates the use:
class Foo
def initialize(k)
(1..k).each do |b|
Boo.new(b)
end
end
end
class Boo
attr_reader :a
def initialize(a)
#a = a
end
def self.find(value)
ObjectSpace.each_object(Boo).select { |b| b.a == value }.first
end
end
Foo.new(10)
Boo.find(2).a
Let me know if this is the solution you were looking for.
Not sure I 100% understand your question but using attr_reader in Boo will allow you to access a as if it were a function.
class Foo
def initialize(k)
(1..k).each do |b|
Boo.new(b)
end
end
end
class Boo
attr_reader :a
def initialize(a)
#a = a
end
end
Foo.new(10)
As for the other part of your question, here is a good article explaining how dynamic finders work
Just for fun, again, but is it possible to take a block that contains method definitions and add those to an object, somehow? The following doesn't work (I never expected it to), but just so you get the idea of what I'm playing around with.
I do know that I can reopen a class with class << existing_object and add methods that way, but is there a way for code to pass that information in a block?
I guess I'm trying to borrow a little Java thinking here.
def new(cls)
obj = cls.new
class << obj
yield
end
obj
end
class Cat
def meow
puts "Meow"
end
end
cat = new(Cat) {
def purr
puts "Prrrr..."
end
}
cat.meow
# => Meow
# Not working
cat.purr
# => Prrrr...
EDIT | Here's the working version of the above, based on edgerunner's answer:
def new(cls, &block)
obj = cls.new
obj.instance_eval(&block)
obj
end
class Cat
def meow
puts "Meow"
end
end
cat = new(Cat) {
def purr
puts "Prrrr..."
end
}
cat.meow
# => Meow
cat.purr
# => Prrrr...
You can use class_eval(also aliased as module_eval) or instance_eval to evaluate a block in the context of a class/module or an object instance respectively.
class Cat
def meow
puts "Meow"
end
end
Cat.module_eval do
def purr
puts "Purr"
end
end
kitty = Cat.new
kitty.meow #=> Meow
kitty.purr #=> Purr
kitty.instance_eval do
def purr
puts "Purrrrrrrrrr!"
end
end
kitty.purr #=> Purrrrrrrrrr!
Yes
I suspect you thought of this and were looking for some other way, but just in case...
class A
def initialize
yield self
end
end
o = A.new do |o|
class << o
def purr
puts 'purr...'
end
end
end
o.purr
=> purr...
For the record, this isn't the usual way to dynamically add a method. Typically, a dynamic method starts life as a block itself, see, for example, *Module#define_method*.
(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