cascading_classes 0.1.0 → 0.2.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/README.md CHANGED
@@ -100,17 +100,16 @@ is equivalent to
100
100
  hair_color :default => "blond"
101
101
  end
102
102
 
103
- or
103
+ or
104
104
 
105
105
  Parent.all! do
106
106
  eye_color :initial => "blue"
107
107
  hair_color :initial => "blond"
108
108
  end
109
109
 
110
- #### More block examples
110
+ ### More block examples
111
111
 
112
112
  The folowing are all equivalent
113
- ==============================
114
113
 
115
114
  Parent.cascade :address, :phone, [:email, "jon@example.com"]
116
115
 
@@ -137,44 +136,69 @@ You can also set a property in a block using ```set_property```
137
136
 
138
137
  ## Quick Summary
139
138
 
140
- * require the gem: ```require cascading_classes```
141
- * extend or include the module
139
+ Note: CC is equivalent to CascadingClasses
140
+
141
+ require the gem
142
+
143
+ require cascading_classes
144
+
145
+ extend or include the module
146
+
142
147
  class Foo
143
148
  extend CascadingClasses
144
149
  end
145
- * CC is equivalent to CascadingClasses
146
- * create some cascading properties by listing them
150
+
151
+ create some cascading properties by listing them
152
+
147
153
  A.cascade :desk, :chair, :lamp
148
- * or by using a block
154
+
155
+ or by using a block
156
+
157
+ A.cascade do
158
+ desk
159
+ chair
160
+ lamp
161
+ end
162
+
163
+ or
164
+
149
165
  A.cascade{ desk; chair; lamp }
150
- * get and set the property on any descendent
166
+
167
+ get and set the property on any descendent
168
+
151
169
  class B < A; end
170
+
152
171
  B.desk = "a large, wooden structure in need of a fix"
172
+
153
173
  class C < A; end
174
+
154
175
  C.lamp = "old and rusty"
155
176
 
156
- ### What's the difference between ```extend CC``` and ```include CC```?
177
+ ## What's the difference between ```extend CC``` and ```include CC```?
178
+
179
+ First, in either case you are dealing with class instance variables and class methods.
157
180
 
158
- First, in either case you are dealing with class instance variables and class methods. ```A.cascade :eye_color; A.eye_color``` sets ```@eye_color``` on the singleton class of A.
181
+ A.cascade :eye_color
182
+ A.eye_color = "blue"
159
183
 
160
- But if you also want the properties to cascade down to instance variables you have three options. The first way is to ```include CC```, which will create cascading instance properties by default.
184
+ sets ```@eye_color``` on the singleton class of ```A```.
161
185
 
162
- For example:
186
+ But if you also want the properties to cascade to instance variables, you have three options. The first is to ```include CC```, which, by default, creates the properties on instances too
163
187
 
164
188
  class Parent
165
189
  include CC
166
190
  end
167
191
 
168
- Parent.cascade [:eye_color, "blue"]
192
+ Parent.cascade [:one, 1]
169
193
  p = Parent.new
170
- p p.eye_color # => "blue"
194
+ p p.one # => 1
171
195
 
172
196
  class Child < Parent; end
173
197
 
174
198
  c = Child.new
175
- p c.eye_color # => "blue"
199
+ p c.one # => 1
176
200
 
177
- The other two ways to make properties cascade down to instances is via:
201
+ The other two ways to ensure instances have cascaded properties is by including a third argument
178
202
 
179
203
  Parent.cascade [:hair_color, "blue", true]
180
204
 
@@ -187,4 +211,3 @@ or
187
211
  In either case, even if you ```extend CC```, all instance variables of all descendents will have the ```hair_color``` property.
188
212
 
189
213
 
190
-
@@ -1,163 +1,49 @@
1
- module CascadingClasses
2
-
3
- # based off
4
- # http://blog.oleganza.com/post/115377756/correct-blankslate-in-ruby
5
- class BlankSlate
6
- class << self; alias __undef_method undef_method; end
7
- keep = [:instance_eval, :object_id]
8
- ancestors.inject([]){|res, a| res + (a.instance_methods - keep)}.uniq.
9
- each{|m| (__undef_method(m) rescue nil) unless m =~ /^__/ }
10
- end
1
+ require 'cascading_classes/dsl'
11
2
 
