caesars 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/CHANGES.txt +17 -4
  2. data/README.rdoc +39 -28
  3. data/bin/example +37 -16
  4. data/caesars.gemspec +1 -1
  5. data/lib/caesars.rb +178 -40
  6. metadata +1 -1
data/CHANGES.txt CHANGED
@@ -1,14 +1,27 @@
1
1
  CAESAR -- CHANGES
2
2
 
3
3
 
4
+ #### 0.4.0 (2009-03-05) ###############################
5
+
6
+ * CHANGE: Removed bloody method. We now parse blocks immediately.
7
+ * CHANGE: Renamed virgin method to chill.
8
+ * NEW: Caesar::Config class for loading DSLs as config files.
9
+ See Example 3.
10
+ * NEW: Added find_deferred method to automatically jump up the
11
+ heirarchy when looking for a specific attribute.
12
+ * NEW: Added to_hash and [] methods to Caesar::Glass to make it
13
+ more hashlike.
14
+ * FIX: "chilled" attributes weren't available by method name
15
+
16
+
4
17
  #### 0.3.2 (2009-03-04) ###############################
5
18
 
6
19
  * FIX: Added file and line info for eval code (better debugging).
7
20
  * CHANGE: The top level DSL method names are now determined by
8
- by the class name. Some::ClassName becomes classname.
9
- This is less confusing than allowing it to be anything
10
- and makes it possible to use several DSLs in the same
11
- namespace.
21
+ by the class name. Some::ClassName becomes classname.
22
+ This is less confusing than allowing it to be anything
23
+ and makes it possible to use several DSLs in the same
24
+ namespace.
12
25
 
13
26
 
14
27
  #### 0.3.1 (2009-03-04) ###############################
data/README.rdoc CHANGED
@@ -1,4 +1,4 @@
1
- = Caesars - v0.3
1
+ = Caesars - v0.4
2
2
 
3
3
  A simple class for rapid DSL prototyping in Ruby.
4
4
 
@@ -17,21 +17,16 @@ Or for GitHub fans:
17
17
  * gem sources -a http://gems.github.com (you only have to do this once, ever...), then:
18
18
  * gem install delano-caesar
19
19
 
20
-
21
- == Usage
22
-
20
+ == EXAMPLE 1 -- Flavour
21
+
23
22
 
24
- # ------------------------------------------------------------------
25
- # EXAMPLE 1 -- Flavour
26
- #
27
-
28
23
  class Flavour < Caesars # Subclass Caesars.
29
24
  end
30
25
 
31
26
  extend Flavour::DSL # Bring the DSL into the current namespace.
32
27
  # This module is created dynamically based
33
28
  # on the name of the subclass.
34
-
29
+
35
30
  flavour do # Start drinking! I mean, start writing your
36
31
  spicy true # domain specific language!
37
32
  clamy true # Use any attribute name you want.
@@ -41,19 +36,17 @@ Or for GitHub fans:
41
36
 
42
37
  p @flavour # => #<Flavour:0x3f56b0 ...>
43
38
  p @flavour.spicy # => true
39
+
40
+
41
+
42
+ == EXAMPLE 2 -- Staff
43
+
44
+ require 'caesars'
44
45
 
46
+ class Staff < Caesars
45
47
 
46
-
47
- # ------------------------------------------------------------------
48
- # EXAMPLE 2 -- Staff
49
- #
50
-
51
- # Tell Caesars which attributes have children using Caesars#bloody and
52
- # which have blocks that you want to execute later using Caesars#virgin.
53
- class Staff < Caesars
54
- bloody :location
55
- bloody :person
56
- virgin :calculate
48
+ chill :calculate # Delay execution of the blocks for the calculate
49
+ # attribute. They will be stored as Procs.
57
50
  end
58
51
 
59
52
  extend Staff::DSL
@@ -90,12 +83,13 @@ Or for GitHub fans:
90
83
  satisfaction :low
91
84
  calculate :salary do
92
85
  self.splashdown.delano.rate.to_f * self.splashdown.delano.hours
86
+
93
87
  end
94
88
  end
95
89
  end
96
90
  end
97
91
 
