activefacts-api 0.8.9 → 0.8.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,409 @@
1
+ #
2
+ # ActiveFacts tests: Value instances in the Runtime API
3
+ # Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
4
+ #
5
+ require 'rspec'
6
+ require 'activefacts/api'
7
+
8
+ VALUE_TYPES = Int, Real, AutoCounter, String, Date, DateTime, Decimal
9
+ RAW_VALUES = [2, 3.0, 4, "5", Date.new(2008, 04, 20), DateTime.new(2008, 04, 20, 10, 28, 14)]
10
+ ALT_VALUES = [3, 4.0, 5, "6", Date.new(2009, 04, 20), DateTime.new(2009, 04, 20, 10, 28, 14)]
11
+ VALUE_SUB_FOR_VALUE = {}
12
+ VALUES_FOR_TYPE = VALUE_TYPES.zip(RAW_VALUES, ALT_VALUES).inject({}) do |h, (vt, v1, v2)|
13
+ next h unless v1 and v2
14
+ h[vt] = [v1, v2]
15
+ h
16
+ end
17
+ VALUE_TYPE_FOR_OBJECT_TYPE = {}
18
+ OBJECT_TYPES = []
19
+
20
+ module TestValueTypesModule
21
+ class ESCID < AutoCounter
22
+ value_type
23
+ end
24
+ BASE_VALUE_TYPE_ROLE_NAMES = VALUE_TYPES.map { |base_type| base_type.name.snakecase }
25
+ VALUE_TYPE_ROLE_NAMES = BASE_VALUE_TYPE_ROLE_NAMES.map { |n| [ :"#{n}_value", :"#{n}_sub_value" ] }.flatten
26
+ VALUE_TYPES.map do |value_type|
27
+ code = <<-END
28
+ class #{value_type.name}Value < #{value_type.name}
29
+ value_type
30
+ end
31
+
32
+ class #{value_type.name}ValueSub < #{value_type.name}Value
33
+ # Note no new "value_type" is required here, it comes through inheritance
34
+ end
35
+
36
+ class #{value_type.name}Entity
37
+ identified_by :#{identifying_role_name = "id_#{value_type.name.snakecase}_value"}
38
+ has_one :#{identifying_role_name}, :class => #{value_type.name}Value
39
+ end
40
+
41
+ class #{value_type.name}EntitySub < #{value_type.name}Entity
42
+ end
43
+
44
+ class #{value_type.name}EntitySubCtr < #{value_type.name}Entity
45
+ identified_by :counter
46
+ has_one :counter, :class => "ESCID"
47
+ end
48
+
49
+ VALUE_SUB_FOR_VALUE[#{value_type.name}Value] = #{value_type.name}ValueSub
50
+ classes = [
51
+ #{value_type.name}Value,
52
+ #{value_type.name}ValueSub,
53
+ #{value_type.name}Entity,
54
+ #{value_type.name}EntitySub,
55
+ #{value_type.name}EntitySubCtr,
56
+ ]
57
+ OBJECT_TYPES.concat(classes)
58
+ classes.each { |klass| VALUE_TYPE_FOR_OBJECT_TYPE[klass] = value_type }
59
+ END
60
+ eval code
61
+ end
62
+ OBJECT_TYPE_NAMES = OBJECT_TYPES.map{|object_type| object_type.basename}
63
+
64
+ class Octopus
65
+ identified_by :zero
66
+ has_one :zero, :class => IntValue
67
+ maybe :has_a_unary
68
+ OBJECT_TYPE_NAMES.each do |object_type_name|
69
+ has_one object_type_name.snakecase.to_sym
70
+ one_to_one ("one_"+object_type_name.snakecase).to_sym, :class => object_type_name
71
+ end
72
+ end
73
+ end
74
+
75
+ describe "Roles of an Object Type" do
76
+
77
+ it "should return a roles collection" do
78
+ roles = TestValueTypesModule::Octopus.roles
79
+ roles.should_not be_nil
80
+ roles.size.should == 2+VALUE_TYPES.size*5*2
81
+
82
+ # Quick check of role metadata:
83
+ roles.each do |role_name, role|
84
+ role.object_type.modspace.should == TestValueTypesModule
85
+ if !role.counterpart
86
+ role.should be_unary
87
+ else
88
+ role.counterpart.object_type.modspace.should == TestValueTypesModule
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ describe "Object type role values" do
95
+ def object_identifying_parameters object_type_name, value
96
+ if object_type_name =~ /^(.*)EntitySubCtr$/
97
+ [{ :"id_#{$1.snakecase}_value" => value, :counter => :new}]
98
+ else
99
+ [value]
100
+ end
101
+ end
102
+
103
+ describe "Instantiating bare objects" do
104
+ OBJECT_TYPES.each do |object_type|
105
+ required_value_type = VALUE_TYPE_FOR_OBJECT_TYPE[object_type]
106
+ object_type_name = object_type.basename
107
+ values = VALUES_FOR_TYPE[required_value_type]
108
+ next unless values
109
+
110
+ it "should allow instantiation of a bare #{object_type_name}" do
111
+ object_identifying_parameters =
112
+ if object_type_name =~ /^(.*)EntitySubCtr$/
113
+ [{ :"id_#{$1.snakecase}_value" => values[0], :counter => :new}]
114
+ else
115
+ [values[0]]
116
+ end
117
+ object = object_type.new(*object_identifying_parameters)
118
+ object.class.should == object_type
119
+ object.constellation.should be_nil
120
+ end
121
+ end
122
+ end
123
+
124
+ describe "A constellation" do
125
+ before :each do
126
+ @constellation = ActiveFacts::API::Constellation.new(TestValueTypesModule)
127
+ end
128
+
129
+ OBJECT_TYPES.each do |object_type|
130
+ required_value_type = VALUE_TYPE_FOR_OBJECT_TYPE[object_type]
131
+ object_type_name = object_type.basename
132
+ values = VALUES_FOR_TYPE[required_value_type]
133
+
134
+ it "should return an initially empty instance index collection for #{object_type_name}" do
135
+ @constellation.send(object_type_name).should be_empty
136
+ end
137
+
138
+ next unless values
139
+
140
+ it "should allow assertion of an #{object_type_name} instance using #{values[0].inspect}" do
141
+ # REVISIT: Assertion of a subtype having the same identifier as a supertype is... dodgey.
142
+ # What should it do? Migrate the previous object to its subtype?
143
+ object = @constellation.send(object_type_name, *object_identifying_parameters(object_type_name, values[0]))
144
+
145
+ # Make sure we got what we expected:
146
+ object.class.should == object_type
147
+
148
+ # Make sure the instance index contains this single object:
149
+ instances = @constellation.send(object_type_name)
150
+ instances.size.should == 1
151
+ instances.map{|k,o| o}.first.should == object
152
+ unless object.class.is_entity_type
153
+ # Look up value types using the value instance, not just the raw value:
154
+ instances[object].should == object
155
+ end
156
+
157
+ # Make sure all the identifying roles are populated correctly:
158
+ if object_type.respond_to?(:identifying_roles)
159
+ object.class.identifying_roles.each do |identifying_role|
160
+ identifying_value = object.send(identifying_role.name)
161
+ identifying_value.should_not be_nil
162
+
163
+ counterpart_object_type = identifying_role.counterpart.object_type
164
+ role_superclasses = [ counterpart_object_type.superclass, counterpart_object_type.superclass.superclass ]
165
+ # Autocounter values do not compare to Integers:
166
+ unless role_superclasses.include?(AutoCounter) or identifying_role.object_type.basename =~ /Entity/
167
+ identifying_value.should == identifying_role.object_type.new(*values[0])
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ if object_type.respond_to?(:identifying_roles)
174
+ # REVISIT: Here, there are many possible problems with re-assigning identifying role values. We need tests!
175
+ # The implementation will need to be reworked to detect problems and reverse any partial changes before chucking an exception
176
+ =begin
177
+ it "should not allow re-assigning a #{object_type_name} entity's identifying role value from #{values[0]} to #{values[1]}" do
178
+ object = @constellation.send(object_type_name, *object_identifying_parameters(object_type_name, values[0]))
179
+ object.class.identifying_roles.each do |identifying_role|
180
+ next if identifying_role.name == :counter
181
+ lambda {
182
+ object.send(:"#{identifying_role.name}=", values[1])
183
+ }.should raise_error
184
+ end
185
+ end
186
+ =end
187
+
188
+ it "should allow nullifying and reassigning a #{object_type_name} entity's identifying role value" do
189
+ object = @constellation.send(object_type_name, *object_identifying_parameters(object_type_name, values[0]))
190
+ object.class.identifying_roles.each do |identifying_role|
191
+ next if identifying_role.name == :counter
192
+ assigned = object.send(:"#{identifying_role.name}=", nil)
193
+ assigned.should be_nil
194
+ object.send(:"#{identifying_role.name}=", values[1])
195
+ end
196
+ end
197
+ else
198
+ it "should allow initialising value type #{object_type.name} with an instance of that value type" do
199
+ bare_value = object_type.new(*object_identifying_parameters(object_type_name, values[0]))
200
+ object = @constellation.send(object_type_name, bare_value)
201
+
202
+ # Now link the bare value to an Octopus:
203
+ octopus = @constellation.Octopus(0)
204
+ octopus_role_name = :"octopus_as_one_#{object_type_name.snakecase}"
205
+ bare_value.send(:"#{octopus_role_name}=", octopus)
206
+ counterpart_name = bare_value.class.roles[octopus_role_name].counterpart.name
207
+
208
+ # Create a reference by assigning the object from a RoleProxy:
209
+ proxy = octopus.send(counterpart_name)
210
+ #proxy.should be_respond_to(:__getobj__)
211
+ object2 = @constellation.send(object_type_name, proxy)
212
+ object2.should == object
213
+ end
214
+ end
215
+ end
216
+
217
+ end
218
+
219
+ describe "Role values" do
220
+ before :each do
221
+ @constellation = ActiveFacts::API::Constellation.new(TestValueTypesModule)
222
+ @object = @constellation.Octopus(0)
223
+ @roles = @object.class.roles
224
+ end
225
+
226
+ it "should return its constellation and vocabulary" do
227
+ # Strictly, these are not role value tests
228
+ @object.constellation.should == @constellation
229
+ @object.constellation.vocabulary.should == TestValueTypesModule
230
+ @object.class.vocabulary.should == TestValueTypesModule
231
+ end
232
+
233
+ TestValueTypesModule::Octopus.roles.each do |role_name, role|
234
+ next if role_name == :zero
235
+
236
+ it "should respond to getting its #{role_name} role" do
237
+ @object.should be_respond_to role.name
238
+ end
239
+
240
+ it "should respond to setting its #{role_name} role" do
241
+ @object.should be_respond_to :"#{role.name}="
242
+ end
243
+
244
+ if role.unary?
245
+ it "should allow its #{role_name} unary role to be assigned and reassigned" do
246
+ @object.has_a_unary.should be_nil
247
+ @object.has_a_unary = true
248
+ @object.has_a_unary.should == true
249
+ @object.has_a_unary = 23
250
+ @object.has_a_unary.should == true
251
+ @object.has_a_unary = false
252
+ @object.has_a_unary.should be_false
253
+ @object.has_a_unary = nil
254
+ @object.has_a_unary.should be_nil
255
+ end
256
+ else
257
+ it "should allow its #{role_name} role to be assigned and reassigned a base value" do
258
+ object_type = role.counterpart.object_type
259
+ required_value_type = VALUE_TYPE_FOR_OBJECT_TYPE[object_type]
260
+ values = VALUES_FOR_TYPE[required_value_type]
261
+ next unless values
262
+ value = object_identifying_parameters(object_type.basename, values[0])
263
+
264
+ # Set the role to the first value:
265
+ assigned = @object.send(:"#{role_name}=", value)
266
+ assigned.class.should == object_type
267
+ fetched = @object.send(role_name)
268
+ fetched.should == assigned
269
+
270
+ if role.counterpart.unique # A one-to-one
271
+ # The counterpart should point back at us
272
+ assigned.send(role.counterpart.name).should == @object
273
+ else # A many-to-one
274
+ # The counterpart should include us in its RoleValues
275
+ reflection = assigned.send(role.counterpart.name)
276
+ reflection.should_not be_empty
277
+ reflection.size.should == 1
278
+ reflection.should be_include(@object)
279
+ end
280
+
281
+ # Update the role to the second value:
282
+ value = object_identifying_parameters(object_type.basename, values[1])
283
+ assigned2 = @object.send(:"#{role_name}=", value)
284
+ assigned2.class.should == object_type
285
+ fetched = @object.send(role_name)
286
+ fetched.should == assigned2
287
+
288
+ if role.counterpart.unique # A one-to-one
289
+ # REVISIT: The old counterpart role should be nullified
290
+ #assigned.send(role.counterpart.name).should be_nil
291
+
292
+ # The counterpart should point back at us
293
+ assigned2.send(role.counterpart.name).should == @object
294
+ else # A many-to-one
295
+ # REVISIT: The old counterpart RoleValues should be empty
296
+ reflection = assigned2.send(role.counterpart.name)
297
+ #reflection.size.should == 0
298
+
299
+ # The counterpart should include us in its RoleValues
300
+ reflection2 = assigned2.send(role.counterpart.name)
301
+ reflection2.size.should == 1
302
+ reflection2.should be_include(@object)
303
+ end
304
+
305
+ # Nullify the role
306
+ nullified = @object.send(:"#{role_name}=", nil)
307
+ nullified.should be_nil
308
+ if role.counterpart.unique # A one-to-one
309
+ assigned2.send(role.counterpart.name).should be_nil
310
+ else # A many-to-one
311
+ reflection3 = assigned2.send(role.counterpart.name)
312
+ reflection3.size.should == 0
313
+ end
314
+ end
315
+
316
+ it "should allow its #{role_name} role to be assigned and reassigned a base value" do
317
+ object_type = role.counterpart.object_type
318
+ required_value_type = VALUE_TYPE_FOR_OBJECT_TYPE[object_type]
319
+ values = VALUES_FOR_TYPE[required_value_type]
320
+ next unless values
321
+ value = object_identifying_parameters(object_type.basename, values[0])
322
+
323
+ # Set the role to the first value:
324
+ assigned = @object.send(:"#{role_name}=", value)
325
+ fetched = @object.send(role_name)
326
+ fetched.class.should == object_type
327
+ end
328
+
329
+ it "should allow its #{role_name} role to be assigned a value instance" do
330
+ object_type = role.counterpart.object_type
331
+ required_value_type = VALUE_TYPE_FOR_OBJECT_TYPE[object_type]
332
+ values = VALUES_FOR_TYPE[required_value_type]
333
+ next unless values
334
+ value = @constellation.send(object_type.basename, *object_identifying_parameters(object_type.basename, values[0]))
335
+
336
+ assigned = @object.send(:"#{role_name}=", value)
337
+ assigned.class.should == object_type
338
+ fetched = @object.send(role_name)
339
+ fetched.should == assigned
340
+
341
+ # Nullify the role
342
+ nullified = @object.send(:"#{role_name}=", nil)
343
+ nullified.should be_nil
344
+ end
345
+
346
+ it "should allow its #{role_name} role to be assigned a value subtype instance, retaining the subtype" do
347
+ object_type = role.counterpart.object_type
348
+ required_value_type = VALUE_TYPE_FOR_OBJECT_TYPE[object_type] # The raw value type
349
+ values = VALUES_FOR_TYPE[required_value_type]
350
+ object_type = VALUE_SUB_FOR_VALUE[object_type] # The value type subtype
351
+ next unless values and object_type
352
+ value = @constellation.send(object_type.basename, *object_identifying_parameters(object_type.basename, values[0]))
353
+ assigned = @object.send(:"#{role_name}=", value)
354
+ # This requires the declared type, not the subtype:
355
+ # assigned.class.should == role.counterpart.object_type
356
+ # This requires the subtype, as the test implies:
357
+ assigned.class.should == object_type
358
+ fetched = @object.send(role_name)
359
+ fetched.should == assigned
360
+ end
361
+ end
362
+
363
+ unless !role.counterpart or # A unary
364
+ role.counterpart.unique or # A one-to-one
365
+ VALUES_FOR_TYPE[VALUE_TYPE_FOR_OBJECT_TYPE[role.counterpart.object_type]] == nil
366
+ describe "Operations on #{role.counterpart.object_type.basename} RoleValues collections" do
367
+ before :each do
368
+ object_type = role.counterpart.object_type
369
+ required_value_type = VALUE_TYPE_FOR_OBJECT_TYPE[object_type]
370
+ values = VALUES_FOR_TYPE[required_value_type]
371
+ return unless values
372
+ value = object_identifying_parameters(object_type.basename, values[0])
373
+ assigned = @object.send(:"#{role_name}=", value)
374
+ @role_values = assigned.send(role.counterpart.name)
375
+ end
376
+
377
+ it "should support Array addition" do
378
+ added = @role_values + ["foo"]
379
+ added.class.should == Array
380
+ added.size.should == 2
381
+ end
382
+
383
+ it "should support Array subtraction" do
384
+ # We only added one value, so subtracting it leaves us empty
385
+ counterpart_value = @role_values.single
386
+ (@role_values - [counterpart_value]).should be_empty
387
+ end
388
+
389
+ it "should support each" do
390
+ count = 0
391
+ @role_values.each { |rv| count += 1 }
392
+ count.should == 1
393
+ end
394
+
395
+ it "should support detect" do
396
+ @role_values.detect { |rv| true }.should be_true
397
+ end
398
+
399
+ it "should verbalise" do
400
+ @role_values.verbalise.should =~ /Octopus.*Zero '0'/
401
+ end
402
+
403
+ end
404
+ end
405
+
406
+ end
407
+
408
+ end
409
+ end
@@ -2,6 +2,7 @@
2
2
  # ActiveFacts tests: Roles of object_type classes in the Runtime API
3
3
  # Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
4
4
  #
5
+ require 'rspec'
5
6
  require 'activefacts/api'
6
7
 
7
8
  describe "Roles" do
@@ -9,7 +10,7 @@ describe "Roles" do
9
10
  Object.send :remove_const, :Mod if Object.const_defined?("Mod")
10
11
  module Mod
11
12
  class Name < String
12
- value_type
13
+ value_type :length => 40, :scale => 0, :restrict => /^[A-Z][a-z]*/
13
14
  end
14
15
  class LegalEntity
15
16
  identified_by :name
@@ -23,9 +24,10 @@ describe "Roles" do
23
24
  class Person < LegalEntity
24
25
  # identified_by # No identifier needed, inherit from superclass
25
26
  # New identifier:
26
- identified_by :family, :given
27
+ identified_by :family, :name
27
28
  has_one :family, :class => Name
28
- has_one :given, :class => Name
29
+ alias :given :name
30
+ alias :given= :name=
29
31
  has_one :related_to, :class => LegalEntity
30
32
  end
31
33
  end
@@ -41,7 +43,13 @@ describe "Roles" do
41
43
  end
42
44
  role = Mod::Existing1.roles(:name)
43
45
  role.should_not be_nil
44
- role.counterpart_object_type.should == Mod::Name
46
+ role.inspect.class.should == String
47
+ role.counterpart.object_type.should == Mod::Name
48
+ end
49
+
50
+ it "should provide value type metadata" do
51
+ Mod::Name.length.should == 40
52
+ Mod::Name.scale.should == 0
45
53
  end
46
54
 
47
55
  it "should inject the respective role name into the matching object_type" do
@@ -51,6 +59,9 @@ describe "Roles" do
51
59
  has_one :name
52
60
  end
53
61
  end
62
+ # REVISIT: need to make more tests for the class's role accessor methods:
63
+ Mod::Name.roles(:all_existing1).should == Mod::Name.all_existing1_role
64
+
54
65
  Mod::Name.roles(:all_existing1).should_not be_nil
55
66
  Mod::LegalEntity.roles(:all_contract_as_first).should_not be_nil
56
67
  end
@@ -65,7 +76,7 @@ describe "Roles" do
65
76
  # print "Mod::Existing2.roles = "; p Mod::Existing2.roles
66
77
  r = Mod::Existing2.roles(:given_name)
67
78
  r.should_not be_nil
68
- Symbol.should === r.counterpart_object_type
79
+ r.counterpart.should be_nil
69
80
  module Mod
70
81
  class GivenName < String
71
82
  value_type
@@ -74,7 +85,7 @@ describe "Roles" do
74
85
  # puts "Should resolve now:"
75
86
  r = Mod::Existing2.roles(:given_name)
76
87
  r.should_not be_nil
77
- r.counterpart_object_type.should == Mod::GivenName
88
+ r.counterpart.object_type.should == Mod::GivenName
78
89
  end
79
90
 
80
91
  it "should handle subtyping a value type" do
@@ -86,8 +97,8 @@ describe "Roles" do
86
97
  end
87
98
  r = Mod::FamilyName.roles(:patriarch)
88
99
  r.should_not be_nil
89
- r.counterpart_object_type.should == Mod::Person
90
- r.counterpart_object_type.roles(:family_name_as_patriarch).counterpart_object_type.should == Mod::FamilyName
100
+ r.counterpart.object_type.should == Mod::Person
101
+ r.counterpart.object_type.roles(:family_name_as_patriarch).counterpart.object_type.should == Mod::FamilyName
91
102
  end
92
103
 
93
104
  it "should instantiate the matching object_type on assignment" do
@@ -114,11 +125,39 @@ describe "Roles" do
114
125
  it "should instantiate subclasses sensibly" do
115
126
  c = ActiveFacts::API::Constellation.new(Mod)
116
127
  bloggs = c.LegalEntity("Bloggs & Co")
117
- #pending
118
128
  p = c.Person("Fred", "Bloggs")
119
129
  p.related_to = "Bloggs & Co"
120
130
  p.related_to.should be_is_a(Mod::LegalEntity)
121
- bloggs.object_id.should == p.related_to.object_id
131
+ p.related_to.should == bloggs
132
+
133
+ # REVISIT: The raw instance doesn't override == to compare itself to a RoleProxy unfortunately...
134
+ # So this test succeeds when we'd like it to fail
135
+ #bloggs.should_not == p.related_to
136
+ end
137
+
138
+ it "should forward missing methods on the role proxies" do
139
+ c = ActiveFacts::API::Constellation.new(Mod)
140
+ p = c.Person("Fred", "Bloggs")
141
+
142
+ # Make sure that RoleProxy's method_missing delegates, then forwards the send
143
+ lambda {
144
+ p.family.foo
145
+ }.should raise_error(NoMethodError)
146
+ end
147
+
148
+ it "should forward re-raise exceptions from missing methods on the role proxies" do
149
+ c = ActiveFacts::API::Constellation.new(Mod)
150
+ p = c.Person("Fred", "Bloggs")
151
+
152
+ # x = p.family.__getobj__
153
+ #def x.barf
154
+ # raise "Yawning..."
155
+ #end
156
+ lambda {
157
+ p.family.barf
158
+ #}.should raise_error(RuntimeError)
159
+ }.should raise_error(NoMethodError)
160
+
122
161
  end
123
162
 
124
163
  end