Slide 1

Slide 1 text

Write your own DSL in Ruby Let's do some metaprogramming! Tom de Bruijn – mastodon.social/@tombruijn – tomdebruijn.com

Slide 2

Slide 2 text

What is a DSL? ? ? ?

Slide 3

Slide 3 text

Domain Speci fi c Language ? ? ? ? ?

Slide 4

Slide 4 text

A sub-language in Ruby *
 for a Domain A gem, or as part of an app * DSLs can also be made in other languages

Slide 5

Slide 5 text

# Rake example task :hello do puts "Hello there" end

Slide 6

Slide 6 text

$ rake hello Hello there

Slide 7

Slide 7 text

class Profile < TomsSchemaGem plugin LabelPlugin attribute :name, :label => "Name" attribute :socials do label "Social media" attribute :twitter, :label => "Twitter" attribute :mastodon, :label => "Mastodon" end end profile = Profile.new(...) profile.name.label # => "Name" profile.name.value # => "Tom de Bruijn" profile.socials.twitter.value # => "@tombruijn"

Slide 8

Slide 8 text

Why use a DSL?

Slide 9

Slide 9 text

Con fi guration

Slide 10

Slide 10 text

# config/application.rb module MyApp class Application < Rails::Application config.load_defaults 7.0 config.time_zone = "UTC" config.app.custom_option = "foo" end end

Slide 11

Slide 11 text

No YAML required 😌

Slide 12

Slide 12 text

Automate writing code Frameworks provide tools to simplify things

Slide 13

Slide 13 text

Rails.application.routes.draw do resources :posts do resources :comments end root "homepage#show" end

Slide 14

Slide 14 text

Rails.application.routes.draw do resources :posts do resources :comments end root "homepage#show" end resources :posts do resources :comments root "homepage#show"

Slide 15

Slide 15 text

Create routes resources :posts • GET /posts • GET /posts/:id • POST /posts • PUT /posts/:id • DELETE /posts/:id

Slide 16

Slide 16 text

Point to the right controller

Slide 17

Slide 17 text

class User < ApplicationRecord validates :name, :presence => true end

Slide 18

Slide 18 text

Validation validates :name, :presence => true • Validation on creation • Validation on update • Validation on demand • Storing error messages

Slide 19

Slide 19 text

No updating 5 locations for every change

Slide 20

Slide 20 text

Make high churn code more maintainable

Slide 21

Slide 21 text

case ARGV.first when "hello" puts "Hello world" when "bye" puts "Goodbye world" end # ruby tasks.rb hello task :hello do puts "Hello world" end task :bye do puts "Goodbye world" end # rake hello

Slide 22

Slide 22 text

How to write your own DSL?

Slide 23

Slide 23 text

config = Config.new( :verbose => true, :format => :documentation )

Slide 24

Slide 24 text

config = Config.new config.verbose = true config.format = :documentation

Slide 25

Slide 25 text

class Config attr_accessor :verbose, :format end config = Config.new config.verbose = true config.format = :documentation pp config # => #

Slide 26

Slide 26 text

class Config attr_accessor :verbose, :format end config = Config.new config.verbose = true config.format = :documentation pp config # => # attr_accessor :verbose, :format config.verbose = true config.format = :documentation # => #

Slide 27

Slide 27 text

config = Config.new config.verbose = true config.format = :documentation pp config.options # => {:verbose=>true, :format=>:documentation}

Slide 28

Slide 28 text

class Config attr_reader :options def initialize @options = {} end def verbose=(value) @options[:verbose] = value end def format=(value) @options[:format] = value end end

Slide 29

Slide 29 text

class Config attr_reader :options def initialize @options = {} end def verbose(value) @options[:verbose] = value end def format(value) @options[:format] = value end end def verbose=(value) @options[:verbose] = value end def format=(value) @options[:format] = value end

Slide 30

Slide 30 text

