Class: DSL::Maker

Inherits:
Object
  • Object
show all
Defined in:
lib/dsl/maker.rb,
lib/dsl/maker/version.rb

Overview

This is the base class we provide.

Defined Under Namespace

Modules: Boolean Classes: Base

Constant Summary collapse

VERSION =

The current version of this library

'0.0.6'
Yes =
On = True = true
No =
Off = False = false
@@types =

FIXME: This may have to be changed when the elements can be altered because it is global to the hierarchy. But, that may be desirable.

{}

Class Method Summary collapse

Class Method Details

.__is_dsl(proto) ⇒ Object



312
313
314
# File 'lib/dsl/maker.rb', line 312

def self.__is_dsl(proto)
  proto.is_a?(Class) && proto.ancestors.include?(DSL::Maker::Base)
end

.__is_entrypoint(name) ⇒ Object



316
317
318
# File 'lib/dsl/maker.rb', line 316

def self.__is_entrypoint(name)
  respond_to?(name.to_sym)
end

.__run_dslObject



299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/dsl/maker.rb', line 299

def self.__run_dsl()
  # add_entrypoint() will use @accumulator to handle multiple entrypoints.
  # Reset it here so that we're only handling the values from this run.
  @accumulator = []

  yield

  if @accumulator.length <= 1
    return @accumulator[0]
  end
  return @accumulator
end

.add_entrypoint(name, args = {}, &defn_block) ⇒ Class

Note:

args could be a Hash (to be passed to generate_dsl()) or the result

Add an entrypoint (top-level DSL element) to this class's DSL.

This delegates to generate_dsl() for the majority of the work.

of a call to generate_dsl().

Parameters:

  • name (String)

    the name of the entrypoint

  • args (Hash) (defaults to: {})

    the elements of the DSL block (passed to generate_dsl)

  • defn_block (Proc)

    what is executed once the DSL block is parsed.

Returns:

  • (Class)

    The class that implements this level's DSL definition.



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/dsl/maker.rb', line 186

def self.add_entrypoint(name, args={}, &defn_block)
  symname = name.to_sym

  if self.respond_to?(symname)
    raise "'#{name.to_s}' is already an entrypoint"
  end

  if __is_dsl(args)
    dsl_class = args
  else
    # Without defn_block, there's no way to give back the result of the
    # DSL parsing. So, raise an error if we don't get one.
    # TODO: Provide a default block that returns the datastructure as a HoH.

    raise "Block required for add_entrypoint" unless block_given?
    dsl_class = generate_dsl(args, &defn_block)
  end

  define_singleton_method(symname) do |*args, &dsl_block|
    obj = dsl_class.new
    Docile.dsl_eval(obj, &dsl_block) if dsl_block
    rv = obj.__apply(*args)

    if @verifications && @verifications.has_key?(symname)
      @verifications[symname].each do |verify|
        failure = verify.call(rv)
        raise failure if failure
      end
    end

    @accumulator.push(rv)
    return rv
  end

  @entrypoints ||= {}
  return @entrypoints[symname] = dsl_class
end

.add_helper(name, &block) ⇒ Object

This adds a helper function that's accessible within the DSL.

Note: These helpers are global to all DSLs.

Parameters:

  • name (String)

    the name of the helper

  • &block (Block)

    The function to be executed when the helper is called.

Returns:

  • nil



245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/dsl/maker.rb', line 245

def self.add_helper(name, &block)
  raise "Block required for add_helper" unless block_given?

  if DSL::Maker::Base.new.respond_to? name.to_sym
    raise "'#{name.to_s}' is already a helper"
  end

  DSL::Maker::Base.class_eval do
    define_method(name.to_sym, &block)
  end

  return
end

.add_type(type, &block) ⇒ Object

This adds a type coercion that's used when creating the DSL.

Note: These type coercions are global to all DSLs.

Your block will receive the following signature: |attr, *args| where 'attr' is the name of the attribute and *args are the arguments passed into your method within the DSL. You are responsible for acting as a mutator. You have __get() and __set() available for your use. These are aliases to instance_variable_get and instance_variable_set, respectively. Please read the coercions provided for you in this source file as examples.

Parameters:

  • type (Object)

    the name of the helper

  • &block (Block)

    The function to be executed when the coercion is exercised.

Returns:

  • nil



81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/dsl/maker.rb', line 81

def self.add_type(type, &block)
  raise "Block required for add_type" unless block_given?
  raise "'#{type}' is already a type coercion" if @@types.has_key? type

  @@types[type] = ->(klass, name, type) {
    klass.class_eval do
      define_method(name.to_sym) do |*args|
        instance_exec('@' + name.to_s, *args, &block)
      end
    end
  }

  return
end

.add_verification(name, &block) ⇒ Object

This adds a verification that's executed after the DSL is finished parsing.

The verification will be called with the value(s) returned by the entrypoint's execution. If the verification returns a true value (of any kind), then that will be raised as a runtime exception.

Note: These verifications are specific to the DSL you add them to. Note: Verifications are called in the order you specify them.

Parameters:

  • name (String)

    the name of the entrypoint to add a verification to

  • &block (Block)

    The function to be executed when verifications execute

Returns:

  • nil



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/dsl/maker.rb', line 272