12
- class Helper < CascadingClasses::BlankSlate
13
-
14
- def method_missing(meth, *args, &block)
15
- case meth
16
- when /inspect/
17
- super
18
- when /set_property/
19
- __property__(args[0], *args[1..-1], &block)
20
- else
21
- __property__(meth, *args, &block)
22
- end
23
- end
3
+ module CascadingClasses
24
4
 
25
- def __property__(name, args={}, &block)
26
- @names << name unless @names.include? name
5
+ def self.cascade_intern(klass, property)
6
+ Module.new do
27
7
 
28
- @defaults[name] = if args.include? :default
29
- args[:default]
30
- elsif args.include? :initial
31
- args[:initial]
32
- elsif args.include? :start
33
- args[:start]
34
- elsif args.include? :begin
35
- args[:begin]
36
- else
37
- nil
8
+ define_method property do
9
+ val = instance_variable_get "@#{property}"
10
+ return val unless (val.nil? and self != klass)
11
+ (self.class == Class) ? superclass.send(property) :
12
+ self.class.send(property)
38
13
  end
39
14
 
40
- @instances_too[name] = if args.include? :classes_only
41
- !args[:classes_only]
42
- elsif args.include? :only_classes
43
- !args[:only_classes]
44
- elsif args.include? :classes_only
45
- !args[:classes_only]
46
- elsif args.include? :exclude_instances
47
- !args[:exclude_instances]
48
- elsif args.include? :skip_instances
49
- !args[:skip_instances]
50
- elsif args.include? :not_instances
51
- !args[:not_instances]
52
- elsif args.include? :apply_to_instances
53
- args[:apply_to_instances]
54
- elsif args.include? :both
55
- args[:both]
56
- elsif args.include? :instances_too
57
- args[:instances_too]
58
- elsif args.include? :instances
59
- args[:instances]
60
- elsif args.include? :instance
61
- args[:instance]
62
- else
63
- @apply_to_instances
15
+ define_method "#{property}=" do |v|
16
+ instance_variable_set "@#{property}", v
64
17
  end
65
- end
66
-
67
- def initialize(apply_to_instances, &block)
68
- @apply_to_instances = apply_to_instances
69
- @names = []
70
- @defaults = {}
71
- @instances_too = {} # values are true/false
72
18
 
73
- return unless block_given?
74
- if block.arity == 0
75
- instance_eval &block
76
- else
77
- raise "ArgumentError: wrong number of args for block: #{block.arity} for 1"
78
- end
79
19
  end
20
+ end
80
21
 
81
- end # class Helper
82
-
83
- # returns an array
84
- # apply_to_instances: whether properties should be set on instances of the classes too
85
- def self.cascading_props(apply_to_instances, *properties, &block)
86
- props = {} # values are defaults
87
- instances_too = {} # values are true/false
22
+ # returns hash: {..., attr => [default, inst_too], .. }
23
+ def self.parse_options(klass, apply_to_instances, *properties, &block)
24
+ res = {}
88
25
  properties.each do |prop|
89
- if Array === prop and prop.size == 3
90
- props[prop[0].to_sym] = prop[1]
91
- instances_too[prop[0].to_sym] = !!prop[2]
92
- elsif Array === prop and prop.size == 2
93
- props[prop[0].to_sym] = prop[1]
94
- else
95
- props[prop.to_sym] = nil
26
+ name, default, inst_too = prop, nil, apply_to_instances
27
+ if Array === prop
28
+ case prop.size
29
+ when 0 then next
30
+ when 1 then name = prop[0]
31
+ when 2 then name, default = prop[0..1]
32
+ else name, default, inst_too = prop[0..2]
33
+ end
96
34
  end
35
+ inst_too = !!inst_too
36
+ res[name.to_sym] = [default, inst_too]
97
37
  end
98
-
99
- props.each{|prop, default| instances_too[prop] =
100
- apply_to_instances unless instances_too.include? prop}
101
38
 
102
39
  if block_given?
103
- a = Helper.new(apply_to_instances, &block)
40
+ a = CascadingClasses::DSL.new(apply_to_instances, &block)
104
41
  names, defaults, inst =
105
42
  a.instance_eval{ [@names, @defaults, @instances_too] }
106
- names.each do |prop|
107
- props[prop] = defaults[prop]
108
- instances_too[prop] = inst[prop]
109
- end
43
+ names.each{|name| res[name] = [defaults[name], inst[name]] }
110
44
  end
111
45
 