98
- p @staff_fte # => #<KitchenStaff: ...>
92
+ p @staff_fte # => #<Staff: ...>
99
93
  p @staff_fte.desc # => Our hard-working, "full-time" staff
100
94
 
101
95
  # Deeper attributes are also available via instance methods
@@ -107,18 +101,35 @@ Or for GitHub fans:
107
101
  # You can also access them using hash syntax
108
102
  p @staff_fte.splashdown[:steve][:role] # => [:manager, :cook]
109
103
 
110
- # The "bloody" attributes keep track of all values that are used. These are available as arrays
111
- # via "NAME_values" methods. The goes for the virgin ones.
112
- p @staff_fte.location_values # => [:splashdown]
113
- p @staff_fte.person_values.uniq # => [:steve, :sheila, :delano, :angela]
114
- p @staff_fte.calculate_values # => [:salary, :salary]
115
-
116
- # The "virgin" methods store their blocks as Procs and are not executed automatically.
104
+ # The "chilled" attributes store their blocks as Procs and are not executed automatically.
117
105
  # You can call them manually and send arguments like you normally would.
118
106
  p @staff_fte.splashdown.delano.salary.call # => 475.95
119
107
  p @staff_fte.splashdown.sheila.salary.call(rand(100)) # => 549.77
108
+ p @staff_fte.splashdown.keys
109
+
120
110
 
111
+ == EXAMPLE 3 -- External Config file
121
112
 
113
+ require 'caesars'
114
+
115
+ class Food < Caesars
116
+ chill :order
117
+ end
118
+ class Drink < Caesars
119
+ end
120
+
121
+ class PartyConfig < Caesars::Config
122
+ dsl Food::DSL
123
+ dsl Drink::DSL
124
+ end
125
+
126
+ conffile = File.join(File.dirname(__FILE__), 'party.conf')
127
+ @config = PartyConfig.new(:path => conffile)
128
+
129
+ p @config.food.order.call # => 8kg
130
+ p @config[:drink][:wine] # => 12L
131
+ p @config # => <PartyConfig:0x3f780c ...>
132
+ p @config.keys # => [:food, :drink]
122
133
 
123
134
  == More Info
124
135
 
data/bin/example CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ # Caesars -- A working example
3
4
  #
4
- # Caesars Example
5
+ # If your reading this via the rdocs you won't be able to see the code
6
+ # See: http://github.com/delano/caesar/blob/master/bin/example
5
7
  #
6
8
  # Usage: bin/example
7
9
  #
8
10
 
9
- $:.unshift(File.join(File.join(File.dirname(__FILE__), '..'), 'lib')) # Make sure our local lib is first in line
11
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) # Make sure our local lib is first in line
10
12
 
11
13
  require 'caesars'
12
14
 
@@ -37,12 +39,10 @@ p @flavour.spicy # => true
37
39
  # EXAMPLE 2 -- Staff
38
40
  #
39
41
 
40
- # Tell Caesars which attributes have children using Caesars#bloody and
41
- # which have blocks that you want to execute later using Caesars#virgin.
42
42
  class Staff < Caesars
43
- bloody :location
44
- bloody :person
45
- virgin :calculate
43
+
44
+ chill :calculate # Delay execution of the blocks for the calculate
45
+ # attribute. They will be stored as Procs.
46
46
  end
47
47
 
48
48
  extend Staff::DSL
@@ -79,12 +79,13 @@ staff :fte do
79
79
  satisfaction :low
80
80
  calculate :salary do
81
81
  self.splashdown.delano.rate.to_f * self.splashdown.delano.hours
82
+
82
83
  end
83
84
  end
84
85
  end
85
86
  end
86
87
 
87
- p @staff_fte # => #<KitchenStaff: ...>
88
+ p @staff_fte # => #<Staff: ...>
88
89
  p @staff_fte.desc # => Our hard-working, "full-time" staff
89
90
 
90
91
  # Deeper attributes are also available via instance methods
@@ -96,14 +97,34 @@ p @staff_fte.splashdown.delano.satisfaction # => :low
96
97
  # You can also access them using hash syntax
