Class: Caesars

Inherits:
Object
  • Object
show all
Defined in:
lib/caesars.rb,
lib/caesars/hash.rb,
lib/caesars/config.rb,
lib/caesars/exceptions.rb

Overview

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

Defined Under Namespace

Classes: Config, Error, Hash, OrderedHash, SyntaxError

Constant Summary collapse

VERSION =
"0.7.4"
HASH_TYPE =
(RUBY_VERSION =~ /1.9/) ? ::Hash : Caesars::OrderedHash
DIGEST_TYPE =
Digest::SHA1
@@debug =
false
@@chilled =
{}
@@forced_array =
{}
@@forced_ignore =
{}
@@known_symbols =
[]
@@known_symbols_by_glass =
{}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name = nil) ⇒ Caesars

Returns a new instance of Caesars.



73
74
75
76
77
78
# 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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(meth, *args, &b) ⇒ Object

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.



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
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
338
339
340
341
# 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

Instance Attribute Details

#caesars_propertiesObject

An instance of Caesars::Hash which contains the data specified by your DSL



71
72
73
# File 'lib/caesars.rb', line 71

def caesars_properties
  @caesars_properties
end

Class Method Details

.add_known_symbol(g, s) ⇒ Object

Add s to the list of global symbols (across all instances of Caesars)



53
54
55
56
57
58
59
# 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

.chill(caesars_meth) ⇒ Object

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


438
439
440
441
442
443
# 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

.chilled?(name) ⇒ Boolean

Is the given name chilled? See Caesars.chill

Returns:

  • (Boolean)


35
36
37
38
# File 'lib/caesars.rb', line 35

def Caesars.chilled?(name)
  return false unless name
  @@chilled.has_key?(name.to_sym)
end

.debug?Boolean

Returns:

  • (Boolean)


32
# File 'lib/caesars.rb', line 32

def Caesars.debug?; @@debug; end

.disable_debugObject



31
# File 'lib/caesars.rb', line 31

def Caesars.disable_debug; @@debug = false; end

.enable_debugObject



30
# File 'lib/caesars.rb', line 30

def Caesars.enable_debug; @@debug = true; end

.forced_array(caesars_meth) ⇒ Object

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


472
473
474
475
476
477
# 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

.forced_array?(name) ⇒ Boolean

Is the given name a forced array? See Caesars.forced_array

Returns:

  • (Boolean)


41
42
43
44
# File 'lib/caesars.rb', line 41

def Caesars.forced_array?(name)
  return false unless name
  @@forced_array.has_key?(name.to_sym)
end

.forced_hash(caesars_meth, &b) ⇒ Object

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


356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# 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

.forced_ignore(caesars_meth) ⇒ Object

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


492
493
494
495
496
497
# 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

.forced_ignore?(name) ⇒ Boolean

Is the given name a forced ignore? See Caesars.forced_ignore

Returns:

  • (Boolean)


47
48
49
50
# File 'lib/caesars.rb', line 47

def Caesars.forced_ignore?(name)
  return false unless name
  @@forced_ignore.has_key?(name.to_sym)
end

.glass(klass) ⇒ Object

Returns the lowercase name of klass. i.e. Some::Taste # => taste



248
# File 'lib/caesars.rb', line 248

def self.glass(klass); (klass.to_s.split(/::/)).last.downcase.to_sym; end

.inherited(modname) ⇒ Object

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


510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
# 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

.known_symbol?(s) ⇒ Boolean

Is s in the global keyword list? (accross all instances of Caesars)

Returns:

  • (Boolean)


62
# File 'lib/caesars.rb', line 62

def Caesars.known_symbol?(s); @@known_symbols.member?(s.to_sym); end

.known_symbol_by_glass?(g, s) ⇒ Boolean

Is s in the keyword list for glass g?

Returns:

  • (Boolean)


64
65
66
67
68
# 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

Instance Method Details

#[](name) ⇒ Object

Act a bit like a hash for the case: @subclass



220
221
222
223
# 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

#[]=(name, value) ⇒ Object

Act a bit like a hash for the case: @subclass = value



227
228
229
# File 'lib/caesars.rb', line 227

def []=(name, value)
  @caesars_properties[name] = value
end

#add_known_symbol(s) ⇒ Object

Add keyword to the list of known symbols for this instances as well as to the master known symbols list. See: known_symbol?



233
234
235
236
237
# 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

#find(*criteria) ⇒ Object

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.



208
209
210
211
212
213
214
215
216
# 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

#find_deferred(*criteria) ⇒ Object

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:

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:

NOTE: There is a maximum depth of 10.

Returns the attribute if found or nil.



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# 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

#find_deferred_old(*criteria) ⇒ Object

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.



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# 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

#glassObject

Returns the lowercase name of the class. i.e. Some::Taste # => taste



245
# File 'lib/caesars.rb', line 245

def glass; @glass ||= (self.class.to_s.split(/::/)).last.downcase.to_sym; end

#keysObject

Returns an array of the available top-level attributes



81
# File 'lib/caesars.rb', line 81

def keys; @caesars_properties.keys; end

#known_symbol?(s) ⇒ Boolean

Has s already appeared as a keyword in the DSL for this glass type?

Returns:

  • (Boolean)


240
241
242
# File 'lib/caesars.rb', line 240

def known_symbol?(s)
  @@known_symbols_by_glass[glass] && @@known_symbols_by_glass[glass].member?(s)
end

#to_hashObject

Returns the parsed tree as a regular hash (instead of a Caesars::Hash)



84
# File 'lib/caesars.rb', line 84

def to_hash; @caesars_properties.to_hash; end