# # The ActiveFacts Runtime API Constellation class # Copyright (c) 2008 Clifford Heath. Read the LICENSE file. # module ActiveFacts module API # A Constellation is a population of instances of the Concept classes of a Vocabulary. # Every concept 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 ##delete 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. # class Constellation attr_reader :vocabulary # All instances are indexed in this hash, keyed by the class object. Each instance is indexed for every supertype it has (including multiply-inherited ones). It's a bad idea to try to modify these indexes! attr_reader :instances # Can say c.instances[MyClass].each{|k, v| ... } # Can also say c.MyClass.each{|k, v| ... } # Create a new empty Constellation over the given Vocabulary def initialize(vocabulary) @vocabulary = vocabulary @instances = Hash.new{|h,k| h[k] = {} } end def inspect #:nodoc: "Constellation:#{object_id}" end # This method removes the given instance from this constellation's indexes def delete(instance) #:nodoc: # REVISIT: Need to search, as key values are gone already. Is there a faster way? ([instance.class]+instance.class.supertypes_transitive).each do |klass| @instances[klass].delete_if{|k,v| v == instance } end end # With parameters, assert an instance of the concept whose name is the missing method. # With no parameters, return the collection of all instances of that concept. def method_missing(m, *args) if klass = @vocabulary.const_get(m) if args.size == 0 # Return the collection of all instances of this class in the constellation: @instances[klass] else # Assert a new ground fact (concept instance) of the specified class, identified by args: # REVISIT: create a constructor method here instead? instance, key = klass.assert_instance(self, args) instance end end end # Constellations verbalise all members of all classes in alphabetical order, showing # non-identifying role values as well def verbalise "Constellation over #{vocabulary.name}:\n" + vocabulary.concept.keys.sort.map{|concept| klass = vocabulary.const_get(concept) # REVISIT: It would be better not to rely on the role name pattern here: single_roles, multiple_roles = klass.roles.keys.sort_by(&:to_s).partition{|r| r.to_s !~ /\Aall_/ } single_roles -= klass.identifying_roles if (klass.respond_to?(:identifying_roles)) # REVISIT: Need to include superclass roles also. instances = send(concept.to_sym) next nil unless instances.size > 0 "\tEvery #{concept}:\n" + instances.map{|key, instance| s = "\t\t" + instance.verbalise if (single_roles.size > 0) role_values = single_roles.map{|role| [ role_name = role.to_s.camelcase(true), value = instance.send(role)] }.select{|role_name, value| value }.map{|role_name, value| "#{role_name} = #{value ? value.verbalise : "nil"}" } s += " where " + role_values*", " if role_values.size > 0 end s } * "\n" }.compact*"\n" end end end end