activefacts-api 0.8.9 → 0.8.10

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.
@@ -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