Class: Caesars

Inherits:
Object
  • Object
show all
Defined in:
lib/caesars.rb

Overview

Caesars – Rapid DSL prototyping in Ruby.

Subclass Caesars and start drinking! I mean, start prototyping your own domain specific language!

See bin/example

Defined Under Namespace

Classes: Config, Error, Hash, OrderedHash, SyntaxError

Constant Summary collapse

VERSION =
"0.7.0"
HASH_TYPE =
(RUBY_VERSION =~ /1.9/) ? ::Hash : Caesars::OrderedHash
@@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.



108
109
110
111
112
113
# File 'lib/caesars.rb', line 108

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.



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
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/caesars.rb', line 289

def method_missing(meth, *args, &b)
  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



92
93
94
# File 'lib/caesars.rb', line 92

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)



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

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


464
465
466
467
468
469
# File 'lib/caesars.rb', line 464

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)


27
28
29
30
# File 'lib/caesars.rb', line 27

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

.debug?Boolean

Returns:

  • (Boolean)


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

def Caesars.debug?; @@debug; end

.disable_debugObject



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

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

.enable_debugObject



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

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


498
499
500
501
502
503
# File 'lib/caesars.rb', line 498

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)


33
34
35
36
# File 'lib/caesars.rb', line 33

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


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
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/caesars.rb', line 390

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?
      
      # TODO: This should be a loop
      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
      
      @caesars_pointer = prev   
    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


518
519
520
521
522
523
# File 'lib/caesars.rb', line 518

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)


39
40
41
42
# File 'lib/caesars.rb', line 39

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



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

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


536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
# File 'lib/caesars.rb', line 536

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)


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

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)


56
57
58
59
60
# File 'lib/caesars.rb', line 56

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



255
256
257
258
# File 'lib/caesars.rb', line 255

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



262
263
264
# File 'lib/caesars.rb', line 262

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?



268
269
270
271
272
# File 'lib/caesars.rb', line 268

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.



243
244
245
246
247
248
249
250
251
# File 'lib/caesars.rb', line 243

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.



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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/caesars.rb', line 189

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.



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/caesars.rb', line 136

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



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

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

#keysObject

Returns an array of the available top-level attributes



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

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)


275
276
277
# File 'lib/caesars.rb', line 275

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)



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

def to_hash; @caesars_properties.to_hash; end