cascading_classes 0.3.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,7 +1,14 @@
1
1
  require 'rake'
2
2
  require 'rake/testtask'
3
3
 
4
- Rake::TestTask.new do |t|
5
- t.pattern = "spec/*_spec.rb"
4
+ desc "Default Task"
5
+ task :default => :test_all
6
+
7
+ desc "Run all tests"
8
+ task :test_all => [:test_units]
9
+
10
+ Rake::TestTask.new("test_units") do |t|
11
+ t.pattern = "spec/**/*_spec.rb"
12
+ ## t.test_files = FileList['spec/*
6
13
  end
7
14
 
@@ -1,97 +1,305 @@
1
1
  require 'cascading_classes/cascading_classes'
2
+ require 'cascading_classes/parse_options'
2
3
 
3
4
  CC = CascadingClasses unless defined? CC
4
5
 
5
- # trivialities:
6
- # module not meant to be both included and extended
7
-
6
+ # todo: add :unset_property functionality
7
+ # todo: allow types to cascade down singleton_class
8
+ # ie: each subclass could potentially have a different type
9
+ # todo: make parents_for() redundant by keeping track of each child's parents
10
+ # todo: Proc properties: (proc_spec.rb) B redefines mood/color procs, C.color(:inherit => false)
11
+ # is this supposed to call C.mood(:inherit => false) too?
8
12
 
9
- # class Foo
10
- # include CC
11
- # cascade :one, :two, :three, ...
12
- # end
13
- # Foo.one = 12
14
- # Foo.new.one # => 12
15
- # Bar = Class.new(Foo).one # => 12
16
- # Bar.new.one # => 12
13
+ # blurb:
14
+ # module not meant to be both included and extended
17
15
 
18
16
  module CascadingClasses
19
17
 
20
18
  # note, VERSION will be undetected unless called directly
21
19
  def self.const_missing(name)
22
20
  case name
23
- when :VERSION then "0.2.0"
21
+ when :VERSION then "0.4.0"
24
22
  else super
25
23
  end
26
24
  end
27
25
 
28
- def respond_to?(const)
26
+ def self.respond_to?(const)
29
27
  (const.to_s == 'VERSION') ? true : super
30
28
  end
31
- module_function :respond_to?
32
29
 
33
- # todo: fill in rest
30
+ def self.class_methods
31
+ [:to_hash, :nearest_parent_for, :top_level_parent_for?]
32
+ end
33
+
34
+ # only applies to desired getter and setter names, not the property itself
34
35
  # todo: allow one of [:cascade, :all!] to be overwritten
36
+ # todo: add :unset_property
35
37
  def self.illegal_names
36
38
  [:cascade, :all!, :set_property, :instance_eval, :instance_exec,
37
- :class_eval]
39
+ :class_eval].concat(class_methods)
40
+ end
41
+
42
+ # global default: whether proc values are evaluated or instance_exec'd
43
+ # global default defaults to false
44
+ def self.proc_inst_exec?
45
+ @proc_inst_exec.nil? ? @proc_inst_exec = false : @proc_inst_exec
38
46
  end
39
47
 
48
+ def self.proc_inst_exec=(val)
49
+ @proc_inst_exec = !!val
50
+ end
51
+
52
+ # global default: whether blocks are yielded to or instance_exec'd
53
+ # global default defaults to true
54
+ def self.block_inst_exec?
55
+ @block_inst_exec.nil? ? @block_inst_exec = true : @block_inst_exec
56
+ end
57
+
58
+ def self.block_inst_exec=(val)
59
+ @block_inst_exec = !!val
60
+ end
61
+
62
+ # klass is the class that included/excluded CascadingClasses
40
63
  def self.apply(klass, instances_too)
64
+
65
+ # cascade method
41
66
  klass.singleton_class.instance_eval do
42
67
 
43
68
  define_method :cascade do |*props, &block|
44
69
  illegal_names = CascadingClasses.illegal_names
45
70
 
