characterizable 0.0.1 → 0.0.2

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/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