class Caesars
Caesars – Rapid DSL prototyping in Ruby.
Subclass Caesars and start drinking! I mean, start prototyping your own domain specific language!
See bin/example
A helper for loading a DSL from a config file.
Usage:
class Staff < Caesars; end; class StaffConfig < Caesars::Config dsl Staff::DSL end @config = StaffConfig.new('/path/2/staff_dsl.rb') p @config.staff # => <Staff:0x7ea450 ... >
Constants
- DIGEST_TYPE
- HASH_TYPE
- VERSION
Attributes
An instance of Caesars::Hash which contains the data specified by your DSL
Public Class Methods
Add s
to the list of global symbols (across all instances of
Caesars)
# File lib/caesars.rb, line 53 def Caesars.add_known_symbol(g, s) g = Caesars.glass(g) STDERR.puts "add_symbol: #{g} => #{s}" if Caesars.debug? @@known_symbols << s.to_sym @@known_symbols_by_glass[g] ||= [] @@known_symbols_by_glass[g] << s.to_sym end
Specify a method that should delay execution of its block. Here's an example:
class Food < Caesars chill :count end food do taste :delicious count do |items| puts items + 2 end end @food.count.call(3) # => 5
# File lib/caesars.rb, line 438 def self.chill(caesars_meth) STDERR.puts "chill: #{caesars_meth}" if Caesars.debug? Caesars.add_known_symbol(self, caesars_meth) @@chilled[caesars_meth.to_sym] = true nil end
Is the given name
chilled? See ::chill
# File lib/caesars.rb, line 35 def Caesars.chilled?(name) return false unless name @@chilled.has_key?(name.to_sym) end
# File lib/caesars.rb, line 32 def Caesars.debug?; @@debug; end
# File lib/caesars.rb, line 31 def Caesars.disable_debug; @@debug = false; end
# File lib/caesars.rb, line 30 def Caesars.enable_debug; @@debug = true; end
Specify a method that should store it's args as nested Arrays Here's an example:
class Food < Caesars forced_array :sauce end food do taste :delicious sauce :tabasco, :worcester sauce :franks end @food.sauce # => [[:tabasco, :worcester], [:franks]]
The blocks for elements that are specified as ::forced_array will be chilled (stored as Proc objects). The block is put at the end of the Array. e.g.
food do sauce :arg1, :arg2 do ... end end @food.sauce # => [[:inline_method, :arg1, :arg2, #<Proc:0x1fa552>]]
# File lib/caesars.rb, line 472 def self.forced_array(caesars_meth) STDERR.puts "forced_array: #{caesars_meth}" if Caesars.debug? Caesars.add_known_symbol(self, caesars_meth) @@forced_array[caesars_meth.to_sym] = true nil end
Is the given name
a forced array? See ::forced_array
# File lib/caesars.rb, line 41 def Caesars.forced_array?(name) return false unless name @@forced_array.has_key?(name.to_sym) end
Force the specified keyword to always be treated as a hash. Example:
startup do disks do create "/path/2" # Available as hash: [action][disks][create][/path/2] == {} create "/path/4" do # Available as hash: [action][disks][create][/path/4] == {size => 14} size 14 end end end
# File lib/caesars.rb, line 356 def self.forced_hash(caesars_meth, &b) STDERR.puts "forced_hash: #{caesars_meth}" if Caesars.debug? Caesars.add_known_symbol(self, caesars_meth) module_eval %Q{ def #{caesars_meth}(*caesars_names,&b) this_meth = :'#{caesars_meth}' add_known_symbol(this_meth) if Caesars.forced_ignore?(this_meth) STDERR.puts "Forced ignore: \#{this_meth}" if Caesars.debug? return end if @caesars_properties.has_key?(this_meth) && caesars_names.empty? && b.nil? return @caesars_properties[this_meth] end return nil if caesars_names.empty? && b.nil? return method_missing(this_meth, *caesars_names, &b) if caesars_names.empty? # Take the first argument in the list provided to "caesars_meth" caesars_name = caesars_names.shift prev = @caesars_pointer @caesars_pointer[this_meth] ||= Caesars::Hash.new hash = Caesars::Hash.new if @caesars_pointer[this_meth].has_key?(caesars_name) STDERR.puts "duplicate key ignored: \#{caesars_name}" return end # The pointer is pointing to the hash that contains "this_meth". # We wan't to make it point to the this_meth hash so when we call # the block, we'll create new entries in there. @caesars_pointer = hash if Caesars.chilled?(this_meth) # We're done processing this_meth so we want to return the pointer # to the level above. @caesars_pointer = prev @caesars_pointer[this_meth][caesars_name] = b else if b # Since the pointer is pointing to the this_meth hash, all keys # created in the block we be placed inside. b.call end # We're done processing this_meth so we want to return the pointer # to the level above. @caesars_pointer = prev @caesars_pointer[this_meth][caesars_name] = hash end # All other arguments provided to "caesars_meth" # will reference the value for the first argument. caesars_names.each do |name| @caesars_pointer[this_meth][name] = @caesars_pointer[this_meth][caesars_name] end @caesars_pointer = prev # Make sure we're back up one level end } nil end
Specify a method that should always be ignored. Here's an example:
class Food < Caesars forced_ignore :taste end food do taste :delicious end @food.taste # => nil
# File lib/caesars.rb, line 492 def self.forced_ignore(caesars_meth) STDERR.puts "forced_ignore: #{caesars_meth}" if Caesars.debug? Caesars.add_known_symbol(self, caesars_meth) @@forced_ignore[caesars_meth.to_sym] = true nil end
Is the given name
a forced ignore? See ::forced_ignore
# File lib/caesars.rb, line 47 def Caesars.forced_ignore?(name) return false unless name @@forced_ignore.has_key?(name.to_sym) end
Returns the lowercase name of klass
. i.e. Some::Taste # =>
taste
# File lib/caesars.rb, line 248 def self.glass(klass); (klass.to_s.split(/::/)).last.downcase.to_sym; end
Executes automatically when Caesars is subclassed. This creates the YourClass::DSL module which contains a single method named after YourClass that is used to catch the top level DSL method.
For example, if your class is called Glasses::HighBall, your top level method would be: highball.
highball :mine do volume "9oz" end
# File lib/caesars.rb, line 510 def self.inherited(modname) STDERR.puts "INHERITED: #{modname}" if Caesars.debug? # NOTE: We may be able to replace this without an eval using Module.nesting meth = (modname.to_s.split(/::/)).last.downcase # Some::HighBall => highball # The method name "meth" is now a known symbol # for the short class name (also "meth"). Caesars.add_known_symbol(meth, meth) # We execute a module_eval form the namespace of the inherited class # so when we define the new module DSL it will be Some::HighBall::DSL. modname.module_eval %Q{ module DSL def #{meth}(*args, &b) name = !args.empty? ? args.first.to_s : nil varname = "@#{meth.to_s}" varname << "_\#{name}" if name inst = instance_variable_get(varname) # When the top level DSL method is called without a block # it will return the appropriate instance variable name return inst if b.nil? # Add to existing instance, if it exists. Otherwise create one anew. # NOTE: Module.nesting[1] == modname (e.g. Some::HighBall) inst = instance_variable_set(varname, inst || Module.nesting[1].new(name)) inst.instance_eval(&b) inst end def self.methname :"#{meth}" end end }, __FILE__, __LINE__ end
Is s
in the global keyword list? (accross all instances of Caesars)
# File lib/caesars.rb, line 62 def Caesars.known_symbol?(s); @@known_symbols.member?(s.to_sym); end
Is s
in the keyword list for glass g
?
# File lib/caesars.rb, line 64 def Caesars.known_symbol_by_glass?(g, s) g &&= g.to_sym @@known_symbols_by_glass[g] ||= [] @@known_symbols_by_glass[g].member?(s.to_sym) end
# File lib/caesars.rb, line 73 def initialize(name=nil) @caesars_name = name if name @caesars_properties = Caesars::Hash.new @caesars_pointer = @caesars_properties init if respond_to?(:init) end
Public Instance Methods
Act a bit like a hash for the case: @subclass
# File lib/caesars.rb, line 220 def [](name) return @caesars_properties[name] if @caesars_properties.has_key?(name) return @caesars_properties[name.to_sym] if @caesars_properties.has_key?(name.to_sym) end
Act a bit like a hash for the case: @subclass = value
# File lib/caesars.rb, line 227 def []=(name, value) @caesars_properties[name] = value end
Add keyword
to the list of known symbols for this instances as
well as to the master known symbols list. See: known_symbol?
# File lib/caesars.rb, line 233 def add_known_symbol(s) @@known_symbols << s.to_sym @@known_symbols_by_glass[glass] ||= [] @@known_symbols_by_glass[glass] << s.to_sym end
Looks for the specific attribute specified. criteria
is an
array of attribute names, orders according to their relationship. The last
element is considered to the desired attribute. It can be an array.
Unlike #find_deferred, it will return only the value specified, otherwise nil.
# File lib/caesars.rb, line 208 def find(*criteria) criteria.flatten! if criteria.first.is_a?(Array) p criteria if Caesars.debug? # BUG: Attributes can be stored as strings and here we only look for symbols str = criteria.collect { |v| "[:'#{v}']" if v }.join eval_str = "@caesars_properties#{str} if defined?(@caesars_properties#{str})" val = eval eval_str val end
Look for an attribute, bubbling up through the parents until it's
found. criteria
is an array of hierarchical attributes,
ordered according to their relationship. The last element is the desired
attribute to find. Looking for 'ami':
find_deferred(:environment, :role, :ami)
First checks at @caesars_properties[:role] Then, @caesars_properties[:ami] Finally, @caesars_properties
If the last element is an Array, it's assumed that only that combination should be returned.
find_deferred(:environment, :role:, [:disks, '/file/path'])
Search order:
- :environment][:disks]['/file/path'
- :environment]['/file/path'
- :disks]['/file/path'
Other nested Arrays are treated special too. We look at the criteria from right to left and remove the first nested element we find.
find_deferred([:region, :zone], :environment, :role, :ami)
Search order:
- :region][:environment][:ami
- :region][:role][:ami
- :environment][:ami
- :environment][:ami
- :ami
NOTE: There is a maximum depth of 10.
Returns the attribute if found or nil.
# File lib/caesars.rb, line 154 def find_deferred(*criteria) # The last element is assumed to be the attribute we're looking for. # The purpose of this function is to bubble up the hierarchy of a # hash to find it. att = criteria.pop # Account for everything being sent as an Array # i.e. find([1, 2, :attribute]) # We don't use flatten b/c we don't want to disturb nested Arrays if criteria.empty? criteria = att att = criteria.pop end found = nil sacrifice = nil while !criteria.empty? found = find(criteria, att) break if found # Nested Arrays are treated special. We look at the criteria from # right to left and remove the first nested element we find. # # i.e. [['a', 'b'], 1, 2, [:first, :second], :attribute] # # In this example, :second will be removed first. criteria.reverse.each_with_index do |el,index| next unless el.is_a?(Array) # Ignore regular criteria next if el.empty? # Ignore empty nested hashes sacrifice = el.pop break end # Remove empty nested Arrays criteria.delete_if { |el| el.is_a?(Array) && el.empty? } # We need to make a sacrifice sacrifice = criteria.pop if sacrifice.nil? break if (limit ||= 0) > 10 # A failsafe limit += 1 sacrifice = nil end found || find(att) # One last try in the root namespace end
DEPRECATED – use #find_deferred
Look for an attribute, bubbling up to the parent if it's not found
criteria
is an array of attribute names, orders according to
their relationship. The last element is considered to the desired
attribute. It can be an array.
# Looking for 'attribute'. # First checks at @caesars_properties[grandparent][parent][attribute] # Then, @caesars_properties[grandparent][attribute] # Finally, @caesars_properties[attribute] find_deferred('grandparent', 'parent', 'attribute')
Returns the attribute if found or nil.
# File lib/caesars.rb, line 101 def find_deferred_old(*criteria) # This is a nasty implementation. Sorry me! I'll enjoy a few # caesars and be right with you. att = criteria.pop val = nil while !criteria.empty? p [criteria, att].flatten if Caesars.debug? val = find(criteria, att) break if val criteria.pop end # One last try in the root namespace val = @caesars_properties[att.to_sym] if defined?(@caesars_properties[att.to_sym]) && !val val end
Returns the lowercase name of the class. i.e. Some::Taste # => taste
# File lib/caesars.rb, line 245 def glass; @glass ||= (self.class.to_s.split(/::/)).last.downcase.to_sym; end
Returns an array of the available top-level attributes
# File lib/caesars.rb, line 81 def keys; @caesars_properties.keys; end
Has s
already appeared as a keyword in the DSL for this glass
type?
# File lib/caesars.rb, line 240 def known_symbol?(s) @@known_symbols_by_glass[glass] && @@known_symbols_by_glass[glass].member?(s) end
This method handles all of the attributes that are not forced hashes It's used in the DSL for handling attributes dyanamically (that weren't defined previously) and also in subclasses of Caesars for returning the appropriate attribute values.
# File lib/caesars.rb, line 254 def method_missing(meth, *args, &b) STDERR.puts "Caesars.method_missing: #{meth}" if Caesars.debug? add_known_symbol(meth) if Caesars.forced_ignore?(meth) STDERR.puts "Forced ignore: #{meth}" if Caesars.debug? return end # Handle the setter, attribute= if meth.to_s =~ /=$/ && @caesars_properties.has_key?(meth.to_s.chop.to_sym) return @caesars_properties[meth.to_s.chop.to_sym] = (args.size == 1) ? args.first : args end return @caesars_properties[meth] if @caesars_properties.has_key?(meth) && args.empty? && b.nil? # We there are no args and no block, we return nil. This is useful # for calls to methods on a Caesars::Hash object that don't have a # value (so we cam treat self[:someval] the same as self.someval). if args.empty? && b.nil? # We make an exception for methods that we are already expecting. if Caesars.forced_array?(meth) return @caesars_pointer[meth] ||= Caesars::Hash.new else return nil end end if b if Caesars.forced_array?(meth) @caesars_pointer[meth] ||= [] args << b # Forced array blocks are always chilled and at the end @caesars_pointer[meth] << args else # We loop through each of the arguments sent to "meth". # Elements are added for each of the arguments and the # contents of the block will be applied to each one. # This is an important feature for Rudy configs since # it allows defining several environments, roles, etc # at the same time. # env :dev, :stage, :prod do # ... # end # Use the name of the method if no name is supplied. args << meth if args.empty? args.each do |name| prev = @caesars_pointer @caesars_pointer[name] ||= Caesars::Hash.new if Caesars.chilled?(meth) @caesars_pointer[name] = b else @caesars_pointer = @caesars_pointer[name] begin b.call if b rescue ArgumentError, SyntaxError => ex STDERR.puts "CAESARS: error in #{meth} (#{args.join(', ')})" raise ex end @caesars_pointer = prev end end end # We've seen this attribute before, add the value to the existing element elsif @caesars_pointer.kind_of?(Hash) && @caesars_pointer[meth] if Caesars.forced_array?(meth) @caesars_pointer[meth] ||= [] @caesars_pointer[meth] << args else # Make the element an Array once there's more than a single value unless @caesars_pointer[meth].is_a?(Array) @caesars_pointer[meth] = [@caesars_pointer[meth]] end @caesars_pointer[meth] += args end elsif !args.empty? if Caesars.forced_array?(meth) @caesars_pointer[meth] = [args] else @caesars_pointer[meth] = args.size == 1 ? args.first : args end end end
Returns the parsed tree as a regular hash (instead of a Caesars::Hash)
# File lib/caesars.rb, line 84 def to_hash; @caesars_properties.to_hash; end