Class: ActiveFacts::API::Constellation

Inherits:
Object
  • Object
show all
Defined in:
lib/activefacts/api/constellation.rb

Overview

A Constellation is a population of instances of the ObjectType classes of a Vocabulary. Every object_type class is either a Value type or an Entity type.

Value types are uniquely identified by their value, and a constellation will only ever have a single instance of a given value of that class.

Entity instances are uniquely identified by their identifying roles, and again, a constellation will only ever have a single entity instance for the values of those identifying roles.

As a result, you cannot “create” an object in a constellation - you merely assert its existence. This is done using method_missing; @constellation.Thing(3) creates an instance (or returns the existing instance) of Thing identified by the value 3. You can also use the populate() method to apply a block of assertions.

You can instance##retract any instance, and that removes it from the constellation (will delete it from the database when the constellation is saved), and nullifies any references to it.

A Constellation may or not be valid according to the vocabulary’s constraints, but it may also represent a portion of a larger population (a database) with which it may be merged to form a valid population. In other words, an invalid Constellation may be invalid only because it lacks some of the facts.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(vocabulary, options = {}) ⇒ Constellation

Create a new empty Constellation over the given Vocabulary



38
39
40
41
# File 'lib/activefacts/api/constellation.rb', line 38

def initialize(vocabulary, options = {})
  @vocabulary = vocabulary
  @options = options
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(m, *args, &b) ⇒ Object

If a missing method is the name of a class in the vocabulary module for this constellation, then we want to access the collection of instances of that class, and perhaps assert new ones. With no parameters, return the collection of all instances of that object_type. With parameters, assert an instance of the object_type identified by the values passed as args.



67
68
69
70
71
72
73
74
75
# File 'lib/activefacts/api/constellation.rb', line 67

def method_missing(m, *args, &b)
  klass = @vocabulary.const_get(m)
  if invalid_object_type klass
    super
  else
    define_class_accessor m, klass
    send(m, *args, &b)
  end
end

Instance Attribute Details

#vocabularyObject (readonly)

Returns the value of attribute vocabulary.



35
36
37
# File 'lib/activefacts/api/constellation.rb', line 35

def vocabulary
  @vocabulary
end

Instance Method Details

#assert(klass, *args) ⇒ Object



43
44
45
46
47
# File 'lib/activefacts/api/constellation.rb', line 43

def assert(klass, *args)
  with_candidates do
    klass.assert_instance self, args
  end
end

#candidate(instance) ⇒ Object



167
168
169
# File 'lib/activefacts/api/constellation.rb', line 167

def candidate instance
  @candidates << instance unless @candidates[-1] == instance
end

#cloneObject



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/activefacts/api/constellation.rb', line 267

def clone
  remaining_object_types = vocabulary.object_type.clone
  constellation = self.class.new(vocabulary, @options)
  correlates = {}
  other_attribute_assignments = []
  until remaining_object_types.empty?
    count = 0
    # Choose an object type we can clone now:
    name, object_type = *remaining_object_types.detect do |name, o|
      (count = @instances[o].size) == 0 or        # There are no instances of this object type; clone is ok
        !o.is_entity_type or                      # It's a value type
        (
          !o.subtypes_transitive.detect do |subtype|# All its subtypes have been cloned
              remaining_object_types.has_key?(subtype.basename)
          end and
          !o.identifying_roles.detect do |role|   # The players of its identifying roles have all been dumped
            next unless role.counterpart              # Unary role, no player
            counterpart = role.counterpart.object_type  # counterpart object

            # The identifying type and its subtypes have been dumped
            ([counterpart]+counterpart.subtypes_transitive).detect do |subtype|
              remaining_object_types.has_key?(subtype.basename)
            end
          end
        )
    end
#         puts "Cloning #{count} instances of #{name}" if count > 0
    remaining_object_types.delete(name)

    key_role_names =
      if object_type.is_entity_type
        ([object_type]+object_type.supertypes_transitive).map { |t| t.identifying_role_names }.flatten.uniq
      else
        nil
      end
    other_roles = object_type.all_role_transitive.map do |role_name, role|
        next if !role.unique or
          role.fact_type.class == ActiveFacts::API::TypeInheritanceFactType or
          role.fact_type.all_role[0] != role or  # Only the first role in a one-to-one pair
          key_role_names.include?(role_name)
        role
      end.compact - Array(key_role_names)

    @instances[object_type].each do |key, object|
      next if object.class != object_type

      # Clone this object

      # Get the identifying values:
      key = object
      if (key_role_names)
        key = key_role_names.inject({}) do |h, krn|
            h[krn] = object.send(krn)
            h
          end
      end
