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.
= 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
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
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
# 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
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
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
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
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
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
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
@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
@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
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
@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
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
@__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
@__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
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
@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
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
@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
@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
@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
@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
@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
@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
= 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?
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?
= 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
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
"./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
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
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
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
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
"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)
"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