97
98
  p @staff_fte.splashdown[:steve][:role] # => [:manager, :cook]
98
99
 
99
- # The "bloody" attributes keep track of all values that are used. These are available as arrays
100
- # via "NAME_values" methods. The goes for the virgin ones.
101
- p @staff_fte.location_values # => [:splashdown]
102
- p @staff_fte.person_values.uniq # => [:steve, :sheila, :delano, :angela]
103
- p @staff_fte.calculate_values # => [:salary, :salary]
104
-
105
- # The "virgin" methods store their blocks as Procs and are not executed automatically.
100
+ # The "chilled" attributes store their blocks as Procs and are not executed automatically.
106
101
  # You can call them manually and send arguments like you normally would.
107
102
  p @staff_fte.splashdown.delano.salary.call # => 475.95
108
103
  p @staff_fte.splashdown.sheila.salary.call(rand(100)) # => 549.77
109
- p @staff_fte.splashdown.keys
104
+ p @staff_fte.splashdown.keys
105
+
106
+
107
+ # ------------------------------------------------------------------
108
+ # EXAMPLE 3 -- External Config file
109
+ #
110
+
111
+ class Food < Caesars
112
+ chill :order
113
+ end
114
+ class Drink < Caesars
115
+ end
116
+
117
+ class PartyConfig < Caesars::Config
118
+ dsl Food::DSL
119
+ dsl Drink::DSL
120
+ end
121
+
122
+ conffile = File.join(File.dirname(__FILE__), 'party.conf')
123
+ @config = PartyConfig.new(:path => conffile)
124
+
125
+ p @config.food.order.call # => 8kg
126
+ p @config[:drink][:wine] # => 12L
127
+ p @config # => <PartyConfig:0x3f780c ...>
128
+ p @config.keys # => [:food, :drink]
129
+
130
+
data/caesars.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  @spec = Gem::Specification.new do |s|
2
2
  s.name = %q{caesars}
3
- s.version = "0.3.2"
3
+ s.version = "0.4.0"
4
4
  s.date = %q{2009-03-04}
5
5
  s.specification_version = 1 if s.respond_to? :specification_version=
6
6
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
data/lib/caesars.rb CHANGED
@@ -1,15 +1,13 @@
1
1
 
2
2
  # Caesars -- A simple class for rapid DSL prototyping.
3
3
  #
4
- # Subclass Caesars, then tell it which attributes have children using
5
- # Caesars.bloody and which have blocks that you want to execute later
6
- # using Caesars.virgin. That's it! Just start drinking! I mean, start
7
- # writing your domain specific language!
4
+ # Subclass Caesars and start drinking! I mean, start prototyping
5
+ # your own domain specific language!
8
6
  #
9
- # See README.rdoc for a usage example.
7
+ # See bin/example
10
8
  #
11
9
  class Caesars
12
- VERSION = "0.3.2"
10
+ VERSION = "0.4.0"
13
11
  # A subclass of ::Hash that provides method names for hash parameters.
14
12
  # It's like a lightweight OpenStruct.
15
13
  # ch = Caesars::Hash[:tabasco => :lots!]
@@ -17,11 +15,15 @@ class Caesars
17
15
  #
18
16
  class Hash < ::Hash
19
17
  def method_missing(meth)
20
- (self.has_key?(meth)) ? self[meth] : nil
18
+ self[meth] if self.has_key?(meth)
21
19
  end
22
20
  end
21
+
23
22
  # An instance of Caesars::Hash which contains the data specified by your DSL
24
23
  attr_accessor :caesars_properties
24
+
25
+ @@caesars_chilled = []
26
+
25
27
  # Creates an instance of Caesars.
26
28
  # +name+ is .
27
29
  def initialize(name=nil)
@@ -29,49 +31,107 @@ class Caesars
29
31
  @caesars_properties = Caesars::Hash.new
30
32
  @caesars_pointer = @caesars_properties
31
33
  end
