cascading_classes 0.1.0 → 0.2.0

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