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.8'
Any =

Create the DSL::Maker::Any type identifier, equivalent to Object.

Object
Yes =
On = True = true
No =
Off = False = false

Class Method Summary collapse

Class Method Details

.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.



178
179
180
181
182
183
184
185
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
# File 'lib/dsl/maker.rb', line 178

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

  if is_entrypoint(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
  
  if @klass
    build_dsl_element(@klass, symname, dsl_class)
  else
    # FIXME: We shouldn't need the blank block here ...
    # This blank block is representative of the implicit (and missing) outermost
    # block around the DSL that we are not putting into place in :parse_dsl or
    # :execute_dsl.
    @klass = generate_dsl({
      symname => dsl_class
    }) {}

    # This marks @klass as the root DSL class.
    @klass.parent_class = self
  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



236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/dsl/maker.rb', line 236

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

Note:

These type coercions are global to all DSLs.

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

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



113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/dsl/maker.rb', line 113

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

Note:

These verifications are specific to the DSL you add them to.

Note:

Verifications are called in the order you specify them.

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.

You can also call add_verification on the return values from generate_dsl() or add_entrypoint(). In those cases, omit the :name because you have already chosen the DSL layer you're adding the verification to.

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



268
269
270
271
272
273
# File 'lib/dsl/maker.rb', line 268

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)

  @entrypoints[name.to_sym].add_verification(&block)
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:

  • Any - whatever you give is returned.
  • String - the string value of 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



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/dsl/maker.rb', line 300

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|
        if (!args.empty? || dsl_block)
          obj = type.new
          Docile.dsl_eval(obj, &dsl_block) if dsl_block
          rv = obj.__apply(*args)

          if v = type.instance_variable_get(:@verifications)
            v.each do |verify|
              failure = verify.call(rv)
              raise failure if failure
            end
          end

          # This is the one place where we pull out the entrypoint results and
          # put them into the control class.
          if klass.parent_class
            # Use the full instance_variable_get() in order to avoid having to
            # create accessors that could be misused outside this class.
            klass.parent_class.instance_variable_get(:@accumulator).push(rv)
          end

          ___set(as_attr, rv)
        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.



220
221
222
223
224
225
226
# File 'lib/dsl/maker.rb', line 220

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



91
92
93
94
95
96
# File 'lib/dsl/maker.rb', line 91

def self.execute_dsl(&block)
  raise 'Must call add_entrypoint before execute_dsl' unless @klass
  raise 'Block required for execute_dsl' unless block_given?

  run_dsl() { @klass.new.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.



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/dsl/maker.rb', line 140

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

  dsl_class = Class.new(DSL::Maker::Base) do
    include DSL::Maker::Boolean

    class << self
      attr_accessor :parent_class, :verifications
    end

    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

.is_dsl(proto) ⇒ Object



353
354
355
# File 'lib/dsl/maker.rb', line 353

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

.is_entrypoint(name) ⇒ Object



357
358
359
360
# File 'lib/dsl/maker.rb', line 357

def self.is_entrypoint(name)
  @entrypoints && @entrypoints.has_key?(name.to_sym)
  #@klass && @klass.new.respond_to?(name.to_sym)
end

.parse_dsl(dsl = nil) ⇒ 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) (defaults to: nil)

    The DSL to be parsed by this class.

Returns:

  • (Object)

    Whatever is returned by the block defined in this class.



76
77
78
79
80
81
# File 'lib/dsl/maker.rb', line 76

def self.parse_dsl(dsl=nil)
  raise 'Must call add_entrypoint before parse_dsl' unless @klass
  raise 'String required for parse_dsl' unless dsl.instance_of? String

  run_dsl() { eval dsl, @klass.new.get_binding }
end

.run_dslObject



339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/dsl/maker.rb', line 339

def self.run_dsl()
  # build_dsl_element() will use @accumulator to handle multiple entrypoints if
  # the class in question is a root DSL class. 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