Lunfardo

Lunfardo is a framework to easily define simple (and not quite so simple) DSLs. For example it can be used to define an object configuration DSL.

Status

Gem Version Build Status Coverage Status

Installation

Add this line to your application's Gemfile:

gem 'lunfardo', '~> 0.1'

And then execute:

$ bundle

Or install it yourself as:

$ gem install lunfardo

Usage

Suppose you have the class

class SomeClass
    def self.filename()
        @filename
    end

    def self.set_filename(filename)
        @filename = filename
    end
end

and you would like to make it configurable:

SomeClass.configure do
    filename 'some_file.txt'
end

To do that, define the DSL:

include 'lunfardo'

class SomeClassConfigurationDSL
    include CabezaDeTermo::Lunfardo::Behaviour

    on :filename do |name|
        perform do |name|
            context.set_filename(name)
        end
    end
end

and then add the configure method to SomeClass:

class SomeClass
    def self.filename()
        @filename
    end

    def self.set_filename(filename)
        @filename = filename
    end

    def self.configure(&block)
        SomeClassConfigurationDSL.evaluate_on(self, &block)
    end
end

and that's it. This is the simpliest use, but it can be used for more complex DSLs too.

API

Accessing the context from the DSL

You can access the context from the DSL sending context:

include 'lunfardo'

class SomeClassConfigurationDSL
    include CabezaDeTermo::Lunfardo::Behaviour

    on :filename do |name|
        perform do |name|
            context.files << name
        end
    end
end

# to be used like

SomeClass.configure do
    filename 'file_1.txt'
    filename 'file_2.txt'
end

Evaluating code at the begining of a configuration scope

You can evaluate code before evaluating the rest of the configuration:

include 'lunfardo'

class SomeClassConfigurationDSL
    include CabezaDeTermo::Lunfardo::Behaviour

    on :filenames do
        before do
            context.files = []
        end

        on :filename do |name|
            perform do |name|
                context.files << name
            end
        end
    end
end

# to be used like

SomeClass.configure do
    filenames do
        filename 'file_1.txt'
        filename 'file_2.txt'
    end
end

Evaluating code at the end of a configuration scope

You can evaluate code after evaluating the rest of the configuration:

include 'lunfardo'

class SomeClassConfigurationDSL
    include CabezaDeTermo::Lunfardo::Behaviour

    on :filenames do
        attr_reader :files

        before do
            @files = []
        end

        on :filename do |name|
            perform do |name|
                outer.files << name
            end
        end

        after do
            context.files = @files.join(', ')
        end
    end
end

# to be used like

SomeClass.configure do
    filenames do
        filename 'file_1.txt'
        filename 'file_2.txt'
    end
end

The #after scope will also return its evaluation:

include 'lunfardo'

class BloatedJoinDSL
    include CabezaDeTermo::Lunfardo::Behaviour

    on :filenames do
        attr_reader :files

        before do
            @files = []
        end

        on :filename do |name|
            perform do |name|
                outer.files << name
            end
        end

        after do
            @files.join(', ')
        end
    end
end

# to be used like

files = BloatedJoinDSL.evaluate do
    filenames do
        filename 'file_1.txt'
        filename 'file_2.txt'
    end
end

files == ['file_1.txt', 'file_2.txt']

Defining instance variables and methods in a configuration scope

In the previous example you can see that you can use instance variables and methods inside a configuration scope.

You can also define any method using define:

include 'lunfardo'

class BloatedJoinDSL
    include CabezaDeTermo::Lunfardo::Behaviour

    on :filenames do
        attr_reader :files

        before do
            @files = []
        end

        on :filename do |name|
            define :extension do
                '.txt'.freeze
            end     

            perform do |name|
                outer.files << name + extension
            end
        end

        after do
            @files.join(', ')
        end
    end
end

# to be used like

files = BloatedJoinDSL.evaluate do
    filenames do
        filename 'file_1'
        filename 'file_2'
    end
end

files == ['file_1.txt', 'file_2.txt']

Accessing the outer scope of a configuration scope

If you need to access the outer scope of your current scope, send outer

on :filename do |name|
    perform do |name|
        outer.files << name
    end
end

or if you need to reach an outer scope several layers above

on :filename do |name|
    perform do |name|
        outer(3).files << name
    end
end

Handling a block instead of evaluating it

Sometimes you won't want to evaluate a block but to handle it in a custom way.

In that case, you must declare in you scope definition (at the #on message) that you expect the &block parameter:

class ValidationsMessageLibraryDSL
    include CabezaDeTermo::Lunfardo::Behaviour

    dsl do
        attr_reader :messages

        before do
            @messages = Hash[]
        end

        on :message_for do |validation_name, &message_block|
            perform do |validation_name, &message_block|
                outer.messages[validation_name] = message_block
            end
        end

        after do
            @messages
        end
    end
end

# to be used like

messages = ValidationsMessageLibraryDSL.evaluate do
    message_for :presence do |validation|
        "can not be left blank"
    end

    message_for :format do |validation|
        "doesn't match expected format: #{validation.expected_value}"
    end
end

Defining recursive configurations

You can define a configuration using recursion like in the example:

class ParamsDSL
    include CabezaDeTermo::Lunfardo::Behaviour

    dsl do
        on :params do
            attr_reader :params

            before do
                @params = Hash[]
            end

            on :param do
                attr_reader :name
                attr_reader :params

                before do |name, value = nil|
                    @name = name
                    @params = outer.params
                end

                perform do |name, value = nil|
                    @params[name] = value.nil? ? Hash[] : value
                end

                on :param do
                    attr_reader :name
                    attr_reader :params

                    before do |name, value = nil|
                        @name = name
                        @params = outer.params[outer.name]
                    end

                    perform do |name, value = nil|
                        @params[name] = value.nil? ? Hash[] : value
                    end

                    recursive :param, scope: self
                end
            end

            after do
                @params
            end
        end
    end
end

# to be used like

params = ParamsDSL.evaluate do
    params do
        param :client do
            param :name, 'Juan Salvo'
            param :address do
                param :city, 'Buenos Aires'
            end
        end
    end
end

params == {:client => {:name=>"Juan Salvo ", :address=>{:city=>"Buenos Aires"}}}

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/cabeza-de-termo/lunfardo.

License

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