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 +1 -1
- data/lib/characterizable.rb +90 -73
- data/test/test_characterizable.rb +74 -34
- metadata +2 -2
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.2
|
data/lib/characterizable.rb
CHANGED
@@ -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 ||=
|
22
|
+
@_characteristics ||= Snapshot.new self
|
22
23
|
end
|
23
24
|
|
24
|
-
def
|
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
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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 ||=
|
112
|
+
@_characteristics ||= SurvivorHash.new
|
91
113
|
end
|
92
114
|
include Blockenspiel::DSL
|
93
115
|
def has(name, options = {}, &block)
|
94
|
-
characteristics
|
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
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
152
|
+
def unknown?(obj)
|
127
153
|
value(obj).nil?
|
128
154
|
end
|
129
|
-
def known?(obj
|
155
|
+
def known?(obj)
|
130
156
|
not unknown?(obj)
|
131
157
|
end
|
132
|
-
def trumped?(obj
|
133
|
-
|
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
|
163
|
+
def requited?(obj)
|
138
164
|
return true if prerequisite.nil?
|
139
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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 [:
|
96
|
-
assert !a.
|
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
|
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.
|
118
|
-
assert !a.
|
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.
|
121
|
-
assert !a.
|
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
|
-
|
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 "
|
150
|
+
should "know what is known on a snapshot" do
|
134
151
|
a = Automobile.new
|
135
152
|
a.make = 'Ford'
|
136
|
-
assert_equal
|
153
|
+
assert_equal [:make], a.characteristics.known.keys
|
137
154
|
end
|
138
155
|
|
139
|
-
should "
|
156
|
+
should "know what is unknown on a snapshot" do
|
140
157
|
a = Automobile.new
|
141
158
|
a.make = 'Ford'
|
142
|
-
|
143
|
-
Automobile.characteristics[:make].value
|
144
|
-
end
|
159
|
+
assert a.characteristics.unknown.keys.include?(:model_year)
|
145
160
|
end
|
146
161
|
|
147
|
-
should "not
|
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
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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 "
|
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
|
-
|
161
|
-
assert_equal [:make],
|
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
|