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

More Decks by Daniel Azuma

Other Decks in Programming

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!