New problem: Create methods for every new option def format=(value) @options[:format] = value end def enabled=(value) @options[:enabled] = value end def fail_fast=(value) @options[:fail_fast] = value end def verbose=(value) @options[:verbose] = value end def format=(value) @options[:format] = value end def enabled=(value) @options[:enabled] = value end def fail_fast=(value) @options[:fail_fast] = value end def verbose=(value) @options[:enabled] = value end def fail_fast=(value) @options[:fail_fast] = value end def verbose=(value) @options[:verbose] = value end def format=(value) @options[:format] = value end def enabled=(value) @options[:enabled] = value end def fail_fast=(value) @options[:fail_fast] = value end def verbose=(value) @options[:verbose] = value end def format=(value) @options[:format] = value end def format=(value) @options[:format] = end def enabled=(value) @options[:enabled] end def fail_fast=(value) @options[:fail_fast end def verbose=(value) @options[:verbose] end def format=(value) @options[:format] = end def enabled=(value) @options[:enabled] end def fail_fast=(value) @options[:fail_fast end def verbose=(value) @options[:verbose]

Slide 31

Slide 31 text

Making it more dynamic with ✨ method_missing ✨

Slide 32

Slide 32 text

def method_missing(method_name, *args) [method_name.to_s, *args] end puts welcome to this talk about dsls # welcome # to # this # talk # about # dsls

Slide 33

Slide 33 text

class Config # ... def method_missing(method_name, value) if method_name.end_with? "=" @options[method_name[0..-1]] = value else super # Fall back behavior end end end config = Config.new config.verbose = true config.format = :documentation

Slide 34

Slide 34 text

method_missing may not
 be a good choice

Slide 35

Slide 35 text

What if you call the
 missing method wrong?

Slide 36

Slide 36 text

class Config def method_missing(method_name, value) # ... end end config = Config.new config.verbose # => `method_missing': # wrong number of arguments (given 1, 
 expected 2) (ArgumentError)

Slide 37

Slide 37 text

Unclear where
 methods are de fi ned

Slide 38

Slide 38 text

Breaks respond_to?

Slide 39

Slide 39 text

config = Config.new config.verbose = true pp config.respond_to? :verbose= # => false

Slide 40

Slide 40 text

Dynamically de fi ne
 con fi g option methods

Slide 41

Slide 41 text

class Config def self.def_options(*option_names) option_names.each do |option_name| define_method "#{option_name}=" do |value| options[option_name] = value end end end def_options :verbose, :format # ... end config = Config.new config.verbose = true config.format = :documentation

Slide 42

Slide 42 text

class Config def self.def_options(*option_names) option_names.each do |option_name| define_method "#{option_name}=" do |value| options[option_name] = value end end end def_options :verbose, :format # ... end config = Config.new config.verbose = true config.format = :documentation def_options :verbose, :format

Slide 43

Slide 43 text

class Config def self.def_options(*option_names) option_names.each do |option_name| define_method "#{option_name}=" do |value| options[option_name] = value end end end def_options :verbose, :format # ... end config = Config.new config.verbose = true config.format = :documentation def self.def_options(*option_names) option_names.each do |option_name|

Slide 44

Slide 44 text

class Config def self.def_options(*option_names) option_names.each do |option_name| define_method "#{option_name}=" do |value| options[option_name] = value end end end def_options :verbose, :format # ... end config = Config.new config.verbose = true config.format = :documentation define_method "#{option_name}=" do |value| options[option_name] = value end

Slide 45

Slide 45 text

⚡ Blocks! ⚡

Slide 46

Slide 46 text

Config.configure do |config| config.verbose = true config.format = :documentation end

Slide 47

Slide 47 text

class Config def self.configure instance = new yield instance instance end # ... end

Slide 48

Slide 48 text

class Config def self.configure instance = new yield instance instance end # ... end def self.configure

Slide 49

Slide 49 text

class Config def self.configure instance = new yield instance instance end # ... end instance = new

Slide 50

Slide 50 text

class Config def self.configure instance = new yield instance instance end # ... end yield instance

Slide 51

Slide 51 text

Config.configure do |config| config.verbose = true config.format = :documentation end |config| config. config.

Slide 52

Slide 52 text

Config.configure do |config| config.verbose = true config.format = :documentation end

Slide 53

Slide 53 text

Config.configure do verbose true format :documentation end

Slide 54

Slide 54 text

class Config def self.configure(&block) instance = new instance.instance_eval(&block) instance end # ... end

Slide 55

Slide 55 text

class Config def self.configure(&block) instance = new instance.instance_eval(&block) instance end # ... end def self.configure(&block)

Slide 56

Slide 56 text

class Config def self.configure(&block) instance = new instance.instance_eval(&block) instance end # ... end instance.instance_eval(&block)

Slide 57

Slide 57 text

Config.configure do verbose true format :documentation end

Slide 58

Slide 58 text

# This won't work! # Ruby will think you're assigning # local variables Config.configure do verbose = true format = :documentation end

Slide 59

Slide 59 text

Config.configure do self.verbose = true self.format = :documentation end self.verbose = self.format =

Slide 60

Slide 60 text

Config.configure do verbose true format :documentation end

Slide 61

Slide 61 text

When to use one or the other?

Slide 62

Slide 62 text

Config.configure do |config| config.verbose = true # Set a value end Config.configure do load_defaults! # Perform an action end

Slide 63

Slide 63 text

# config/puma.rb workers 3 preload_app!

Slide 64

Slide 64 text

Our tools so far •Dynamically de fi ne methods •Ruby blocks

Slide 65

Slide 65 text

Share DSL behavior with modules

Slide 66

Slide 66 text

# Multiple configuration classes Config.configure do enabled true end Output.configure do verbose true format :documentation end

Slide 67

Slide 67 text

class Config include Configurable def_options :enabled end class Output include Configurable def_options :verbose, :format end

Slide 68

Slide 68 text

module Configurable def self.included(base) base.extend ClassMethods end module ClassMethods def def_options(*option_names) # ... end def configure(&block) # ... end end # ...

Slide 69

Slide 69 text

Config.configure do enabled true output do verbose true format :documentation end end

Slide 70

Slide 70 text

class Config include Configurable def_options :enabled class Output include Configurable def_options :verbose, :format end def output(&block) @output ||= Output.new @output.instance_eval(&block) if block_given? @output end end

Slide 71

Slide 71 text

class Config include Configurable def_options :enabled class Output include Configurable def_options :verbose, :format end def output(&block) @output ||= Output.new @output.instance_eval(&block) if block_given? @output end end def output(&block) @output ||= Output.new @output.instance_eval(&block) if block_given? @output end

Slide 72

Slide 72 text

Module builder pattern dejimata.com/2017/5/20/the-ruby-module-builder-pattern

Slide 73

Slide 73 text

class Config include ConfigOption.define(:verbose, :format) end

Slide 74

Slide 74 text

class Config def self.def_options(*option_names) option_names.each do |option_name| define_method option_name do |value| options[option_name] = value end end end def_options :verbose, :format # ... end

Slide 75

Slide 75 text

module ConfigOption def self.define(*option_names) Module.new do def options @options ||= {} end option_names.each do |option_name| define_method option_name do |value| options[option_name] = value end end end

Slide 76

Slide 76 text

module ConfigOption def self.define(*option_names) Module.new do def options @options ||= {} end option_names.each do |option_name| define_method option_name do |value| options[option_name] = value end end end def self.define(*option_names)

Slide 77

Slide 77 text

module ConfigOption def self.define(*option_names) Module.new do def options @options ||= {} end option_names.each do |option_name| define_method option_name do |value| options[option_name] = value end end end define_method option_name do |value| options[option_name] = value end

Slide 78

Slide 78 text

module ConfigOption def self.define(*option_names) Module.new do def options @options ||= {} end option_names.each do |option_name| define_method option_name do |value| options[option_name] = value end end end Module.new do end

Slide 79

Slide 79 text

class Config include ConfigOption.define(:verbose, :format) end

Slide 80

Slide 80 text

Advantage:
 Easy to reuse everywhere

Slide 81

Slide 81 text

Advantage:
 No con fi g option de fi nition logic in the class itself

Slide 82

Slide 82 text

# Anonymous module pp Config.included_modules # => [#, Kernel]

Slide 83

Slide 83 text

class Config include ConfigOption.new(:verbose, :format) end

Slide 84

Slide 84 text

class Config include ConfigOption.new(:verbose, :format) end new

Slide 85

Slide 85 text

class ConfigOption < Module def initialize(*option_names) define_method :options do @options ||= {} end option_names.each do |option_name| define_method option_name do |value| options[option_name] = value end end

Slide 86

Slide 86 text

class ConfigOption < Module def initialize(*option_names) define_method :options do @options ||= {} end option_names.each do |option_name| define_method option_name do |value| options[option_name] = value end end class ConfigOption < Module

Slide 87

Slide 87 text

class ConfigOption < Module def initialize(*option_names) define_method :options do @options ||= {} end option_names.each do |option_name| define_method option_name do |value| options[option_name] = value end end def initialize(*option_names)

Slide 88

Slide 88 text

class ConfigOption < Module def initialize(*option_names) define_method :options do @options ||= {} end option_names.each do |option_name| define_method option_name do |value| options[option_name] = value end end define_method :options do @options ||= {} end define_method option_name do |value| options[option_name] = value end

Slide 89

Slide 89 text

# Anonymous module pp Config.included_modules # => [#, Kernel] # Named module pp Config.included_modules # => [#, Kernel]

Slide 90

Slide 90 text

Read this post: dejimata.com/2017/5/20/the-ruby-module-builder-pattern

Slide 91

Slide 91 text

Create DSL objects Keep the DSL and created object separate

Slide 92

Slide 92 text

class Config def self.configure(&block) dsl = ConfigDSL.dsl(&block) new(dsl.options) end # ... end Config.configure do verbose true format :documentation end class ConfigDSL include ConfigOption.new( :verbose, :format) def self.dsl(&block) instance = new instance .instance_eval(&block) instance end end

Slide 93

Slide 93 text

class Config def self.configure(&block) dsl = ConfigDSL.dsl(&block) new(dsl.options) end # ... end Config.configure do verbose true format :documentation end class ConfigDSL include ConfigOption.new( :verbose, :format) def self.dsl(&block) instance = new instance .instance_eval(&block) instance end end dsl = ConfigDSL.dsl(&block) new(dsl.options) class ConfigDSL def self.dsl(&block)

Slide 94

Slide 94 text

Make small modules, like plugins Small modules are easier to maintain than very large classes

Slide 95

Slide 95 text

class Profile < TomsSchemaGem plugin LabelPlugin attribute :name, :label => "Name" attribute :socials do label "Social media" attribute :twitter, :label => "Twitter" attribute :mastodon, :label => "Mastodon" end end profile = Profile.new(...) profile.name.label # => "Name" profile.name.value # => "Tom de Bruijn" profile.socials.twitter.value # => "@tombruijn"

Slide 96

Slide 96 text

class Profile < TomsSchemaGem plugin LabelPlugin attribute :name, :label => "Name" attribute :socials do label "Social media" attribute :twitter, :label => "Twitter" attribute :mastodon, :label => "Mastodon" end end profile = Profile.new(...) profile.name.label # => "Name" profile.name.value # => "Tom de Bruijn" profile.socials.twitter.value # => "@tombruijn" plugin LabelPlugin label "Social media" profile.name.label # => "Name"

Slide 97

Slide 97 text

class Profile < TomsSchemaGem plugin LabelPlugin attribute :name, :label => "Name" attribute :socials do label "Social media" profile.name.label # => "Name" plugin LabelPlugin :label => "Name" label "Social media" profile.name.label # => "Name"

Slide 98

Slide 98 text

module LabelPlugin < PluginDSL option :label attribute_method :label do options[:label] end end

Slide 99

Slide 99 text

In summary • method_missing • de fi ne_method • Ruby blocks • yield • instance_eval • Modules • share behavior between classes • Module builder pattern • DSL objects - move DSL logic to separate classes • Create plugins

Slide 100

Slide 100 text

Now go forth and make your DSL!

Slide 101

Slide 101 text

Thank you! Do something nice for someone that’s not yourself. Join a union. DSL DS DSL Tom de Bruijn – mastodon.social/@tombruijn – tomdebruijn.com