#           puts "\tcloning #{object.class} #{key.inspect}"
#           puts "\t\talso copy #{other_roles.map(&:name)*', '}"

      new_object = constellation.assert(object_type, key)
      correlates[object] = new_object

      other_roles.each do |role|
        value = object.send(role.getter)
        next unless value
        other_attribute_assignments << proc do
          new_object.send(role.setter, correlates[value])
        end
      end
    end
  end

  # Now, assign all non-identifying facts
#       puts "Assigning #{other_attribute_assignments.size} additional roles"
  other_attribute_assignments.each do |assignment|
    assignment.call
  end

  constellation
end

#define_class_accessor(m, klass) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/activefacts/api/constellation.rb', line 77

def define_class_accessor m, klass
  (class << self; self; end).
    send(:define_method, m) do |*args|
      if args.size == 0
        # Return the collection of all instances of this class in the constellation:
        instances[klass]
      else
        # Assert a new ground fact (object_type instance) of the specified class, identified by args:
        assert(klass, *args)
      end
  end
end

#deindex_instance(instance) ⇒ Object

This method removes the given instance from this constellation’s indexes It must be called before the identifying roles get deleted or nullified.



177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/activefacts/api/constellation.rb', line 177

def deindex_instance(instance) #:nodoc:
  last_irns = nil
  last_irvs = instance
  ([instance.class]+instance.class.supertypes_transitive).each do |klass|
    if instance.is_a?(Entity) and last_irns != (n = klass.identifying_role_names)
      # Build new identifying_role_values only when the identifying_role_names change:
      last_irvs = instance.identifying_role_values(klass)
      last_irns = n
    end
    deleted = instances[klass].delete(last_irvs)
    # The RBTree class sometimes returns a different object than what was deleted! Check non-nil:
    raise "Internal error: deindex #{instance.class} as #{klass} failed" if deleted == nil
  end
end

#fork(instance, attrs = {}) ⇒ Object

Make a new instance like “instance”, but with some new attributes assigned. All identifiers should overall be different from the forked instance, and all one-to-ones must be assigned new values (otherwise we change old objects)



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/activefacts/api/constellation.rb', line 242

def fork instance, attrs = {}
  object_type = instance.class

  role_value_map =
    object_type.all_role_transitive.inject({}) do |hash, (role_name, role)|
      next hash if !role.unique
      next hash if role.fact_type.class == ActiveFacts::API::TypeInheritanceFactType
      old_value = instance.send(role.getter)
      if role.counterpart && role.counterpart.unique && old_value != nil
        # It's a one-to-one which is populated. We must not change the counterpart
        if role.mandatory && !attrs.include?(role.name)
          # and cannot just nullify the value
          raise "#{object_type.basename} cannot be forked unless a replacement value for #{role.name} is provided"
        end
        value = attrs[role_name]
      else
        value = attrs.include?(role_name) ? attrs[role_name] : instance.send(role.getter)
      end
      hash[role_name] = value if value != nil
      hash
    end

    assert(object_type, role_value_map)
end

#has_candidate(klass, key) ⇒ Object



171
172
173
# File 'lib/activefacts/api/constellation.rb', line 171

def has_candidate klass, key
  @candidates && @candidates.detect{|c| c.is_a?(klass) && c.identifying_role_values(klass) == key }
end

#inspectObject

:nodoc:



90
91
92
93
# File 'lib/activefacts/api/constellation.rb', line 90

def inspect #:nodoc:
  total = instances.values.map(&:size).inject(:+) || 0
  "Constellation:#{object_id} over #{@vocabulary.name} with #{total}/#{instances.size}"
end

#instancesObject

“instances” is an index (keyed by the Class object) of indexes to instances. Each instance is indexed for every supertype it has (including multiply-inherited ones). The method_missing definition supports the syntax: c.MyClass.each{|k, v| … }



116
117
118
119
120
121
122
123
# File 'lib/activefacts/api/constellation.rb', line 116

