Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Writing Your Own Ruby DSL - Tropical Ruby 2015

Writing Your Own Ruby DSL - Tropical Ruby 2015

In this talk we'll go through some concepts on the Ruby language that will help us creating our own custom Domain Specific Language. It's a very hands-on talk. We'll play with some Ruby concepts like dynamic methods, blocks, procs and self, but our main goal is to write (and learn how) our own Pizza Cookbook DSL

2395c715fd3c144ab7bf0a9de682f891?s=128

Ricardo Nacif

March 06, 2015
Tweet

Transcript

  1. Writing Your Own Ruby DSL March/2015

  2. Ricardo Nacif

  3. Automation QA Engineer at Avenue Code

  4. Rails Developer

  5. Bad time with Developers

  6. None
  7. None
  8. None
  9. None
  10. None
  11. None
  12. Mineirês

  13. UAI

  14. None
  15. Po pô pó?

  16. Pó pô

  17. Oncetá?

  18. Cadiquê?

  19. Nó!

  20. Nossa Senhora de Perpétuo Socorro

  21. DSL

  22. Domain-Specific Language

  23. Linguagem de programação focada em um domínio/tarefa específico

  24. DSL vs GPL

  25. Remove código desnecessário ao realizar uma tarefa

  26. Aumenta legibilidade

  27. driver = Selenium::WebDriver.for :chrome driver.navigate.to "google.com" input = driver.find_element(:name, 'q')

    input.send_keys('ruby') button = driver.find_element(:name, 'lst-ib') button.click #capybara visit 'google.com' fill_in 'Search', with: 'ruby' click_button 'Google Search'
  28. driver = Selenium::WebDriver.for :chrome driver.navigate.to "google.com" input = driver.find_element(:name, 'q')

    input.send_keys('ruby') button = driver.find_element(:name, 'lst-ib') button.click #capybara visit 'google.com' fill_in 'Search', with: 'ruby' click_button 'Google Search'
  29. External Internal

  30. External

  31. HTML <html> <head> <title></title> </head> <body> </body> </html>

  32. CSS html, body { width: 100%; height: 100%; overflow: hidden;

    }
  33. Gherkin (Cucumber) Feature: Serve coffee Coffee should not be served

    until paid for Coffee should not be served until the button has been pressed If there is no coffee left then money should be refunded Scenario: Buy last coffee Given there are 1 coffees left in the machine And I have deposited 1$ When I press the coffee button Then I should be served a coffee
  34. Internal

  35. RSpec # these two expectations... expect(alphabet).to start_with("a") expect(alphabet).to end_with("z") #

    ...can be combined into one expression: expect(alphabet).to start_with("a").and end_with( "z")
  36. Factory Girl FactoryGirl.define do factory :user do first_name "John" last_name

    "Doe" admin false end factory :admin, class: User do first_name "Admin" last_name "User" admin true end end
  37. Writing your own Ruby DSL

  38. yield struct instance_eval self procs send lambda class method_missing blocks

  39. Pizza Cookbook DSL

  40. Quit the bullshit, let’s code!

  41. new_pizza 'Frango Catupiry' do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'catupiry'

    add_topping 'chicken' end
  42. new_pizza('Frango Catupiry’) do add_vegetable(‘tomatoes') add_sauce('barbecue') add_cheese('catupiry') add_topping(‘chicken') end

  43. block yield struct

  44. block [1, 2, 3].each do |n| puts n end #=>

    1 #=> 2 #=> 3
  45. yield def execute_this_code yield end execute_this_code { puts 'Hey guys!'

    } #=> Hey guys!
  46. class User attr_accessor :name, :age def initialize name, age @name

    = name @age = age end end User = Struct.new(:name, :age) struct
  47. new_pizza 'Frango Catupiry' do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'catupiry'

    add_topping 'chicken' end
  48. Pizza = Struct.new(:name, :vegetable, :cheese, :sauce, :topping) def add_vegetable name

    @pizza.vegetable = name end #def add_sauce, add_cheese... def create_pizza name @pizza = Pizza.new(name) yield if block_given? end new_pizza 'Frango Catupiry' do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'catupiry' add_topping 'chicken' end
  49. Pizza = Struct.new(:name, :vegetable, :cheese, :sauce, :topping) def add_vegetable name

    @pizza.vegetable = name end #def add_sauce, add_cheese... def create_pizza name @pizza = Pizza.new(name) yield if block_given? end new_pizza 'Frango Catupiry' do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'catupiry' add_topping 'chicken' end
  50. Pizza = Struct.new(:name, :vegetable, :cheese, :sauce, :topping) def add_vegetable name

    @pizza.vegetable = name end #def add_sauce, add_cheese... def create_pizza name @pizza = Pizza.new(name) yield if block_given? end new_pizza 'Frango Catupiry' do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'catupiry' add_topping 'chicken' end
  51. Pizza = Struct.new(:name, :vegetable, :cheese, :sauce, :topping) def add_vegetable name

    @pizza.vegetable = name end #def add_sauce, add_cheese... def create_pizza name @pizza = Pizza.new(name) yield if block_given? end new_pizza 'Frango Catupiry' do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'catupiry' add_topping 'chicken' end
  52. Pizza = Struct.new(:name, :vegetable, :cheese, :sauce, :topping) def add_vegetable name

    @pizza.vegetable = name end #def add_sauce, add_cheese... def create_pizza name @pizza = Pizza.new(name) yield if block_given? end new_pizza 'Frango Catupiry' do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'catupiry' add_topping 'chicken' end
  53. 2 grandes problemas

  54. poluindo o namespace global

  55. só um topping?

  56. Cookbook.new_pizza "X-Tudo" do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'cheddar' add_toppings

    'chicken', 'peperoni', 'bacon' end
  57. Cookbook.new_pizza "X-Tudo" do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'cheddar' add_toppings

    'chicken', 'peperoni', 'bacon' end
  58. Cookbook.new_pizza "X-Tudo" do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'cheddar' add_toppings

    'chicken', 'peperoni', 'bacon' end
  59. multiple arguments

  60. multiple arguments def my_method *args puts args end my_method 1,

    2, '3' #=> [1, 2, '3']
  61. Proc/lambda times_two = Proc.new do |n| n * 2 end

    times_two = lambda do |n| n * 2 end times_two.call(2) #=> 4
  62. ampersand square = Proc.new do |n| n ** 2 end

    [1, 2].each(square) #won't work [1, 2].each(&square)#will convert #the proc to a block
  63. ampersand square = Proc.new do |n| n ** 2 end

    [1, 2].each(square) #won't work [1, 2].each(&square)#will convert #the proc to a block
  64. class Alphabet def self.each_letter(&block) #converts block into proc ('A'..'Z').each(&block) #converts

    proc into block end end Alphabet.each_letter { |l| puts l } ampersand
  65. class Alphabet def self.each_letter(&block) #converts block into proc ('A'..'Z').each(&block) #converts

    proc into block end end Alphabet.each_letter { |l| puts l } ampersand
  66. class Alphabet def self.each_letter(&block) #converts block into proc ('A'..'Z').each(&block) #converts

    proc into block end end Alphabet.each_letter { |l| puts l } ampersand
  67. class Alphabet def self.each_letter(&block) #converts block into proc ('A'..'Z').each(&block) #converts

    proc into block end end Alphabet.each_letter { |l| puts l } #=> 'A' #=> 'B' #=> 'C' ampersand
  68. instance_eval

  69. instance_eval class User attr_accessor :name end user = User.new user.name

    #=> nil user.instance_eval do @name = 'My new name' end user.name #=> 'My new name'
  70. instance_eval class User attr_accessor :name end user = User.new user.name

    #=> nil user.instance_eval do @name = 'My new name' end user.name #=> 'My new name'
  71. instance_eval class User attr_accessor :name end user = User.new user.name

    #=> nil user.instance_eval do @name = 'My new name' end user.name #=> 'My new name'
  72. instance_eval class User attr_accessor :name end user = User.new user.name

    #=> nil user.instance_eval do @name = 'My new name' end user.name #=> 'My new name'
  73. instance_eval class User attr_accessor :name end user = User.new user.name

    #=> nil user.instance_eval do @name = 'My new name' end user.name #=> 'My new name'
  74. Cookbook.new_pizza "X-Tudo" do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'cheddar' add_toppings

    'chicken', 'peperoni', 'bacon' end
  75. #pizza.rb class Pizza attr_accessor :name, :vegetable, :cheese, :sauce, :toppings def

    initialize name @name = name end def add_vegetable name @vegetable = name end def add_sauce name @sauce = name end def add_cheese name @cheese = name end def add_toppings *args raise "Too many toppings man!" if args.size > 4 @toppings = args end end
  76. #pizza.rb class Pizza attr_accessor :name, :vegetable, :cheese, :sauce, :toppings def

    initialize name @name = name end def add_vegetable name @vegetable = name end def add_sauce name @sauce = name end def add_cheese name @cheese = name end def add_toppings *args raise "Too many toppings man!" if args.size > 4 @toppings = args end end
  77. #pizza.rb class Pizza #... def add_toppings *args raise "Too many

    toppings man!" if args.size > 4 @toppings = args end end
  78. #cookbook.rb class Cookbook @pizzas = [] def self.new_pizza name, &block

    pizza = Pizza.new(name) pizza.instance_eval(&block) if block_given? @pizzas << pizza end def self.pizzas @pizzas end end
  79. #cookbook.rb class Cookbook @pizzas = [] def self.new_pizza name, &block

    pizza = Pizza.new(name) pizza.instance_eval(&block) if block_given? @pizzas << pizza end def self.pizzas @pizzas end end
  80. #cookbook.rb class Cookbook @pizzas = [] def self.new_pizza name, &block

    pizza = Pizza.new(name) pizza.instance_eval(&block) if block_given? @pizzas << pizza end def self.pizzas @pizzas end end
  81. #cookbook.rb class Cookbook @pizzas = [] def self.new_pizza name, &block

    pizza = Pizza.new(name) pizza.instance_eval(&block) if block_given? @pizzas << pizza end def self.pizzas @pizzas end end
  82. Cookbook.new_pizza "X-Tudo" do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'cheddar' add_toppings

    'chicken', 'peperoni', 'bacon' end
  83. DRY?

  84. #pizza.rb class Pizza # ... def add_vegetable name @vegetable =

    name end def add_sauce name @sauce = name end def add_cheese name @cheese = name end # ... end
  85. #pizza.rb class Pizza # ... def add_vegetable name @vegetable =

    name end def add_sauce name @sauce = name end def add_cheese name @cheese = name end # ... end
  86. self send method_missing

  87. None
  88. self

  89. class User attr_accessor :name def forget_my_name! self.name = nil end

    end user = User.new user.name = 'Joao' user.name #=> 'Joao' user.forget_my_name! user.name #=> nil
  90. class User attr_accessor :name def forget_my_name! self.name = nil end

    end user = User.new user.name = 'Joao' user.name #=> 'Joao' user.forget_my_name! user.name #=> nil
  91. class User attr_accessor :name def forget_my_name! self.name = nil end

    end user = User.new user.name = 'Joao' user.name #=> 'Joao' user.forget_my_name! user.name #=> nil
  92. class User attr_accessor :name def forget_my_name! self.name = nil end

    end user = User.new user.name = 'Joao' user.name #=> 'Joao' user.forget_my_name! user.name #=> nil
  93. class User attr_accessor :name def forget_my_name! self.name = nil end

    end user = User.new user.name = 'Joao' user.name #=> 'Joao' user.forget_my_name! user.name #=> nil
  94. class User attr_accessor :name def forget_my_name! self.name = nil end

    end user = User.new user.name = 'Joao' user.name #=> 'Joao' user.forget_my_name! user.name #=> nil
  95. None
  96. self

  97. None
  98. send

  99. user = User.new user.name = 'Joao' user.name #=> "Joao" user.send

    'name' #=> "Joao" user.send 'name=', 'Pedro' user.name #=> Pedro
  100. user = User.new user.name = 'Joao' user.name #=> "Joao" user.send

    'name' #=> "Joao" user.send 'name=', 'Pedro' user.name #=> Pedro
  101. user = User.new user.name = 'Joao' user.name #=> "Joao" user.send

    'name' #=> "Joao" user.send 'name=', 'Pedro' user.name #=> Pedro
  102. user = User.new user.name = 'Joao' user.name #=> "Joao" user.send

    'name' #=> "Joao" user.send 'name=', 'Pedro' user.name #=> Pedro
  103. method_missing

  104. class Comment def method_missing(method_name, *args) puts "Voce tentou chamar o

    metodo #{method_name} com os argumentos #{args}" end end comment = Comment.new comment.nao_existe_blabla 1, 3, '5' #=>Voce tentou chamar o metodo nao_existe_blabla com os argumentos [1,3,'5']
  105. class Comment def method_missing(method_name, *args) puts "Voce tentou chamar o

    metodo #{method_name} com os argumentos #{args}" end end comment = Comment.new comment.nao_existe_blabla 1, 3, '5' #=>Voce tentou chamar o metodo nao_existe_blabla com os argumentos [1,3,'5']
  106. class Comment def method_missing(method_name, *args) puts "Voce tentou chamar o

    metodo #{method_name} com os argumentos #{args}" end end comment = Comment.new comment.nao_existe_blabla 1, 3, '5' #=>Voce tentou chamar o metodo nao_existe_blabla com os argumentos [1,3,'5']
  107. #pizza.rb class Pizza # ... def add_vegetable name @vegetable =

    name end def add_sauce name @sauce = name end def add_cheese name @cheese = name end # ... end
  108. class Pizza #... private def method_missing method_sym, *args method_name =

    method_sym.to_s if method_name.start_with? "add_" attribute_name = method_name.split(/^add_/)[1] self.send(attribute_name + "=", args[0]) else super end end end
  109. class Pizza #... private def method_missing method_sym, *args method_name =

    method_sym.to_s if method_name.start_with? "add_" attribute_name = method_name.split(/^add_/)[1] self.send(attribute_name + "=", args[0]) else super end end end
  110. class Pizza #... private def method_missing method_sym, *args method_name =

    method_sym.to_s if method_name.start_with? "add_" attribute_name = method_name.split(/^add_/)[1] self.send(attribute_name + "=", args[0]) else super end end end
  111. class Pizza #... private def method_missing method_sym, *args method_name =

    method_sym.to_s if method_name.start_with? "add_" attribute_name = method_name.split(/^add_/)[1] self.send(attribute_name + "=", args[0]) else super end end end
  112. class Pizza #... private def method_missing method_sym, *args method_name =

    method_sym.to_s if method_name.start_with? "add_" attribute_name = method_name.split(/^add_/)[1] self.send(attribute_name + "=", args[0]) else super end end end
  113. class Pizza #... private def method_missing method_sym, *args method_name =

    method_sym.to_s if method_name.start_with? "add_" attribute_name = method_name.split(/^add_/)[1] self.send(attribute_name + "=", args[0]) else super end end end
  114. class Pizza #... private def method_missing method_sym, *args method_name =

    method_sym.to_s if method_name.start_with? "add_" attribute_name = method_name.split(/^add_/)[1] self.send(attribute_name + "=", args[0]) #pizza.cheese= 'cheddar' else super end end end
  115. #pizza.rb class Pizza # ... def add_vegetable name @vegetable =

    name end def add_sauce name @sauce = name end def add_cheese name @cheese = name end # ... end
  116. class Pizza ... private def method_missing method_sym, *args method_name =

    method_sym.to_s if method_name.start_with? "add_" attribute_name = method_name.split(/^add_/)[1] self.send(attribute_name + "=", args[0]) else super end end end
  117. Cookbook.new_pizza "X-Tudo" do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'cheddar' add_toppings

    'chicken', 'peperoni', 'bacon' end
  118. RSpec it "should start with 'a'" do expect(alphabet).to start_with("a") end

  119. Cookbook.new_pizza "Frango Catupiry" do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'catupiry'

    add_toppings 'chicken', 'bacon' bake_for(15.minutes).on(:high_temperature) end
  120. #bake_time.rb class BakeTime attr_accessor :duration, :temperature def initialize duration @duration

    = duration end def on temperature @temperature = temperature end end
  121. #bake_time.rb class BakeTime attr_accessor :duration, :temperature def initialize duration @duration

    = duration end def on temperature @temperature = temperature end end
  122. #bake_time.rb class BakeTime attr_accessor :duration, :temperature def initialize duration @duration

    = duration end def on temperature @temperature = temperature end end
  123. #bake_time.rb class BakeTime attr_accessor :duration, :temperature def initialize duration @duration

    = duration end def on temperature @temperature = temperature end end
  124. class Pizza attr_accessor :bake_time, #:cheese... def bake_for duration @bake_time =

    BakeTime.new(duration) end #... end
  125. class Pizza attr_accessor :bake_time, #:cheese... def bake_for duration @bake_time =

    BakeTime.new(duration) end #... end
  126. Cookbook.new_pizza "Frango Catupiry" do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'catupiry'

    add_toppings 'chicken', 'bacon' bake_for(15.minutes).on(:high_temperature) end
  127. Cookbook.new_pizza "Frango Catupiry" do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'catupiry'

    add_toppings 'chicken', 'bacon' bake_for(15.minutes).on(:high_temperature) end
  128. Cookbook.new_pizza "Frango Catupiry" do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'catupiry'

    add_toppings 'chicken', 'bacon' bake_for(15.minutes).on(:high_temperature) end
  129. Cookbook.new_pizza "Frango Catupiry" do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'catupiry'

    add_toppings 'chicken', 'bacon' bake_for(15.minutes).on(:high_temperature) end
  130. Cookbook.new_pizza "Frango Catupiry" do add_vegetable 'tomatoes' add_sauce 'barbecue' add_cheese 'catupiry'

    add_toppings 'chicken', 'bacon' bake_for(15.minutes).on(:high_temperature) end
  131. github.com/Ricardonacif/ pizza_cookbook_dsl

  132. Obrigado! @ricardonacif nacif.ricardo@gmail.com