cascading_classes 0.3.0 → 0.6.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/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