A small DSL to generate DSLs.
Define DSLs quickly and avoid the boilerplate to write getters and setters. Oh, and it does validation too.
# 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.
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.
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'
The gem was/is used in production with the following Ruby versions:
- ✅ Ruby 2.7
- ✅ Ruby 3.0
- ✅ Ruby 3.1
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
The gem is available as open source under the terms of the MIT License.
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.