34
+
35
+ def keys
36
+ @caesars_properties.keys
37
+ end
38
+
39
+ def to_hash
40
+ @caesars_properties
41
+ end
42
+
43
+ # Look for an attribute, bubbling up to the parent if it's not found
44
+ # +criteria+ is an array of attribute names, orders according to their
45
+ # relationship.
46
+ #
47
+ # # Looking for 'attribute'.
48
+ # # First checks at @caesars_properties[grandparent][parent][attribute]
49
+ # # Then, @caesars_properties[grandparent][attribute]
50
+ # # Finally, @caesars_properties[attribute]
51
+ # find_deferred('grandparent', 'parent', 'attribute')
52
+ #
53
+ # Returns the attribute if found or nil
54
+ #
55
+ def find_deferred(*criteria)
56
+ # This is a nasty implementation. Sorry me! I'll enjoy a few
57
+ # caesars and be right with you.
58
+ att = criteria.pop
59
+ val = nil
60
+ while !criteria.empty?
61
+ str = criteria.collect { |v| "[:#{v}]" }.join << "[:#{att}]"
62
+ val = eval "@caesars_properties#{str} if defined?(@caesars_properties#{str})"
63
+ break if val
64
+ criteria.pop
65
+ end
66
+ # One last try in the root namespace
67
+ val = @caesars_properties[att.to_sym] if defined?(@caesars_properties[att.to_sym]) && !val
68
+ val
69
+ end
70
+
71
+ # Act a bit like a hash for the case:
72
+ # @subclass[:property]
73
+ def [](name)
74
+ return @caesars_properties[name] if @caesars_properties.has_key?(name)
75
+ return @caesars_properties[name.to_sym] if @caesars_properties.has_key?(name.to_sym)
76
+ end
77
+
32
78
  # This method handles all of the attributes that do not contain blocks.
33
- def method_missing(name, *args, &b)
34
- return @caesars_properties[name] if @caesars_properties.has_key?(name) && args.empty? && b.nil?
35
- if @caesars_pointer[name]
36
- @caesars_pointer[name] = [@caesars_pointer[name]] unless @caesars_pointer[name].is_a?(Array)
37
- @caesars_pointer[name] += args
79
+ # It's used in the DSL for handling attributes dyanamically (that weren't defined
80
+ # previously) and also in subclasses of Caesar for returning the appropriate
81
+ # attribute values.
82
+ def method_missing(meth, *args, &b)
83
+ return @caesars_properties[meth] if @caesars_properties.has_key?(meth) && args.empty? && b.nil?
84
+ return nil if args.empty? && b.nil?
85
+
86
+ if b
87
+ # Use the name of the bloody method if no name is supplied.
88
+ args << meth if args.empty?
89
+ args.each do |name|
90
+ prev = @caesars_pointer
91
+ @caesars_pointer[name] ||= Caesars::Hash.new
92
+ @caesars_pointer = @caesars_pointer[name]
93
+ b.call if b
94
+ @caesars_pointer = prev
95
+ end
96
+
97
+ elsif @caesars_pointer[meth]
98
+ @caesars_pointer[meth] = [@caesars_pointer[meth]] unless @caesars_pointer[meth].is_a?(Array)
99
+ @caesars_pointer[meth] += args
38
100
  elsif !args.empty?
39
- @caesars_pointer[name] = args.size == 1 ? args.first : args
101
+ @caesars_pointer[meth] = args.size == 1 ? args.first : args
40
102
  end
103
+
41
104
  end
42
- # see bin/example for usage.
43
- def self.virgin(meth)
44
- self.bloody(meth, false)
45
- end
46
- # see bin/example for usage.
47
- def self.bloody(meth, execute=true)
105
+
106
+ def self.chill(meth)
48
107
  define_method(meth) do |*names,&b| # |*names,&b| syntax does not parse in Ruby 1.8
49
- all = instance_variable_get("@" << meth.to_s) || []
108
+ # caesar.toplevel.unnamed_chilled_attribute
109
+ return @caesars_pointer[meth] if names.empty? && b.nil?
50
110
 
111
+ # Use the name of the bloody method if no name is supplied.
112
+ names << meth if names.empty?
113
+
51
114
  names.each do |name|
