characterizable 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1
1
+ 0.0.2
@@ -1,3 +1,4 @@
1
+ require 'set'
1
2
  require 'blockenspiel'
2
3
  require 'active_support'
3
4
  require 'active_support/version'
@@ -18,31 +19,80 @@ module Characterizable
18
19
  end
19
20
 
20
21
  def characteristics
21
- @_characteristics ||= ArrayOfBoundCharacteristics.new self
22
+ @_characteristics ||= Snapshot.new self
22
23
  end
23
24
 
24
- def clear_characteristics
25
+ def dirty_characteristics!
25
26
  @_characteristics = nil
26
27
  end
27
-
28
- def known_characteristics
29
- characteristics.select do |c|
30
- c.known? and not c.trumped?
31
- end
32
- end
33
28
 
34
- def unknown_characteristics
35
- characteristics.select do |c|
36
- c.unknown? and c.requited? and not c.trumped?
29
+ # hashes that survive as such when you select/reject/slice them
30
+ class SurvivorHash < Hash
31
+ attr_reader :survivor_args
32
+ def initialize(*survivor_args)
33
+ @survivor_args = survivor_args
34
+ end
35
+ def reject(&block)
36
+ inject(self.class.new(*survivor_args)) do |memo, ary|
37
+ unless block.call(*ary)
38
+ memo[ary[0]] = ary[1]
39
+ end
40
+ memo
41
+ end
42
+ end
43
+ def select(&block)
44
+ inject(self.class.new(*survivor_args)) do |memo, ary|
45
+ if block.call(*ary)
46
+ memo[ary[0]] = ary[1]
47
+ end
48
+ memo
49
+ end
50
+ end
51
+ def slice(*keys)
52
+ inject(self.class.new(*survivor_args)) do |memo, ary|
53
+ if keys.include?(ary[0])
54
+ memo[ary[0]] = ary[1]
55
+ end
56
+ memo
57
+ end
37
58
  end
38
59
  end
39
60
 
40
- def visible_known_characteristics
41
- known_characteristics.reject { |c| c.hidden? }
42
- end
43
-
44
- def visible_unknown_characteristics
45
- unknown_characteristics.reject { |c| c.hidden? }
61
+ class Snapshot < SurvivorHash
62
+ def initialize(*survivor_args)
63
+ super
64
+ take_snapshot
65
+ end
66
+ def snapshotted_obj
67
+ survivor_args.first
68
+ end
69
+ def []=(key, value)
70
+ snapshotted_obj.dirty_characteristics!
71
+ super
72
+ end
73
+ def take_snapshot
74
+ snapshotted_obj.characterizable_base.characteristics.each do |k, c|
75
+ if c.known?(snapshotted_obj) and c.requited?(snapshotted_obj) and not c.trumped?(snapshotted_obj)
76
+ self[k] = snapshotted_obj.send c.name
77
+ end
78
+ end
79
+ end
80
+ def known
81
+ snapshotted_obj.characterizable_base.characteristics.select do |_, c|
82
+ c.known?(self) and c.requited?(self) and not c.trumped?(self)
83
+ end
84
+ end
85
+ def unknown
86
+ snapshotted_obj.characterizable_base.characteristics.select do |_, c|
87
+ c.unknown?(self) and c.requited?(self) and not c.trumped?(self)
88
+ end
89
+ end
90
+ def visible_known
91
+ known.reject { |_, c| c.hidden? }
92
+ end
93
+ def visible_unknown
94
+ unknown.reject { |_, c| c.hidden? }
95
+ end
46
96
  end
47
97
 
48
98
  module ClassMethods
@@ -53,51 +103,28 @@ module Characterizable
53
103
  delegate :characteristics, :to => :characterizable_base
54
104
  end
55
105
 
56
- # don't want to use a Hash, because that would be annoying to select from
57
- class ArrayOfCharacteristics < Array
58
- def [](key_or_index)
59
- case key_or_index
60
- when String, Symbol
61
- detect { |c| c.name == key_or_index }
62
- else
63
- super
64
- end
65
- end
66
- end
67
-
68
- class ArrayOfBoundCharacteristics < ArrayOfCharacteristics
69
- attr_reader :bound_obj
70
- def initialize(*args)
71
- @bound_obj = args.pop
72
- super
73
- load_bound_characteristics
74
- end
75
- def load_bound_characteristics
76
- bound_obj.characterizable_base.characteristics.each do |c|
77
- b_c = c.dup
78
- b_c.bound_obj = bound_obj
79
- push b_c
80
- end
81
- end
82
- end
83
-
84
106
  class Base