112
- props.map do |prop, default|
113
- mod = Module.new do
114
-
115
- define_method :extended do |klass|
116
- klass.class_eval{ instance_variable_set "@#{prop}", default }
117
- end
118
- module_function :extended
119
-
120
- define_method :included do |klass|
121
- klass.class_eval{ instance_variable_set "@#{prop}", default }
122
- end
123
- module_function :included
124
-
125
- # getter has dual role as setter:
126
- # m.prop val <=> m.prop = val
127
- define_method prop do |*args|
128
- if args.empty? # getter
129
- val = instance_variable_get "@#{prop}"
130
- if val.nil?
131
- # reach up into the class first, then into superclass
132
- if self.class == Class
133
- try_super = superclass.instance_eval{ respond_to? "#{prop}" }
134
- return superclass.send("#{prop}") if try_super
135
- else
136
- try_klass_methd = self.class.send :respond_to?, prop
137
- return self.class.send(prop) if try_klass_methd
138
- end
139
- end
140
- val
141
- else # setter
142
- if args.size == 1 and Array === args[0]
143
- send("#{prop}=", args[0])
144
- else
145
- send("#{prop}=", *args)
146
- end
147
- end
148
- end
149
-
150
- # todo: setter validations ??
151
- define_method "#{prop}=" do |*vals|
152
- if vals.size == 1 and Array === vals[0]
153
- vals = vals[0]
154
- end
155
- instance_variable_set "@#{prop}", vals[0]
156
- end
157
-
158
- end # new module
159
- [prop, props[prop], instances_too[prop], mod]
160
- end
46
+ res
161
47
  end
162
48
 
163
49
  end # module CascadingClasses
@@ -0,0 +1,83 @@
1
+ module CascadingClasses
2
+
3
+ # based off
4
+ # http://blog.oleganza.com/post/115377756/correct-blankslate-in-ruby
5
+ class BlankSlate
6
+ class << self; alias __undef_method undef_method; end
7
+ keep = [:instance_eval, :object_id]
8
+ ancestors.inject([]){|res, a| res + (a.instance_methods - keep)}.uniq.
9
+ each{|m| (__undef_method(m) rescue nil) unless m =~ /^__/ }
10
+ end
11
+
12
+ class DSL < BlankSlate
13
+
14
+ def method_missing(meth, *args, &block)
15
+ case meth
16
+ when /inspect/
17
+ super
18
+ when /set_property/
19
+ __property__(args[0], *args[1..-1], &block)
20
+ else
21
+ __property__(meth, *args, &block)
22
+ end
23
+ end
24
+
25
+ def __property__(name, args={}, &block)
26
+ @names << name unless @names.include? name
27
+
28
+ @defaults[name] = if args.include? :default
29
+ args[:default]
30
+ elsif args.include? :initial
31
+ args[:initial]
32
+ elsif args.include? :start
33
+ args[:start]
34
+ elsif args.include? :begin
35
+ args[:begin]
36
+ else
37
+ nil
38
+ end
39
+
40
+ @instances_too[name] = if args.include? :classes_only
41
+ !args[:classes_only]
42
+ elsif args.include? :only_classes
43
+ !args[:only_classes]
44
+ elsif args.include? :classes_only
45
+ !args[:classes_only]
46
+ elsif args.include? :exclude_instances
47
+ !args[:exclude_instances]
48
+ elsif args.include? :skip_instances
49
+ !args[:skip_instances]
50
+ elsif args.include? :not_instances
51
+ !args[:not_instances]
52
+ elsif args.include? :apply_to_instances
53
+ args[:apply_to_instances]
54
+ elsif args.include? :both
55
+ args[:both]
56
+ elsif args.include? :instances_too
57
+ args[:instances_too]
58
+ elsif args.include? :instances
59
+ args[:instances]
60
+ elsif args.include? :instance
61
+ args[:instance]
62
+ else
63
+ @apply_to_instances
64
+ end
65
+ end
66
+
67
+ def initialize(apply_to_instances, &block)
68
+ @apply_to_instances = apply_to_instances
69
+ @names = []
70
+ @defaults = {}
71
+ @instances_too = {} # values are true/false
72
+
73
+ return unless block_given?
74
+ if block.arity == 0
75
+ instance_eval &block
76
+ else
77
+ raise "ArgumentError: wrong number of args for block: #{block.arity} for 1"
78
+ end
79
+ end
80
+
81
+ end # class Helper
82
+
83
+ end # module CascadingClasses
@@ -11,17 +11,17 @@ CC = CascadingClasses unless defined? CC
11
11
  # cascade :one, :two, :three, ...