46
- props = CascadingClasses.parse_options(klass, instances_too, *props, &block)
71
+ props = CascadingClasses.parse_options(instances_too, *props, &block)
47
72
  props.each{|prop, v|
48
- default, inst_too, getters, setters = v
49
- getters.each{|getter|
73
+ v[:getters].each{|getter|
50
74
  raise NameError, "Illegal property getter: #{getter} for #{prop}" if
51
75
  illegal_names.include? getter }
52
76
  }
53
77
 
54
- props.each do |prop, v|
55
- default, inst_too, getters, setters = v
78
+ props.each do |prop, settings|
79
+ raise "Hell" unless Symbol === prop # sanity
80
+ getters, setters = settings[:getters], settings[:setters]
81
+ getter, setter = getters.first, setters.first
56
82
 
57
- klass.singleton_class.instance_eval do
58
- undef_method prop.to_sym rescue nil
59
- undef_method "#{prop}=".to_sym rescue nil
83
+ # set default value on top level parent
84
+ me = settings[:ensure_type].call(settings[:default])
85
+ instance_variable_set("@#{prop}", me)
86
+
87
+ slf = self
88
+ # property metadata saved in singleton class
89
+ singleton_class.instance_eval do
90
+ @properties ||= {}
91
+ @properties.delete prop # in case it's already there
92
+ @properties[prop] = settings
93
+ @properties[prop][:class] = slf
60
94
  end
61
95
 
62
- klass.instance_eval{ instance_variable_set "@#{prop}", default }
96
+ ## mod = CascadingClasses.cascade_intern(self, prop, settings)
97
+ mod = CascadingClasses.cascade_intern(prop, settings)
63
98
 
64
- mod = CascadingClasses.cascade_intern(klass, prop, getters[0], setters[0])
65
- klass.extend mod
66
- klass.singleton_class.instance_eval do
67
- getters[1..-1].each{|getter| alias_method getter, getters[0]} unless
68
- getters[0].nil?
69
- setters[1..-1].each{|setter| alias_method setter, setters[0]} unless
70
- setters[0].nil?
99
+ self.extend mod
100
+ singleton_class.instance_eval do
101
+ getters[1..-1].each{|g| alias_method g, getter} unless getter.nil?
102
+ setters[1..-1].each{|s| alias_method s, setter} unless setter.nil?
71
103
  end
72
104
 
73
- if inst_too
74
- klass.instance_eval do
75
- undef_method prop.to_sym rescue nil
76
- undef_method "#{prop}=".to_sym rescue nil
77
- end
78
- klass.send(:include, mod)
79
- klass.instance_eval do
80
- getters[1..-1].each{|getter| alias_method getter, getters[0]} unless
81
- getters[0].nil?
82
- setters[1..-1].each{|setter| alias_method setter, setters[0]} unless
83
- setters[0].nil?
105
+ if settings[:instances_too]
106
+ send(:include, mod)
107
+ instance_eval do
108
+ getters[1..-1].each{|g| alias_method g, getter} unless getter.nil?
109
+ setters[1..-1].each{|s| alias_method s, setter} unless setter.nil?
84
110
  end
85
111
  end
86
112
  end
113
+ end
114
+
115
+ alias_method(:all!, :cascade) unless instance_methods.include?(:all!)
116
+ end
117
+
118
+ klass_methods = Module.new do
119
+
120
+ # whether property was originally defined on self
121
+ # uses the fact that 'properties' hash can be defined multiple times
122
+ # for the same property on the class tree
123
+ # name: property name
124
+ # instance: (boolean) whether callee is an instance
125
+ define_method :top_level_parent_for? do |name, instance=false|
126
+ return false if instance
127
+ name = name.to_sym
128
+ props = singleton_class.properties
129
+ raise "neither #{self} nor its ancestors have #{name}" unless
130
+ props and props[name]
131
+ props2 = superclass.singleton_class.properties rescue nil
132
+ (props2 and props2[name]) ? false : true
133
+ end
134
+
135
+ # returns array of parents starting (excludes self)
136
+ # for property:'name'
137
+ ### will include self if self is a class
138
+ # never includes self, even if a class
139
+ # name: property name
140
+ # instance: (boolean) whether 'callee' is an instance
141
+ # always called by a class, not an instance
142
+ define_method :parents_for do |name, instance=false|
143
+ return [] if top_level_parent_for?(name, instance)
144
+ s = instance ? self : self.superclass
145
+ res = [s]
146
+
147
+ until s.top_level_parent_for?(name)
148
+ raise "Hell" if s.nil?
149
+ s = s.superclass
150
+ res << s
151
+ end
152
+ res
153
+ end
154
+
155
+ # differs from 'parents_for' in that it is not
156
+ # property specific. a property defined on a
157
+ # descendent of the top-most class will only
158
+ # go as high as that descendent in the 'parents_for'.
159
+ # hence any call to 'parents_for' will be a sub array
160
+ # of a call to 'ancestor_chain'
161
+ #
162
+ # never includes self
163
+ define_method :ancestor_chain do |instance=false|
164
+ top_parent = oldest_cascading_class
165
+ return [] unless top_parent # todo: raise "Hell" instead
166
+ ## return [self] if self == top_parent
167
+ return [] if self == top_parent
168
+ res = []
169
+ up = self
170
+ while up != top_parent
171
+ res << up unless up == self and instance == false
172
+ up = up.superclass
173
+ end
174
+ res << top_parent
175
+ res
176
+ end
177
+
178
+ # slightly hackish
179
+ # todo: propose adding marker variable to top-level class
180
+ define_method :oldest_cascading_class do
181
+ res = singleton_class.instance_variable_get("@properties").nil? ? nil : self
182
+ tmp = self
183
+ while tmp = tmp.superclass
184
+ if s = tmp.singleton_class.instance_variable_get("@properties")
185
+ res = tmp if Hash === s
186
+ end
187
+ end
188
+ res
189
+ end
190
+
191
+ # calculates number of generations to ancestor
192
+ # *eval* todo: resolve
193
+ # note: a class is considered one generations away from its instances
194
+ # todo: explain why not using self.ancestors
195
+ # ancestor: class name to take generations from
196
+ # instance: (boolean) whether 'callee' is an instance
197
+ # always called by a class, not an instance
198
+ define_method :generations_from do |ancestor, instance=false|
199
+ ancestor = eval(ancestor.to_s) if Symbol === ancestor # *eval*
200
+ s = self
201
+ num = instance ? 1 : 0
202
+
203
+ while s != ancestor
204
+ s = s.superclass
205
+ raise "NoAncestor" if s.nil?
206
+ num += 1
207
+ end
208
+ num
209
+ end
210
+
211
+ # todo: instances
212
+ # collects properties and their values
213
+ # blanks: whether to include values that are blank
214
+ define_method :to_hash do |blanks=true|
215
+ s = self.class == Class ? self : self.class
216
+ ## props = s.singleton_class.properties rescue {}
217
+ ## return props if props.empty?
218
+ props = s.singleton_class.properties
219
+ getters = Hash[ props.map{|name, v| [name, v[:getters].first]} ]
220
+
221
+ res = if blanks
222
+ props.map{|name, v| [name, s.send(getters[name])] }
223
+ else
224
+ prop_names = props.keys.delete_if{|name| send("#{name}_is_blank?") }
225
+ prop_names.map{|name| [name, s.send(getters[name])] }
226
+ end
227
+ Hash[res]
228
+ end
229
+
230
+ # closest ancestor with a non-blank property
231
+ # returns nil if there aren't any
232
+ # name: property name
233
+ # instance: (boolean) whether 'callee' is an instance
234
+ # always called by a class, not an instance
235
+ define_method :youngest_valid_ancestor_for do |name, instance=false|
236
+ parents = parents_for(name, instance)
237
+ return nil if parents.empty?
238
+ parents.each{|ancestor| return ancestor unless
239
+ ancestor.send("#{name}_is_blank?") }
240
+ nil
241
+ end
242
+
243
+ =begin
244
+ define_method :youngest_valid_ancestor_for_OLD do |name, instance=false|
245
+ parents = parents_for(name, instance)
246
+ return self if parents.empty?
247
+ parents.each{|ancestor|
248
+ return ancestor unless ancestor.send("#{name}_is_blank?") }
249
+ self
250
+ end
251
+ =end
252
+
253
+ # name: property name
254
+ # instance: (boolean) whether 'callee' is an instance
255
+ # always called by a class, not an instance
256
+ define_method :oldest_valid_ancestor_for do |name, instance=false|
257
+ ## s = self.class == Class ? self : self.class
258
+ ## props = s.singleton_class.properties
259
+ props = singleton_class.properties
260
+ raise "No ancestor of #{self} has #{name}" unless
261
+ props and props[name.to_sym]
262
+
263
+ s = self
264
+ begin
265
+ tmp = s.superclass
266
+ while props2 = tmp.singleton_class.properties and
267
+ props2[name.to_sym]
268
+ s = tmp
269
+ tmp = s.superclass
270
+ end
271
+ rescue NoMethodError
272
+ end
273
+ s
87
274
  end
88
275
 
89
- unless instance_methods.include? :all!
90
- alias_method :all!, :cascade
276
+ end
277
+ klass.extend klass_methods
278
+ ## klass.send(:include, klass_and_inst_methods) if instances_too
279
+
280
+ klass.singleton_class.instance_eval do
281
+ @properties ||= {}
282
+
283
+ singleton_class.instance_eval do
284
+ define_method :properties do
285
+ res = instance_variable_get("@properties") || {}
286
+ return res if self.superclass == Object.singleton_class
287
+ up = superclass.send(:properties) rescue {}
288
+ up.merge(res)
289
+ end
290
+
291
+ define_method :children do
292
+ @children ||= []
293
+ end
91
294
  end
92
295
 
296
+ define_method :inherited do |subklass|
297
+ c = self.singleton_class.send(:children)
298
+ c.delete subklass # if present
299
+ c << subklass
300
+ end
93
301
  end
94
- end
302
+ end # self.apply
95
303
 
96
304
  def self.included(klass)
97
305
  apply(klass, true)
@@ -1,60 +1,214 @@
1
1
  require 'cascading_classes/dsl'
2
+ require 'cascading_classes/types'
3
+ require 'set'
4
+
5
+ # todo: support regexp's natively??
2
6
 
3
7
  module CascadingClasses
4
8
 
5
- def self.cascade_intern(klass, property, getter, setter)
6
- Module.new do
9
+ def self.cascade_parse_options(arg1, arg2, opts, settings)
10
+ r = {}
7
11
 
8
- if getter
9
- define_method getter do
10
- val = instance_variable_get "@#{property}"
11
- return val unless (val.nil? and self != klass)
12
- (self.class == Class) ? superclass.send(getter) :
13
- self.class.send(getter)
14
- end
12
+ # ':default' given? -> whether to ignore own value and
13
+ # return top-level 'default' set in 'cascade' call
14
+ r[:default] =
15
+ if arg1 == :default and (arg2 or arg2 == :undef)
16
+ true
17
+ elsif Hash === arg1 and arg1.include?(:default)
18
+ !!arg1[:default]
19
+ else
20
+ false
15
21
  end
22
+ return r if r[:default]
16
23
 
17
- if setter
18
- define_method setter do |v|
19
- instance_variable_set "@#{property}", v
20
- end
24
+ r[:proc_inst_exec] = opts.include?(:proc_inst_exec) ?
25
+ !!opts[:proc_inst_exec] : settings[:proc_inst_exec]
26
+ r[:block_inst_exec] = opts.include?(:block_inst_exec) ?
27
+ !!opts[:block_inst_exec] : settings[:block_inst_exec]
28
+
29
+ is_container = CascadingClasses.is_container?(settings[:type])
30
+
31
+ # inherit_age unless it's given manually
32
+ r[:inherit_age] =
33
+ if settings[:inherit] == :undef
34
+ raise "Hell" unless CascadingClasses.undefined_type?(settings[:type]) # sanity
35
+ -1
36
+ else
37
+ settings[:inherit]
21
38
  end
22
39
 
40
+ =begin
41
+ r[:inherit_age] = settings[:inherit]
42
+ if r[:inherit_age] == :undef and
43
+ not CascadingClasses.undefined_type?(settings[:type])
44
+ # only happens in corner case of unknown type
45
+ r[:inherit_age] = is_container ? 0 : -1
23
46
  end
47
+ =end
48
+
49
+ r[:inherit_age] =
50
+ if Hash === arg1 and arg1.include?(:inherit)
51
+ arg1[:inherit]
52
+ elsif arg1 == :inherit
53
+ arg2 == :undef ? -1 : arg2
54
+ elsif opts.include?(:inherit)
55
+ opts[:inherit]
56
+ else
57
+ (arg1 == :undef) ? r[:inherit_age] : arg1
58
+ end
59
+
60
+ r[:inherit_age] =
61
+ case r[:inherit_age]
62
+ when Fixnum then r[:inherit_age]
63
+ when true then -1
64
+ when false, nil then 0
65
+ else
66
+ raise "inherit_age: #{r[:inherit_age]} must be one of true, false, nil, <fixnum>"
67
+ end
68
+
69
+ r
24
70
  end
25
71
 
26
- # returns hash: {..., attr => [default, inst_too], .. }
27
- def self.parse_options(klass, apply_to_instances, *properties, &block)
28
- res = {}
29
- properties.each do |prop|
30
- name, default, inst_too = prop, nil, apply_to_instances
31
- getters, setters = [], []
32
- if Array === prop
33
- case prop.size
34
- when 0 then next
35
- when 1 then name = prop[0]
36
- when 2 then name, default = prop[0..1]
72
+ ## def self.cascade_intern(klass, property, settings)
73
+ def self.cascade_intern(property, settings)
74
+ property = property.to_sym
75
+ Module.new do
76
+
77
+ getter, setter = settings[:getters].first, settings[:setters].first
78
+ raise "Hell" unless getter
79
+
80
+ # opts:
81
+ # proc_context: class to evaluate any proc in
82
+ # block_inst_exec: whether to call block (false) or
83
+ # to send block to instance_exec (true)
84
+ # proc_inst_exec: whether to call proc (false) or
85
+ # to convert to a block and send to instance_exec (true)
86
+ # inherit: whether property (if blank) should inherit its value from above
87
+ # --->top_most_parent_overall vs top_most_parent_property
88
+ define_method getter do |arg1=:undef, arg2=:undef, opts={}, &block|
89
+ is_instance = (self.class == Class) ? false : true
90
+ klass = is_instance ? self.class : self
91
+
92
+ # todo: allow an option to decide which chain to use
93
+ chain = klass.parents_for(property, is_instance)
94
+ ## chain = klass.ancestor_chain # incorrect value for A.new.prop{|val, parents| parents}
95
+
96
+ if send("#{property}_is_unset?") and
97
+ not CascadingClasses.undefined_type?(settings[:type])
98
+ instance_variable_set "@#{property}", settings[:new].call
99
+ end
100
+
101
+ # parse options
102
+ r = CascadingClasses.cascade_parse_options(arg1, arg2, opts, settings)
103
+ proc_inst_exec, block_inst_exec = r[:proc_inst_exec], r[:block_inst_exec]
104
+ inherit_age = r[:inherit_age] || -1
105
+
106
+ if r[:default]
107
+ val, is_blank = settings[:default], false
37
108
  else
38
- name, default, inst_too = prop[0..2]
39
- getters = [*prop[3]] if prop.size >= 4
40
- setters = [*prop[4]] if prop.size >= 5
109
+ val = instance_variable_get "@#{property}"
110
+ is_blank = send("#{property}_is_blank?")
111
+ end
112
+ raise "Hell" if val and CascadingClasses.undefined_type?(settings[:type]) # sanity
113
+ return nil if CascadingClasses.undefined_type?(settings[:type]) # corner case
114
+
115
+ ## cb = settings[:ensure_type] # callback
116
+ cb = (settings[:type] == :Proc) ? Proc.new{|v| v} : settings[:ensure_type]
117
+ if settings[:after_get]
118
+ cb = Proc.new{|v| cb.call(settings[:after_get].call(v))}
41
119
  end
120
+
121
+ first_valid_ancestor = is_blank ?
122
+ klass.youngest_valid_ancestor_for(property, is_instance) : self
123
+
124
+ # todo: double-check
125
+ unless first_valid_ancestor
126
+ # there are no valid ancestors (from self) for property
127
+ raise "Hell" unless
128
+ klass.top_level_parent_for?(property, is_instance) or is_blank
129
+
130
+ me = cb.call(val)
131
+ return block ? (block_inst_exec ? instance_exec(self, chain, &block) :
132
+ block.call(me, chain)) : me
133
+ end
134
+
135
+ ancestor_age = (self == first_valid_ancestor) ? 0 :
136
+ klass.generations_from(first_valid_ancestor, is_instance)
137
+ inherit_age = (inherit_age < 0) ? ancestor_age : inherit_age
138
+
139
+ if is_blank
140
+ if ancestor_age > 0 and inherit_age >= ancestor_age
141
+ if settings[:type] == :Proc and not opts.include?(:proc_context)
142
+ opts.merge!({:proc_context => self})
143
+ end
144
+
145
+ # fix: hackish
146
+ up_val = first_valid_ancestor.instance_variable_get "@#{property}"
147
+ val = (settings[:type] == :Proc or Proc === up_val) ?
148
+ up_val : first_valid_ancestor.send(getter, :undef, :undef, opts)
149
+ end
150
+ end
151
+
152
+ if is_blank and inherit_age.zero? and settings[:type] == :Proc
153
+ val = settings[:default]
154
+ end
155
+
156
+ if Proc === val
157
+ val = if opts[:proc_context]
158
+ opts[:proc_context].instance_exec(self, chain, &val)
159
+ else
160
+ opts[:proc_inst_exec] ? instance_exec(self, chain, &val) :
161
+ val.call(self, chain)
162
+ end
163
+ end
164
+
165
+ me = cb.call(val)
166
+ block ? (block_inst_exec ? instance_exec(me, chain, &block) :
167
+ block.call(me, chain)) : me
42
168
  end
43
- getters = [name.to_sym] if getters.empty?
44
- setters = ["#{name}=".to_sym] if setters.empty?
45
- inst_too = !!inst_too
46
- res[name.to_sym] = [default, inst_too, getters, setters]
47
- end
48
169
 
49
- if block_given?
50
- a = CascadingClasses::DSL.new(apply_to_instances, &block)
51
- names, defaults, inst, getters, setters =
52
- a.instance_eval{ [@names, @defaults, @instances_too, @getters, @setters] }
53
- names.each{|name|
54
- res[name] = [defaults[name], inst[name], getters[name], setters[name]] }
55
- end
170
+ if setter
171
+ define_method setter do |v|
172
+ raise "Hell" if settings[:type] == :NilClass # sanity
173
+ if CascadingClasses.undefined_type?(settings[:type]) # or
174
+ ## settings[:type] == :NilClass
175
+ if v and v.class.to_s.to_sym != settings[:type]
176
+ type = settings[:type] = v.class.to_s.to_sym
177
+ unless settings[:options_given].include?(:blank)
178
+ settings[:blank] = CascadingClasses.default_blank(type)
179
+ end
180
+ unless settings[:options_given].include?(:new)
181
+ settings[:new] = CascadingClasses.default_new(type)
182
+ end
183
+ unless settings[:options_given].include?(:ensure_type)
184
+ settings[:ensure_type] =
185
+ CascadingClasses.default_ensure_type type,
186
+ settings[:blank],
187
+ settings[:new]
188
+ end
189
+ unless settings[:options_given].include?(:inherit)
190
+ settings[:inherit] = CascadingClasses.is_container?(type) ? false : true
191
+ end
192
+ end
193
+ end
194
+
195
+ v = settings[:ensure_type].call(v) unless Proc === v
196
+ instance_variable_set("@#{property}", v)
197
+ end
56
198
 
57
- res
199
+ end
200
+
201
+ define_method "#{property}_is_unset?" do
202
+ not instance_variables.include? "@#{property}".to_sym
203
+ end
204
+
205
+ define_method "#{property}_is_blank?" do
206
+ return true if self.send "#{property}_is_unset?"
207
+ val = instance_variable_get "@#{property}"
208
+ Proc === val ? false : settings[:blank].call(val)
209
+ end
210
+
211
+ end
58
212
  end
59
213
 
60
214
  end # module CascadingClasses