85
107
  attr_reader :klass
86
108
  def initialize(klass)
87
109
  @klass = klass
88
110
  end
89
111
  def characteristics
90
- @_characteristics ||= ArrayOfCharacteristics.new
112
+ @_characteristics ||= SurvivorHash.new
91
113
  end
92
114
  include Blockenspiel::DSL
93
115
  def has(name, options = {}, &block)
94
- characteristics.push Characteristic.new(self, name, options, &block)
116
+ characteristics[name] = Characteristic.new(self, name, options, &block)
117
+ klass.module_eval(%{
118
+ def #{name}_with_dirty_characteristics=(new_#{name})
119
+ dirty_characteristics!
120
+ self.#{name}_without_dirty_characteristics = new_#{name}
121
+ end
122
+ alias_method_chain :#{name}=, :dirty_characteristics
123
+ }, __FILE__, __LINE__) if klass.instance_methods.include?("#{name}=")
95
124
  end
96
125
  end
97
126
 
98
127
  class Characteristic
99
- class TreatedBoundAsUnbound < RuntimeError; end
100
- class TreatedUnboundAsBound < RuntimeError; end
101
128
  attr_reader :base
102
129
  attr_reader :name
103
130
  attr_reader :trumps
@@ -106,7 +133,7 @@ module Characterizable
106
133
  attr_reader :options
107
134
  def initialize(base, name, options = {}, &block)
108
135
  @base = base
109
- @name = name.to_sym
136
+ @name = name
110
137
  @trumps = Array.wrap(options.delete(:trumps))
111
138
  @prerequisite = options.delete :prerequisite
112
139
  @hidden = options.delete :hidden
@@ -114,29 +141,28 @@ module Characterizable
114
141
  Blockenspiel.invoke block, self if block_given?
115
142
  end
116
143
  delegate :characteristics, :to => :base
117
- attr_accessor :bound_obj
118
- def effective_obj(obj = nil)
119
- raise TreatedBoundAsUnbound, "Can't treat as unbound if bound object is set" if obj and bound_obj
120
- raise TreatedUnboundAsBound, "Need an object if unbound" unless obj or bound_obj
121
- obj || bound_obj
122
- end
123
- def value(obj = nil)
124
- effective_obj(obj).send name
144
+ def value(obj)
145
+ case obj
146
+ when Hash
147
+ obj[name]
148
+ else
149
+ obj.send name
150
+ end
125
151
  end
126
- def unknown?(obj = nil)
152
+ def unknown?(obj)
127
153
  value(obj).nil?
128
154
  end
129
- def known?(obj = nil)
155
+ def known?(obj)
130
156
  not unknown?(obj)
131
157
  end
132
- def trumped?(obj = nil)
133
- effective_obj(obj).characteristics.any? do |c|
158
+ def trumped?(obj)
159
+ characteristics.any? do |_, c|
134
160
  c.known?(obj) and c.trumps.include?(name)
135
161
  end
136
162
  end
137
- def requited?(obj = nil)
163
+ def requited?(obj)
138
164
  return true if prerequisite.nil?
139
- effective_obj(obj).characteristics[prerequisite].known? obj
165
+ characteristics[prerequisite].known? obj
140
166
  end
141
167
  def hidden?
142
168
  hidden
@@ -144,15 +170,6 @@ module Characterizable
144
170
  include Blockenspiel::DSL
145
171
  def reveals(other_name, other_options = {}, &block)
146
172
  base.has other_name, other_options.merge(:prerequisite => name), &block
147
- base.klass.module_eval %{
148
- def #{name}_with_dependent_#{other_name}=(new_#{name})
149
- if new_#{name}.nil?
150
- self.#{other_name} = nil
151
- end
152
- self.#{name}_without_dependent_#{other_name} = new_#{name}
153
- end
154
- alias_method_chain :#{name}=, :dependent_#{other_name}
155
- }, __FILE__, __LINE__
156
173
  end
157
174
  end
158
175
  end
@@ -53,16 +53,35 @@ class TestCharacterizable < Test::Unit::TestCase
53
53
  end
54
54
  end
55
55
 
