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

Ruby Ate My DSL!

E06aa8f63d2a1753a2b352bc1cabbde2?s=47 Daniel Azuma
November 18, 2019

Ruby Ate My DSL!

Presentation by Daniel Azuma at RubyConf 2019. Covers how DSLs can clash with "normal" Ruby code, and techniques for hardening a DSL against such conflicts.

E06aa8f63d2a1753a2b352bc1cabbde2?s=128

Daniel Azuma

November 18, 2019
Tweet

Transcript

  1. ruby ate my dsl! Daniel Azuma Google Cloud Platform Nov

    18, 2019
  2. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  3. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  4. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  5. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  6. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end LocalJumpError: no block given (yield) .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' shell_app.rb:7:in `block in <main>' .../lib/sinatra/base.rb:1635:in `call' .../lib/sinatra/base.rb:1635:in `block in compile!' .../lib/sinatra/base.rb:987:in `block (3 levels) in route!' .../lib/sinatra/base.rb:1006:in `route_eval' .../lib/sinatra/base.rb:987:in `block (2 levels) in route!' .../lib/sinatra/base.rb:1035:in `block in process_route' .../lib/sinatra/base.rb:1033:in `catch' .../lib/sinatra/base.rb:1033:in `process_route' .../lib/sinatra/base.rb:985:in `block in route!' .../lib/sinatra/base.rb:984:in `each' .../lib/sinatra/base.rb:984:in `route!' .../lib/sinatra/base.rb:1097:in `block in dispatch!' .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' .../lib/sinatra/base.rb:1094:in `dispatch!' .../lib/sinatra/base.rb:919:in `block in call!' ...
  7. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end LocalJumpError: no block given (yield) .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' shell_app.rb:7:in `block in <main>' .../lib/sinatra/base.rb:1635:in `call' .../lib/sinatra/base.rb:1635:in `block in compile!' .../lib/sinatra/base.rb:987:in `block (3 levels) in route!' .../lib/sinatra/base.rb:1006:in `route_eval' .../lib/sinatra/base.rb:987:in `block (2 levels) in route!' .../lib/sinatra/base.rb:1035:in `block in process_route' .../lib/sinatra/base.rb:1033:in `catch' .../lib/sinatra/base.rb:1033:in `process_route' .../lib/sinatra/base.rb:985:in `block in route!' .../lib/sinatra/base.rb:984:in `each' .../lib/sinatra/base.rb:984:in `route!' .../lib/sinatra/base.rb:1097:in `block in dispatch!' .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' .../lib/sinatra/base.rb:1094:in `dispatch!' .../lib/sinatra/base.rb:919:in `block in call!' ...
  8. Domain- Specific Languages

  9. Daniel Azuma @danielazuma https://daniel-azuma.com/

  10. Daniel Azuma @danielazuma https://daniel-azuma.com/

  11. DSL:

  12. DSL: A set of “bare” methods that are not part

    of core Ruby
  13. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  14. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end
  15. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end method added to the “main” object
  16. None
  17. # routing.rb Rails.application.routes.draw do resources :brands, only: [:index, :show] do

    resources :products, only: [:index, :show] end resource :basket, only: [:show, :update, :destroy] resolve("Basket") { route_for(:basket) } end
  18. # routing.rb Rails.application.routes.draw do resources :brands, only: [:index, :show] do

    resources :products, only: [:index, :show] end resource :basket, only: [:show, :update, :destroy] resolve("Basket") { route_for(:basket) } end methods of the class ActionDispatch::Routing::Mapper
  19. # routing.rb Rails.application.routes.draw do resources :brands, only: [:index, :show] do

    resources :products, only: [:index, :show] end resource :basket, only: [:show, :update, :destroy] resolve("Basket") { route_for(:basket) } end methods of the class ActionDispatch::Routing::Mapper uses instance_eval to set self to a mapper instance
  20. DSL howto: • Add methods to an existing object •

    Change “self” within a block
  21. It’s Still Ruby!

  22. “nanospec”

  23. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  24. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end “describe” block
  25. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end “describe” block spec
  26. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end # nanospec.rb
  27. # nanospec.rb module Kernel def describe name, &block end end

    # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  28. # nanospec.rb class Entity def initialize name @name = name

    end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity end end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  29. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end # nanospec.rb class Entity def initialize name @name = name end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity end end
  30. # nanospec.rb class Entity def initialize name @name = name

    end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  31. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end # nanospec.rb class Entity def initialize name @name = name end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end
  32. # nanospec.rb class Entity def initialize name @name = name

    end def it name, &block end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  33. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end # nanospec.rb class Entity def initialize name @name = name @specs = {} end def it name, &block @specs[name] = block end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end
  34. # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  35. # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  36. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end # nanospec.rb class Entity def initialize name @name = name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end
  37. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do @name = "Ruby" puts "My name is #{@name}" end it "sleeps" do puts "zzz" end end
  38. # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do @name = "Ruby" puts "My name is #{@name}" end it "sleeps" do puts "zzz" end end
  39. # shell_app.rb require "sinatra" require "json" get "/" do output

    = invoke format_response output end def invoke `echo "this is the output"` end def format_response output JSON.dump({"output" => output}) end LocalJumpError: no block given (yield) .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' shell_app.rb:7:in `block in <main>' .../lib/sinatra/base.rb:1635:in `call' .../lib/sinatra/base.rb:1635:in `block in compile!' .../lib/sinatra/base.rb:987:in `block (3 levels) in route!' .../lib/sinatra/base.rb:1006:in `route_eval' .../lib/sinatra/base.rb:987:in `block (2 levels) in route!' .../lib/sinatra/base.rb:1035:in `block in process_route' .../lib/sinatra/base.rb:1033:in `catch' .../lib/sinatra/base.rb:1033:in `process_route' .../lib/sinatra/base.rb:985:in `block in route!' .../lib/sinatra/base.rb:984:in `each' .../lib/sinatra/base.rb:984:in `route!' .../lib/sinatra/base.rb:1097:in `block in dispatch!' .../lib/sinatra/base.rb:1071:in `block in invoke' .../lib/sinatra/base.rb:1071:in `catch' .../lib/sinatra/base.rb:1071:in `invoke' .../lib/sinatra/base.rb:1094:in `dispatch!' .../lib/sinatra/base.rb:919:in `block in call!' ...
  40. DSL

  41. TIP: Use a naming convention for instance variables and private

    methods
  42. TIP: Use a naming convention for instance variables and private

    methods # nanospec.rb class Entity def initialize name @name = name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end
  43. # nanospec.rb class Entity def initialize name @__name = name

    @__specs = {} end def it name, &block @__specs[name] = block end def check @__specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end TIP: Use a naming convention for instance variables and private methods
  44. # nanospec.rb class Entity def initialize name @__name = name

    @__specs = {} end def it name, &block @__specs[name] = block end def __check @__specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end TIP: Use a naming convention for instance variables and private methods
  45. With a DSL... There’s no such thing as “private”

  46. TIP: Delegate implementation to a separate object

  47. TIP: # nanospec.rb class Entity def initialize name @name =

    name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end both the DSL and its implementation Delegate implementation to a separate object
  48. # nanospec.rb class EntityImpl def initialize name @name = name

    @specs = {} end def add name, block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end TIP: class EntityDSL def initialize impl @__impl = impl end def it name, &block @__impl.add name, block end end module Kernel def describe name, &block entity = EntityImpl.new name (@entities ||= []) << entity entity_dsl = EntityDSL.new entity entity_dsl.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end DSL only Implementation only Delegate implementation to a separate object
  49. TIP: class EntityDSL def initialize impl @__impl = impl end

    def it name, &block @__impl.add name, block end end module Kernel def describe name, &block entity = EntityImpl.new name (@entities ||= []) << entity entity_dsl = EntityDSL.new entity entity_dsl.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end DSL only # nanospec.rb class EntityImpl def initialize name @name = name @specs = {} end def add name, block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end Implementation only Delegate implementation to a separate object
  50. # nanospec.rb class EntityImpl def initialize name @name = name

    @specs = {} end def add name, block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end Implementation only TIP: class EntityDSL def initialize impl @__impl = impl end def it name, &block @__impl.add name, block end end module Kernel def describe name, &block entity = EntityImpl.new name (@entities ||= []) << entity entity_dsl = EntityDSL.new entity entity_dsl.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end DSL only Delegate implementation to a separate object
  51. # nanospec.rb class EntityImpl def initialize name @name = name

    @specs = {} end def add name, block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end TIP: class EntityDSL def initialize impl @__impl = impl end def it name, &block @__impl.add name, block end end module Kernel def describe name, &block entity = EntityImpl.new name (@entities ||= []) << entity entity_dsl = EntityDSL.new entity entity_dsl.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end DSL only Implementation only Delegate implementation to a separate object
  52. DSL

  53. DSL ❤

  54. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  55. # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module Kernel def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end at_exit do @entities.each { |entity| entity.check } end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  56. # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module NanospecDSL def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end extend NanospecDSL at_exit do @entities.each { |entity| entity.check } end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  57. # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end module NanospecDSL def describe name, &block entity = Entity.new name (@entities ||= []) << entity entity.instance_eval &block end end extend NanospecDSL at_exit do @entities.each { |entity| entity.check } end # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end TIP: Add methods only where needed
  58. DSL ❤

  59. Helper methods

  60. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hi" end end describe "bar" do it "speaks" do puts "hello" end it "sleeps" do puts "zzz" end end
  61. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    puts "hello" end end
  62. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end end def try_speaking puts "testing 1...2...3..." end
  63. private method of Object # nanospec_test.rb require "./nanospec.rb" describe "foo"

    do it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end
  64. # nanospec.rb class Entity def initialize name @name = name

    @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end ... subclass of Object # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end
  65. # nanospec.rb class Entity < BasicObject def initialize name @name

    = name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end ... # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end BasicObject?
  66. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end end def try_speaking puts "testing 1...2...3..." end FAIL # nanospec.rb class Entity < BasicObject def initialize name @name = name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end ... BasicObject?
  67. # nanospec.rb class Entity < BasicObject def initialize name @name

    = name @specs = {} end def it name, &block @specs[name] = block end def check @specs.each do |spec, block| puts "** #{@name} #{spec}" block.call end end end ... # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end TIP: BasicObject is usually NOT an effective base class for a DSL
  68. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end end def try_speaking puts "testing 1...2...3..." end
  69. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end def try_speaking puts "testing 1...2...3..." end end
  70. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end def try_speaking puts "testing 1...2...3..." end end TIP: Design DSLs to honor lexical scoping
  71. # nanospec_test.rb require "./nanospec.rb" describe "foo" do it "speaks" do

    try_speaking end def try_speaking puts "testing 1...2...3..." end end TIP: Design DSLs to honor lexical scoping method of singleton class block executed with instance_eval
  72. TIP: Design DSLs to honor lexical scoping # nanospec_test.rb require

    "./nanospec.rb" describe "foo" do it "speaks" do try_speaking end def try_speaking puts "testing 1...2...3..." end end describe "bar" do it "speaks" do try_speaking end def try_speaking puts "I'm a different object." end end separate scopes
  73. # nanospec_test.rb require "./nanospec.rb" describe "outer" do describe "inner" do

    it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end end TIP: Design DSLs to honor lexical scoping nested block
  74. # nanospec_test.rb require "./nanospec.rb" describe "outer" do describe "inner" do

    it "speaks" do try_speaking end end def try_speaking puts "testing 1...2...3..." end end TIP: Design DSLs to honor lexical scoping delegate methods
  75. # nanospec_test.rb require "./nanospec.rb" describe "bar" do it "speaks" do

    try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end
  76. Each “describe” block creates a class # nanospec_test.rb require "./nanospec.rb"

    describe "bar" do it "speaks" do try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end TIP: Consider modeling blocks as classes
  77. Each test is executed within a separate instance # nanospec_test.rb

    require "./nanospec.rb" describe "bar" do it "speaks" do try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end TIP: Consider modeling blocks as classes
  78. # nanospec_test.rb require "./nanospec.rb" describe "bar" do it "speaks" do

    try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end Method of the described class TIP: Consider modeling blocks as classes
  79. # nanospec_test.rb require "./nanospec.rb" describe "bar" do it "speaks" do

    try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end Method of the described class Use class_eval instead of instance_eval TIP: Consider modeling blocks as classes
  80. TIP: Consider modeling blocks as classes # nanospec_test.rb require "./nanospec.rb"

    describe "bar" do it "speaks" do try_speaking end it "sleeps" do puts "zzz" end def try_speaking puts "testing 1...2...3..." end end
  81. Constants

  82. ≈ ≈ # nanospec_test.rb require "./nanospec.rb" describe "foo" do MESSAGE

    = "Hello world!" it "speaks" do puts MESSAGE end end describe "bar" do MESSAGE = "Hello RubyConf!" it "speaks" do puts MESSAGE end end
  83. ≈ # nanospec_test.rb require "./nanospec.rb" describe "foo" do MESSAGE =

    "Hello world!" it "speaks" do puts MESSAGE end end describe "bar" do MESSAGE = "Hello RubyConf!" it "speaks" do puts MESSAGE end end Constant redefined warning (both constants defined on Object)
  84. TIP: Provide alternatives to constants

  85. TIP: Provide alternatives to constants # rspec_example.rb require "rspec" describe

    "foo" do let(:message) { "Hello, world!" } it "speaks" do puts message end end describe "bar" do let(:message) { "Hello, RubyConf!" } it "speaks" do puts message end end
  86. None
  87. DSL

  88. Use a naming convention for instance variables and private methods

    Delegate implementation to a separate object DSL
  89. ❤ DSL

  90. Add methods only where needed BasicObject is usually NOT an

    effective base class for a DSL Design DSLs to honor lexical scoping Consider modeling blocks as classes ❤ DSL
  91. DSL

  92. DSL DSR (Domain-Specific Ruby)

  93. It’s Just Ruby!

  94. Daniel Azuma Google Cloud Platform @danielazuma https://daniel-azuma.com/ ruby ate my

    dsl!