12
12
  # end
13
13
  # Foo.one = 12
14
- # Foo.new.one
15
- # Bar = Class.new(Foo).one
14
+ # Foo.new.one # => 12
15
+ # Bar = Class.new(Foo).one # => 12
16
+ # Bar.new.one # => 12
16
17
 
17
18
  module CascadingClasses
18
19
 
19
- # VERSION: "1.0.0"
20
20
  # note, VERSION will be undetected unless called directly
21
21
  def self.const_missing(name)
22
22
  case name
23
- when :VERSION then "1.0.0"
24
- else raise NameError
23
+ when :VERSION then "0.2.0"
24
+ else super
25
25
  end
26
26
  end
27
27
 
@@ -30,46 +30,57 @@ module CascadingClasses
30
30
  end
31
31
  module_function :respond_to?
32
32
 
33
- def self.included(klass)
34
- has_all = klass.instance_methods.include? :all!
35
- klass.instance_eval do
36
- def cascade(*props, &block)
37
- res = CascadingClasses.cascading_props(true, *props, &block)
38
- res.map! do |prop, default, apply_to_instances, mod|
39
- send(:extend, mod)
40
- send(:include, mod) if apply_to_instances
41
- [prop, [default, apply_to_instances]]
33
+ # todo: fill in rest
34
+ # todo: allow one of [:cascade, :all!] to be overwritten
35
+ def self.illegal_names
36
+ [:cascade, :all!, :instance_eval, :instance_exec, :class_eval]
37
+ end
38
+
39
+ def self.apply(klass, instances_too)
40
+ klass.singleton_class.instance_eval do
41
+
42
+ define_method :cascade do |*props, &block|
43
+ illegal_names = CascadingClasses.illegal_names
44
+
45
+ props = CascadingClasses.parse_options(klass, instances_too, *props, &block)
46
+ props.each{|prop, v| raise NameError, "Illegal property name: #{prop}" if
47
+ illegal_names.include? prop }
48
+
49
+ props.each do |prop, v|
50
+ default, inst_too = v
51
+
52
+ klass.singleton_class.instance_eval do
53
+ undef_method prop.to_sym rescue nil
54
+ undef_method "#{prop}=".to_sym rescue nil
55
+ end
56
+
57
+ klass.instance_eval{ instance_variable_set "@#{prop}", default }
58
+
59
+ mod = CascadingClasses.cascade_intern(klass, prop)
60
+ klass.extend mod
61
+ if inst_too
62
+ klass.instance_eval do
63
+ undef_method prop.to_sym rescue nil
64
+ undef_method "#{prop}=".to_sym rescue nil
65
+ end
66
+ klass.send(:include, mod)
67
+ end
42
68
  end
43
- {self.name => Hash[res]}
44
69
  end
45
70
 
46
- class << klass
47
- unless instance_methods.include? :all!
48
- alias_method :all!, :cascade
49
- end
71
+ unless instance_methods.include? :all!
72
+ alias_method :all!, :cascade
50
73
  end
74
+
51
75
  end
52
76
  end
53
77
 
54
- def self.extended(klass)
55
- has_all = klass.instance_methods.include? :all
56
- klass.instance_eval do
57
- def cascade(*props, &block)
58
- res = CascadingClasses.cascading_props(false, *props, &block)
59
- res.map! do |prop, default, apply_to_instances, mod|
60
- send(:extend, mod)
61
- send(:include, mod) if apply_to_instances
62
- [prop, [default, apply_to_instances]]
63
- end
64
- {self.name => Hash[res]}
65
- end
78
+ def self.included(klass)
79
+ apply(klass, true)
80
+ end
66
81
 
67
- class << klass
68
- unless instance_methods.include? :all!
69
- alias_method :all!, :cascade
70
- end
71
- end
72
- end
82
+ def self.extended(klass)
83
+ apply(klass, false)
73
84
  end
74
85
 
75
86
  end # module CascadingClases
@@ -27,6 +27,16 @@ describe "when a class extends CascadingClasses" do
27
27
  GrandChild = Class.new(Child)
28
28
  end
29
29
 
30
+ describe "when a parent has a default" do
31
+ before do
32
+ Parent.cascade [:eyes, "brown"]
33
+ end
34
+
35
+ it "sets the property to the default" do
36
+ Parent.eyes.must_equal "brown"
37
+ end
38
+ end
39
+
30
40
  describe "when a property is set from above" do
31
41
  before do
32
42
  Parent.cascade(:hair_color)