56
+ should "survive as a certain kind of hash" do
57
+ a = SimpleAutomobile.new
58
+
59
+ assert_equal Characterizable::SurvivorHash, SimpleAutomobile.characteristics.class
60
+ assert_equal Characterizable::SurvivorHash, SimpleAutomobile.characteristics.select { false }.class
61
+ assert_equal Characterizable::SurvivorHash, SimpleAutomobile.characteristics.slice(:hello).class
62
+ assert_equal Characterizable::SurvivorHash, SimpleAutomobile.characteristics.merge({:hi => 'there'}).class
63
+
64
+ assert_equal Characterizable::SurvivorHash, a.characteristics.known.class
65
+ assert_equal Characterizable::SurvivorHash, a.characteristics.known.select { false }.class
66
+ assert_equal Characterizable::SurvivorHash, a.characteristics.known.slice(:hello).class
67
+ assert_equal Characterizable::SurvivorHash, a.characteristics.known.merge({:hi => 'there'}).class
68
+
69
+ assert_equal Characterizable::Snapshot, a.characteristics.class
70
+ assert_equal Characterizable::Snapshot, a.characteristics.select { false }.class
71
+ assert_equal Characterizable::Snapshot, a.characteristics.slice(:hello).class
72
+ assert_equal Characterizable::Snapshot, a.characteristics.merge({:hi => 'there'}).class
73
+ end
74
+
56
75
  should "tell you what characteristics are known" do
57
76
  a = SimpleAutomobile.new
58
77
  a.make = 'Ford'
59
- assert_equal [:make], a.known_characteristics.map(&:name)
78
+ assert_equal [:make], a.characteristics.known.keys
60
79
  end
61
80
 
62
81
  should "tell you what characteristics are unknown" do
63
82
  a = SimpleAutomobile.new
64
83
  a.make = 'Ford'
65
- assert_equal [:model, :variant], a.unknown_characteristics.map(&:name)
84
+ assert_equal [:model, :variant], a.characteristics.unknown.keys
66
85
  end
67
86
 
68
87
  should "present a concise set of known characteristics by getting rid of those that have been trumped" do
@@ -70,38 +89,39 @@ class TestCharacterizable < Test::Unit::TestCase
70
89
  a.make = 'Ford'
71
90
  a.model = 'Taurus'
72
91
  a.variant = 'Taurus V6 DOHC'
73
- assert_equal [:make, :variant], a.known_characteristics.map(&:name)
92
+ assert_equal [:make, :variant], a.characteristics.known.keys
74
93
  end
75
94
 
76
95
  should "not mention a characteristic as unknown if, in fact, it has been trumped" do
77
96
  a = SimpleAutomobile.new
78
97
  a.make = 'Ford'
79
98
  a.variant = 'Taurus V6 DOHC'
80
- assert_equal [], a.unknown_characteristics.map(&:name)
99
+ assert_equal [], a.characteristics.unknown.keys
81
100
  end
82
101
 
83
102
  should "not mention a characteristic as unknown if it is waiting on something else to be revealed" do
84
103
  a = Automobile.new
85
- assert !a.unknown_characteristics.map(&:name).include?(:model_year)
104
+ assert !a.characteristics.unknown.keys.include?(:model_year)
86
105
  end
87
106
 
88
107
  should "make sure that trumping works even within revealed characteristics" do
89
108
  a = Automobile.new
90
- assert a.unknown_characteristics.map(&:name).include?(:size_class)
109
+ assert a.characteristics.unknown.keys.include?(:size_class)
91
110
  a.make = 'Ford'
92
111
  a.model_year = 1999
93
112
  a.model = 'Taurus'
94
113
  a.size_class = 'mid-size'
95
- assert_equal [:model, :model_year, :make], a.known_characteristics.map(&:name)
96
- assert !a.unknown_characteristics.map(&:name).include?(:size_class)
114
+ assert_equal [:make, :model_year, :model], a.characteristics.known.keys
115
+ assert !a.characteristics.unknown.keys.include?(:size_class)
97
116
  end
98
117
 
99
- should "enforce prerequisites by using an object's setter" do
118
+ should "not enforce prerequisites by using an object's setter" do
100
119
  a = Automobile.new
101
120
  a.make = 'Ford'
102
121
  a.model_year = 1999
103
122
  a.make = nil
104
- assert_equal nil, a.model_year
123
+ assert_equal 1999, a.model_year
124
+ assert_equal nil, a.characteristics.known[:model_year]
105
125
  end
106
126
 
107
127
  should "keep user-defined options on a characteristic" do
@@ -114,50 +134,70 @@ class TestCharacterizable < Test::Unit::TestCase
114
134
 
115
135
  should "know which characteristics are 'visible'" do
116
136
  a = Automobile.new
