class DSL::Maker

This is the base class we provide.

Constants

Any

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

ArrayOf
No
VERSION

The current version of this library

Yes

Attributes

parent_class[RW]
verifications[RW]

Public Class Methods

AliasOf(name) click to toggle source
# File lib/dsl/maker.rb, line 75
def self.AliasOf(name)
  @@aliases[name] ||= Alias.new(name)
end
add_entrypoint(name, args={}, &defn_block) click to toggle source

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

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

@note `args` could be a Hash (to be passed to generate_dsl()) or the result of a call to generate_dsl().

@param name [String] the name of the entrypoint @param args [Hash] the elements of the DSL block (passed to generate_dsl) @param defn_block [Proc] what is executed once the DSL block is parsed.

@return [Class] The class that implements this level's DSL definition.

# File lib/dsl/maker.rb, line 212
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) click to toggle source

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

Note: These helpers are global to all DSLs.

@param name [String] the name of the helper @param &block [Block] The function to be executed when the helper is called.

@return nil

# File lib/dsl/maker.rb, line 270
def self.add_helper(name, &block)
  raise "Block required for add_helper" unless block_given?

  if has_helper? name
    raise "'#{name.to_s}' is already a helper"
  end

  base_class.class_eval do
    define_method(name.to_sym, &block)
  end

  return
end
add_type(type, &block) click to toggle source

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

@note These type coercions are global to all DSLs.

@param type [Object] the name of the helper @param &block [Block] The function to be executed when the coercion is exercised.

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.

@return nil

# File lib/dsl/maker.rb, line 153
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] = block

  return
end
add_verification(name, &block) click to toggle source

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.

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

@note Verifications are called in the order you specify them.

@param name [String] the name of the entrypoint to add a verification to @param &block [Block] The function to be executed when verifications execute

@return nil

# File lib/dsl/maker.rb, line 326
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
entrypoint(name) click to toggle source

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

@param name [String] the name of the entrypoint

@return [Class] The class that implements this name's DSL definition.

# File lib/dsl/maker.rb, line 254
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) click to toggle source

Execute the DSL provided in the block.

@param &block [Block] The DSL to be executed by this class.

@return [Array] Whatever is returned by the block defined in this class.

# File lib/dsl/maker.rb, line 131
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) click to toggle source

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.

@param args [Hash] the elements of the DSL block (passed to generate_dsl) @param defn_block [Proc] what is executed once the DSL block is parsed.

@return [Class] The class that implements this level's DSL definition.

# File lib/dsl/maker.rb, line 174
def self.generate_dsl(args={}, &defn_block)
  raise 'Block required for generate_dsl' unless block_given?

  dsl_class = Class.new(base_class) 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
has_helper?(name) click to toggle source

This returns if the helper has been added with add_helper

@param name [String] the name of the helper

@return Boolean

# File lib/dsl/maker.rb, line 304
def self.has_helper?(name)
  base_class.method_defined?(name.to_sym)
end
is_alias?(type) click to toggle source
# File lib/dsl/maker.rb, line 78
def self.is_alias?(type)
  type.instance_of? Alias
end
is_array?(type) click to toggle source
# File lib/dsl/maker.rb, line 96
def self.is_array?(type)
  type.instance_of? ArrayType
end
parse_dsl(dsl=nil) click to toggle source

Parse the DSL provided in the parameter.

@param dsl [String] The DSL to be parsed by this class.

@return [Array] Whatever is returned by the block defined in this class.

# File lib/dsl/maker.rb, line 119
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
remove_helper(name) click to toggle source

This removes a helper function that's been added with add_helper

@param name [String] the name of the helper

@return nil

# File lib/dsl/maker.rb, line 289
def self.remove_helper(name)
  unless has_helper? name
    raise "'#{name.to_s}' is not a helper"
  end

  base_class.class_eval do
    remove_method(name.to_sym)
  end
end

Private Class Methods

base_class() click to toggle source
# File lib/dsl/maker.rb, line 487
def self.base_class
  # This is the only time we *know* that the :default helper doesn't exist yet.
  unless @base_class
    @base_class = Class.new(DSL::Maker::Base)
    add_helper(:default) do |method_name, args, position=0|
      method = method_name.to_sym
      if args.length >= (position + 1) && !self.send(method)
        self.send(method, args[position])
      end
      return
    end
  end
  @base_class
end
build_dsl_element(klass, name, type) click to toggle source

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.
* AliasOf(<name>) - this aliases a name to another name.
* ArrayOf[<type>] - this creates an array of the <type> coercion.

@param klass [Class] The class representing this level in the DSL. @param name [String] The name of the element we're working on. @param type [Class] The type of this element we're working on.

This is the type coercion spoken above.

@return nil

# File lib/dsl/maker.rb, line 360
def self.build_dsl_element(klass, name, type)
  if @@types.has_key?(type)
    klass.class_eval do
      define_method(name.to_sym) do |*args|
        instance_exec('@' + name.to_s, *args, &@@types[type])
      end
    end
  elsif is_hash?(type)
    as_attr = '@' + name.to_s

    klass.class_eval do
      define_method(name.to_sym) do |*args, &dsl_block|
        if (!args.empty? || dsl_block)
          rv = {}
          Docile.dsl_eval(HashType.new(rv), &dsl_block) if dsl_block

          # 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
  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
  elsif is_alias?(type)
    klass.class_eval do
      alias_method name, type.real_name
    end
  elsif is_array?(type)
    as_attr = '@' + name.to_s

    klass.class_eval do
      define_method(name.to_sym) do |*args, &dsl_block|
        rv = ___get(as_attr)
        ___set(as_attr, rv = []) unless rv

        if dsl_block
          # This code is copy-pasted from the is_dsl?() section above. Figure out
          # how to hoist this code into something reusable. But, we don't need
          # the parent_class section (do we?)
          obj = type.base_type.new
          Docile.dsl_eval(obj, &dsl_block)
          dsl_value = obj.__apply(*args)

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

          rv.push(dsl_value)
        elsif !args.empty?
          rv.concat(
            args.flatten.map do |item|
              klass.new.instance_exec('@__________', item, &@@types[type.base_type])
            end
          )
        end

        rv
      end
    end
  else
    raise "Unrecognized element type '#{type}'"
  end

  return
end
is_dsl?(proto) click to toggle source
# File lib/dsl/maker.rb, line 479
def self.is_dsl?(proto)
  proto.is_a?(Class) && proto.ancestors.include?(DSL::Maker::Base)
end
is_entrypoint?(name) click to toggle source
# File lib/dsl/maker.rb, line 483
def self.is_entrypoint?(name)
  @entrypoints && @entrypoints.has_key?(name.to_sym)
end
is_hash?(proto) click to toggle source
# File lib/dsl/maker.rb, line 475
def self.is_hash?(proto)
  proto == Hash
end
run_dsl() { || ... } click to toggle source
# File lib/dsl/maker.rb, line 464
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

  return @accumulator
end