def instances
  @instances ||= Hash.new do |h,k|
      if reason = invalid_object_type(k)
        raise InvalidObjectType.new(@vocabulary, k, reason)
      end
      h[k] = InstanceIndex.new(self, k, (@options.include?(:sort) ? @options[:sort] : API::sorted))
    end
end

#invalid_object_type(klass) ⇒ Object



100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/activefacts/api/constellation.rb', line 100

def invalid_object_type klass
  case
  when !klass.is_a?(Class)
    'is not a Class'
  when klass.modspace != @vocabulary
    "is defined in #{klass.modspace}, not #{@vocabulary.name}"
  when !klass.respond_to?(:assert_instance)
    "is not declared as an object type"
  else
    nil
  end
end

#loggersObject

Loggers is an array of callbacks



96
97
98
# File 'lib/activefacts/api/constellation.rb', line 96

def loggers
  @loggers ||= []
end

#populate(&block) ⇒ Object

Evaluate assertions against the population of this Constellation



58
59
60
61
# File 'lib/activefacts/api/constellation.rb', line 58

def populate &block
  instance_eval(&block)
  self
end

#retract(*instances) ⇒ Object

Delete instances from the constellation, nullifying (or cascading) the roles each plays



50
51
52
53
54
55
# File 'lib/activefacts/api/constellation.rb', line 50

def retract(*instances)
  Array(instances).each do |i|
    i.retract
  end
  self
end

#verbaliseObject

Constellations verbalise all members of all classes in alphabetical order, showing non-identifying role values as well



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/activefacts/api/constellation.rb', line 194

def verbalise
  "Constellation over #{vocabulary.name}:\n" +
  vocabulary.object_type.keys.sort.map do |object_type|
      klass = vocabulary.const_get(object_type)

      single_roles, multiple_roles = klass.all_role.
          partition do |n, r|
            r.unique &&               # Show only single-valued roles
              !r.is_identifying &&    # Unless identifying
              (r.unary? || !r.counterpart.is_identifying) # Or identifies a counterpart
          end.
          map do |rs|
            rs.map{|n, r| n}.
              sort_by(&:to_s)
          end

      instances = send(object_type.to_sym)
      next nil unless instances.size > 0
      "\tEvery #{object_type}:\n" +
        instances.map do |key, instance|
            s = "\t\t" + instance.verbalise
            if (single_roles.size > 0)
              role_values = 
                single_roles.map do |role_name|
                    #p klass, klass.all_role.keys; exit
                    next nil if klass.all_role(role_name).fact_type.is_a?(TypeInheritanceFactType)
                    value =
                      if instance.respond_to?(role_name)
                        value = instance.send(role_name)
                      else
                        instance.class.all_role(role_name) # This role has not yet been realised
                      end
                    [ role_name.to_s.camelcase, value ]
                  end.compact.select do |role_name, value|
                    value
                  end.map do |role_name, value|
                    "#{role_name} = #{value ? value.verbalise : "nil"}"
                  end
              s += " where " + role_values*", " if role_values.size > 0
            end
            s
          end * "\n"
    end.compact*"\n"
end

#when_admitted(&b) ⇒ Object



159
160
161
162
163
164
165
# File 'lib/activefacts/api/constellation.rb', line 159

def when_admitted &b
  if @candidates.nil?
    b.call self
  else
    @on_admission << b
  end
end

#with_candidates(&b) ⇒ Object

Candidates is an array of object instances that do not already exist in the constellation but will be added if an assertion succeeds. After the assertion is found to be acceptable, these objects are indexed in the constellation and in the counterparts of their identifying roles, and the candidates array is nullified.



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/activefacts/api/constellation.rb', line 130

def with_candidates &b
  # Multiple assignment reduces (s)teps while debugging
  outermost, @candidates, @on_admission = @candidates.nil?, (@candidates || []), (@on_admission || [])
  begin
    b.call
  rescue Exception
    # Do not accept any of these candidates, there was a problem:
    @candidates = [] if outermost
    raise
  ensure
    if outermost
      while @candidates
        # Index the accepted instances in the constellation:
        candidates = @candidates
        on_admission = @on_admission
        @candidates = nil
        @on_admission = nil
        candidates.each do |instance|
          instance.class.index_instance(self, instance)
          loggers.each{|l| l.call(:assert, instance.class, instance.identifying_role_values)}
        end
        on_admission.each do |b|
          b.call
        end
      end
    end
  end
end