52
- instance_variable_set("@" << meth.to_s, all << name)
53
-
54
- if execute
55
- prev = @caesars_pointer
56
- @caesars_pointer[name] ||= Caesars::Hash.new
57
- @caesars_pointer = @caesars_pointer[name]
58
- b.call if b
59
- @caesars_pointer = prev
60
- else
61
- @caesars_pointer[name] = b
62
- end
63
-
115
+ @caesars_pointer[name] = b
64
116
  end
117
+
65
118
  nil
66
119
  end
67
- define_method("#{meth}_values") do ||
68
- instance_variable_get("@" << meth.to_s) || []
69
- end
120
+ #define_method("#{meth}_values") do ||
121
+ # instance_variable_get("@" << meth.to_s) || []
122
+ #end
70
123
  end
71
124
  # Executes automatically when Caesars is subclassed. This creates the
72
- # YourClass::DSL module which contains a single method: method_missing.
73
- # This is used to catch the top level DSL method. That's why you can
74
- # used any method name you like.
125
+ # YourClass::DSL module which contains a single method named after YourClass
126
+ # that is used to catch the top level DSL method.
127
+ #
128
+ # For example, if your class is called Glasses::HighBall, your top level method
129
+ # would be: highball.
130
+ #
131
+ # highball :mine do
132
+ # volume 9.oz
133
+ # end
134
+ #
75
135
  def self.inherited(modname)
76
136
  meth = (modname.to_s.split(/::/))[-1].downcase # Some::ClassName => classname
77
137
  module_eval %Q{
@@ -80,13 +140,91 @@ class Caesars
80
140
  name = !args.empty? ? args.first.to_s : nil
81
141
  varname = "@#{meth.to_s}"
82
142
  varname << "_\#{name}" if name
83
- i = instance_variable_set(varname, #{modname.to_s}.new(name))
84
- i.instance_eval(&b) if b
143
+
144
+ # When the top level DSL method is called without a block
145
+ # it will return the appropriate instance variable name
146
+ if b.nil?
147
+ i = instance_variable_get(varname)
148
+ else
149
+ i = instance_variable_set(varname, #{modname.to_s}.new(name))
150
+ i.instance_eval(&b)
151
+ end
85
152
  i
86
153
  end
154
+
155
+ def self.methname
156
+ :"#{meth}"
157
+ end
158
+
87
159
  end
88
160
  }, __FILE__, __LINE__
89
161
  end
162
+
163
+ end
164
+
165
+
166
+ # A helper for loading a DSL from a config file.
167
+ #
168
+ # Usage:
169
+ #
170
+ # class Staff < Caesars; end;
171
+ # class StaffConfig < Caesars::Config
172
+ # dsl Staff::DSL
173
+ # end
174
+ # @config = StaffConfig.new(:path => '/path/2/staff_dsl.rb')
175
+ # p @config.staff # => <Staff:0x7ea450 ... >
176
+ #
177
+ class Caesars::Config
178
+ attr_accessor :path
179
+ attr_accessor :verbose
180
+
181
+ @@glasses = []
182
+
183
+ def initialize(args={:path=>'', :verbose=>false})
184
+ args.each_pair do |n,v|
185
+ self.send("#{n}=", v)
186
+ end
187
+
188
+ refresh
189
+ end
190
+
191
+ def self.dsl(glass)
192
+ @@glasses << glass
193
+ end
194
+
195
+ def [](name)
196
+ self.send(name) if respond_to?(name)
197
+ end
198
+
199
+ def keys
200
+ @@glasses.collect { |glass| glass.methname }
201
+ end
202
+
203
+ def refresh
204
+
205
+ if exists?
206
+ puts "Loading config from #{@path}" if @verbose
207
+
208
+ begin
209
+ @@glasses.each { |glass| extend glass }
210
+ dsl = File.read @path
211
+
212
+ # We're using eval so the DSL code can be executed in this
213
+ # namespace.
214
+ eval %Q{
215
+ #{dsl}
216
+ }
217
+
218
+ rescue SyntaxError => ex
219
+ puts "Syntax error in #{@path}."
220
+ exit 1
221
+ end
222
+ end
223
+ end
224
+
225
+ def exists?
226
+ File.exists?(@path)
227
+ end
90
228
  end
91
229
 
92
230
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: caesars
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum