activefacts 1.3.0 → 1.5.0
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.
- checksums.yaml +4 -4
- data/Manifest.txt +9 -0
- data/examples/CQL/Metamodel.cql +5 -0
- data/lib/activefacts/cql/compiler.rb +25 -2
- data/lib/activefacts/cql/compiler/constraint.rb +9 -1
- data/lib/activefacts/cql/compiler/fact_type.rb +1 -0
- data/lib/activefacts/cql/compiler/shared.rb +5 -1
- data/lib/activefacts/dependency_analyser.rb +182 -0
- data/lib/activefacts/generate/composition.rb +118 -0
- data/lib/activefacts/generate/cql.rb +3 -1
- data/lib/activefacts/generate/helpers/inject.rb +16 -0
- data/lib/activefacts/generate/rails/models.rb +1 -1
- data/lib/activefacts/generate/stats.rb +69 -0
- data/lib/activefacts/generate/topics.rb +265 -0
- data/lib/activefacts/generate/traits/oo.rb +73 -0
- data/lib/activefacts/generate/traits/ordered.rb +33 -0
- data/lib/activefacts/generate/traits/ruby.rb +210 -0
- data/lib/activefacts/input/orm.rb +158 -121
- data/lib/activefacts/persistence/columns.rb +1 -11
- data/lib/activefacts/persistence/foreignkey.rb +5 -6
- data/lib/activefacts/persistence/reference.rb +21 -1
- data/lib/activefacts/version.rb +1 -1
- data/lib/activefacts/vocabulary/extensions.rb +236 -8
- data/lib/activefacts/vocabulary/metamodel.rb +11 -0
- data/lib/activefacts/vocabulary/query_evaluator.rb +304 -0
- metadata +11 -2
@@ -188,7 +188,7 @@ module ActiveFacts
|
|
188
188
|
|
189
189
|
# If we emitted a reading for the refmode, it'll include any role_value_constraint already
|
190
190
|
if nonstandard_readings.size == 0 and c = value_role.role_value_constraint
|
191
|
-
constraint_text = " "+c.
|
191
|
+
constraint_text = " "+c.as_cql
|
192
192
|
end
|
193
193
|
(entity_type.is_independent ? ' independent' : '') +
|
194
194
|
" identified by its #{value_residual}#{constraint_text}#{mapping_pragma(entity_type, true)}" +
|
@@ -651,6 +651,8 @@ module ActiveFacts
|
|
651
651
|
parameters
|
652
652
|
}#{
|
653
653
|
unit && " "+unit.name
|
654
|
+
}#{
|
655
|
+
transaction_phase && " auto-assigned at "+transaction_phase
|
654
656
|
}#{
|
655
657
|
concept.all_context_note_as_relevant_concept.map do |cn|
|
656
658
|
cn.verbalise
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'activefacts/vocabulary'
|
2
|
+
|
3
|
+
module ActiveFacts
|
4
|
+
module TraitInjector
|
5
|
+
def self.included other
|
6
|
+
overlap = Metamodel.constants & other.constants
|
7
|
+
overlap.each do |const|
|
8
|
+
mix_into = Metamodel.const_get(const)
|
9
|
+
mix_in = other.const_get(const)
|
10
|
+
mix_into.instance_exec {
|
11
|
+
include(mix_in)
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -151,7 +151,7 @@ module ActiveFacts
|
|
151
151
|
|
152
152
|
ref = fk.jump_reference
|
153
153
|
[
|
154
|
-
"\n \# #{fk.verbalised_path}" +
|
154
|
+
"\n \# #{fk.verbalised_path(true)}" +
|
155
155
|
"\n" +
|
156
156
|
%Q{ #{association_type} :#{association_name}} +
|
157
157
|
%Q{, :class_name => '#{fk.from.rails_class_name}'} +
|
@@ -0,0 +1,69 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Generators.
|
3
|
+
# Generate metamodel statistics fora compiled vocabulary
|
4
|
+
#
|
5
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
6
|
+
#
|
7
|
+
require 'activefacts/persistence'
|
8
|
+
|
9
|
+
module ActiveFacts
|
10
|
+
module Generate
|
11
|
+
# Generate a text verbalisation of the metamodel constellation created for an ActiveFacts vocabulary.
|
12
|
+
# Invoke as
|
13
|
+
# afgen --text <file>.cql
|
14
|
+
class Statistics
|
15
|
+
private
|
16
|
+
def initialize(vocabulary)
|
17
|
+
@vocabulary = vocabulary
|
18
|
+
@vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
|
19
|
+
end
|
20
|
+
|
21
|
+
public
|
22
|
+
def generate(out = $>)
|
23
|
+
constellation = @vocabulary.constellation
|
24
|
+
object_types = constellation.ObjectType.values
|
25
|
+
fact_types = constellation.FactType.values
|
26
|
+
|
27
|
+
# All metamodel object types:
|
28
|
+
object_count = 0
|
29
|
+
populated_object_type_count = 0
|
30
|
+
fact_types_processed = {}
|
31
|
+
fact_count = 0
|
32
|
+
role_played_count = 0
|
33
|
+
constellation.vocabulary.object_type.map do |object_type_name, object_type|
|
34
|
+
objects = constellation.send(object_type_name)
|
35
|
+
next unless objects.size > 0
|
36
|
+
puts "\t#{object_type_name}: #{objects.size} instances (which play #{object_type.all_role.size} roles)"
|
37
|
+
populated_object_type_count += 1
|
38
|
+
object_count += objects.size
|
39
|
+
|
40
|
+
#puts "#{object_type_name} has #{object_type.all_role.size} roles"
|
41
|
+
object_type.all_role.each do |name, role|
|
42
|
+
next unless role.unique
|
43
|
+
next if fact_types_processed[role.fact_type]
|
44
|
+
next if role.fact_type.is_a?(ActiveFacts::API::TypeInheritanceFactType)
|
45
|
+
role_population_count =
|
46
|
+
objects.values.inject(0) do |count, object|
|
47
|
+
count += 1 if object.send(role.name) != nil
|
48
|
+
count
|
49
|
+
end
|
50
|
+
puts "\t\t#{object_type_name}.#{role.name} has #{role_population_count} instances" if role_population_count > 0
|
51
|
+
fact_count += role_population_count
|
52
|
+
role_played_count += role_population_count*role.fact_type.all_role.size
|
53
|
+
|
54
|
+
fact_types_processed[role.fact_type] = true
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
puts "#{@vocabulary.name} has"
|
59
|
+
puts "\t#{object_types.size} object types"
|
60
|
+
puts "\t#{fact_types.size} fact types"
|
61
|
+
puts "\tcompiles to #{object_count} objects in total, of #{populated_object_type_count} metamodel types"
|
62
|
+
puts "\tcompiles to #{fact_count} facts in total, of #{fact_types_processed.size} metamodel fact types"
|
63
|
+
puts "\tcompiles to #{role_played_count} role instances in total"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
ActiveFacts::Registry.generator('records', ActiveFacts::Generate::Statistics)
|
@@ -0,0 +1,265 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Generators.
|
3
|
+
# Generate metamodel topic hierarchy (topologically sorted) for a compiled vocabulary
|
4
|
+
#
|
5
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
6
|
+
#
|
7
|
+
require 'activefacts/dependency_analyser'
|
8
|
+
require 'activefacts/persistence'
|
9
|
+
|
10
|
+
module ActiveFacts
|
11
|
+
module Generate
|
12
|
+
# Generate a topic hierarchy of the metamodel constellation created for an ActiveFacts vocabulary.
|
13
|
+
# Invoke as
|
14
|
+
# afgen --topics <file>.cql
|
15
|
+
class Topics
|
16
|
+
private
|
17
|
+
def initialize(vocabulary)
|
18
|
+
@vocabulary = vocabulary
|
19
|
+
@vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
|
20
|
+
end
|
21
|
+
|
22
|
+
public
|
23
|
+
def generate(out = $>)
|
24
|
+
@constellation = @vocabulary.constellation
|
25
|
+
|
26
|
+
@concepts = @constellation.Concept.values.select do |c|
|
27
|
+
!c.embodied_as.is_a?(ActiveFacts::Metamodel::Role) &&
|
28
|
+
!c.implication_rule
|
29
|
+
end
|
30
|
+
|
31
|
+
@concept_deps = ActiveFacts::DependencyAnalyser.new(@concepts) {|c| c.precursors }
|
32
|
+
|
33
|
+
# list_concepts(@concept_deps)
|
34
|
+
# list_precursors(@concept_deps)
|
35
|
+
# list_followers(@concept_deps)
|
36
|
+
# list_chasers(@concept_deps)
|
37
|
+
# list_ranks(@concept_deps)
|
38
|
+
|
39
|
+
# @constellation.Topic.values.each do |topic|
|
40
|
+
# puts "#{topic.topic_name} depends on #{topic.precursors.map(&:topic_name)*', '}"
|
41
|
+
# end
|
42
|
+
|
43
|
+
# analyse_topic_sequence
|
44
|
+
|
45
|
+
list_groups_by_rank @concept_deps
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
def list_concepts concept_deps
|
50
|
+
puts "Concepts are"
|
51
|
+
concept_deps.each do |concept|
|
52
|
+
puts "\t#{concept.describe}"
|
53
|
+
end
|
54
|
+
puts
|
55
|
+
end
|
56
|
+
|
57
|
+
def list_precursors(concept_deps)
|
58
|
+
puts "Precursors are"
|
59
|
+
concept_deps.each do |concept|
|
60
|
+
next unless (p = concept_deps.precursors(concept)).size > 0
|
61
|
+
puts "#{concept.describe}:"
|
62
|
+
p.each do |precursor|
|
63
|
+
puts "\t#{precursor.describe}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def list_followers(concept_deps)
|
69
|
+
puts "Followers are:"
|
70
|
+
concept_deps.each do |concept|
|
71
|
+
next unless (f = concept_deps.followers(concept)).size > 0
|
72
|
+
puts "#{concept.describe}:"
|
73
|
+
f.each do |follower|
|
74
|
+
puts "\t#{follower.describe}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def list_chasers(concept_deps)
|
80
|
+
puts "Chasers are:"
|
81
|
+
concept_deps.each do |concept|
|
82
|
+
next unless (f = concept_deps.chasers(concept)).size > 0
|
83
|
+
puts "#{concept.describe}:"
|
84
|
+
f.each do |follower|
|
85
|
+
puts "\t#{follower.describe}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def list_ranks(concept_deps)
|
91
|
+
puts "Ranks are:"
|
92
|
+
page_rank = concept_deps.page_rank
|
93
|
+
page_rank.keys.sort_by{|k| -page_rank[k]}.each do |concept|
|
94
|
+
puts("%8.4f\t%s" % [page_rank[concept], concept.describe])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def analyse_topic_sequence
|
99
|
+
if @constellation.Topic.size == 1
|
100
|
+
@topics = @constellation.Topic.values.first
|
101
|
+
else
|
102
|
+
puts "=== Analysing existing topics ==="
|
103
|
+
@topic_analyser = ActiveFacts::DependencyAnalyser.new(@constellation.Topic.values) do |topic|
|
104
|
+
topic.precursors
|
105
|
+
end
|
106
|
+
|
107
|
+
@topics = []
|
108
|
+
failed_topics = @topic_analyser.tsort { |topic|
|
109
|
+
@topics << topic
|
110
|
+
puts "#{topic.topic_name} including #{topic.all_concept.size} concepts"
|
111
|
+
}
|
112
|
+
if failed_topics
|
113
|
+
puts "Topological sort of topics is impossible"
|
114
|
+
|
115
|
+
# REVISIT: The strategy here should be to look at the structure of the dependency loop
|
116
|
+
# involving the highest-rank topic. Choose the loop member that has fewest dependencies
|
117
|
+
# involving individual concepts, and dump that (it will need to flip topic to dump
|
118
|
+
# those dependencies). That should break the loop in a minimal way, so we can continue.
|
119
|
+
|
120
|
+
failed_topics.each do |topic, precursor_topics|
|
121
|
+
puts "#{topic.topic_name} depends on:"
|
122
|
+
precursor_topics.each do |precursor_topic|
|
123
|
+
puts "\t#{precursor_topic.topic_name} because of:"
|
124
|
+
|
125
|
+
blocking_concept =
|
126
|
+
precursor_topic.all_concept.select{|concept|
|
127
|
+
concept.precursors.detect{|cp|
|
128
|
+
cp.topic == precursor_topic
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
132
|
+
blocking_concept.each do |concept|
|
133
|
+
puts "\t\t#{concept.describe} (depends on #{concept.precursors.map(&:topic).uniq.map(&:topic_name).sort.inspect})"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
return
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def list_groups_by_rank concept_deps
|
143
|
+
puts "=== Concepts listed in Ranking Order with chasers ==="
|
144
|
+
# List the highest-ranked items that have no un-listed precursors.
|
145
|
+
# After each listed item, list the chasers *transitively* before
|
146
|
+
# choosing the next highest ranked item.
|
147
|
+
@listed = {}
|
148
|
+
|
149
|
+
start = Time.now
|
150
|
+
@page_rank = concept_deps.page_rank # Get the page_rank
|
151
|
+
# puts "done in #{Time.now-start}"
|
152
|
+
|
153
|
+
@ranked = @page_rank.keys.sort_by{|k| -@page_rank[k]} # Sort decreasing
|
154
|
+
@topic = nil
|
155
|
+
until @listed.size >= @ranked.size
|
156
|
+
vt = nil # Capture the first listable ValueType, which we'll choose if we must
|
157
|
+
chosen = @ranked.detect do |concept|
|
158
|
+
next if @listed[concept]
|
159
|
+
|
160
|
+
unlisted_precursors =
|
161
|
+
concept_deps.precursors_transitive(concept).
|
162
|
+
reject do |precursor|
|
163
|
+
is_value_type_concept(precursor) or
|
164
|
+
precursor == concept or
|
165
|
+
@listed[precursor] or
|
166
|
+
is_value_type_concept(precursor)
|
167
|
+
end
|
168
|
+
next if unlisted_precursors.size > 0 # Still has precursors
|
169
|
+
vt ||= concept
|
170
|
+
next if is_value_type_concept(concept)
|
171
|
+
concept
|
172
|
+
end
|
173
|
+
chosen ||= vt # Choose a value type if there's no other
|
174
|
+
|
175
|
+
@cluster = []
|
176
|
+
|
177
|
+
descend concept_deps, chosen
|
178
|
+
|
179
|
+
cluster_ranks = @cluster.sort_by{|c| -@page_rank[c]}
|
180
|
+
head = cluster_ranks.detect{|c| c.object_type} || cluster_ranks.first
|
181
|
+
puts "/*\n * #{head.object_type ? head.object_type.name : head.describe}\n */"
|
182
|
+
@cluster.each do |concept|
|
183
|
+
puts '*'*10+" TOPIC #{@topic = concept.topic.topic_name}" if @topic != concept.topic.topic_name
|
184
|
+
puts concept.describe
|
185
|
+
end
|
186
|
+
puts # Put a line between sections
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Descend the concept, also the items that are necessarily dumped with concept, so we don't select them again
|
191
|
+
def descend(concept_deps, concept, tab = '')
|
192
|
+
@causation ||= []
|
193
|
+
@cluster ||= []
|
194
|
+
return if @listed[concept] || @causation.include?(concept) # Recursion prevention
|
195
|
+
begin
|
196
|
+
@causation << concept # Reversed by the "ensure" below.
|
197
|
+
|
198
|
+
concept_deps.precursors(concept).each do |precursor|
|
199
|
+
descend(concept_deps, precursor, tab) unless @listed[precursor] || precursor == concept
|
200
|
+
end
|
201
|
+
|
202
|
+
#puts tab+concept.describe
|
203
|
+
@listed[concept] = true
|
204
|
+
@cluster << concept
|
205
|
+
|
206
|
+
body = concept.embodied_as
|
207
|
+
case body
|
208
|
+
when ActiveFacts::Metamodel::FactType
|
209
|
+
body.internal_presence_constraints.each do |pc|
|
210
|
+
# puts "Listing #{pc.concept.describe} as internal_presence_constraint of #{concept.describe}"
|
211
|
+
descend(concept_deps, pc.concept, tab+"\t")
|
212
|
+
end
|
213
|
+
if body.entity_type
|
214
|
+
# puts "Listing #{body.entity_type.concept.describe} as objectification of #{concept.describe}"
|
215
|
+
descend(concept_deps, body.entity_type.concept, tab+"\t")
|
216
|
+
end
|
217
|
+
body.all_role.each do |role|
|
218
|
+
role.all_ring_constraint.each do |rc|
|
219
|
+
# puts "Listing #{rc.concept.describe} as ring_constraint of #{concept.describe}"
|
220
|
+
descend(concept_deps, rc.concept, tab+"\t")
|
221
|
+
end
|
222
|
+
end
|
223
|
+
when ActiveFacts::Metamodel::EntityType
|
224
|
+
body.preferred_identifier.role_sequence.all_role_ref.each do |rr|
|
225
|
+
# puts "Listing #{rr.role.fact_type.concept.describe} as existential fact of #{concept.describe}"
|
226
|
+
descend(concept_deps, rr.role.fact_type.concept, tab+"\t")
|
227
|
+
end
|
228
|
+
descend(concept_deps, body.preferred_identifier.concept, tab+"\t")
|
229
|
+
when ActiveFacts::Metamodel::ValueType
|
230
|
+
if body.value_constraint
|
231
|
+
# puts "Listing #{body.value_constraint.concept.describe} as value constraint of #{concept.describe}"
|
232
|
+
descend(concept_deps, body.value_constraint.concept, tab+"\t")
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Follow up with unlisted chasers (that have no remaining precursors) in order of decreasing importance:
|
237
|
+
chasers =
|
238
|
+
concept_deps.followers(concept).reject do |follower|
|
239
|
+
@listed[follower] or
|
240
|
+
is_value_type_concept(follower) or
|
241
|
+
concept_deps.precursors(follower).detect{|p| !@listed[p] && !is_value_type_concept(p) } or
|
242
|
+
# Exclude subtypes of entity types from the followers:
|
243
|
+
concept.object_type && concept.object_type.is_a?(ActiveFacts::Metamodel::EntityType) && concept.object_type.subtypes.include?(follower.object_type)
|
244
|
+
end.
|
245
|
+
sort_by{|nc| -@page_rank[nc]}
|
246
|
+
|
247
|
+
chasers.each do |chaser|
|
248
|
+
# puts "**** Listing #{chaser.describe} as chaser of #{concept.describe}"
|
249
|
+
descend(concept_deps, chaser, tab)
|
250
|
+
end
|
251
|
+
|
252
|
+
ensure
|
253
|
+
@causation.pop
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def is_value_type_concept concept
|
258
|
+
o = concept.object_type and o.is_a?(ActiveFacts::Metamodel::ValueType)
|
259
|
+
end
|
260
|
+
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
ActiveFacts::Registry.generator('topics', ActiveFacts::Generate::Topics)
|
@@ -0,0 +1,73 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Generators.
|
3
|
+
# Base class for generators of class libraries in any object-oriented language that supports the ActiveFacts API.
|
4
|
+
#
|
5
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
6
|
+
#
|
7
|
+
module ActiveFacts
|
8
|
+
module Generate
|
9
|
+
module OOTraits
|
10
|
+
module ObjectType
|
11
|
+
# Map the ObjectType name to an OO class name
|
12
|
+
def oo_type_name
|
13
|
+
name.words.capcase
|
14
|
+
end
|
15
|
+
|
16
|
+
# Map the OO class name to a default role name
|
17
|
+
def oo_default_role_name
|
18
|
+
name.words.snakecase
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module Role
|
23
|
+
def oo_role_definition
|
24
|
+
return if fact_type.entity_type
|
25
|
+
|
26
|
+
if fact_type.all_role.size == 1
|
27
|
+
return " maybe :#{preferred_role_name}\n"
|
28
|
+
elsif fact_type.all_role.size != 2
|
29
|
+
# Shouldn't come here, except perhaps for an invalid model
|
30
|
+
return # ternaries and higher are always objectified
|
31
|
+
end
|
32
|
+
|
33
|
+
# REVISIT: TypeInheritance
|
34
|
+
if fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
|
35
|
+
# trace "Ignoring role #{self} in #{fact_type}, subtype fact type"
|
36
|
+
# REVISIT: What about secondary subtypes?
|
37
|
+
# REVISIT: What about dumping the relational mapping when using separate tables?
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
return unless is_functional
|
42
|
+
|
43
|
+
counterpart_role = fact_type.all_role.select{|r| r != self}[0]
|
44
|
+
counterpart_type = counterpart_role.object_type
|
45
|
+
counterpart_role_name = counterpart_role.preferred_role_name
|
46
|
+
counterpart_type_default_role_name = counterpart_type.oo_default_role_name
|
47
|
+
|
48
|
+
# It's a one_to_one if there's a uniqueness constraint on the other role:
|
49
|
+
one_to_one = counterpart_role.is_functional
|
50
|
+
return if one_to_one &&
|
51
|
+
false # REVISIT: !@object_types_dumped[counterpart_role.object_type]
|
52
|
+
|
53
|
+
# Find role name:
|
54
|
+
role_method = preferred_role_name
|
55
|
+
counterpart_role_method = one_to_one ? role_method : "all_"+role_method
|
56
|
+
# puts "---"+role.role_name if role.role_name
|
57
|
+
if counterpart_role_name != counterpart_type.oo_default_role_name and
|
58
|
+
role_method == self.object_type.oo_default_role_name
|
59
|
+
# debugger
|
60
|
+
counterpart_role_method += "_as_#{counterpart_role_name}"
|
61
|
+
end
|
62
|
+
|
63
|
+
role_name = role_method
|
64
|
+
role_name = nil if role_name == object_type.oo_default_role_name
|
65
|
+
|
66
|
+
as_binary(counterpart_role_name, counterpart_type, is_mandatory, one_to_one, nil, role_name, counterpart_role_method)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
include ActiveFacts::TraitInjector # Must be last in this module, after all submodules have been defined
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|