Skip to content

Define DSLs quickly and avoid the boilerplate write getters and setters. Oh, and it does validation too.

License

Notifications You must be signed in to change notification settings

motine/dsl_factory

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DSL Factory

A small DSL to generate DSLs.
Define DSLs quickly and avoid the boilerplate to write getters and setters. Oh, and it does validation too.

Example

# add this to your Gemfile: gem 'dsl_factory'

# define the DSL
LakeDsl = DslFactory.define_dsl do
  string :lake_name
  numeric :max_depth
  array :fishes, String
end

# use it in any class
class LakeSuperior
  extend LakeDsl

  lake_name 'Lake Superior'
  max_depth 406
  fish 'trout'
  fish 'northern pike'
end

# and you can access the values
LakeSuperior.lake_name # => "Lake Superior"
LakeSuperior.fishes # => ["trout", "northern pike"]

This gem came about during my time at Netskin GmbH. Check it out, we do great (Rails) work there.

Usage

Definition

Basic Types

# the following data types are available
# if a value is given which does not fit the type a `DslFactory::ValidationError` is raised
LakeDsl = DslFactory.define_dsl do
  string :lake_name
  symbol :group
  numeric :max_depth
  boolean :protected_habitat
  callable :current_temperature_handler
  any :continent # does not validate the given contents later
end

# now we can use the definition
class LakeSuperior
  extend LakeDsl

  lake_name 'Lake Superior'
  group :great_lakes
  max_depth 406
  protected_habitat true
  current_temperature_handler ->() { self.temperature = FetchService.receive_temperature } # the proc is only saved, DslFactory will not call it
  continent Continent::NorthAmerica
end

Arrays

BookDsl = DslFactory.define_dsl do
  array :authors            # we must use the plural!
  array :publishers, String # validates that the item is of the given (Ruby) class
  # see below for nested DSLs
end

class SuperBook
  extend BookDsl
  authors ['Manfred', 'Dieter'] # we can use the plural form to set the whole array
  author 'Heinz'                # or the singular form to add an item

  publisher 'abc'
end

SuperBook.authors # => ['Manfred', 'Dieter', 'Heinz']
SuperBook.publishers # => ['abc']

Hashes

GeographyDsl = DslFactory.define_dsl do
  hash :capital_for_countries         # we must use the plural!
  hash :country_sizes, String, Numeric # validate key and value (key must be of String class, value of Symbol) 
  # see below for nested DSLs
end

class World
  extend GeographyDsl
  capital_for_country 'Berlin', 'Germany' # here we use the signular
  capital_for_country 'Copenhagen', 'Denmark'

  country_size 'Germany', 357_022
end

World.capital_for_countries # => { 'Berlin' => 'Germany', 'Copenhagen' => 'Denmark'}
World.country_sizes # => { 'Germany' => 357022 }

Nested DSLs

PersonDsl = DslFactory.define_dsl do
  array :parents do
    string :name
    numeric :age
  end

  hash :citizenships, String do # validate key as String
    symbol :status
    any :expiry
  end
end

class Sabine
  extend PersonDsl
  # note that nested DSLs can only be used via the singular form

  parent do
    name 'Karla'
    age 88
  end

  citizenship 'Germany' do
    status :revoked
    expiry Time.new(2000)
  end
end

Sabine.parents.first.name # => 'Karla'
Sabine.citizenships['Germany'].status # => :revoked

Callbacks

Sometimes we might want to do something when the DSL method is called. This can be achived via callbacks.

ButtonDsl = DslFactory.define_dsl do
  # all types outlined above support callbacks
  any :trigger, callback: ->(value) { puts "#{value} was triggered" }
  any :snicker, callback: ->(value) { arg1, arg2 = value; puts "snicker: #{arg1} & #{arg2}" } # we can pass arguments via the value
  any :clicker, callback: ->(value) { self.do_the_click } # see method definition in using class
  numeric :width, callback: ->(value) { raise DslFactory::ValidationError, 'buttons must be small' if width > 100 }

  # for arrays the callback always receives an array (even if it was used in singular form)
  # for hashes the callback receives two arguments: key, value
