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

Ruby Ate My DSL!

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.

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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 '

    .../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!'

    ...

    View Slide

  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 '

    .../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!'

    ...

    View Slide

  8. Domain-
    Specific
    Languages

    View Slide

  9. Daniel Azuma
    @danielazuma

    https://daniel-azuma.com/

    View Slide

  10. Daniel Azuma
    @danielazuma

    https://daniel-azuma.com/

    View Slide

  11. DSL:

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  16. View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  20. DSL howto:
    • Add methods to an existing object

    • Change “self” within a block

    View Slide

  21. It’s Still Ruby!

    View Slide

  22. “nanospec”

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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 '

    .../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!'

    ...

    View Slide

  40. DSL

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  45. With a DSL...
    There’s no such thing as “private”

    View Slide

  46. TIP: Delegate implementation to a
    separate object

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  52. DSL

    View Slide

  53. DSL

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  58. DSL

    View Slide

  59. Helper methods

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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?

    View Slide

  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?

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  81. Constants

    View Slide



  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

    View Slide


  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)

    View Slide

  84. TIP: Provide alternatives to constants

    View Slide

  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

    View Slide

  86. View Slide

  87. DSL

    View Slide

  88. Use a naming convention for instance
    variables and private methods
    Delegate implementation to a
    separate object
    DSL

    View Slide


  89. DSL

    View Slide

  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

    View Slide

  91. DSL

    View Slide

  92. DSL
    DSR
    (Domain-Specific Ruby)

    View Slide

  93. It’s Just Ruby!

    View Slide

  94. Daniel Azuma
    Google Cloud Platform
    @danielazuma

    https://daniel-azuma.com/
    ruby ate
    my dsl!

    View Slide