I'm trying to make a simple DSL and the following code works in returning an array of the "Pizza" in the console.
class PizzaIngredients
def initialize
##fullOrder = []
end
#this takes in our toppings and creates the touple
def spread (topping)
##fullOrder << "Spread #{topping}"
end
def bake
##fullOrder
end
#this handles whatever is not a spread and is expected to take in a list in the format topping top1, top2, top3 and so on
def toppings (*toppingList)
array = []
toppingList.each {|topping| array << "topping #{topping}"}
array.each {|iter| ##fullOrder << iter}
end
end
# hadels if any methods are missing
def method_missing(name, *args, &block)
"#{name}"
end
#this is our entry into the dsl or manages it
module Pizza #smokestack
#this keeps a list of your order preserving the order in which the components where added
##order = []
def self.create(&block)
if block_given?
pizza = PizzaIngredients.new
##order << pizza.instance_eval(&block)
else
puts "making the pizza with no block"
end
end
end
def create (ingnore_param, &block)
Pizza.create(&block)
end
create pizza do
spread cheese
spread sauce
toppings oregano, green_pepper, onions, jalapenos
spread sauce
bake
end
However, when I try to run tests using Rake, I get the following errors:
C:/Ruby21-x64/bin/ruby.exe -w -I"lib" -I"C:/Ruby21-x64/lib/ruby/gems/2.1.0/gems/rake-10.4.2/lib" "C:/Ruby21-x64/lib/ruby/gems/2.1.0/gems/rake-10.4.2/lib/rake/rake_test_loader.rb" "test/PizzaBuilder_test.rb"
C:/Ruby21-x64/lib/ruby/gems/2.1.0/gems/test-unit-3.0.9/lib/test/unit/autorunner.rb:142:in `exist?': can't convert String to IO (String#to_io gives String) (TypeError)
from C:/Ruby21-x64/lib/ruby/gems/2.1.0/gems/test-unit-3.0.9/lib/test/unit/autorunner.rb:142:in `initialize'
from C:/Ruby21-x64/lib/ruby/gems/2.1.0/gems/test-unit-3.0.9/lib/test/unit/autorunner.rb:55:in `new'
from C:/Ruby21-x64/lib/ruby/gems/2.1.0/gems/test-unit-3.0.9/lib/test/unit/autorunner.rb:55:in `run'
from C:/Ruby21-x64/lib/ruby/gems/2.1.0/gems/test-unit-3.0.9/lib/test/unit.rb:502:in `block (2 levels) in <top (required)>'
rake aborted!
Command failed with status (1): [ruby -w -I"lib" -I"C:/Ruby21-x64/lib/ruby/gems/2.1.0/gems/rake-10.4.2/lib" "C:/Ruby21-x64/lib/ruby/gems/2.1.0/gems/rake-10.4.2/lib/rake/rake_test_loader.rb" "test/PizzaBuilder_test.rb" ]
This is PizzaBuilder_test.rb, I took out the tests to try and make it work but no luck.
require "test/unit"
require "./main/PizzaBuilder"
class TestMyApplication < Test::Unit::TestCase
def testCase
assert(true, "dummy case failed")
end
end
This is the Rakefile:
require 'rake'
require 'rake/testtask'
Rake::TestTask.new do |t|
t.libs = ["lib"]
t.warning = true
t.verbose = true
t.test_files = FileList['test/*_test.rb']
end
task default:[:test]
I think your problem is the way you're defining method_missing you're not defining it inside of a module or class, so it's being defined for the main Object.
It's not a great idea to override method_missing, especially as a catch all like you did. I would recommend rewriting your code to use strings instead of overwriting method_missing.
Also your create method seems unnecessary. If you remove that the code below should work fine:
class PizzaIngredients
def initialize
##fullOrder = []
end
#this takes in our toppings and creates the touple
def spread (topping)
##fullOrder << "Spread #{topping}"
end
def bake
##fullOrder
end
#this handles whatever is not a spread and is expected to take in a list in the format topping top1, top2, top3 and so on
def toppings (*toppingList)
array = []
toppingList.each {|topping| array << "topping #{topping}"}
array.each {|iter| ##fullOrder << iter}
end
end
#this is our entry into the dsl or manages it
module Pizza #smokestack
#this keeps a list of your order preserving the order in which the components where added
##order = []
def self.create(&block)
if block_given?
pizza = PizzaIngredients.new
##order << pizza.instance_eval(&block)
else
puts "making the pizza with no block"
end
end
end
Pizza.create do
spread 'cheese'
spread 'sauce'
toppings 'oregano', 'green pepper', 'onions', 'jalapenos'
spread 'sauce'
bake
end
Related
I wrote a simple DSL that works with a call like:
create Pizza do
spread cheese
spread sauce
toppings shoes, jalapeno, apples
bake
end
I would like to remove the uppercase P to make this call a little cleaner but I'm having trouble finding a way to make that possible.
Here is my code for PizzaBuilder.rb
class PizzaIngredients #factory
def initialize
##order = {}
end
def method_missing(name, *args, &block)
name.to_s
end
def spread (spread)
##order[:spreads] ||= []
##order[:spreads] << spread
end
def bake
##order
end
def create (pizza)
"making the #{pizza}"
end
def toppings (*toppingList)
##order[:toppings] ||= []
##order[:toppings] += toppingList
end
end
module Pizza #smokestack
#order = []
def self.order
#order
end
def self.create(&block)
pizza = PizzaIngredients.new
pizza.instance_eval(&block)
end
end
def create (param, &block)
Pizza.create(&block)
end
#suavocado - The capital letter is part of the conventions used by Ruby programmers all over the world...
Constants start with capital letters. variables use small letters... It's part of how we code and how we design....
These conventions are hard coded into the Ruby interperter.
Try the following code and see for your self:
class Pizza
def self.test
"hello!"
end
end
Object.const_set 'Pita', Pizza # => okay
Object.const_set 'pizza', Pizza # => error
As some of the comments mentioned, you could use a symbol instead of a constant for your DSL, i.e.
create :pizza do
# ...
end
I fixed it by using this in the global scope
def method_missing(name, *args, &block)
name.to_s == "pizza"? name.to_s : super
end
Lets say you write a gem containing classes within a module. If one installs that gem and wishes to create an object instance from that class, how do they successfully do that in another rb document? Here is my gem class.
require "Sentencemate/version"
module Sentencemate
#Object used to emulate a single word in text.
class Word
def initialize(word)
#word = word
end
def append(str)
#word = #word << str
return #word
end
def get
return #word
end
def setword(str)
#word = str
end
def tag(str)
#tag = str
end
end
# Object used to emulate a sentence in text.
class Sentence
def initialize(statement)
#sentence = statement
statement = statement.chop
statement = statement.downcase
lst = statement.split(" ")
#words = []
for elem in lst
#words << Word.new(elem)
end
if #sentence[#sentence.length-1] == "?"
#question = true
else
#question = false
end
end
def addword(str)
#words << Word.new(str)
end
def addword_to_place(str, i)
#words.insert(i, Word.new(str))
end
def set_word(i, other)
#words[i].setword(other)
end
def [](i)
#words[i].get()
end
def length
#words.length
end
def addpunc(symbol)
#words[self.length-1].setword(#words[self.length-1].get << symbol)
end
def checkforword(str)
for elem in #words
if elem.get == str
return true
end
end
return false
end
end
end
in Rubymine, I will try the following in the Irb console:
/usr/bin/ruby -e $stdout.sync=true;$stderr.sync=true;load($0=ARGV.shift) /usr/bin/irb --prompt simple
Switch to inspect mode.
>> require 'Sentencemate'
=> true
>> varfortesting = Sentence.new("The moon is red.")
NameError: uninitialized constant Sentence
from (irb):2
from /usr/bin/irb:12:in `<top (required)>'
from -e:1:in `load'
from -e:1:in `<main>'
What would be the proper way to be able to use the classes in the gem that I installed?
In your Sentence class
#words << Word.new(elem)
Word is resolved correctly because ruby looks in current namespace first (that being Sentencemate module).
Outside of that module, one has to use fully-qualified names, such as Sentencemate::Word. This is necessary to differentiate this Word from a dozen other Word classes user's app might have.
trying to pick up ruby through this programming ruby site and i'm stuck on this syntax
class SongList
def initialize
#songs = Array.new
end
def append(aSong)
#songs.push(aSong)
self
end
def deleteFirst
#songs.shift
end
def deleteLast
#songs.pop
end
end
When i go to add a song...
list = SongList.new
list.append(Song.new('title1', 'artist1', 1))
I get this error message:
NameError: uninitialized constant Song ...Programming Ruby
I saw that i need to require the variable Song, but I'm not sure where to do it within the SongList class....
You can use Ruby Struct class :
A Struct is a convenient way to bundle a number of attributes together, using accessor methods, without having to write an explicit class.
class SongList
def initialize
#songs = [] # use [] instead of Array.new
end
def append(aSong)
#songs.push(aSong)
self
end
def delete_first
#songs.shift
end
def delete_last
#songs.pop
end
end
Song = Struct.new(:song_name, :singer, :var)
list = SongList.new
list.append(Song.new('title1', 'artist1', 1))
# => #<SongList:0x9763870
# #songs=[#<struct Song song_name="title1", singer="artist1", var=1>]> var=1>]>
Is there any way to get hold of the invoked object inside the block thats being called. For instance, is there any way for the blocks to get access to the scope of the method batman or the class SuperHeros
class SuperHeros
attr_accessor :news
def initialize
#news = []
end
def batman task
puts "Batman: #{task} - done"
yield "feed cat"
#news << task
end
end
cat_woman = lambda do |task|
puts "Cat Woman: #{task} - done"
# invoker.news << task
end
robin = lambda do |task|
puts "Robin: #{task} - done"
# invoker.news << task
end
characters = SuperHeros.new
characters.batman("kick Joker's ass", &cat_woman)
characters.batman("break Bane's bones", &robin)
You can use something similar to Instance eval with delegation pattern, used - for example - in Savon gem:
def batman(task, &block)
#original_self = eval('self', block.binding)
puts "Batman: #{task} - done"
instance_exec('feed cat', &block)
#news << task
end
private
def method_missing(method, *args, &block)
if #original_self
#original_self.send(method, *args, &block)
else
super
end
end
In this approach, when you call method (with implicit receiver) inside block passed into batman method, it's called in the context of SuperHeros instance. If there is no such method available, the call goes (through method_missing) to original block self.
The simplest way to get the receiver object inside a block is assigning the object to an instance variable.
This example illustrate more clearly how the lambdas cat_woman and robin can access to attributes of the receiver objects of blocks:
class SuperHeros
attr_accessor :news, :name, :current_task
def initialize(a_name)
#name = a_name
#news = []
end
def batman(task)
puts "Inside the method batman of #{name}: #{task} in progress ..."
#current_task = task
yield
#news << task
end
end
cat_woman = lambda do |extra_task|
puts "cat_woman even #{extra_task} before doing #{#caller_obj.current_task}"
puts "Cat Woman: #{#caller_obj.current_task} - done by #{#caller_obj.name}"
# invoker.news << task
end
robin = lambda do |extra_task|
puts "robin even #{extra_task} before doing #{#caller_obj.current_task}"
puts "Robin: #{#caller_obj.current_task} - done by #{#caller_obj.name}"
end
character_1 = SuperHeros.new('batman_1')
(#caller_obj = character_1).batman("kick Joker's ass") { cat_woman['eats some burger'] }
puts
character_2 = SuperHeros.new('batman_2')
(#caller_obj = character_2).batman("break Bane's bones") { robin['drinks some beer'] }
The output will be:
Inside the method batman of batman_1: kick Joker's ass in progress ...
cat_woman even eats some burger before doing kick Joker's ass
Cat Woman: kick Joker's ass - done by batman_1
Inside the method batman of batman_2: break Bane's bones in progress ...
robin even drinks some beer before doing break Bane's bones
Robin: break Bane's bones - done by batman_2
My first thoughts are some thing like this:
class AbstractBuilder
attr_reader :time_taken
def build_with_timer
started_at = Time.now
build
#time_taken = Time.now - started_at
end
def build
raise 'Implement this method in a subclass'
end
end
class MyBuilder < AbstractBuilder
def build
sleep(5)
end
end
builder = MyBuilder.new.build_with_timer
puts builder.time_taken
I would suspect there is a better way which offers better flexibility, for example ideally I'd like to call 'build' on an instance of MyBuilder instead of 'build_with_timer' and always have the execution time recorded.
I did consider using alias_method from initialize or even using a module mixin instead of class inheritance which would override the build method calling super in the middle (not sure if that would work). Before I go down the rabbit hole I thought I'd see if there is an established practice.
I had a stab at a version to achieve what you want. This version doesn't require the subclass to have any extra code either.
class AbstractBuilder
##disable_override = false
def before_method
puts "before"
end
def after_method
puts "after"
end
def self.method_added name
unless ##disable_override
if name == :build
##disable_override = true # to stop the new build method
self.send :alias_method, :sub_build, :build
self.send :remove_method, :build
self.send :define_method, :build do
before_method
sub_build
after_method
end
##disable_override = false
else
puts "defining other method #{name}"
end
end
end
end
class MyBuilder < AbstractBuilder
def build
puts "starting build"
sleep(5)
puts "built."
end
def unnaffected_method
# this method won't get redefined
end
end
b = MyBuilder.new
b.build
Outputs
defining other method unnaffected_method
before
starting build
built.
after
I'd play with alias_method:
module Timeable
def time_methods *meths
meths.each do |meth|
alias_method "old_#{meth}", meth
define_method meth do |*args|
started_at = Time.now
res = send "old_#{meth}", *args
puts "Execution took %f seconds" % (Time.now - started_at)
res
end
end
end
end
class Foo
def bar str
puts str
end
end
Foo.extend Timeable
Foo.time_methods :bar
Foo.new.bar('asd')
#=>asd
#=>Execution took 0.000050 seconds
Sounds like you're looking for hooks into object lifecycle events. You'll have to build this into your base object and provide a little DSL -- I'm thinking you're after something like ActiveRecord Callbacks. Here's how we might modify your example to allow something like that:
class AbstractBuilder
attr_reader :time_taken
def construct! # i.e., build, and also call your hooks
##prebuild.each { |sym| self.send(sym) }
build
##postbuild.each { |sym| self.send(sym) }
end
def construct_with_timer
started_at = Time.now
construct!
#time_taken = Time.now - started_at
puts "!!! Build time: ##time_taken"
end
class << self
def before_build(fn); ##prebuild ||= []; ##prebuild << fn; end
def after_build(fn); ##postbuild ||= []; ##postbuild << fn; end
end
end
class MyBuilder < AbstractBuilder
before_build :preprocess
after_build :postprocess
def build; puts "BUILDING"; sleep(3); end
def preprocess; puts "Preparing to build..."; end
def postprocess; puts "Done building. Thank you for waiting."; end
end
builder = MyBuilder.new
builder.construct_with_timer
# => Preparing to build...
# => BUILDING
# => Done building. Thank you for waiting.
# => !!! Build time: 3.000119
This is a textbook-definition use case for Aspect-Oriented Programming. It generally offers a cleaner separation of concerns. In this arena, Ruby offers Aquarium and AspectR. However, you may not want to add another dependency to your project. As such, you might still consider using one of the other approaches.