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
Public Class Methods
# File lib/dsl/maker.rb, line 75 def self.AliasOf(name) @@aliases[name] ||= Alias.new(name) end
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
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
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
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
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 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
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
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
# File lib/dsl/maker.rb, line 78 def self.is_alias?(type) type.instance_of? Alias end
# File lib/dsl/maker.rb, line 96 def self.is_array?(type) type.instance_of? ArrayType end
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
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
# 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
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
# File lib/dsl/maker.rb, line 479 def self.is_dsl?(proto) proto.is_a?(Class) && proto.ancestors.include?(DSL::Maker::Base) end
# File lib/dsl/maker.rb, line 483 def self.is_entrypoint?(name) @entrypoints && @entrypoints.has_key?(name.to_sym) end
# File lib/dsl/maker.rb, line 475 def self.is_hash?(proto) proto == Hash end
# 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