caesars 0.3.2 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|