end

class MonsterButton
  extend ButtonDsl
  # if we want to call a method of the class, we need to define it before the first usage of the DSL method
  def self.do_the_click
    puts 'Click!'
  end

  trigger 'abc'            # -> abc was triggered
  snicker ['haha', 'hihi'] # -> snicker: haha & hihi
  clicker nil              # make sure to alway pass a value; -> Click!
  width 2000               # -> DslFactory::ValidationError: buttons must be small
end

MonsterButton.trigger # values are still set; => 'abc'

Inspection

For debugging it is useful to introspect the DSL values. When inspectable is set to true (DslFactory.define_dsl(inspectable: true)) the module will provide an inspect method. All sub-DSL modules provide an such a method by default.

Use of the definition

Usually we extend a class like so:

class LakeSuperior
  extend LakeDsl
  lake_name 'Lake Superior'
end

LakeSuperior.lake_name # => 'Lake Superior'

However we can also use the DSL in a variable:

@config = Module.new.extend(LakeDsl)
@config.lake_name 'Müritz'
@config.lake_name # => 'Müritz'

# or with the configuration pattern
@config = Module.new.extend(LakeDsl).tap do |c|
  c.lake_name 'Summter See'
end
@config.lake_name # => 'Summter See'

# or even without any prefix
@config = Module.new.extend(LakeDsl)
@config.instance_exec do
  lake_name 'Mühlenbecker See'
end
@config.lake_name # => 'Mühlenbecker See'

Compatibility

The gem was/is used in production with the following Ruby versions:

  • ✅ Ruby 2.7
  • ✅ Ruby 3.0
  • ✅ Ruby 3.1

Development

docker run --rm -ti -v (pwd):/app -w /app ruby:2.7 bash
bundle install
rake test # run the tests
pry # require_relative 'lib/dsl_factory.rb'

# to release a new version, update `CHANGELOG.md` and the version number in `version.rb`, then run
bundle exec rake release

License

The gem is available as open source under the terms of the MIT License.

Alternatives

Download counts are from 01.03.2022.

  • dslh (178.000 downloads) Allows to define a Hash as a DSL.
  • dsl_maker (80.700 downloads) allows defining DSLs with structs.
  • genki-dsl_accessor (5.600 downloads) allows defining hybrid accessors for class.
  • configuration_dsl (4.100 downloads) nice way to define configuration DSLs. quite outdated.
  • blockenspiel (2.869.000 downloads) allows to nicely define configuration blocks. does not facilitate assigning variables.
  • dsl_accessors (3.700 downloads) provides helpers to facilitate variable setting via DSL. exactly what we need, but unfortunately quite outdated.
  • open_dsl (17.000 downloads) dynamically defines OpenStructs to collect data for DSLs. really nice idea and quite close to what we need. unfortunately a little outdated.
  • alki-dsl (12.900 downloads) allows to define DSL methods nicely. does not assist in variable getting/setting.

Honerable Mentions

  • cleanroom (5.560.000 downloads) allows to safely define DSLs. does not really facilitate the data assignments.
  • dsl_block 4.900 downloads) allows you to use classes to define blocks with commands. does not really facilitate setting/getting variables.
  • declarative (110.990.000 downloads) define declarative schemas.
  • dslkit (54.500 downloads) allows defining DSL to be read from files (maybe). documentation hard to find. outdated.
  • dsltasks (4.100 downloads) allows to define hierarchical DSLs. does not facilitate variable assignments.
  • opendsl (8.800 downloads) allows to simply define DSLs. does not help assigning variables. outdated.

Not Applicable

  • dsl defines delegators. i am not sure when this is useful.
  • dsl_eval only defines an alias for instance_eval.
  • instant_dsl could not find repo/docs. quite outdated.
  • dsl_companion no documentation.
  • def_dsl no documentation.
  • dslr no documentation.

About

Define DSLs quickly and avoid the boilerplate write getters and setters. Oh, and it does validation too.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages