activefacts-api 0.8.9
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/LICENSE.txt +19 -0
- data/README.rdoc +41 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/lib/activefacts/api.rb +44 -0
- data/lib/activefacts/api/constellation.rb +128 -0
- data/lib/activefacts/api/entity.rb +260 -0
- data/lib/activefacts/api/instance.rb +60 -0
- data/lib/activefacts/api/instance_index.rb +84 -0
- data/lib/activefacts/api/numeric.rb +175 -0
- data/lib/activefacts/api/object_type.rb +411 -0
- data/lib/activefacts/api/role.rb +80 -0
- data/lib/activefacts/api/role_proxy.rb +71 -0
- data/lib/activefacts/api/role_values.rb +117 -0
- data/lib/activefacts/api/standard_types.rb +87 -0
- data/lib/activefacts/api/support.rb +66 -0
- data/lib/activefacts/api/value.rb +135 -0
- data/lib/activefacts/api/vocabulary.rb +82 -0
- data/spec/api/autocounter_spec.rb +84 -0
- data/spec/api/constellation_spec.rb +129 -0
- data/spec/api/entity_type_spec.rb +103 -0
- data/spec/api/instance_spec.rb +462 -0
- data/spec/api/roles_spec.rb +124 -0
- data/spec/api/value_type_spec.rb +114 -0
- data/spec/spec_helper.rb +12 -0
- metadata +154 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2008 Clifford Heath.
|
2
|
+
|
3
|
+
This software is provided 'as-is', without any express or implied warranty.
|
4
|
+
In no event will the authors be held liable for any damages arising from the
|
5
|
+
use of this software.
|
6
|
+
|
7
|
+
Permission is granted to anyone to use this software for any purpose,
|
8
|
+
including commercial applications, and to alter it and redistribute it
|
9
|
+
freely, subject to the following restrictions:
|
10
|
+
|
11
|
+
1. The origin of this software must not be misrepresented; you must not
|
12
|
+
claim that you wrote the original software. If you use this software
|
13
|
+
in a product, an acknowledgment in the product documentation would be
|
14
|
+
appreciated but is not required.
|
15
|
+
|
16
|
+
2. Altered source versions must be plainly marked as such, and must not be
|
17
|
+
misrepresented as being the original software.
|
18
|
+
|
19
|
+
3. This notice may not be removed or altered from any source distribution.
|
data/README.rdoc
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
= activefacts-api
|
2
|
+
|
3
|
+
The ActiveFacts API provides the fact-oriented information management API
|
4
|
+
for the ActiveFacts project. It is a Ruby DSL for managing constellations
|
5
|
+
of elementary facts. Each fact is either existential (a value or an entity),
|
6
|
+
characteristic (boolean) or binary relational (A rel B). Relational facts are
|
7
|
+
consistently co-referenced, so you can traverse them efficiently in any
|
8
|
+
direction. Each constellation maintains constraints over the fact population.
|
9
|
+
|
10
|
+
Contrary to object-oriented and relational modeling, fact oriented models
|
11
|
+
do not use the concept of attributes. Fact types which express one-to-one or
|
12
|
+
many-to-one relationships are fully mutual relationships between independent
|
13
|
+
objects, which play the respective roles in the fact relationship. In addition,
|
14
|
+
all objects are intrinsically identified, not by an external object-id. A
|
15
|
+
constellation can not contain more than one instance of an object having the
|
16
|
+
same identification. Accordingly there is no 'new' or 'delete' operations,
|
17
|
+
just 'assert' and 'retract'. This prevents problems caused by having duplicate
|
18
|
+
representations of the same object.
|
19
|
+
|
20
|
+
The constellation is a universal and liberating data structure.
|
21
|
+
|
22
|
+
* http://dataconstellation.com/ActiveFacts/
|
23
|
+
|
24
|
+
== INSTALL:
|
25
|
+
|
26
|
+
* sudo gem install activefacts-api
|
27
|
+
|
28
|
+
== Contributing to activefacts-api
|
29
|
+
|
30
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
31
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
32
|
+
* Fork the project
|
33
|
+
* Start a feature/bugfix branch
|
34
|
+
* Commit and push until you are happy with your contribution
|
35
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
36
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
37
|
+
|
38
|
+
== Copyright
|
39
|
+
|
40
|
+
Copyright (c) 2008-2011 Clifford Heath. See LICENSE.txt for further details.
|
41
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
require 'jeweler'
|
5
|
+
Jeweler::Tasks.new do |gem|
|
6
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
7
|
+
gem.name = "activefacts-api"
|
8
|
+
gem.homepage = "http://github.com/cjheath/activefacts-api"
|
9
|
+
gem.license = "MIT"
|
10
|
+
gem.summary = "A semantic modeling and query language (CQL) and application runtime (the Constellation API)"
|
11
|
+
gem.description = %q{
|
12
|
+
The ActiveFacts API is a Ruby DSL for managing constellations of elementary facts.
|
13
|
+
Each fact is either existential (a value or an entity), characteristic (boolean) or
|
14
|
+
binary relational (A rel B). Relational facts are consistently co-referenced, so you
|
15
|
+
can traverse them efficiently in any direction. Each constellation maintains constraints
|
16
|
+
over the fact population.
|
17
|
+
}
|
18
|
+
gem.email = "clifford.heath@gmail.com"
|
19
|
+
gem.authors = ["Clifford Heath"]
|
20
|
+
# Include your dependencies below. Runtime dependencies are required when using your gem,
|
21
|
+
# and development dependencies are only needed for development (ie running rake tasks, tests, etc)
|
22
|
+
gem.add_development_dependency "rspec", "~> 2.3.0"
|
23
|
+
gem.add_development_dependency "bundler", "~> 1.0.0"
|
24
|
+
gem.add_development_dependency "jeweler", "~> 1.5.2"
|
25
|
+
# gem.add_development_dependency "rcov", ">= 0"
|
26
|
+
end
|
27
|
+
Jeweler::RubygemsDotOrgTasks.new
|
28
|
+
|
29
|
+
require 'rspec/core'
|
30
|
+
require 'rspec/core/rake_task'
|
31
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
32
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
33
|
+
end
|
34
|
+
|
35
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
36
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
37
|
+
spec.rcov = true
|
38
|
+
end
|
39
|
+
|
40
|
+
task :default => :spec
|
41
|
+
|
42
|
+
require 'rake/rdoctask'
|
43
|
+
Rake::RDocTask.new do |rdoc|
|
44
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
45
|
+
|
46
|
+
rdoc.rdoc_dir = 'rdoc'
|
47
|
+
rdoc.title = "activefacts-api #{version}"
|
48
|
+
rdoc.rdoc_files.include('README*')
|
49
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
50
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.8.9
|
@@ -0,0 +1,44 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Runtime API.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
5
|
+
#
|
6
|
+
# The ActiveFacts API is heavily metaprogrammed, so difficult to document.
|
7
|
+
#
|
8
|
+
# It operates on the principle that a Ruby module is used to encapsulate
|
9
|
+
# a Vocabulary (the methods of the class Vocabulary are extend()ed into
|
10
|
+
# the module). A Vocabulary contains classes that either derive from a
|
11
|
+
# builtin Value type class (see standard_types.rb), or that use the method
|
12
|
+
# Class#_identified_by_ to become an Entity (their classes are extend()ed
|
13
|
+
# by the class Entity::ClassMethods). Each Value and Entity class also
|
14
|
+
# contains the methods of the class ObjectType.
|
15
|
+
#
|
16
|
+
# A module becomes a Vocabulary when the first ObjectType class is defined within it.
|
17
|
+
# A Constellation is a unique collection of ObjectType instances; no two instances may
|
18
|
+
# exist of the same value of a ValueType, or having the same identifying roles for
|
19
|
+
# an Entity type.
|
20
|
+
#
|
21
|
+
# Both kinds of ObjectTypes play Roles, which are either binary or unary. Each Role
|
22
|
+
# corresponds to an accessor method on Instances which are used to access the
|
23
|
+
# counterpart. Roles are created by the class methods *has_one*, *one_to_one*,
|
24
|
+
# and *maybe*. The former two create *two* roles, since the role has a counterpart
|
25
|
+
# object_type that also plays a role. In the case of a has_one role, the counterpart
|
26
|
+
# role is a set, implemented by the RoleValues class, and the accessor method is
|
27
|
+
# named beginning with *all_*.
|
28
|
+
#
|
29
|
+
# The roles of any Instance of any ObjectType may only be played by another Instance
|
30
|
+
# of the counterpart ObjectType. There are no raw values, only instances of ValueType
|
31
|
+
# classes.
|
32
|
+
|
33
|
+
require 'activefacts/api/support' # General support code and core patches
|
34
|
+
require 'activefacts/api/vocabulary' # A Ruby module may become a Vocabulary
|
35
|
+
require 'activefacts/api/role_proxy' # Experimental proxy for has_one/one_to_one role accessors
|
36
|
+
require 'activefacts/api/instance_index' # The index used by a constellation to record every instance
|
37
|
+
require 'activefacts/api/constellation' # A Constellation is a query result or fact population
|
38
|
+
require 'activefacts/api/object_type' # A Ruby class may become a ObjectType in a Vocabulary
|
39
|
+
require 'activefacts/api/role' # A ObjectType has a collection of Roles
|
40
|
+
require 'activefacts/api/role_values' # The container used for sets of role players in many_one's
|
41
|
+
require 'activefacts/api/instance' # An Instance is an instance of a ObjectType class
|
42
|
+
require 'activefacts/api/value' # A Value is an Instance of a value class (String, Numeric, etc)
|
43
|
+
require 'activefacts/api/entity' # An Entity class is an Instance not of a value class
|
44
|
+
require 'activefacts/api/standard_types' # Value classes are augmented so their subclasses may become Value Types
|
@@ -0,0 +1,128 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Runtime API
|
3
|
+
# Constellation class
|
4
|
+
#
|
5
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
6
|
+
#
|
7
|
+
|
8
|
+
module ActiveFacts
|
9
|
+
module API #:nodoc:
|
10
|
+
# A Constellation is a population of instances of the ObjectType classes of a Vocabulary.
|
11
|
+
# Every object_type class is either a Value type or an Entity type.
|
12
|
+
#
|
13
|
+
# Value types are uniquely identified by their value, and a constellation will only
|
14
|
+
# ever have a single instance of a given value of that class.
|
15
|
+
#
|
16
|
+
# Entity instances are uniquely identified by their identifying roles, and again, a
|
17
|
+
# constellation will only ever have a single entity instance for the values of those
|
18
|
+
# identifying roles.
|
19
|
+
#
|
20
|
+
# As a result, you cannot "create" an object in a constellation - you merely _assert_
|
21
|
+
# its existence. This is done using method_missing; @constellation.Thing(3) creates
|
22
|
+
# an instance (or returns the existing instance) of Thing identified by the value 3.
|
23
|
+
# You can also use the populate() method to apply a block of assertions.
|
24
|
+
#
|
25
|
+
# You can instance##retract any instance, and that removes it from the constellation (will
|
26
|
+
# delete it from the database when the constellation is saved), and nullifies any
|
27
|
+
# references to it.
|
28
|
+
#
|
29
|
+
# A Constellation may or not be valid according to the vocabulary's constraints,
|
30
|
+
# but it may also represent a portion of a larger population (a database) with
|
31
|
+
# which it may be merged to form a valid population. In other words, an invalid
|
32
|
+
# Constellation may be invalid only because it lacks some of the facts.
|
33
|
+
#
|
34
|
+
class Constellation
|
35
|
+
attr_reader :vocabulary
|
36
|
+
# 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!
|
37
|
+
attr_reader :instances # Can say c.instances[MyClass].each{|k, v| ... }
|
38
|
+
# Can also say c.MyClass.each{|k, v| ... }
|
39
|
+
|
40
|
+
# Create a new empty Constellation over the given Vocabulary
|
41
|
+
def initialize(vocabulary)
|
42
|
+
@vocabulary = vocabulary
|
43
|
+
@instances = Hash.new do |h,k|
|
44
|
+
raise "A constellation over #{@vocabulary.name} can only index instances of object_types in that vocabulary, not #{k.inspect}" unless k.is_a?(Class) and k.modspace == vocabulary
|
45
|
+
h[k] = InstanceIndex.new
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def inspect #:nodoc:
|
50
|
+
"Constellation:#{object_id}"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Evaluate assertions against the population of this Constellation
|
54
|
+
def populate *args, &block
|
55
|
+
# REVISIT: Use args for something? Like options to enable/disable validation?
|
56
|
+
instance_eval(&block)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Delete instances from the constellation, nullifying (or cascading) the roles each plays
|
60
|
+
def retract(*instances)
|
61
|
+
Array(instances).each do |i|
|
62
|
+
i.retract
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Constellations verbalise all members of all classes in alphabetical order, showing
|
67
|
+
# non-identifying role values as well
|
68
|
+
def verbalise
|
69
|
+
"Constellation over #{vocabulary.name}:\n" +
|
70
|
+
vocabulary.object_type.keys.sort.map{|object_type|
|
71
|
+
klass = vocabulary.const_get(object_type)
|
72
|
+
|
73
|
+
# REVISIT: It would be better not to rely on the role name pattern here:
|
74
|
+
single_roles, multiple_roles = klass.roles.keys.sort_by(&:to_s).partition{|r| r.to_s !~ /\Aall_/ }
|
75
|
+
single_roles -= klass.identifying_role_names if (klass.is_entity_type)
|
76
|
+
# REVISIT: Need to include superclass roles also.
|
77
|
+
|
78
|
+
instances = send(object_type.to_sym)
|
79
|
+
next nil unless instances.size > 0
|
80
|
+
"\tEvery #{object_type}:\n" +
|
81
|
+
instances.map{|key, instance|
|
82
|
+
s = "\t\t" + instance.verbalise
|
83
|
+
if (single_roles.size > 0)
|
84
|
+
role_values =
|
85
|
+
single_roles.map{|role|
|
86
|
+
[ role_name = role.to_s.camelcase,
|
87
|
+
value = instance.send(role)]
|
88
|
+
}.select{|role_name, value|
|
89
|
+
value
|
90
|
+
}.map{|role_name, value|
|
91
|
+
"#{role_name} = #{value ? value.verbalise : "nil"}"
|
92
|
+
}
|
93
|
+
s += " where " + role_values*", " if role_values.size > 0
|
94
|
+
end
|
95
|
+
s
|
96
|
+
} * "\n"
|
97
|
+
}.compact*"\n"
|
98
|
+
end
|
99
|
+
|
100
|
+
# This method removes the given instance from this constellation's indexes
|
101
|
+
# It must be called before the identifying roles get deleted or nullified.
|
102
|
+
def __retract(instance) #:nodoc:
|
103
|
+
# REVISIT: Need to search, as key values are gone already. Is there a faster way?
|
104
|
+
([instance.class]+instance.class.supertypes_transitive).each do |klass|
|
105
|
+
@instances[klass].delete_if{|k,v| v == instance }
|
106
|
+
end
|
107
|
+
# REVISIT: Need to nullify all the roles this object plays.
|
108
|
+
# If mandatory on the counterpart side, this may/must propagate the delete (without mutual recursion!)
|
109
|
+
end
|
110
|
+
|
111
|
+
# With parameters, assert an instance of the object_type whose name is the missing method, identified by the values passed as *args*.
|
112
|
+
# With no parameters, return the collection of all instances of that object_type.
|
113
|
+
def method_missing(m, *args)
|
114
|
+
if klass = @vocabulary.const_get(m)
|
115
|
+
if args.size == 0
|
116
|
+
# Return the collection of all instances of this class in the constellation:
|
117
|
+
@instances[klass]
|
118
|
+
else
|
119
|
+
# Assert a new ground fact (object_type instance) of the specified class, identified by args:
|
120
|
+
# REVISIT: create a constructor method here instead?
|
121
|
+
instance, key = klass.assert_instance(self, args)
|
122
|
+
instance
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Runtime API
|
3
|
+
# Entity class (a mixin module for the class Class)
|
4
|
+
#
|
5
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
6
|
+
#
|
7
|
+
module ActiveFacts
|
8
|
+
module API
|
9
|
+
# An Entity type is any ObjectType that isn't a value type.
|
10
|
+
# All Entity types must have an identifier made up of one or more roles.
|
11
|
+
module Entity
|
12
|
+
include Instance
|
13
|
+
|
14
|
+
# Assign the identifying roles to initialise a new Entity instance.
|
15
|
+
# The role values are asserted in the constellation first, so you
|
16
|
+
# can pass bare values (array, string, integer, etc) for any role
|
17
|
+
# whose instances can be constructed using those values.
|
18
|
+
#
|
19
|
+
# A value must be provided for every identifying role, but if the
|
20
|
+
# last argument is a hash, they may come from there.
|
21
|
+
#
|
22
|
+
# Any additional (non-identifying) roles may also be passed in the final hash.
|
23
|
+
def initialize(*args)
|
24
|
+
super(args)
|
25
|
+
klass = self.class
|
26
|
+
hash = {}
|
27
|
+
hash = args.pop.clone if Hash === args[-1]
|
28
|
+
|
29
|
+
# Pick any missing identifying roles out of the hash if possible:
|
30
|
+
while args.size < (ir = klass.identifying_role_names).size
|
31
|
+
value = hash[role = ir[args.size]]
|
32
|
+
hash.delete(role)
|
33
|
+
args.push value
|
34
|
+
end
|
35
|
+
|
36
|
+
# If one arg is expected but more are passed, they might be the args for the object that plays the identifying role:
|
37
|
+
args = [args] if klass.identifying_role_names.size == 1 && args.size > 1
|
38
|
+
|
39
|
+
# This should now only occur when there are too many args passed:
|
40
|
+
raise "Wrong number of parameters to #{klass}.new, " +
|
41
|
+
"expect (#{klass.identifying_role_names*","}) " +
|
42
|
+
"got (#{args.map{|a| a.to_s.inspect}*", "})" if args.size != klass.identifying_role_names.size
|
43
|
+
|
44
|
+
# Assign the identifying roles in order, then the other roles passed as a hash:
|
45
|
+
(klass.identifying_role_names.zip(args) + hash.entries).each do |role_name, value|
|
46
|
+
role = klass.roles(role_name)
|
47
|
+
send("#{role_name}=", value)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def inspect #:nodoc:
|
52
|
+
"\#<#{
|
53
|
+
self.class.basename
|
54
|
+
}:#{
|
55
|
+
object_id
|
56
|
+
}#{
|
57
|
+
constellation ? " in #{constellation.inspect}" : ""
|
58
|
+
} #{
|
59
|
+
# REVISIT: Where there are one-to-one roles, this cycles
|
60
|
+
self.class.identifying_role_names.map{|role| "@#{role}="+send(role).inspect }*" "
|
61
|
+
}>"
|
62
|
+
end
|
63
|
+
|
64
|
+
# When used as a hash key, the hash key of this entity instance is calculated
|
65
|
+
# by hashing the values of its identifying roles
|
66
|
+
def hash
|
67
|
+
self.class.identifying_role_names.map{|role|
|
68
|
+
instance_variable_get("@#{role}")
|
69
|
+
}.inject(0) { |h,v|
|
70
|
+
h ^= v.hash
|
71
|
+
h
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
# When used as a hash key, this entity instance is compared with another by
|
76
|
+
# comparing the values of its identifying roles
|
77
|
+
def eql?(other)
|
78
|
+
return false unless self.class == other.class
|
79
|
+
self.class.identifying_role_names.each{|role|
|
80
|
+
return false unless send(role).eql?(other.send(role))
|
81
|
+
}
|
82
|
+
return true
|
83
|
+
end
|
84
|
+
|
85
|
+
# Verbalise this entity instance
|
86
|
+
def verbalise(role_name = nil)
|
87
|
+
"#{role_name || self.class.basename}(#{
|
88
|
+
self.class.identifying_role_names.map{|role_sym|
|
89
|
+
value = send(role_sym)
|
90
|
+
role_name = self.class.roles(role_sym).name.to_s.camelcase
|
91
|
+
value ? value.verbalise(role_name) : "nil"
|
92
|
+
}*", "
|
93
|
+
})"
|
94
|
+
end
|
95
|
+
|
96
|
+
# Return the array of the values of this entity instance's identifying roles
|
97
|
+
def identifying_role_values
|
98
|
+
self.class.identifying_role_names.map{|role|
|
99
|
+
send(role)
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
# All classes that become Entity types receive the methods of this class as class methods:
|
104
|
+
module ClassMethods
|
105
|
+
include Instance::ClassMethods
|
106
|
+
|
107
|
+
# Return the array of Role objects that define the identifying relationships of this Entity type:
|
108
|
+
def identifying_role_names
|
109
|
+
@identifying_role_names ||= []
|
110
|
+
end
|
111
|
+
|
112
|
+
def identifying_roles
|
113
|
+
debug :persistence, "Identifying roles for #{basename}" do
|
114
|
+
@identifying_role_names.map{|name|
|
115
|
+
role = roles[name] || (!superclass.is_entity_type || superclass.roles[name])
|
116
|
+
debug :persistence, "#{name} -> #{role ? "found" : "NOT FOUND"}"
|
117
|
+
role
|
118
|
+
}
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Convert the passed arguments into an array of Instance objects that can identify an instance of this Entity type:
|
123
|
+
def identifying_role_values(*args)
|
124
|
+
#puts "Getting identifying role values #{identifying_role_names.inspect} of #{basename} using #{args.inspect}"
|
125
|
+
|
126
|
+
# If the single arg is an instance of the correct class or a subclass,
|
127
|
+
# use the instance's identifying_role_values
|
128
|
+
if (args.size == 1 and
|
129
|
+
(arg = args[0]).is_a?(self)) # REVISIT: or a secondary supertype
|
130
|
+
arg = arg.__getobj__ if RoleProxy === arg
|
131
|
+
return arg.identifying_role_values
|
132
|
+
end
|
133
|
+
|
134
|
+
ir = identifying_role_names
|
135
|
+
args, arg_hash = ActiveFacts::extract_hash_args(ir, args)
|
136
|
+
|
137
|
+
if args.size > ir.size
|
138
|
+
raise "You've provided too many values for the identifier of #{basename}, which expects (#{ir*', '})"
|
139
|
+
end
|
140
|
+
|
141
|
+
role_args = ir.map{|role_sym| roles(role_sym)}.zip(args)
|
142
|
+
role_args.map do |role, arg|
|
143
|
+
#puts "Getting identifying_role_value for #{role.counterpart_object_type.basename} using #{arg.inspect}"
|
144
|
+
next !!arg unless role.counterpart # Unary
|
145
|
+
arg = arg.__getobj__ if RoleProxy === arg
|
146
|
+
if arg.is_a?(role.counterpart_object_type) # REVISIT: or a secondary supertype
|
147
|
+
# Note that with a secondary supertype, it must still return the values of these identifying_role_names
|
148
|
+
next arg.identifying_role_values
|
149
|
+
end
|
150
|
+
if arg == nil # But not false
|
151
|
+
if role.mandatory
|
152
|
+
raise "You must provide a #{role.counterpart_object_type.name} to identify a #{basename}"
|
153
|
+
end
|
154
|
+
else
|
155
|
+
role.counterpart_object_type.identifying_role_values(*arg)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def assert_instance(constellation, args) #:nodoc:
|
161
|
+
# Build the key for this instance from the args
|
162
|
+
# The key of an instance is the value or array of keys of the identifying values.
|
163
|
+
# The key values aren't necessarily present in the constellation, even after this.
|
164
|
+
key = identifying_role_values(*args)
|
165
|
+
|
166
|
+
# Find and return an existing instance matching this key
|
167
|
+
instances = constellation.instances[self] # All instances of this class in this constellation
|
168
|
+
instance = instances[key]
|
169
|
+
# DEBUG: puts "assert #{self.basename} #{key.inspect} #{instance ? "exists" : "new"}"
|
170
|
+
return instance, key if instance # A matching instance of this class
|
171
|
+
|
172
|
+
# Now construct each of this object's identifying roles
|
173
|
+
ir = identifying_role_names
|
174
|
+
args, arg_hash = ActiveFacts::extract_hash_args(ir, args)
|
175
|
+
role_values = ir.map{|role_sym| roles(role_sym)}.zip(args)
|
176
|
+
key = [] # Gather the actual key (AutoCounters are special)
|
177
|
+
values = role_values.map do |role, arg|
|
178
|
+
if !arg
|
179
|
+
value = role_key = nil # No value
|
180
|
+
elsif !role.counterpart
|
181
|
+
value = role_key = !!arg # Unary
|
182
|
+
elsif arg.is_a?(role.counterpart_object_type) # REVISIT: or a secondary supertype
|
183
|
+
arg = arg.__getobj__ if RoleProxy === arg
|
184
|
+
raise "Connecting values across constellations" unless arg.constellation == constellation
|
185
|
+
value, role_key = arg, arg.identifying_role_values
|
186
|
+
else
|
187
|
+
value, role_key = role.counterpart_object_type.assert_instance(constellation, Array(arg))
|
188
|
+
end
|
189
|
+
key << role_key
|
190
|
+
value
|
191
|
+
end
|
192
|
+
values << arg_hash if arg_hash and !arg_hash.empty?
|
193
|
+
|
194
|
+
#puts "Creating new #{basename} using #{values.inspect}"
|
195
|
+
instance = new(*values)
|
196
|
+
|
197
|
+
# Make the new entity instance a member of this constellation:
|
198
|
+
instance.constellation = constellation
|
199
|
+
return *index_instance(instance, key, ir)
|
200
|
+
end
|
201
|
+
|
202
|
+
def index_instance(instance, key = nil, key_roles = nil) #:nodoc:
|
203
|
+
# Derive a new key if we didn't receive one or if the roles are different:
|
204
|
+
unless key && key_roles && key_roles == identifying_role_names
|
205
|
+
key = (key_roles = identifying_role_names).map do |role_name|
|
206
|
+
instance.send role_name
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Index the instance for this class in the constellation
|
211
|
+
instances = instance.constellation.instances[self]
|
212
|
+
instances[key] = instance
|
213
|
+
# DEBUG: puts "indexing entity #{basename} using #{key.inspect} in #{constellation.object_id}"
|
214
|
+
|
215
|
+
# Index the instance for each supertype:
|
216
|
+
supertypes.each do |supertype|
|
217
|
+
supertype.index_instance(instance, key, key_roles)
|
218
|
+
end
|
219
|
+
|
220
|
+
return instance, key
|
221
|
+
end
|
222
|
+
|
223
|
+
# A object_type that isn't a ValueType must have an identification scheme,
|
224
|
+
# which is a list of roles it plays. The identification scheme may be
|
225
|
+
# inherited from a superclass.
|
226
|
+
def initialise_entity_type(*args) #:nodoc:
|
227
|
+
#puts "Initialising entity type #{self} using #{args.inspect}"
|
228
|
+
@identifying_role_names = superclass.identifying_role_names if superclass.is_entity_type
|
229
|
+
# REVISIT: @identifying_role_names here are the symbols passed in, not the Role objects we should use.
|
230
|
+
# We'd need late binding to use Role objects...
|
231
|
+
@identifying_role_names = args if args.size > 0 || !@identifying_role_names
|
232
|
+
end
|
233
|
+
|
234
|
+
def inherited(other) #:nodoc:
|
235
|
+
other.identified_by *identifying_role_names
|
236
|
+
subtypes << other unless subtypes.include? other
|
237
|
+
#puts "#{self.name} inherited by #{other.name}"
|
238
|
+
vocabulary.__add_object_type(other)
|
239
|
+
end
|
240
|
+
|
241
|
+
# verbalise this object_type
|
242
|
+
def verbalise
|
243
|
+
"#{basename} is identified by #{identifying_role_names.map{|role_sym| role_sym.to_s.camelcase}*" and "};"
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def Entity.included other #:nodoc:
|
248
|
+
other.send :extend, ClassMethods
|
249
|
+
|
250
|
+
# Register ourselves with the parent module, which has become a Vocabulary:
|
251
|
+
vocabulary = other.modspace
|
252
|
+
# puts "Entity.included(#{other.inspect})"
|
253
|
+
unless vocabulary.respond_to? :object_type # Extend module with Vocabulary if necessary
|
254
|
+
vocabulary.send :extend, Vocabulary
|
255
|
+
end
|
256
|
+
vocabulary.__add_object_type(other)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|