def self.add_verification(name, &block)
  raise "Block required for add_verification" unless block_given?
  raise "'#{name.to_s}' is not an entrypoint for a verification" unless __is_entrypoint(name)

  @verifications ||= {}
  @verifications[name.to_sym] ||= []

  # This craziness converts the block provided into a proc that can be called
  # in add_entrypoint(). Taken from http://stackoverflow.com/a/2946734/1732954
  # Note: self is not preserved. This should be okay because the verification
  # should only care about the value provided.
  obj = Object.new
  obj.define_singleton_method(:_, &block)
  @verifications[name.to_sym].push(obj.method(:_).to_proc)

  return
end

.build_dsl_element(klass, name, type) ⇒ Object

Add a single element of a DSL to a class representing a level in a DSL.

Each of the types represents a coercion - a guarantee and check of the value in that name. The standard type coercions are:

  • String - whatever you give is returned.
  • Integer - the integer value of whatever you give is returned.
  • Boolean - the truthiness of whatever you give is returned.
  • generate_dsl() - this represents a new level of the DSL.

Parameters:

  • klass (Class)

    The class representing this level in the DSL.

  • name (String)

    The name of the element we're working on.

  • type (Class)

    The type of this element we're working on. This is the type coercion spoken above.

Returns:

  • nil



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/dsl/maker.rb', line 112

def self.build_dsl_element(klass, name, type)
  if @@types.has_key?(type)
    @@types[type].call(klass, name, type)
  elsif __is_dsl(type)
    as_attr = '@' + name.to_s
    klass.class_eval do
      define_method(name.to_sym) do |*args, &dsl_block|
        unless (args.empty? && !dsl_block)
          obj = type.new
          Docile.dsl_eval(obj, &dsl_block) if dsl_block
          ___set(as_attr, obj.__apply(*args))
        end
        ___get(as_attr)
      end
    end
  else
    raise "Unrecognized element type '#{type}'"
  end

  return
end

.entrypoint(name) ⇒ Class

This returns the DSL corresponding to the entrypoint's name.

Parameters:

  • name (String)

    the name of the entrypoint

Returns:

  • (Class)

    The class that implements this name's DSL definition.



229
230
231
232
233
234
235
# File 'lib/dsl/maker.rb', line 229

def self.entrypoint(name)
  unless __is_entrypoint(name)
    raise "'#{name.to_s}' is not an entrypoint"
  end

  return @entrypoints[name.to_sym]
end

.execute_dsl(&block) ⇒ Object

Note:

If the DSL contains multiple entrypoints, then this will return an

Execute the DSL provided in the block.

Array. This is desirable.

Parameters:

  • &block (Block)

    The DSL to be executed by this class.

Returns:

  • (Object)

    Whatever is returned by &block



56
57
58
59
60
# File 'lib/dsl/maker.rb', line 56

def self.execute_dsl(&block)
  raise 'Block required for execute_dsl' unless block_given?

  __run_dsl { instance_eval(&block) }
end

.generate_dsl(args = {}, &defn_block) ⇒ Class

Add the meat of a DSL block to some level of this class's DSL.

In order for Docile to parse a DSL, each level must be represented by a different class. This method creates anonymous classes that each represents a different level in the DSL's structure.

The creation of each DSL element is delegated to build_dsl_element.

Parameters:

  • args (Hash) (defaults to: {})

    the elements of the DSL block (passed to generate_dsl)

  • defn_block (Proc)

    what is executed once the DSL block is parsed.

Returns:

  • (Class)

    The class that implements this level's DSL definition.



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/dsl/maker.rb', line 146

def self.generate_dsl(args={}, &defn_block)
  raise 'Block required for generate_dsl' unless block_given?

  # Inherit from the Boolean class to gain access to the useful methods
  # TODO: Convert DSL::Maker::Boolean into a Role
  # TODO: Create a DSL::Maker::Base class to inherit from
  dsl_class = Class.new(DSL::Maker::Base) do
    include DSL::Maker::Boolean

    # This instance method exists because we cannot seem to inline its work
    # where we call it. Could it be a problem of incorrect binding?
    # It has to be defined here because it needs access to &defn_block
    define_method(:__apply) do |*args|
      instance_exec(*args, &defn_block)
    end
  end

  args.each do |name, type|
    if dsl_class.new.respond_to? name.to_sym
      raise "Illegal attribute name '#{name}'"
    end

    build_dsl_element(dsl_class, name, type)
  end

  return dsl_class
end

.get_bindingBinding

Returns the binding as needed by parse_dsl() and execute_dsl()

Returns:

  • (Binding)

    The binding of the invoking class.



295
296
297
# File 'lib/dsl/maker.rb', line 295

def self.get_binding
  binding
end

.parse_dsl(dsl) ⇒ Object

Note:

If the DSL contains multiple entrypoints, then this will return an

Parse the DSL provided in the parameter.

Array. This is desirable.

Parameters:

  • dsl (String)

    The DSL to be parsed by this class.

Returns:

  • (Object)

    Whatever is returned by the block defined in this class.



44
45
46
# File 'lib/dsl/maker.rb', line 44

def self.parse_dsl(dsl)
  __run_dsl { eval dsl, self.get_binding }
end