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

Write your own Domain Specific Language in Ruby: Let's do some metaprogramming!

Write your own Domain Specific Language in Ruby: Let's do some metaprogramming!

Tom de Bruijn

November 15, 2022
Tweet

More Decks by Tom de Bruijn

Other Decks in Programming

Transcript

  1. Write your own DSL in Ruby Let's do some metaprogramming!

    Tom de Bruijn – mastodon.social/@tombruijn – tomdebruijn.com
  2. A sub-language in Ruby *
 for a Domain A gem,

    or as part of an app * DSLs can also be made in other languages
  3. 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"
  4. # 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
  5. Rails.application.routes.draw do resources :posts do resources :comments end root "homepage#show"

    end resources :posts do resources :comments root "homepage#show"
  6. Create routes resources :posts • GET /posts • GET /posts/:id

    • POST /posts • PUT /posts/:id • DELETE /posts/:id
  7. Validation validates :name, :presence => true • Validation on creation

    • Validation on update • Validation on demand • Storing error messages
  8. 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
  9. class Config attr_accessor :verbose, :format end config = Config.new config.verbose

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

    = true config.format = :documentation pp config # => #<Config:... @format=:documentation, @verbose=true> attr_accessor :verbose, :format config.verbose = true config.format = :documentation # => #<Config:... @format=:documentation, @verbose=true>
  11. config = Config.new config.verbose = true config.format = :documentation pp

    config.options # => {:verbose=>true, :format=>:documentation}
  12. class Config attr_reader :options def initialize @options = {} end

    def verbose=(value) @options[:verbose] = value end def format=(value) @options[:format] = value end end
  13. 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
  14. 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]
  15. 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
  16. 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
  17. 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)
  18. 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
  19. 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
  20. 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|
  21. 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
  22. # This won't work! # Ruby will think you're assigning

    # local variables Config.configure do verbose = true format = :documentation end
  23. Config.configure do |config| config.verbose = true # Set a value

    end Config.configure do load_defaults! # Perform an action end
  24. module Configurable def self.included(base) base.extend ClassMethods end module ClassMethods def

    def_options(*option_names) # ... end def configure(&block) # ... end end # ...
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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)
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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)
  35. 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
  36. # Anonymous module pp Config.included_modules # => [#<Module:...>, Kernel] #

    Named module pp Config.included_modules # => [#<ConfigOption:...>, Kernel]
  37. 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
  38. 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)
  39. 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"
  40. 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"
  41. 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"
  42. 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
  43. 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