@@ -80,7 +90,7 @@ describe "when a class extends CascadingClasses" do
80
90
  Parent.cascade :has_house
81
91
  end
82
92
 
83
- all "descendents should reflect the same" do
93
+ all "descendents should reflect nil" do
84
94
  Parent.has_house.must_equal nil
85
95
  Child.has_house.must_equal nil
86
96
  GrandChild.has_house.must_equal nil
@@ -102,13 +112,12 @@ describe "when a class extends CascadingClasses" do
102
112
  end
103
113
  end
104
114
 
105
- describe "when a parent has a property with a default set" do
115
+ describe "when a parent has a property with a default" do
106
116
  before do
107
- ## Parent.cascade{ :time, :default => 32}
108
117
  Parent.cascade [:time, 32]
109
118
  end
110
119
 
111
- it "won't matter if the property is first accessed by the child" do
120
+ it "won't matter whether the property is first accessed by the child" do
112
121
  Child.time
113
122
 
114
123
  Child.time.must_equal 32
@@ -116,7 +125,7 @@ describe "when a class extends CascadingClasses" do
116
125
  end
117
126
  end
118
127
 
119
- describe "when a parent has a property with a default set" do
128
+ describe "when a parent has a property with a default" do
120
129
  before do
121
130
  Parent.cascade [:time, 32]
122
131
  end
@@ -129,5 +138,30 @@ describe "when a class extends CascadingClasses" do
129
138
  end
130
139
  end
131
140
 
132
-
141
+ describe "when a property is set to false" do
142
+ before do
143
+ Parent.cascade [:has_time, false]
144
+ end
145
+
146
+ it "should return false, not nil" do
147
+ Parent.has_time.must_equal false
148
+ end
149
+ end
150
+
151
+ describe "when a property name is not allowed" do
152
+ before do
153
+ property = CascadingClasses.illegal_names.sample
154
+ end
155
+
156
+ it "should raise a NameError" do
157
+ res = begin
158
+ Parent.cascade property
159
+ rescue NameError
160
+ true
161
+ else
162
+ false
163
+ end
164
+ res.must_equal true
165
+ end
166
+ end
133
167
  end
@@ -0,0 +1,7 @@
1
+ $:.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'cascading_classes'
3
+ require 'minitest/autorun'
4
+
5
+ class << MiniTest::Spec
6
+ alias_method :all, :it
7
+ end
@@ -82,4 +82,16 @@ describe "when CascadingClasses is included by a class" do
82
82
  @c.table.must_equal nil
83
83
  end
84
84
  end
85
+
86
+ describe "when a property is set to false" do
87
+ before do
88
+ A.cascade [:has_time, false]
89
+ end
90
+
91
+ it "should return false, not nil" do
92
+ A.new.has_time.must_equal false
93
+ end
94
+ end
95
+
96
+
85
97
  end
data/spec/note.txt ADDED
@@ -0,0 +1,2 @@
1
+ you should expect to see many "already initialized constant" warnings
2
+ This happens because the same constants must get reloaded for every test
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cascading_classes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,9 +9,10 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-08-03 00:00:00.000000000Z
12
+ date: 2011-08-08 00:00:00.000000000Z
13
13
  dependencies: []
14
- description:
14
+ description: Similar to active_support/core_ext/class/attribute.rb but better. Also,
15
+ doesn't monkey-patch Class
15
16
  email: jeremy.gables@gmail.com
16
17
  executables: []
17
18
  extensions: []
@@ -21,9 +22,12 @@ files:
21
22
  - Rakefile
22
23
  - lib/cascading_classes.rb
23
24
  - lib/cascading_classes/cascading_classes.rb
25
+ - lib/cascading_classes/dsl.rb
26
+ - spec/helper_spec.rb
24
27
  - spec/extended_spec.rb
25
28
  - spec/included_spec.rb
26
29
  - spec/alternative_syntax_spec.rb
30
+ - spec/note.txt
27
31
  homepage: http://github.com/gables/cascading_classes
28
32
  licenses: []
29
33
  post_install_message:
@@ -49,6 +53,8 @@ signing_key:
49
53
  specification_version: 3
50
54
  summary: Easily create properties whose values cascade down class trees
51
55
  test_files:
56
+ - spec/helper_spec.rb
52
57
  - spec/extended_spec.rb
53
58
  - spec/included_spec.rb
54
59
  - spec/alternative_syntax_spec.rb
60
+ - spec/note.txt