I have a class and an instance of that class:
class Thing
def initialize
@name = name
end
end
a = Thing.new("a")
I want to dynamically create methods and set their return values by calling a DSL like this:
If I write a.is_a.person
:
- a
person?
method should be created fora
a.person?
should returntrue
If I write a.is_not_a.man
- a
man?
method should be created fora
a.man?
should returnfalse
If I write a.is_the.parent_of.joe
- a
parent_of
method should be created fora
a.parent_of
should returnjoe
I tried doing this
class Thing
attr_reader :name
def initialize(name)
@name = name
end
def is_a
Class.new do
def initialize base
@base = base
end
def method_missing name
@base.define_singleton_method "#{name}?" do
true
end
end
end.new self
end
def is_not_a
Class.new do
def initialize base
@base = base
end
def method_missing name
@base.define_singleton_method "#{name}?" do
false
end
end
end.new self
end
end
And it works for is_a
and is_not_a
as defined above:
jane = Thing.new('Jane')
jane.is_a.person
jane.person? #=> true
jane.is_a.woman
jane.woman? #=> true
jane.is_not_a.man
jane.man? #=> false
But for the jane.is_the.parent_of.joe
, like more deep chaining ones, I'm not able to figure out how I can implement it.
How do I approach this?
CodePudding user response:
With Class.new
you create a new anonymous class each time is_a
or is_not_a
is called. Although this works, there's a
much more lightweight approach. You can create a helper that will invoke a given block each time it receives a message:
class MethodCallback
def initialize(&block)
@block = block
end
def method_missing(name, *args)
@block.call(name, *args)
end
end
Example usage:
m = MethodCallback.new { |name| puts "#{name} was called"}
m.foo
# foo was called
m.bar
# bar was called
With this helper, your class can be simplified like this:
class Thing
def initialize(name)
@name = name
end
def is_a
MethodCallback.new do |name|
define_singleton_method(:"#{name}?") { true }
end
end
def is_not_a
MethodCallback.new do |name|
define_singleton_method(:"#{name}?") { false }
end
end
end
jane = Thing.new('Jane')
jane.is_a.person
jane.person? #=> true
jane.is_a.woman
jane.woman? #=> true
jane.is_not_a.man
jane.man? #=> false
Now, for the chaining one jane.is_the.parent_of.joe
, instead of defining a new method right-away, we have to
invoke / return another MethodCallback
instance in order to get an extra level of method chaining:
class Thing
# ...
def is_the
MethodCallback.new do |name|
MethodCallback.new do |value|
define_singleton_method(name) { value.to_s }
end
end
end
end
In the above code, the first method name after is_the
will be assigned to name
and the second will be assigned to value
.
The method we create will be called just like name
and it will return value
:
jane.is_the.parent_of.joe
# ^^^^^^^^^ ^^^
# name value
jane.parent_of #=> 'joe'
Another one:
jane.is_the.major_of.london
jane.major_of #=> 'london'
CodePudding user response:
If you are not stuck on using method chaining to define your object, there are lots of alternatives. Here is one that uses a "builder" approach.
class Thing
attr_reader :name
def self.define(name, &block)
thing = Thing.new(name)
thing.instance_eval(&block)
thing
end
def initialize(name)
@name = name
end
def is(str)
case str
when /^\s*a\s (\w )\s*$/
define_a($1)
when /^\s*not\s a\s (\w )\s*$/
define_not_a($1)
when /^\s*the\s*(\w )\s of\s (\w )\s*$/
define_relationship($1, $2)
end
end
def define_a(str)
define_singleton_method("#{str}?".to_sym) {true}
end
def define_not_a(str)
define_singleton_method("#{str}?".to_sym) {false}
end
def define_relationship(relation, to)
define_singleton_method("#{relation}_of".to_sym) {to}
end
end
a = Thing.define("a") do
is "a person"
is "not a man"
is "the parent of joe"
end
puts a.name # => a
puts a.person? # => true
puts a.man? # => false
puts a.parent_of # => joe