caesars 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGES.txt +17 -4
- data/README.rdoc +39 -28
- data/bin/example +37 -16
- data/caesars.gemspec +1 -1
- data/lib/caesars.rb +178 -40
- 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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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.
|
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
|
-
|
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 # => #<
|
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 "
|
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
|
-
#
|
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.
|
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
|
-
|
44
|
-
|
45
|
-
|
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 # => #<
|
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 "
|
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
|
+
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
|
5
|
-
#
|
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
|
7
|
+
# See bin/example
|
10
8
|
#
|
11
9
|
class Caesars
|
12
|
-
VERSION = "0.
|
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
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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[
|
101
|
+
@caesars_pointer[meth] = args.size == 1 ? args.first : args
|
40
102
|
end
|
103
|
+
|
41
104
|
end
|
42
|
-
|
43
|
-
def self.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
73
|
-
#
|
74
|
-
#
|
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
|
-
|
84
|
-
|
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
|
|