117
- assert a.unknown_characteristics.map(&:name).include?(:record_creation_date)
118
- assert !a.visible_unknown_characteristics.map(&:name).include?(:record_creation_date)
137
+ assert a.characteristics.unknown.keys.include?(:record_creation_date)
138
+ assert !a.characteristics.visible_unknown.keys.include?(:record_creation_date)
119
139
  a.record_creation_date = 'yesterday'
120
- assert a.known_characteristics.map(&:name).include?(:record_creation_date)
121
- assert !a.visible_known_characteristics.map(&:name).include?(:record_creation_date)
140
+ assert a.characteristics.known.keys.include?(:record_creation_date)
141
+ assert !a.characteristics.visible_known.keys.include?(:record_creation_date)
122
142
  end
123
-
143
+
124
144
  should "be able to access values" do
125
145
  a = Automobile.new
126
146
  a.make = 'Ford'
127
- b = Automobile.new
128
- b.make = 'Pontiac'
129
- assert_equal 'Ford', Automobile.characteristics[:make].value(a)
130
- assert_equal 'Pontiac', Automobile.characteristics[:make].value(b)
147
+ assert_equal 'Ford', a.characteristics[:make]
131
148
  end
132
149
 
133
- should "give back characteristics with values when accessed from an instance" do
150
+ should "know what is known on a snapshot" do
134
151
  a = Automobile.new
135
152
  a.make = 'Ford'
136
- assert_equal 'Ford', a.characteristics[:make].value
153
+ assert_equal [:make], a.characteristics.known.keys
137
154
  end
138
155
 
139
- should "not allow treating [unbound] characteristics like bound ones" do
156
+ should "know what is unknown on a snapshot" do
140
157
  a = Automobile.new
141
158
  a.make = 'Ford'
142
- assert_raises(Characterizable::Characteristic::TreatedUnboundAsBound) do
143
- Automobile.characteristics[:make].value
144
- end
159
+ assert a.characteristics.unknown.keys.include?(:model_year)
145
160
  end
146
161
 
147
- should "not allow treating bound characteristics like unbound ones" do
162
+ should "not reveal unknown characteristics in snapshots" do
163
+ a = Automobile.new
164
+ a.model_year = 1999
165
+ assert_equal [], a.characteristics.known.keys
166
+ assert_equal nil, a.characteristics[:model_year]
167
+ end
168
+
169
+ should "not reveal unknown characteristics in snapshots, even if it was previously revealed" do
148
170
  a = Automobile.new
149
171
  a.make = 'Ford'
150
- b = Automobile.new
151
- b.make = 'Pontiac'
152
- assert_raises(Characterizable::Characteristic::TreatedBoundAsUnbound) do
153
- a.characteristics[:make].value :anything
154
- end
172
+ a.model_year = 1999
173
+ assert_equal [:make, :model_year], a.characteristics.known.keys
174
+ a.make = nil
175
+ assert_equal [], a.characteristics.known.keys
155
176
  end
156
177
 
157
- should "eagely populate bound characteristics" do
178
+ should "keep snapshots separately" do
179
+ a = Automobile.new
180
+ a.make = 'Ford'
181
+ a.model_year = 1999
182
+ snapshot = a.characteristics
183
+ assert_equal [:make, :model_year], snapshot.known.keys
184
+ a.make = nil
185
+ assert_equal [], a.characteristics.known.keys
186
+ assert_equal [:make, :model_year], snapshot.known.keys
187
+ end
188
+
189
+ should "work when passed around as a snapshot" do
158
190
  a = Automobile.new
159
191
  a.make = 'Ford'
160
- assert_equal ['Ford'], a.known_characteristics.map(&:value)
161
- assert_equal [:make], a.known_characteristics.map(&:name)
192
+ snapshot = a.characteristics
193
+ assert_equal [:make], snapshot.known.keys
194
+ assert snapshot.unknown.keys.include?(:model_year)
195
+ snapshot[:model_year] = 1999
196
+ assert_equal 1999, snapshot[:model_year]
197
+ assert_equal [:make, :model_year], snapshot.known.keys
198
+ assert !snapshot.unknown.keys.include?(:model_year)
199
+ assert_equal nil, a.model_year
200
+ assert_equal nil, a.characteristics[:model_year]
201
+ assert_equal 1999, snapshot[:model_year]
162
202
  end
163
203
  end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 0
8
- - 1
9
- version: 0.0.1
8
+ - 2
9
+ version: 0.0.2
10
10
  platform: ruby
11
11
  authors:
12
12
  - Andy Rossmeissl